mirror of
https://github.com/zitadel/zitadel.git
synced 2025-04-23 06:41:32 +00:00

# 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)
164 lines
5.0 KiB
Go
164 lines
5.0 KiB
Go
//go:build integration
|
|
|
|
package oidc_test
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zitadel/oidc/v3/pkg/client/profile"
|
|
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
|
"golang.org/x/oauth2"
|
|
|
|
oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/integration"
|
|
"github.com/zitadel/zitadel/pkg/grpc/auth"
|
|
)
|
|
|
|
func TestServer_JWTProfile(t *testing.T) {
|
|
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 {
|
|
name string
|
|
username string
|
|
updated time.Time
|
|
resourceOwnerID any
|
|
resourceOwnerName any
|
|
resourceOwnerPrimaryDomain any
|
|
orgDomain any
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
keyData []byte
|
|
scope []string
|
|
wantClaims claims
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "success",
|
|
keyData: keyData,
|
|
scope: []string{oidc.ScopeOpenID},
|
|
},
|
|
{
|
|
name: "openid, profile, email",
|
|
keyData: keyData,
|
|
scope: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail},
|
|
wantClaims: claims{
|
|
name: name,
|
|
username: name,
|
|
updated: user.GetDetails().GetChangeDate().AsTime(),
|
|
},
|
|
},
|
|
{
|
|
name: "openid, profile, email, zitadel",
|
|
keyData: keyData,
|
|
scope: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, domain.ProjectScopeZITADEL},
|
|
wantClaims: claims{
|
|
name: name,
|
|
username: name,
|
|
updated: user.GetDetails().GetChangeDate().AsTime(),
|
|
},
|
|
},
|
|
{
|
|
name: "org id and domain scope",
|
|
keyData: keyData,
|
|
scope: []string{
|
|
oidc.ScopeOpenID,
|
|
domain.OrgIDScope + Instance.DefaultOrg.Id,
|
|
domain.OrgDomainPrimaryScope + Instance.DefaultOrg.PrimaryDomain,
|
|
},
|
|
wantClaims: claims{
|
|
resourceOwnerID: Instance.DefaultOrg.Id,
|
|
resourceOwnerName: Instance.DefaultOrg.Name,
|
|
resourceOwnerPrimaryDomain: Instance.DefaultOrg.PrimaryDomain,
|
|
orgDomain: Instance.DefaultOrg.PrimaryDomain,
|
|
},
|
|
},
|
|
{
|
|
name: "invalid org domain filtered",
|
|
keyData: keyData,
|
|
scope: []string{
|
|
oidc.ScopeOpenID,
|
|
domain.OrgDomainPrimaryScope + Instance.DefaultOrg.PrimaryDomain,
|
|
domain.OrgDomainPrimaryScope + "foo"},
|
|
wantClaims: claims{
|
|
orgDomain: Instance.DefaultOrg.PrimaryDomain,
|
|
},
|
|
},
|
|
{
|
|
name: "invalid org id filtered",
|
|
keyData: keyData,
|
|
scope: []string{oidc.ScopeOpenID,
|
|
domain.OrgIDScope + Instance.DefaultOrg.Id,
|
|
domain.OrgIDScope + "foo",
|
|
},
|
|
wantClaims: claims{
|
|
resourceOwnerID: Instance.DefaultOrg.Id,
|
|
resourceOwnerName: Instance.DefaultOrg.Name,
|
|
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) {
|
|
tokenSource, err := profile.NewJWTProfileTokenSourceFromKeyFileData(CTX, Instance.OIDCIssuer(), tt.keyData, tt.scope)
|
|
require.NoError(t, err)
|
|
|
|
var tokens *oauth2.Token
|
|
require.EventuallyWithT(
|
|
t, func(collect *assert.CollectT) {
|
|
tokens, err = tokenSource.TokenCtx(CTX)
|
|
if tt.wantErr {
|
|
assert.Error(collect, err)
|
|
return
|
|
}
|
|
assert.NoError(collect, err)
|
|
assert.NotNil(collect, tokens)
|
|
},
|
|
time.Minute, time.Second,
|
|
)
|
|
if tt.wantErr {
|
|
return
|
|
}
|
|
|
|
provider, err := rp.NewRelyingPartyOIDC(CTX, Instance.OIDCIssuer(), "", "", redirectURI, tt.scope)
|
|
require.NoError(t, err)
|
|
userinfo, err := rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, oidc.BearerToken, user.GetUserId(), provider)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.wantClaims.resourceOwnerID, userinfo.Claims[oidc_api.ClaimResourceOwnerID])
|
|
assert.Equal(t, tt.wantClaims.resourceOwnerName, userinfo.Claims[oidc_api.ClaimResourceOwnerName])
|
|
assert.Equal(t, tt.wantClaims.resourceOwnerPrimaryDomain, userinfo.Claims[oidc_api.ClaimResourceOwnerPrimaryDomain])
|
|
assert.Equal(t, tt.wantClaims.orgDomain, userinfo.Claims[domain.OrgDomainPrimaryClaim])
|
|
assert.Equal(t, tt.wantClaims.name, userinfo.Name)
|
|
assert.Equal(t, tt.wantClaims.username, userinfo.PreferredUsername)
|
|
assertOIDCTime(t, userinfo.UpdatedAt, tt.wantClaims.updated)
|
|
assert.Empty(t, userinfo.UserInfoProfile.FamilyName)
|
|
assert.Empty(t, userinfo.UserInfoProfile.GivenName)
|
|
assert.Empty(t, userinfo.UserInfoEmail)
|
|
assert.Empty(t, userinfo.UserInfoPhone)
|
|
assert.Empty(t, userinfo.Address)
|
|
|
|
_, err = Instance.Client.Auth.GetMyUser(integration.WithAuthorizationToken(CTX, tokens.AccessToken), &auth.GetMyUserRequest{})
|
|
if slices.Contains(tt.scope, domain.ProjectScopeZITADEL) {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|