zitadel/internal/api/oidc/introspect.go

233 lines
6.9 KiB
Go
Raw Normal View History

2023-11-02 17:27:30 +02:00
package oidc
import (
"context"
2023-11-05 13:18:17 +02:00
"database/sql"
2023-11-02 17:27:30 +02:00
"errors"
"slices"
"strings"
"time"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/zitadel/internal/command"
2023-11-05 13:18:17 +02:00
"github.com/zitadel/zitadel/internal/crypto"
2023-11-02 17:27:30 +02:00
errz "github.com/zitadel/zitadel/internal/errors"
2023-11-05 13:18:17 +02:00
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
2023-11-02 17:27:30 +02:00
)
func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionRequest]) (_ *op.Response, err error) {
2023-11-03 17:17:49 +02:00
ctx, cancel := context.WithCancel(ctx)
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 {
2023-11-02 17:27:30 +02:00
return nil, err
}
2023-11-03 17:17:49 +02:00
// all other errors should result in a response with active: false.
2023-11-02 17:27:30 +02:00
response := new(oidc.IntrospectionResponse)
if err != nil {
// TODO: log error
return op.NewResponse(response), nil
}
2023-11-03 17:17:49 +02:00
if err = validateIntrospectionAudience(token.audience, client.clientID, client.projectID); err != nil {
// TODO: log error
2023-11-02 17:27:30 +02:00
return op.NewResponse(response), nil
}
2023-11-03 17:17:49 +02:00
userInfo, err := s.storage.query.GetOIDCUserinfo(ctx, token.userID, token.scope, []string{client.projectID})
2023-11-02 17:27:30 +02:00
if err != nil {
2023-11-03 17:17:49 +02:00
// TODO: log error
2023-11-02 17:27:30 +02:00
return op.NewResponse(response), nil
}
2023-11-03 17:17:49 +02:00
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
2023-11-02 17:27:30 +02:00
response.Active = true
return op.NewResponse(response), nil
}
2023-11-03 17:17:49 +02:00
type instrospectionClientResult struct {
clientID string
projectID string
err error
}
func (s *Server) instrospectionClientAuth(ctx context.Context, cc *op.ClientCredentials, rc chan<- *instrospectionClientResult) {
2023-11-05 13:18:17 +02:00
ctx, span := tracing.NewSpan(ctx)
2023-11-03 17:17:49 +02:00
2023-11-05 13:18:17 +02:00
clientID, projectID, err := func() (string, string, error) {
client, err := s.clientFromCredentials(ctx, cc)
2023-11-02 17:27:30 +02:00
if err != nil {
2023-11-05 13:18:17 +02:00
return "", "", err
2023-11-03 17:17:49 +02:00
}
2023-11-05 13:18:17 +02:00
if cc.ClientAssertion != "" {
verifier := op.NewJWTProfileVerifierKeySet(keySetMap(client.PublicKeys), op.IssuerFromContext(ctx), time.Hour, time.Second)
if _, err := op.VerifyJWTAssertion(ctx, cc.ClientAssertion, verifier); err != nil {
return "", "", oidc.ErrUnauthorizedClient().WithParent(err)
}
} else {
if err := crypto.CompareHash(client.ClientSecret, []byte(cc.ClientSecret), s.hashAlg); err != nil {
return "", "", oidc.ErrUnauthorizedClient().WithParent(err)
2023-11-03 17:17:49 +02:00
}
2023-11-02 17:27:30 +02:00
}
2023-11-03 17:17:49 +02:00
2023-11-05 13:18:17 +02:00
return client.ClientID, client.ProjectID, nil
}()
2023-11-02 17:27:30 +02:00
2023-11-05 13:18:17 +02:00
span.EndWithError(err)
2023-11-03 17:17:49 +02:00
rc <- &instrospectionClientResult{
clientID: clientID,
projectID: projectID,
2023-11-05 13:18:17 +02:00
err: err,
}
}
// clientFromCredentials parses the client ID early,
// and makes a single query for the client for either auth methods.
func (s *Server) clientFromCredentials(ctx context.Context, cc *op.ClientCredentials) (client *query.IntrospectionClient, err error) {
if cc.ClientAssertion != "" {
claims := new(oidc.JWTTokenRequest)
if _, err := oidc.ParseToken(cc.ClientAssertion, claims); err != nil {
return nil, oidc.ErrUnauthorizedClient().WithParent(err)
}
client, err = s.storage.query.GetIntrospectionClientByID(ctx, claims.Issuer, true)
} else {
client, err = s.storage.query.GetIntrospectionClientByID(ctx, cc.ClientID, false)
}
if errors.Is(err, sql.ErrNoRows) {
return nil, oidc.ErrUnauthorizedClient().WithParent(err)
2023-11-02 17:27:30 +02:00
}
2023-11-05 13:18:17 +02:00
// any other error is regarded internal and should not be reported back to the client.
return client, err
2023-11-02 17:27:30 +02:00
}
2023-11-03 17:17:49 +02:00
type introspectionTokenResult struct {
tokenID string
userID string
subject string
clientID string
audience []string
scope []string
tokenCreation time.Time
tokenExpiration time.Time
isPAT bool
err error
}
func (s *Server) introspectionToken(ctx context.Context, accessToken string, rc chan<- *introspectionTokenResult) {
var tokenID, subject string
if tokenIDSubject, err := s.Provider().Crypto().Decrypt(accessToken); err == nil {
split := strings.Split(tokenIDSubject, ":")
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
2023-11-02 17:27:30 +02:00
}
2023-11-03 17:17:49 +02:00
tokenID, subject = claims.JWTID, claims.Subject
2023-11-02 17:27:30 +02:00
}
2023-11-03 17:17:49 +02:00
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
2023-11-02 17:27:30 +02:00
}
2023-11-03 17:17:49 +02:00
token, err := s.storage.repo.TokenByIDs(ctx, subject, tokenID)
2023-11-02 17:27:30 +02:00
if err != nil {
2023-11-03 17:17:49 +02:00
rc <- &introspectionTokenResult{
err: errz.ThrowPermissionDenied(err, "OIDC-Dsfb2", "token is not valid or has expired"),
}
return
2023-11-02 17:27:30 +02:00
}
2023-11-03 17:17:49 +02:00
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,
2023-11-02 17:27:30 +02:00
}
2023-11-03 17:17:49 +02:00
}
func validateIntrospectionAudience(audience []string, clientID, projectID string) error {
if slices.ContainsFunc(audience, func(entry string) bool {
return entry == clientID || entry == projectID
2023-11-02 17:27:30 +02:00
}) {
2023-11-03 17:17:49 +02:00
return nil
2023-11-02 17:27:30 +02:00
}
2023-11-03 17:17:49 +02:00
return errz.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
2023-11-02 17:27:30 +02:00
}