zitadel/internal/api/oidc/client_credentials.go
Tim Möhlmann 5fd2061770
feat(oidc): allow returning of parent errors to client (#8376)
# Which Problems Are Solved

Currently the OIDC API of ZITADEL only prints parent errors to the logs.
Where 4xx status are typically warn level and 5xx error level. This
makes it hard to debug certain errors for client in multi-instance
environments like ZITADEL cloud, where there is no direct access to
logs. In case of support requests we often can't correlate past
log-lines to the error that was reported.

This change adds the possibility to return the parent error in the
response to the OIDC client. For the moment this only applies to JSON
body responses, not error redirects to the RP.

# How the Problems Are Solved

- New instance-level feature flag: `debug_oidc_parent_error`
- Use the new `WithReturnParentToClient()` function from the oidc lib
introduced in https://github.com/zitadel/oidc/pull/629 for all cases
where `WithParent` was already used and the request context is
available.

# Additional Changes

none

# Additional Context

- Depends on: https://github.com/zitadel/oidc/pull/629
- Related to: https://github.com/zitadel/zitadel/issues/8362

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
2024-08-20 06:45:24 +00:00

156 lines
5.3 KiB
Go

package oidc
import (
"context"
"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/query"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type clientCredentialsRequest struct {
sub string
audience []string
scopes []string
}
// GetSubject returns the subject for token to be created because of the client credentials request
// the subject will be the id of the service user
func (c *clientCredentialsRequest) GetSubject() string {
return c.sub
}
// GetAudience returns the audience for token to be created because of the client credentials request
func (c *clientCredentialsRequest) GetAudience() []string {
return c.audience
}
func (c *clientCredentialsRequest) GetScopes() []string {
return c.scopes
}
func (s *Server) clientCredentialsAuth(ctx context.Context, clientID, clientSecret string) (op.Client, error) {
user, err := s.query.GetUserByLoginName(ctx, false, clientID)
if zerrors.IsNotFound(err) {
return nil, oidc.ErrInvalidClient().WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError).WithDescription("client not found")
}
if err != nil {
return nil, err // defaults to server error
}
if user.Machine == nil || user.Machine.EncodedSecret == "" {
return nil, zerrors.ThrowPreconditionFailed(nil, "OIDC-pieP8", "Errors.User.Machine.Secret.NotExisting")
}
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "passwap.Verify")
updated, err := s.hasher.Verify(user.Machine.EncodedSecret, clientSecret)
spanPasswordComparison.EndWithError(err)
if err != nil {
s.command.MachineSecretCheckFailed(ctx, user.ID, user.ResourceOwner)
return nil, zerrors.ThrowInvalidArgument(err, "OIDC-VoXo6", "Errors.User.Machine.Secret.Invalid")
}
s.command.MachineSecretCheckSucceeded(ctx, user.ID, user.ResourceOwner, updated)
return &clientCredentialsClient{
id: clientID,
user: user,
}, nil
}
type clientCredentialsClient struct {
id string
user *query.User
}
// AccessTokenType returns the AccessTokenType for the token to be created because of the client credentials request
// machine users currently only have opaque tokens ([op.AccessTokenTypeBearer])
func (c *clientCredentialsClient) AccessTokenType() op.AccessTokenType {
return accessTokenTypeToOIDC(c.user.Machine.AccessTokenType)
}
// GetID returns the client_id (username of the machine user) for the token to be created because of the client credentials request
func (c *clientCredentialsClient) GetID() string {
return c.id
}
// RedirectURIs returns nil as there are no redirect uris
func (c *clientCredentialsClient) RedirectURIs() []string {
return nil
}
// PostLogoutRedirectURIs returns nil as there are no logout redirect uris
func (c *clientCredentialsClient) PostLogoutRedirectURIs() []string {
return nil
}
// ApplicationType returns [op.ApplicationTypeWeb] as the machine users is a confidential client
func (c *clientCredentialsClient) ApplicationType() op.ApplicationType {
return op.ApplicationTypeWeb
}
// AuthMethod returns the allowed auth method type for machine user.
// It returns Basic Auth
func (c *clientCredentialsClient) AuthMethod() oidc.AuthMethod {
return oidc.AuthMethodBasic
}
// ResponseTypes returns nil as the types are only required for an authorization request
func (c *clientCredentialsClient) ResponseTypes() []oidc.ResponseType {
return nil
}
// GrantTypes returns the grant types supported by the machine users, which is currently only client credentials ([oidc.GrantTypeClientCredentials])
func (c *clientCredentialsClient) GrantTypes() []oidc.GrantType {
return []oidc.GrantType{
oidc.GrantTypeClientCredentials,
}
}
// LoginURL returns an empty string as there is no login UI involved
func (c *clientCredentialsClient) LoginURL(_ string) string {
return ""
}
// IDTokenLifetime returns 0 as there is no id_token issued
func (c *clientCredentialsClient) IDTokenLifetime() time.Duration {
return 0
}
// DevMode returns false as there is no dev mode
func (c *clientCredentialsClient) DevMode() bool {
return false
}
// RestrictAdditionalIdTokenScopes returns nil as no id_token is issued
func (c *clientCredentialsClient) RestrictAdditionalIdTokenScopes() func(scopes []string) []string {
return nil
}
// RestrictAdditionalAccessTokenScopes returns the scope allowed for the token to be created because of the client credentials request
// currently it allows all scopes to be used in the access token
func (c *clientCredentialsClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string {
return func(scopes []string) []string {
return scopes
}
}
func (c *clientCredentialsClient) IsScopeAllowed(scope string) bool {
return isScopeAllowed(scope) || strings.HasPrefix(scope, ScopeProjectRolePrefix)
}
// IDTokenUserinfoClaimsAssertion returns null false as no id_token is issued
func (c *clientCredentialsClient) IDTokenUserinfoClaimsAssertion() bool {
return false
}
// ClockSkew enable handling clock skew of the token validation. The duration (0-5s) will be added to exp claim and subtracted from iats,
// auth_time and nbf of the token to be created because of the client credentials request.
// It returns 0 as clock skew is not implemented on machine users.
func (c *clientCredentialsClient) ClockSkew() time.Duration {
return 0
}