zitadel/internal/api/oidc/access_token.go
Tim Möhlmann 6398349c24
feat(oidc): token exchange impersonation (#7516)
* add token exchange feature flag

* allow setting reason and actor to access tokens

* impersonation

* set token types and scopes in response

* upgrade oidc to working draft state

* fix tests

* audience and scope validation

* id toke and jwt as input

* return id tokens

* add grant type  token exchange to app config

* add integration tests

* check and deny actors in api calls

* fix instance setting tests by triggering projection on write and cleanup

* insert sleep statements again

* solve linting issues

* add translations

* pin oidc v3.15.0

* resolve comments, add event translation

* fix refreshtoken test

* use ValidateAuthReqScopes from oidc

* apparently the linter can't make up its mind

* persist actor thru refresh tokens and check in tests

* remove unneeded triggers
2024-03-20 10:18:46 +00:00

119 lines
3.8 KiB
Go

package oidc
import (
"context"
"errors"
"strings"
"time"
"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/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/user/model"
"github.com/zitadel/zitadel/internal/zerrors"
)
type accessToken struct {
tokenID string
userID string
resourceOwner string
subject string
clientID string
audience []string
scope []string
authMethods []domain.UserAuthMethodType
authTime time.Time
tokenCreation time.Time
tokenExpiration time.Time
isPAT bool
actor *domain.TokenActor
}
var ErrInvalidTokenFormat = errors.New("invalid token format")
func (s *Server) verifyAccessToken(ctx context.Context, tkn string) (*accessToken, error) {
var tokenID, subject string
if tokenIDSubject, err := s.Provider().Crypto().Decrypt(tkn); err == nil {
split := strings.Split(tokenIDSubject, ":")
if len(split) != 2 {
return nil, zerrors.ThrowPermissionDenied(ErrInvalidTokenFormat, "OIDC-rei1O", "token is not valid or has expired")
}
tokenID, subject = split[0], split[1]
} else {
verifier := op.NewAccessTokenVerifier(op.IssuerFromContext(ctx), s.accessTokenKeySet)
claims, err := op.VerifyAccessToken[*oidc.AccessTokenClaims](ctx, tkn, verifier)
if err != nil {
return nil, zerrors.ThrowPermissionDenied(err, "OIDC-Eib8e", "token is not valid or has expired")
}
tokenID, subject = claims.JWTID, claims.Subject
}
if strings.HasPrefix(tokenID, command.IDPrefixV2) {
token, err := s.query.ActiveAccessTokenByToken(ctx, tokenID)
if err != nil {
return nil, err
}
return accessTokenV2(tokenID, subject, token), nil
}
token, err := s.repo.TokenByIDs(ctx, subject, tokenID)
if err != nil {
return nil, zerrors.ThrowPermissionDenied(err, "OIDC-Dsfb2", "token is not valid or has expired")
}
return accessTokenV1(tokenID, subject, token), nil
}
func accessTokenV1(tokenID, subject string, token *model.TokenView) *accessToken {
return &accessToken{
tokenID: tokenID,
userID: token.UserID,
resourceOwner: token.ResourceOwner,
subject: subject,
clientID: token.ApplicationID,
audience: token.Audience,
scope: token.Scopes,
tokenCreation: token.CreationDate,
tokenExpiration: token.Expiration,
isPAT: token.IsPAT,
actor: token.Actor,
}
}
func accessTokenV2(tokenID, subject string, token *query.OIDCSessionAccessTokenReadModel) *accessToken {
return &accessToken{
tokenID: tokenID,
userID: token.UserID,
resourceOwner: token.ResourceOwner,
subject: subject,
clientID: token.ClientID,
audience: token.Audience,
scope: token.Scope,
authMethods: token.AuthMethods,
authTime: token.AuthTime,
tokenCreation: token.AccessTokenCreation,
tokenExpiration: token.AccessTokenExpiration,
actor: token.Actor,
}
}
func (s *Server) assertClientScopesForPAT(ctx context.Context, token *accessToken, clientID, projectID string) error {
token.audience = append(token.audience, clientID, projectID)
projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(projectID)
if err != nil {
return zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal")
}
roles, err := s.query.SearchProjectRoles(ctx, authz.GetFeatures(ctx).TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}})
if err != nil {
return err
}
for _, role := range roles.ProjectRoles {
token.scope = append(token.scope, ScopeProjectRolePrefix+role.Key)
}
return nil
}