feat(API): support V2 token and session token usage (#6180)

This PR adds support for userinfo and introspection of V2 tokens. Further V2 access tokens and session tokens can be used for authentication on the ZITADEL API (like the current access tokens).
This commit is contained in:
Livio Spring
2023-07-14 13:16:16 +02:00
committed by GitHub
parent 4589ddad4a
commit 80961125a7
38 changed files with 1309 additions and 181 deletions

View File

@@ -15,7 +15,9 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/authz/repository/eventsourcing/view"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@@ -28,7 +30,6 @@ import (
type TokenVerifierRepo struct {
TokenVerificationKey crypto.EncryptionAlgorithm
IAMID string
Eventstore v1.Eventstore
View *view.View
Query *query.Queries
@@ -92,6 +93,21 @@ func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenStrin
if !ok {
return "", "", "", "", "", caos_errs.ThrowUnauthenticated(nil, "APP-Reb32", "invalid token")
}
if strings.HasPrefix(tokenID, command.IDPrefixV2) {
userID, clientID, resourceOwner, err = repo.verifyAccessTokenV2(ctx, tokenID, verifierClientID, projectID)
return
}
if sessionID, ok := strings.CutPrefix(tokenID, authz.SessionTokenPrefix); ok {
userID, clientID, resourceOwner, err = repo.verifySessionToken(ctx, sessionID, tokenString)
return
}
return repo.verifyAccessTokenV1(ctx, tokenID, subject, verifierClientID, projectID)
}
func (repo *TokenVerifierRepo) verifyAccessTokenV1(ctx context.Context, tokenID, subject, verifierClientID, projectID string) (userID string, agentID string, clientID, prefLang, resourceOwner string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
_, tokenSpan := tracing.NewNamedSpan(ctx, "token")
token, err := repo.tokenByID(ctx, tokenID, subject)
tokenSpan.EndWithError(err)
@@ -104,12 +120,89 @@ func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenStrin
if token.IsPAT {
return token.UserID, "", "", "", token.ResourceOwner, nil
}
for _, aud := range token.Audience {
if verifierClientID == aud || projectID == aud {
return token.UserID, token.UserAgentID, token.ApplicationID, token.PreferredLanguage, token.ResourceOwner, nil
}
if err = verifyAudience(token.Audience, verifierClientID, projectID); err != nil {
return "", "", "", "", "", err
}
return "", "", "", "", "", caos_errs.ThrowUnauthenticated(nil, "APP-Zxfako", "invalid audience")
return token.UserID, token.UserAgentID, token.ApplicationID, token.PreferredLanguage, token.ResourceOwner, nil
}
func (repo *TokenVerifierRepo) verifyAccessTokenV2(ctx context.Context, token, verifierClientID, projectID string) (userID, clientID, resourceOwner string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
activeToken, err := repo.Query.ActiveAccessTokenByToken(ctx, token)
if err != nil {
return "", "", "", err
}
if err = verifyAudience(activeToken.Audience, verifierClientID, projectID); err != nil {
return "", "", "", err
}
if err = repo.checkAuthentication(ctx, activeToken.AuthMethods, activeToken.UserID); err != nil {
return "", "", "", err
}
return activeToken.UserID, activeToken.ClientID, activeToken.ResourceOwner, nil
}
func (repo *TokenVerifierRepo) verifySessionToken(ctx context.Context, sessionID, token string) (userID, clientID, resourceOwner string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
session, err := repo.Query.SessionByID(ctx, false, sessionID, token)
if err != nil {
return "", "", "", err
}
if err = repo.checkAuthentication(ctx, authMethodsFromSession(session), session.UserFactor.UserID); err != nil {
return "", "", "", err
}
return session.UserFactor.UserID, "", session.UserFactor.ResourceOwner, nil
}
// checkAuthentication ensures the session or token was authenticated (at least a single [domain.UserAuthMethodType]).
// It will also check if there was a multi factor authentication, if either MFA is forced by the login policy or if the user has set up any
func (repo *TokenVerifierRepo) checkAuthentication(ctx context.Context, authMethods []domain.UserAuthMethodType, userID string) error {
if len(authMethods) == 0 {
return caos_errs.ThrowPermissionDenied(nil, "AUTHZ-Kl3p0", "authentication required")
}
if domain.HasMFA(authMethods) {
return nil
}
availableAuthMethods, forceMFA, err := repo.Query.ListUserAuthMethodTypesRequired(setCallerCtx(ctx, userID), userID, false)
if err != nil {
return err
}
if forceMFA || domain.HasMFA(availableAuthMethods) {
return caos_errs.ThrowPermissionDenied(nil, "AUTHZ-Kl3p0", "mfa required")
}
return nil
}
func authMethodsFromSession(session *query.Session) []domain.UserAuthMethodType {
types := make([]domain.UserAuthMethodType, 0, domain.UserAuthMethodTypeIDP)
if !session.PasswordFactor.PasswordCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypePassword)
}
if !session.PasskeyFactor.PasskeyCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypePasswordless)
}
if !session.IntentFactor.IntentCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeIDP)
}
// TODO: add checks with https://github.com/zitadel/zitadel/issues/5477
/*
if !session.TOTPFactor.TOTPCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeOTP)
}
if !session.U2FFactor.U2FCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeU2F)
}
*/
return types
}
func setCallerCtx(ctx context.Context, userID string) context.Context {
ctxData := authz.GetCtxData(ctx)
ctxData.UserID = userID
return authz.SetCtxData(ctx, ctxData)
}
func (repo *TokenVerifierRepo) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error) {
@@ -184,6 +277,15 @@ func (repo *TokenVerifierRepo) decryptAccessToken(token string) (string, error)
return tokenIDSubject, nil
}
func verifyAudience(audience []string, verifierClientID, projectID string) error {
for _, aud := range audience {
if verifierClientID == aud || projectID == aud {
return nil
}
}
return caos_errs.ThrowUnauthenticated(nil, "APP-Zxfako", "invalid audience")
}
type openIDKeySet struct {
*query.Queries
}