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 315503beab)
This commit is contained in:
Livio Spring
2025-03-31 12:45:11 +02:00
parent ae1e86ed9a
commit 4fde7822d8
3 changed files with 17 additions and 5 deletions

View File

@@ -21,7 +21,9 @@ import (
) )
func TestServer_JWTProfile(t *testing.T) { 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) require.NoError(t, err)
type claims struct { type claims struct {
@@ -104,6 +106,12 @@ func TestServer_JWTProfile(t *testing.T) {
resourceOwnerPrimaryDomain: Instance.DefaultOrg.PrimaryDomain, resourceOwnerPrimaryDomain: Instance.DefaultOrg.PrimaryDomain,
}, },
}, },
{
name: "key expired",
keyData: keyDataExpired,
scope: []string{oidc.ScopeOpenID},
wantErr: true,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -123,6 +131,9 @@ func TestServer_JWTProfile(t *testing.T) {
}, },
time.Minute, time.Second, time.Minute, time.Second,
) )
if tt.wantErr {
return
}
provider, err := rp.NewRelyingPartyOIDC(CTX, Instance.OIDCIssuer(), "", "", redirectURI, tt.scope) provider, err := rp.NewRelyingPartyOIDC(CTX, Instance.OIDCIssuer(), "", "", redirectURI, tt.scope)
require.NoError(t, err) require.NoError(t, err)

View File

@@ -438,7 +438,7 @@ func (i *Instance) CreateOIDCCredentialsClientInactive(ctx context.Context) (mac
return machine, name, secret.GetClientId(), secret.GetClientSecret(), nil 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() name = gofakeit.Username()
machine, err = i.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{ machine, err = i.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{
Name: name, Name: name,
@@ -451,7 +451,7 @@ func (i *Instance) CreateOIDCJWTProfileClient(ctx context.Context) (machine *man
keyResp, err := i.Client.Mgmt.AddMachineKey(ctx, &management.AddMachineKeyRequest{ keyResp, err := i.Client.Mgmt.AddMachineKey(ctx, &management.AddMachineKeyRequest{
UserId: machine.GetUserId(), UserId: machine.GetUserId(),
Type: authn.KeyType_KEY_TYPE_JSON, Type: authn.KeyType_KEY_TYPE_JSON,
ExpirationDate: timestamppb.New(time.Now().Add(time.Hour)), ExpirationDate: timestamppb.New(time.Now().Add(keyLifetime)),
}) })
if err != nil { if err != nil {
return nil, "", nil, err return nil, "", nil, err

View File

@@ -8,4 +8,5 @@ join projections.users14_machines m
and u.id = m.user_id and u.id = m.user_id
where k.instance_id = $1 where k.instance_id = $1
and k.id = $2 and k.id = $2
and u.id = $3; and u.id = $3
and k.expiration > current_timestamp;