Files
zitadel/internal/api/oidc/integration_test/token_jwt_profile_test.go
T
5344a7f584 fix: invalid jwt assertion error handling (#11933)
# Which Problems Are Solved

Incorrectly mapped errors on the `/oauth/v2/token` endpoint lead to a
large number of HTTP status 500 errors.

# How the Problems Are Solved

By mapping invalid jwt assertion errors to OIDC `invalid_client` errors,
which returns a HTTP status 400.

# Additional Changes

N/A

# Additional Context

- Closes https://github.com/zitadel/zitadel/issues/11924
- Follow-up: 
- Fix the [error
handling](https://github.com/zitadel/oidc/blob/main/pkg/oidc/verifier.go#L195)
in the oidc package

---------

Co-authored-by: Copilot <[email protected]>
2026-04-01 10:44:02 +02:00

178 lines
5.6 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)
project := Instance.CreateProject(CTX, t, "", integration.ProjectName(), false, false)
_, appKeyData, err := Instance.CreateOIDCWebClientJWT(CTX, redirectURI, logoutRedirectURI, project.GetId())
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
expectedErr error
}{
{
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,
expectedErr: oidc.ErrInvalidGrant().WithDescription("invalid assertion"),
},
{
name: "key type application",
keyData: appKeyData,
scope: []string{oidc.ScopeOpenID},
wantErr: true,
expectedErr: oidc.ErrInvalidGrant().WithDescription("invalid assertion"),
},
}
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 {
assert.ErrorIs(t, err, tt.expectedErr)
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)
}
})
}
}