2023-11-21 12:11:38 +00:00
|
|
|
package oidc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"database/sql"
|
|
|
|
"errors"
|
|
|
|
"slices"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
|
|
|
"github.com/zitadel/oidc/v3/pkg/op"
|
|
|
|
|
2024-02-28 08:55:54 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
2024-05-31 10:10:18 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/domain"
|
2023-11-21 12:11:38 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/query"
|
|
|
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
2023-12-08 14:30:55 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
2023-11-21 12:11:38 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionRequest]) (resp *op.Response, err error) {
|
|
|
|
ctx, span := tracing.NewSpan(ctx)
|
2024-01-18 06:10:49 +00:00
|
|
|
defer func() {
|
|
|
|
err = oidcError(err)
|
|
|
|
span.EndWithError(err)
|
|
|
|
}()
|
2023-11-21 12:11:38 +00:00
|
|
|
|
2024-02-28 08:55:54 +00:00
|
|
|
features := authz.GetFeatures(ctx)
|
|
|
|
if features.LegacyIntrospection {
|
2023-11-21 12:11:38 +00:00
|
|
|
return s.LegacyServer.Introspect(ctx, r)
|
|
|
|
}
|
2024-02-28 08:55:54 +00:00
|
|
|
if features.TriggerIntrospectionProjections {
|
2023-11-21 12:11:38 +00:00
|
|
|
query.TriggerIntrospectionProjections(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
defer cancel()
|
|
|
|
|
2023-12-05 17:01:03 +00:00
|
|
|
clientChan := make(chan *introspectionClientResult)
|
|
|
|
go s.introspectionClientAuth(ctx, r.Data.ClientCredentials, clientChan)
|
2023-11-21 12:11:38 +00:00
|
|
|
|
|
|
|
tokenChan := make(chan *introspectionTokenResult)
|
|
|
|
go s.introspectionToken(ctx, r.Data.Token, tokenChan)
|
|
|
|
|
|
|
|
var (
|
2023-12-05 17:01:03 +00:00
|
|
|
client *introspectionClientResult
|
2023-11-21 12:11:38 +00:00
|
|
|
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
|
2024-06-17 09:09:00 +00:00
|
|
|
if resErr != nil {
|
|
|
|
// we prioritize the client error over the token error
|
|
|
|
err = resErr
|
|
|
|
cancel()
|
|
|
|
}
|
2023-11-21 12:11:38 +00:00
|
|
|
case token = <-tokenChan:
|
|
|
|
resErr = token.err
|
2024-06-17 09:09:00 +00:00
|
|
|
if resErr == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// we prioritize the client error over the token error
|
|
|
|
if err == nil {
|
|
|
|
err = resErr
|
|
|
|
}
|
2023-11-21 12:11:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// only client auth errors should be returned
|
|
|
|
var target *oidc.Error
|
|
|
|
if errors.As(err, &target) && target.ErrorType == oidc.UnauthorizedClient {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-12-28 13:31:41 +00:00
|
|
|
// remaining errors shouldn't be returned to the client,
|
2023-11-21 12:11:38 +00:00
|
|
|
// so we catch errors here, log them and return the response
|
|
|
|
// with active: false
|
|
|
|
defer func() {
|
|
|
|
if err != nil {
|
2024-05-31 08:11:32 +00:00
|
|
|
if zerrors.IsInternal(err) {
|
|
|
|
s.getLogger(ctx).ErrorContext(ctx, "oidc introspection", "err", err)
|
|
|
|
} else {
|
|
|
|
s.getLogger(ctx).InfoContext(ctx, "oidc introspection", "err", err)
|
|
|
|
}
|
2023-11-21 12:11:38 +00:00
|
|
|
resp, err = op.NewResponse(new(oidc.IntrospectionResponse)), nil
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: can we get rid of this separate query?
|
|
|
|
if token.isPAT {
|
|
|
|
if err = s.assertClientScopesForPAT(ctx, token.accessToken, client.clientID, client.projectID); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = validateIntrospectionAudience(token.audience, client.clientID, client.projectID); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-05-31 10:10:18 +00:00
|
|
|
userInfo, err := s.userInfo(
|
|
|
|
token.userID,
|
|
|
|
token.scope,
|
|
|
|
client.projectID,
|
|
|
|
client.projectRoleAssertion,
|
|
|
|
true,
|
|
|
|
true,
|
|
|
|
)(ctx, true, domain.TriggerTypePreUserinfoCreation)
|
2023-11-21 12:11:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
introspectionResp := &oidc.IntrospectionResponse{
|
2024-03-20 10:18:46 +00:00
|
|
|
Active: true,
|
|
|
|
Scope: token.scope,
|
|
|
|
ClientID: token.clientID,
|
|
|
|
TokenType: oidc.BearerToken,
|
|
|
|
Expiration: oidc.FromTime(token.tokenExpiration),
|
|
|
|
IssuedAt: oidc.FromTime(token.tokenCreation),
|
|
|
|
AuthTime: oidc.FromTime(token.authTime),
|
|
|
|
NotBefore: oidc.FromTime(token.tokenCreation),
|
|
|
|
Audience: token.audience,
|
|
|
|
AuthenticationMethodsReferences: AuthMethodTypesToAMR(token.authMethods),
|
|
|
|
Issuer: op.IssuerFromContext(ctx),
|
|
|
|
JWTID: token.tokenID,
|
|
|
|
Actor: actorDomainToClaims(token.actor),
|
2023-11-21 12:11:38 +00:00
|
|
|
}
|
|
|
|
introspectionResp.SetUserInfo(userInfo)
|
|
|
|
return op.NewResponse(introspectionResp), nil
|
|
|
|
}
|
|
|
|
|
2023-12-05 17:01:03 +00:00
|
|
|
type introspectionClientResult struct {
|
2024-04-09 13:15:35 +00:00
|
|
|
clientID string
|
|
|
|
projectID string
|
|
|
|
projectRoleAssertion bool
|
|
|
|
err error
|
2023-11-21 12:11:38 +00:00
|
|
|
}
|
|
|
|
|
2023-12-28 13:31:41 +00:00
|
|
|
var errNoClientSecret = errors.New("client has no configured secret")
|
|
|
|
|
2023-12-05 17:01:03 +00:00
|
|
|
func (s *Server) introspectionClientAuth(ctx context.Context, cc *op.ClientCredentials, rc chan<- *introspectionClientResult) {
|
2023-11-21 12:11:38 +00:00
|
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
|
|
|
2024-04-09 13:15:35 +00:00
|
|
|
clientID, projectID, projectRoleAssertion, err := func() (string, string, bool, error) {
|
2023-11-21 12:11:38 +00:00
|
|
|
client, err := s.clientFromCredentials(ctx, cc)
|
|
|
|
if err != nil {
|
2024-04-09 13:15:35 +00:00
|
|
|
return "", "", false, err
|
2023-11-21 12:11:38 +00: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 {
|
2024-08-20 06:45:24 +00:00
|
|
|
return "", "", false, oidc.ErrUnauthorizedClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError)
|
2023-11-21 12:11:38 +00:00
|
|
|
}
|
2024-04-09 13:15:35 +00:00
|
|
|
return client.ClientID, client.ProjectID, client.ProjectRoleAssertion, nil
|
2023-12-28 13:31:41 +00:00
|
|
|
|
|
|
|
}
|
2024-04-05 09:35:49 +00:00
|
|
|
if client.HashedSecret != "" {
|
|
|
|
if err := s.introspectionClientSecretAuth(ctx, client, cc.ClientSecret); err != nil {
|
2024-08-20 06:45:24 +00:00
|
|
|
return "", "", false, oidc.ErrUnauthorizedClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError)
|
2023-11-21 12:11:38 +00:00
|
|
|
}
|
2024-04-09 13:15:35 +00:00
|
|
|
return client.ClientID, client.ProjectID, client.ProjectRoleAssertion, nil
|
2023-11-21 12:11:38 +00:00
|
|
|
}
|
2024-08-20 06:45:24 +00:00
|
|
|
return "", "", false, oidc.ErrUnauthorizedClient().WithParent(errNoClientSecret).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError)
|
2023-11-21 12:11:38 +00:00
|
|
|
}()
|
|
|
|
|
|
|
|
span.EndWithError(err)
|
|
|
|
|
2023-12-05 17:01:03 +00:00
|
|
|
rc <- &introspectionClientResult{
|
2024-04-09 13:15:35 +00:00
|
|
|
clientID: clientID,
|
|
|
|
projectID: projectID,
|
|
|
|
projectRoleAssertion: projectRoleAssertion,
|
|
|
|
err: err,
|
2023-11-21 12:11:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-05 09:35:49 +00:00
|
|
|
var errNoAppType = errors.New("introspection client without app type")
|
|
|
|
|
|
|
|
func (s *Server) introspectionClientSecretAuth(ctx context.Context, client *query.IntrospectionClient, secret string) error {
|
2024-08-28 18:19:50 +00:00
|
|
|
var updateCommand func(ctx context.Context, appID, projectID, resourceOwner, updated string)
|
|
|
|
|
2024-04-05 09:35:49 +00:00
|
|
|
switch client.AppType {
|
|
|
|
case query.AppTypeAPI:
|
2024-08-28 18:19:50 +00:00
|
|
|
updateCommand = s.command.APIUpdateSecret
|
2024-04-05 09:35:49 +00:00
|
|
|
case query.AppTypeOIDC:
|
2024-08-28 18:19:50 +00:00
|
|
|
updateCommand = s.command.OIDCUpdateSecret
|
2024-04-05 09:35:49 +00:00
|
|
|
default:
|
|
|
|
return zerrors.ThrowInternal(errNoAppType, "OIDC-ooD5Ot", "Errors.Internal")
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "passwap.Verify")
|
|
|
|
updated, err := s.hasher.Verify(client.HashedSecret, secret)
|
|
|
|
spanPasswordComparison.EndWithError(err)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-08-28 18:19:50 +00:00
|
|
|
if updated != "" {
|
|
|
|
updateCommand(ctx, client.AppID, client.ProjectID, client.ResourceOwner, updated)
|
|
|
|
}
|
2024-04-05 09:35:49 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-11-21 12:11:38 +00:00
|
|
|
// 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) {
|
2024-08-20 06:45:24 +00:00
|
|
|
clientID, assertion, err := clientIDFromCredentials(ctx, cc)
|
2023-12-05 17:01:03 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2023-11-21 12:11:38 +00:00
|
|
|
}
|
2023-12-05 17:01:03 +00:00
|
|
|
client, err = s.query.GetIntrospectionClientByID(ctx, clientID, assertion)
|
2023-11-21 12:11:38 +00:00
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
2024-08-20 06:45:24 +00:00
|
|
|
return nil, oidc.ErrUnauthorizedClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError)
|
2023-11-21 12:11:38 +00:00
|
|
|
}
|
|
|
|
// any other error is regarded internal and should not be reported back to the client.
|
|
|
|
return client, err
|
|
|
|
}
|
|
|
|
|
|
|
|
type introspectionTokenResult struct {
|
|
|
|
*accessToken
|
|
|
|
err error
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) introspectionToken(ctx context.Context, tkn string, rc chan<- *introspectionTokenResult) {
|
|
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
|
|
token, err := s.verifyAccessToken(ctx, tkn)
|
|
|
|
span.EndWithError(err)
|
|
|
|
|
|
|
|
rc <- &introspectionTokenResult{
|
|
|
|
accessToken: token,
|
|
|
|
err: err,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func validateIntrospectionAudience(audience []string, clientID, projectID string) error {
|
|
|
|
if slices.ContainsFunc(audience, func(entry string) bool {
|
|
|
|
return entry == clientID || entry == projectID
|
|
|
|
}) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-12-05 17:01:03 +00:00
|
|
|
return zerrors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
|
2023-11-21 12:11:38 +00:00
|
|
|
}
|