From 4fde7822d8f728007fb0781690a4f8a6d47a1868 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Mon, 31 Mar 2025 12:45:11 +0200 Subject: [PATCH] fix(oauth): check key expiry on JWT Profile Grant # Which Problems Are Solved ZITADEL allows the use of JSON Web Token (JWT) Profile OAuth 2.0 for Authorization Grants in machine-to-machine (M2M) authentication. Multiple keys can be managed for a single machine account (service user), each with an individual expiry. A vulnerability existed where expired keys can be used to retrieve tokens. Specifically, ZITADEL fails to properly check the expiration date of the JWT key when used for Authorization Grants. This allows an attacker with an expired key to obtain valid access tokens. This vulnerability does not affect the use of JWT Profile for OAuth 2.0 Client Authentication on the Token and Introspection endpoints, which correctly reject expired keys. # How the Problems Are Solved Added proper validation of the expiry of the stored public key. # Additional Changes None # Additional Context None (cherry picked from commit 315503beabd679f2e6aec0c004f0f9d2f5b53ed3) --- .../oidc/integration_test/token_jwt_profile_test.go | 13 ++++++++++++- internal/integration/oidc.go | 4 ++-- internal/query/authn_key_user.sql | 5 +++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/internal/api/oidc/integration_test/token_jwt_profile_test.go b/internal/api/oidc/integration_test/token_jwt_profile_test.go index 5845713317..b4ae0d777a 100644 --- a/internal/api/oidc/integration_test/token_jwt_profile_test.go +++ b/internal/api/oidc/integration_test/token_jwt_profile_test.go @@ -21,7 +21,9 @@ import ( ) func TestServer_JWTProfile(t *testing.T) { - user, name, keyData, err := Instance.CreateOIDCJWTProfileClient(CTX) + user, name, keyData, err := Instance.CreateOIDCJWTProfileClient(CTX, time.Hour) + require.NoError(t, err) + _, _, keyDataExpired, err := Instance.CreateOIDCJWTProfileClient(CTX, 10*time.Second) require.NoError(t, err) type claims struct { @@ -104,6 +106,12 @@ func TestServer_JWTProfile(t *testing.T) { resourceOwnerPrimaryDomain: Instance.DefaultOrg.PrimaryDomain, }, }, + { + name: "key expired", + keyData: keyDataExpired, + scope: []string{oidc.ScopeOpenID}, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -123,6 +131,9 @@ func TestServer_JWTProfile(t *testing.T) { }, time.Minute, time.Second, ) + if tt.wantErr { + return + } provider, err := rp.NewRelyingPartyOIDC(CTX, Instance.OIDCIssuer(), "", "", redirectURI, tt.scope) require.NoError(t, err) diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index 19742741ab..159fcb0119 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -438,7 +438,7 @@ func (i *Instance) CreateOIDCCredentialsClientInactive(ctx context.Context) (mac return machine, name, secret.GetClientId(), secret.GetClientSecret(), nil } -func (i *Instance) CreateOIDCJWTProfileClient(ctx context.Context) (machine *management.AddMachineUserResponse, name string, keyData []byte, err error) { +func (i *Instance) CreateOIDCJWTProfileClient(ctx context.Context, keyLifetime time.Duration) (machine *management.AddMachineUserResponse, name string, keyData []byte, err error) { name = gofakeit.Username() machine, err = i.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{ Name: name, @@ -451,7 +451,7 @@ func (i *Instance) CreateOIDCJWTProfileClient(ctx context.Context) (machine *man keyResp, err := i.Client.Mgmt.AddMachineKey(ctx, &management.AddMachineKeyRequest{ UserId: machine.GetUserId(), Type: authn.KeyType_KEY_TYPE_JSON, - ExpirationDate: timestamppb.New(time.Now().Add(time.Hour)), + ExpirationDate: timestamppb.New(time.Now().Add(keyLifetime)), }) if err != nil { return nil, "", nil, err diff --git a/internal/query/authn_key_user.sql b/internal/query/authn_key_user.sql index 5b7eaca63a..80de7a5167 100644 --- a/internal/query/authn_key_user.sql +++ b/internal/query/authn_key_user.sql @@ -3,9 +3,10 @@ from projections.authn_keys2 k join projections.users14 u on k.instance_id = u.instance_id and k.identifier = u.id -join projections.users14_machines m +join projections.users14_machines m on u.instance_id = m.instance_id and u.id = m.user_id where k.instance_id = $1 and k.id = $2 - and u.id = $3; + and u.id = $3 + and k.expiration > current_timestamp;