zitadel/internal/api/oidc/token_jwt_profile.go
Tim Möhlmann 58a7eb1f26
perf(oidc): remove get user by ID from jwt profile grant (#8580)
# Which Problems Are Solved

Improve performance by removing a GetUserByID call. The call also
executed a Trigger on projections, which significantly impacted
concurrent requests.

# How the Problems Are Solved

Token creation needs information from the user, such as the resource
owner and access token type.

For client credentials this is solved in a single search. By getting the
user by username (`client_id`), the user details and secret were
obtained in a single query. After that verification and token creation
can proceed. For JWT profile it is a bit more complex. We didn't know
anything about the user until after JWT verification.
The verification did a query for the AuthN key and after that we did a
GetUserByID to get remaining details.

This change uses a joined query when the OIDC library calls the
`GetKeyByIDAndClientID` method on the token storage. The found user
details are set to the verifieer object and returned after verification
is completed.
It is safe because the `jwtProfileKeyStorage` is a single-use object as
a wrapper around `query.Queries`.
This way getting the public key and user details are obtained in a
single query.

# Additional Changes

- Correctly set the `client_id` field with machine's username.

# Additional Context

- Related to: https://github.com/zitadel/zitadel/issues/8352
2024-09-11 12:04:09 +03:00

102 lines
2.5 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/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(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,
scope,
domain.AddAudScopeToAudience(ctx, nil, r.Data.Scope),
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePrivateKey},
time.Now(),
"",
nil,
nil,
domain.TokenReasonJWTProfile,
nil,
false,
"",
)
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, err
}
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
}