Files
zitadel/internal/api/oidc/token_jwt_profile.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

105 lines
2.8 KiB
Go

package oidc
import (
"context"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
func (s *Server) JWTProfile(ctx context.Context, r *op.Request[oidc.JWTProfileGrantRequest]) (_ *op.Response, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() {
span.EndWithError(err)
err = oidcError(ctx, err)
}()
user, err := s.verifyJWTProfile(ctx, r.Data)
if err != nil {
return nil, err
}
client := &clientCredentialsClient{
clientID: user.Username,
userID: user.UserID,
resourceOwner: user.ResourceOwner,
tokenType: user.TokenType,
}
scope, err := op.ValidateAuthReqScopes(client, r.Data.Scope)
if err != nil {
return nil, err
}
scope, err = s.checkOrgScopes(ctx, client.resourceOwner, scope)
if err != nil {
return nil, err
}
session, err := s.command.CreateOIDCSession(ctx,
client.userID,
client.resourceOwner,
client.clientID,
"", // backChannelLogoutURI not needed for service account session
scope,
domain.AddAudScopeToAudience(ctx, nil, r.Data.Scope),
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePrivateKey},
time.Now(),
"",
nil,
nil,
domain.TokenReasonJWTProfile,
nil,
false,
"",
domain.OIDCResponseTypeUnspecified,
)
if err != nil {
return nil, err
}
return response(s.accessTokenResponseFromSession(ctx, client, session, "", "", false, true, true, false))
}
func (s *Server) verifyJWTProfile(ctx context.Context, req *oidc.JWTProfileGrantRequest) (_ *query.AuthNKeyUser, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
storage := &jwtProfileKeyStorage{query: s.query}
verifier := op.NewJWTProfileVerifier(
storage, op.IssuerFromContext(ctx),
time.Hour, time.Second,
)
_, err = op.VerifyJWTAssertion(ctx, req.Assertion, verifier)
if err != nil {
return nil, oidc.ErrInvalidGrant().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError).WithDescription("invalid assertion")
}
return storage.user, nil
}
type jwtProfileKeyStorage struct {
query *query.Queries
user *query.AuthNKeyUser // only populated after GetKeyByIDAndClientID is called
}
func (s *jwtProfileKeyStorage) GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (_ *jose.JSONWebKey, err error) {
s.user, err = s.query.GetAuthNKeyUser(ctx, keyID, userID)
if err != nil {
return nil, err
}
publicKey, err := crypto.BytesToPublicKey(s.user.PublicKey)
if err != nil {
return nil, err
}
return &jose.JSONWebKey{
KeyID: keyID,
Use: "sig",
Key: publicKey,
}, nil
}