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/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).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 }