mirror of
https://github.com/zitadel/zitadel.git
synced 2025-10-20 15:11:22 +00:00
concurrent token and client checks
This commit is contained in:
@@ -14,97 +14,202 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionRequest]) (_ *op.Response, err error) {
|
func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionRequest]) (_ *op.Response, err error) {
|
||||||
clientID, err := s.authenticateResourceClient(ctx, r.Data.ClientCredentials)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
if err != nil {
|
defer cancel()
|
||||||
|
|
||||||
|
clientChan := make(chan *instrospectionClientResult)
|
||||||
|
go s.instrospectionClientAuth(ctx, r.Data.ClientCredentials, clientChan)
|
||||||
|
|
||||||
|
tokenChan := make(chan *introspectionTokenResult)
|
||||||
|
go s.introspectionToken(ctx, r.Data.Token, tokenChan)
|
||||||
|
|
||||||
|
var (
|
||||||
|
client *instrospectionClientResult
|
||||||
|
token *introspectionTokenResult
|
||||||
|
)
|
||||||
|
|
||||||
|
// make sure both channels are always read,
|
||||||
|
// and cancel the context on first error
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
var resErr error
|
||||||
|
|
||||||
|
select {
|
||||||
|
case client = <-clientChan:
|
||||||
|
resErr = client.err
|
||||||
|
case token = <-tokenChan:
|
||||||
|
resErr = token.err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resErr == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// we only care for the first error that occured,
|
||||||
|
// as the next error is most probably a context error.
|
||||||
|
if err == nil {
|
||||||
|
err = resErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only client auth errors should be returned
|
||||||
|
var target *oidc.Error
|
||||||
|
if errors.As(err, &target) && target.ErrorType == oidc.UnauthorizedClient {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// all other errors should result in a response with active: false.
|
||||||
response := new(oidc.IntrospectionResponse)
|
response := new(oidc.IntrospectionResponse)
|
||||||
tokenID, subject, err := s.getTokenIDAndSubject(ctx, r.Data.Token)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: log error
|
// TODO: log error
|
||||||
return op.NewResponse(response), nil
|
return op.NewResponse(response), nil
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(tokenID, command.IDPrefixV2) {
|
if err = validateIntrospectionAudience(token.audience, client.clientID, client.projectID); err != nil {
|
||||||
err = s.introspect(ctx, response, tokenID, subject, clientID)
|
// TODO: log error
|
||||||
return op.NewResponse(response), nil
|
return op.NewResponse(response), nil
|
||||||
}
|
}
|
||||||
|
userInfo, err := s.storage.query.GetOIDCUserinfo(ctx, token.userID, token.scope, []string{client.projectID})
|
||||||
err = s.storage.SetIntrospectionFromToken(ctx, response, tokenID, subject, clientID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// TODO: log error
|
||||||
return op.NewResponse(response), nil
|
return op.NewResponse(response), nil
|
||||||
}
|
}
|
||||||
|
response.SetUserInfo(userinfoToOIDC(userInfo, token.scope))
|
||||||
|
response.Scope = token.scope
|
||||||
|
response.ClientID = token.clientID
|
||||||
|
response.TokenType = oidc.BearerToken
|
||||||
|
response.Expiration = oidc.FromTime(token.tokenExpiration)
|
||||||
|
response.IssuedAt = oidc.FromTime(token.tokenCreation)
|
||||||
|
response.NotBefore = oidc.FromTime(token.tokenCreation)
|
||||||
|
response.Audience = token.audience
|
||||||
|
response.Issuer = op.IssuerFromContext(ctx)
|
||||||
|
response.JWTID = token.tokenID
|
||||||
response.Active = true
|
response.Active = true
|
||||||
return op.NewResponse(response), nil
|
return op.NewResponse(response), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) authenticateResourceClient(ctx context.Context, cc *op.ClientCredentials) (clientID string, err error) {
|
type instrospectionClientResult struct {
|
||||||
|
clientID string
|
||||||
|
projectID string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) instrospectionClientAuth(ctx context.Context, cc *op.ClientCredentials, rc chan<- *instrospectionClientResult) {
|
||||||
|
clientID := cc.ClientID
|
||||||
|
|
||||||
if cc.ClientAssertion != "" {
|
if cc.ClientAssertion != "" {
|
||||||
verifier := op.NewJWTProfileVerifier(s.storage, op.IssuerFromContext(ctx), 1*time.Hour, time.Second)
|
verifier := op.NewJWTProfileVerifier(s.storage, op.IssuerFromContext(ctx), 1*time.Hour, time.Second)
|
||||||
profile, err := op.VerifyJWTAssertion(ctx, cc.ClientAssertion, verifier)
|
profile, err := op.VerifyJWTAssertion(ctx, cc.ClientAssertion, verifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
rc <- &instrospectionClientResult{
|
||||||
|
err: oidc.ErrUnauthorizedClient().WithParent(err),
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return profile.Issuer, nil
|
clientID = profile.Issuer
|
||||||
}
|
} else {
|
||||||
|
if err := s.storage.AuthorizeClientIDSecret(ctx, cc.ClientID, cc.ClientSecret); err != nil {
|
||||||
if err = s.storage.AuthorizeClientIDSecret(ctx, cc.ClientID, cc.ClientSecret); err != nil {
|
if err != nil {
|
||||||
if err != nil {
|
rc <- &instrospectionClientResult{
|
||||||
return "", err
|
err: oidc.ErrUnauthorizedClient().WithParent(err),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return cc.ClientID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) getTokenIDAndSubject(ctx context.Context, accessToken string) (idToken, subject string, err error) {
|
|
||||||
provider := s.Provider()
|
|
||||||
tokenIDSubject, err := provider.Crypto().Decrypt(accessToken)
|
|
||||||
if err == nil {
|
|
||||||
splitToken := strings.Split(tokenIDSubject, ":")
|
|
||||||
if len(splitToken) != 2 {
|
|
||||||
return "", "", errors.New("invalid token format")
|
|
||||||
}
|
|
||||||
return splitToken[0], splitToken[1], nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
verifier := op.NewAccessTokenVerifier(op.IssuerFromContext(ctx), s.storage.keySet)
|
|
||||||
accessTokenClaims, err := op.VerifyAccessToken[*oidc.AccessTokenClaims](ctx, accessToken, verifier)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
return accessTokenClaims.JWTID, accessTokenClaims.Subject, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) introspect(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) (err error) {
|
|
||||||
// TODO: give clients their own aggregate, so we can skip this query
|
// TODO: give clients their own aggregate, so we can skip this query
|
||||||
projectID, err := s.storage.query.ProjectIDFromClientID(ctx, clientID, false)
|
projectID, err := s.storage.query.ProjectIDFromClientID(ctx, clientID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errz.ThrowPermissionDenied(nil, "OIDC-Adfg5", "client not found")
|
rc <- &instrospectionClientResult{err: err}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := s.storage.query.ActiveAccessTokenByToken(ctx, tokenID)
|
rc <- &instrospectionClientResult{
|
||||||
if err != nil {
|
clientID: clientID,
|
||||||
return err
|
projectID: projectID,
|
||||||
}
|
}
|
||||||
if !slices.ContainsFunc(token.Audience, func(aud string) bool {
|
}
|
||||||
return aud == token.ClientID || aud == projectID
|
|
||||||
}) {
|
type introspectionTokenResult struct {
|
||||||
return errz.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
|
tokenID string
|
||||||
}
|
userID string
|
||||||
|
subject string
|
||||||
userInfo, err := s.storage.query.GetOIDCUserinfo(ctx, token.UserID, token.Scope, []string{projectID})
|
clientID string
|
||||||
if err != nil {
|
audience []string
|
||||||
return err
|
scope []string
|
||||||
}
|
tokenCreation time.Time
|
||||||
introspection.SetUserInfo(userinfoToOIDC(userInfo, token.Scope))
|
tokenExpiration time.Time
|
||||||
introspection.Scope = token.Scope
|
isPAT bool
|
||||||
introspection.ClientID = token.ClientID
|
|
||||||
introspection.TokenType = oidc.BearerToken
|
err error
|
||||||
introspection.Expiration = oidc.FromTime(token.AccessTokenExpiration)
|
}
|
||||||
introspection.IssuedAt = oidc.FromTime(token.AccessTokenCreation)
|
|
||||||
introspection.NotBefore = oidc.FromTime(token.AccessTokenCreation)
|
func (s *Server) introspectionToken(ctx context.Context, accessToken string, rc chan<- *introspectionTokenResult) {
|
||||||
introspection.Audience = token.Audience
|
var tokenID, subject string
|
||||||
introspection.Issuer = op.IssuerFromContext(ctx)
|
|
||||||
introspection.JWTID = tokenID
|
if tokenIDSubject, err := s.Provider().Crypto().Decrypt(accessToken); err == nil {
|
||||||
|
split := strings.Split(tokenIDSubject, ":")
|
||||||
return nil
|
if len(split) != 2 {
|
||||||
|
rc <- &introspectionTokenResult{err: errors.New("invalid token format")}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tokenID, subject = split[0], split[1]
|
||||||
|
} else {
|
||||||
|
verifier := op.NewAccessTokenVerifier(op.IssuerFromContext(ctx), s.storage.keySet)
|
||||||
|
claims, err := op.VerifyAccessToken[*oidc.AccessTokenClaims](ctx, accessToken, verifier)
|
||||||
|
if err != nil {
|
||||||
|
rc <- &introspectionTokenResult{err: err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tokenID, subject = claims.JWTID, claims.Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(tokenID, command.IDPrefixV2) {
|
||||||
|
token, err := s.storage.query.ActiveAccessTokenByToken(ctx, tokenID)
|
||||||
|
if err != nil {
|
||||||
|
rc <- &introspectionTokenResult{err: err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rc <- &introspectionTokenResult{
|
||||||
|
tokenID: tokenID,
|
||||||
|
userID: token.UserID,
|
||||||
|
subject: subject,
|
||||||
|
clientID: token.ClientID,
|
||||||
|
audience: token.Audience,
|
||||||
|
scope: token.Scope,
|
||||||
|
tokenCreation: token.AccessTokenCreation,
|
||||||
|
tokenExpiration: token.AccessTokenExpiration,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := s.storage.repo.TokenByIDs(ctx, subject, tokenID)
|
||||||
|
if err != nil {
|
||||||
|
rc <- &introspectionTokenResult{
|
||||||
|
err: errz.ThrowPermissionDenied(err, "OIDC-Dsfb2", "token is not valid or has expired"),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rc <- &introspectionTokenResult{
|
||||||
|
tokenID: tokenID,
|
||||||
|
userID: token.UserID,
|
||||||
|
subject: subject,
|
||||||
|
clientID: token.ApplicationID, // check correctness?
|
||||||
|
audience: token.Audience,
|
||||||
|
scope: token.Scopes,
|
||||||
|
tokenCreation: token.CreationDate,
|
||||||
|
tokenExpiration: token.Expiration,
|
||||||
|
isPAT: token.IsPAT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateIntrospectionAudience(audience []string, clientID, projectID string) error {
|
||||||
|
if slices.ContainsFunc(audience, func(entry string) bool {
|
||||||
|
return entry == clientID || entry == projectID
|
||||||
|
}) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errz.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user