refactor: deprecate proprietary key file use for JWT Profile (#801)

While reviewing #750, we noticed that the `KeyFile` struct and
corresponding methods are proprietary to Zitadel and should have never
been part of the pure OIDC library.

This PR deprecates the corresponding parts. For users of Zitadel, the
corresponding code is moved to zitadel/zitadel-go#516

### Definition of Ready

- [x] I am happy with the code
- [x] Short description of the feature/issue is added in the pr
description
- [x] PR is linked to the corresponding user story
- [x] Acceptance criteria are met
- [ ] All open todos and follow ups are defined in a new ticket and
justified
- [ ] Deviations from the acceptance criteria and design are agreed with
the PO and documented.
- [x] No debug or dead code
- [x] My code has no repetitions
- [x] Critical parts are tested automatically
- [ ] Where possible E2E tests are implemented
- [x] Documentation/examples are up-to-date
- [x] All non-functional requirements are met
- [x] Functionality of the acceptance criteria is checked manually on
the dev system.
This commit is contained in:
Livio Spring
2025-09-23 08:44:48 +02:00
committed by GitHub
parent df140a781b
commit adddf0e4b3
10 changed files with 62 additions and 11 deletions
+7 -1
View File
@@ -16,6 +16,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
@@ -30,6 +31,7 @@ func main() {
clientID := os.Getenv("CLIENT_ID")
clientSecret := os.Getenv("CLIENT_SECRET")
keyPath := os.Getenv("KEY_PATH")
keyID := os.Getenv("KEY_ID")
issuer := os.Getenv("ISSUER")
port := os.Getenv("PORT")
scopes := strings.Split(os.Getenv("SCOPES"), " ")
@@ -71,7 +73,11 @@ func main() {
options = append(options, rp.WithPKCE(cookieHandler))
}
if keyPath != "" {
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
signingKey, err := os.ReadFile(keyPath)
if err != nil {
logrus.Fatalf("error reading key file %s", err.Error())
}
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyAndKeyID(signingKey, keyID)))
}
if pkce {
options = append(options, rp.WithPKCE(cookieHandler))
+6 -1
View File
@@ -60,6 +60,7 @@ func main() {
clientID := os.Getenv("CLIENT_ID")
clientSecret := os.Getenv("CLIENT_SECRET")
keyPath := os.Getenv("KEY_PATH")
keyID := os.Getenv("KEY_ID")
issuer := os.Getenv("ISSUER")
scopes := strings.Split(os.Getenv("SCOPES"), " ")
@@ -70,7 +71,11 @@ func main() {
options = append(options, rp.WithPKCE(cookieHandler))
}
if keyPath != "" {
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
signingKey, err := os.ReadFile(keyPath)
if err != nil {
logrus.Fatalf("error reading key file %s", err.Error())
}
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyAndKeyID(signingKey, keyID)))
}
provider, err := rp.NewRelyingPartyOIDC(ctx, issuer, clientID, clientSecret, "", scopes, options...)
-1
View File
@@ -1 +0,0 @@
{"type":"serviceaccount","keyId":"key1","key":"-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQD21E+180rCAzp15zy2X/JOYYHtxYhF51pWCsITeChJd7sFWxp1\ntxSHTiomQYBiBWgcCavsdu/VLPQJhO3PTIyglxc1XRGsM48oDT5MkFsAVDvbjuWk\nF0lstQyw4pr8Wg0Ecf1aL6YlvVKB9h5rAgZ9T+elNJ7q5takMAvNhu7zMQIDAQAB\nAoGAeLRw2qjEaUZM43WWchVPmFcEw/MyZgTyX1tZd03uXacolUDtGp3ScyydXiHw\nF39PX063fabYOCaInNMdvJ9RsQz2OcZuS/K6NOmWhzBfLgs4Y1tU6ijoY/gBjHgu\nCV0KjvoWIfEtKl/On/wTrAnUStFzrc7U4dpKFP1fy2ZTTnECQQD8aP2QOxmKUyfg\nBAjfonpkrNeaTRNwTULTvEHFiLyaeFd1PAvsDiKZtpk6iHLb99mQZkVVtAK5qgQ4\n1OI72jkVAkEA+lcAamuZAM+gIiUhbHA7BfX9OVgyGDD2tx5g/kxhMUmK6hIiO6Ul\n0nw5KfrCEUU3AzrM7HejUg3q61SYcXTgrQJBALhrzbhwNf0HPP9Ec2dSw7KDRxSK\ndEV9bfJefn/hpEwI2X3i3aMfwNAmxlYqFCH8OY5z6vzvhX46ZtNPV+z7SPECQQDq\nApXi5P27YlpgULEzup2R7uZsymLZdjvJ5V3pmOBpwENYlublNnVqkrCk60CqADdy\nj26rxRIoS9ZDcWqm9AhpAkEAyrNXBMJh08ghBMb3NYPFfr/bftRJSrGjhBPuJ5qr\nXzWaXhYVMMh3OSAwzHBJbA1ffdQJuH2ebL99Ur5fpBcbVw==\n-----END RSA PRIVATE KEY-----\n","userId":"service"}
+15
View File
@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQD21E+180rCAzp15zy2X/JOYYHtxYhF51pWCsITeChJd7sFWxp1
txSHTiomQYBiBWgcCavsdu/VLPQJhO3PTIyglxc1XRGsM48oDT5MkFsAVDvbjuWk
F0lstQyw4pr8Wg0Ecf1aL6YlvVKB9h5rAgZ9T+elNJ7q5takMAvNhu7zMQIDAQAB
AoGAeLRw2qjEaUZM43WWchVPmFcEw/MyZgTyX1tZd03uXacolUDtGp3ScyydXiHw
F39PX063fabYOCaInNMdvJ9RsQz2OcZuS/K6NOmWhzBfLgs4Y1tU6ijoY/gBjHgu
CV0KjvoWIfEtKl/On/wTrAnUStFzrc7U4dpKFP1fy2ZTTnECQQD8aP2QOxmKUyfg
BAjfonpkrNeaTRNwTULTvEHFiLyaeFd1PAvsDiKZtpk6iHLb99mQZkVVtAK5qgQ4
1OI72jkVAkEA+lcAamuZAM+gIiUhbHA7BfX9OVgyGDD2tx5g/kxhMUmK6hIiO6Ul
0nw5KfrCEUU3AzrM7HejUg3q61SYcXTgrQJBALhrzbhwNf0HPP9Ec2dSw7KDRxSK
dEV9bfJefn/hpEwI2X3i3aMfwNAmxlYqFCH8OY5z6vzvhX46ZtNPV+z7SPECQQDq
ApXi5P27YlpgULEzup2R7uZsymLZdjvJ5V3pmOBpwENYlublNnVqkrCk60CqADdy
j26rxRIoS9ZDcWqm9AhpAkEAyrNXBMJh08ghBMb3NYPFfr/bftRJSrGjhBPuJ5qr
XzWaXhYVMMh3OSAwzHBJbA1ffdQJuH2ebL99Ur5fpBcbVw==
-----END RSA PRIVATE KEY-----
+2 -2
View File
@@ -19,7 +19,7 @@ import (
)
// serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant
// the corresponding private key is in the service-key1.json (for demonstration purposes)
// the corresponding private key is in the service-key1.pem (for demonstration purposes)
var serviceKey1 = &rsa.PublicKey{
N: func() *big.Int {
n, _ := new(big.Int).SetString("00f6d44fb5f34ac2033a75e73cb65ff24e6181edc58845e75a560ac21378284977bb055b1a75b714874e2a2641806205681c09abec76efd52cf40984edcf4c8ca09717355d11ac338f280d3e4c905b00543bdb8ee5a417496cb50cb0e29afc5a0d0471fd5a2fa625bd5281f61e6b02067d4fe7a5349eeae6d6a4300bcd86eef331", 16)
@@ -105,7 +105,7 @@ func NewStorageWithClients(userStore UserStore, clients map[string]*Client) *Sto
services: map[string]Service{
userStore.ExampleClientID(): {
keys: map[string]*rsa.PublicKey{
"key1": serviceKey1,
ServiceUserKeyID: serviceKey1,
},
},
},
+8 -1
View File
@@ -9,6 +9,13 @@ import (
"golang.org/x/text/language"
)
const (
// ServiceUserID is the ID of the service user.
ServiceUserID = "service"
// ServiceUserKeyID is the key ID of the service user.
ServiceUserKeyID = "key1"
)
type User struct {
ID string
Username string
@@ -85,7 +92,7 @@ func NewUserStore(issuer string) UserStore {
// ExampleClientID is only used in the example server
func (u userStore) ExampleClientID() string {
return "service"
return ServiceUserID
}
func (u userStore) GetUserByID(id string) *User {
+6
View File
@@ -10,6 +10,8 @@ const (
applicationKey = "application"
)
// Deprecated: use [github.com/zitadel/zitadel-go/v3/pkg/client.KeyFile] instead.
// The type will be removed in the next major release.
type KeyFile struct {
Type string `json:"type"` // serviceaccount or application
KeyID string `json:"keyId"`
@@ -23,6 +25,8 @@ type KeyFile struct {
ClientID string `json:"clientId"`
}
// Deprecated: use [github.com/zitadel/zitadel-go/v3/pkg/client.ConfigFromKeyFile] instead.
// The type will be removed in the next major release.
func ConfigFromKeyFile(path string) (*KeyFile, error) {
data, err := os.ReadFile(path)
if err != nil {
@@ -31,6 +35,8 @@ func ConfigFromKeyFile(path string) (*KeyFile, error) {
return ConfigFromKeyFileData(data)
}
// Deprecated: use [github.com/zitadel/zitadel-go/v3/pkg/client.ConfigFromKeyFileData] instead.
// The type will be removed in the next major release.
func ConfigFromKeyFileData(data []byte) (*KeyFile, error) {
var f KeyFile
if err := json.Unmarshal(data, &f); err != nil {
+7 -1
View File
@@ -34,6 +34,9 @@ type jwtProfileTokenSource struct {
// therefore sending an `assertion` by singing a JWT with the provided private key from jsonFile.
//
// The passed context is only used for the call to the Discover endpoint.
//
// Deprecated: use [github.com/zitadel/zitadel-go/v3/pkg/client.ConfigFromKeyFileData] instead.
// The function will be removed in the next major release.
func NewJWTProfileTokenSourceFromKeyFile(ctx context.Context, issuer, jsonFile string, scopes []string, options ...func(source *jwtProfileTokenSource)) (TokenSource, error) {
keyData, err := client.ConfigFromKeyFile(jsonFile)
if err != nil {
@@ -47,6 +50,9 @@ func NewJWTProfileTokenSourceFromKeyFile(ctx context.Context, issuer, jsonFile s
// therefore sending an `assertion` by singing a JWT with the provided private key in jsonData.
//
// The passed context is only used for the call to the Discover endpoint.
//
// Deprecated: use [github.com/zitadel/zitadel-go/v3/pkg/client.ConfigFromKeyFileData] instead.
// The function will be removed in the next major release.
func NewJWTProfileTokenSourceFromKeyFileData(ctx context.Context, issuer string, jsonData []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (TokenSource, error) {
keyData, err := client.ConfigFromKeyFileData(jsonData)
if err != nil {
@@ -55,7 +61,7 @@ func NewJWTProfileTokenSourceFromKeyFileData(ctx context.Context, issuer string,
return NewJWTProfileTokenSource(ctx, issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...)
}
// NewJWTProfileSource returns an implementation of oauth2.TokenSource
// NewJWTProfileTokenSource returns an implementation of oauth2.TokenSource
// It will request a token using the OAuth2 JWT Profile Grant,
// therefore sending an `assertion` by singing a JWT with the provided private key.
//
+6 -1
View File
@@ -15,6 +15,7 @@ import (
"golang.org/x/oauth2/clientcredentials"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/client"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
@@ -324,7 +325,7 @@ func WithVerifierOpts(opts ...VerifierOption) Option {
// WithClientKey specifies the path to the key.json to be used for the JWT Profile Client Authentication on the token endpoint
//
// deprecated: use WithJWTProfile(SignerFromKeyPath(path)) instead
// Deprecated: use WithJWTProfile(SignerFromKeyPath(path)), resp. WithJWTProfile(SignerFromKeyAndKeyID(key, keyID) instead.
func WithClientKey(path string) Option {
return WithJWTProfile(SignerFromKeyPath(path))
}
@@ -363,6 +364,8 @@ func WithSigningAlgsFromDiscovery() Option {
type SignerFromKey func() (jose.Signer, error)
// Deprecated: use [SignerFromKeyAndKeyID] instead.
// The function will be removed in the next major release.
func SignerFromKeyPath(path string) SignerFromKey {
return func() (jose.Signer, error) {
config, err := client.ConfigFromKeyFile(path)
@@ -373,6 +376,8 @@ func SignerFromKeyPath(path string) SignerFromKey {
}
}
// Deprecated: use [SignerFromKeyAndKeyID] instead.
// The function will be removed in the next major release.
func SignerFromKeyFile(fileData []byte) SignerFromKey {
return func() (jose.Signer, error) {
config, err := client.ConfigFromKeyFileData(fileData)
+5 -3
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
@@ -14,21 +15,22 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/example/server/storage"
"github.com/zitadel/oidc/v3/pkg/client"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
)
func jwtProfile() (string, error) {
keyData, err := client.ConfigFromKeyFile("../../example/server/service-key1.json")
data, err := os.ReadFile("../../example/server/service-key1.pem")
if err != nil {
return "", err
}
signer, err := client.NewSignerFromPrivateKeyByte([]byte(keyData.Key), keyData.KeyID)
signer, err := client.NewSignerFromPrivateKeyByte(data, storage.ServiceUserKeyID)
if err != nil {
return "", err
}
return client.SignedJWTProfileAssertion(keyData.UserID, []string{testIssuer}, time.Hour, signer)
return client.SignedJWTProfileAssertion(storage.ServiceUserID, []string{testIssuer}, time.Hour, signer)
}
func TestServerRoutes(t *testing.T) {