mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 04:17:32 +00:00
feat: support client_credentials for service users (#5134)
Request an access_token for service users with OAuth 2.0 Client Credentials Grant. Added functionality to generate and remove a secret on service users.
This commit is contained in:
@@ -94,29 +94,7 @@ func (o *OPStorage) ValidateJWTProfileScopes(ctx context.Context, subject string
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := len(scopes) - 1; i >= 0; i-- {
|
||||
scope := scopes[i]
|
||||
if strings.HasPrefix(scope, domain.OrgDomainPrimaryScope) {
|
||||
var orgID string
|
||||
org, err := o.query.OrgByPrimaryDomain(ctx, strings.TrimPrefix(scope, domain.OrgDomainPrimaryScope))
|
||||
if err == nil {
|
||||
orgID = org.ID
|
||||
}
|
||||
if orgID != user.ResourceOwner {
|
||||
scopes[i] = scopes[len(scopes)-1]
|
||||
scopes[len(scopes)-1] = ""
|
||||
scopes = scopes[:len(scopes)-1]
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(scope, domain.OrgIDScope) {
|
||||
if strings.TrimPrefix(scope, domain.OrgIDScope) != user.ResourceOwner {
|
||||
scopes[i] = scopes[len(scopes)-1]
|
||||
scopes[len(scopes)-1] = ""
|
||||
scopes = scopes[:len(scopes)-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return scopes, nil
|
||||
return o.checkOrgScopes(ctx, user, scopes)
|
||||
}
|
||||
|
||||
func (o *OPStorage) AuthorizeClientIDSecret(ctx context.Context, id string, secret string) (err error) {
|
||||
@@ -209,6 +187,68 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection
|
||||
return errors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
|
||||
}
|
||||
|
||||
func (o *OPStorage) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scope []string) (op.TokenRequest, error) {
|
||||
loginname, err := query.NewUserLoginNamesSearchQuery(clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := o.query.GetUser(ctx, false, false, loginname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope, err = o.checkOrgScopes(ctx, user, scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &clientCredentialsRequest{
|
||||
sub: user.ID,
|
||||
scopes: scope,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (o *OPStorage) ClientCredentials(ctx context.Context, clientID, clientSecret string) (op.Client, error) {
|
||||
loginname, err := query.NewUserLoginNamesSearchQuery(clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := o.query.GetUser(ctx, false, false, loginname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := o.command.VerifyMachineSecret(ctx, user.ID, user.ResourceOwner, clientSecret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &clientCredentialsClient{
|
||||
id: clientID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (o *OPStorage) checkOrgScopes(ctx context.Context, user *query.User, scopes []string) ([]string, error) {
|
||||
for i := len(scopes) - 1; i >= 0; i-- {
|
||||
scope := scopes[i]
|
||||
if strings.HasPrefix(scope, domain.OrgDomainPrimaryScope) {
|
||||
var orgID string
|
||||
org, err := o.query.OrgByPrimaryDomain(ctx, strings.TrimPrefix(scope, domain.OrgDomainPrimaryScope))
|
||||
if err == nil {
|
||||
orgID = org.ID
|
||||
}
|
||||
if orgID != user.ResourceOwner {
|
||||
scopes[i] = scopes[len(scopes)-1]
|
||||
scopes[len(scopes)-1] = ""
|
||||
scopes = scopes[:len(scopes)-1]
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(scope, domain.OrgIDScope) {
|
||||
if strings.TrimPrefix(scope, domain.OrgIDScope) != user.ResourceOwner {
|
||||
scopes[i] = scopes[len(scopes)-1]
|
||||
scopes[len(scopes)-1] = ""
|
||||
scopes = scopes[:len(scopes)-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return scopes, nil
|
||||
}
|
||||
|
||||
func (o *OPStorage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter, userID, applicationID string, scopes []string) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
120
internal/api/oidc/client_credentials.go
Normal file
120
internal/api/oidc/client_credentials.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v2/pkg/op"
|
||||
)
|
||||
|
||||
type clientCredentialsRequest struct {
|
||||
sub string
|
||||
scopes []string
|
||||
}
|
||||
|
||||
func (c *clientCredentialsRequest) GetSubject() string {
|
||||
return c.sub
|
||||
}
|
||||
|
||||
// GetAudience returns the audience for token to be created because of the client credentials request
|
||||
// return nil as the audience is set during the token creation in command.addUserToken
|
||||
func (c *clientCredentialsRequest) GetAudience() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clientCredentialsRequest) GetScopes() []string {
|
||||
return c.scopes
|
||||
}
|
||||
|
||||
type clientCredentialsClient struct {
|
||||
id string
|
||||
}
|
||||
|
||||
// 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 op.AccessTokenTypeBearer
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// IsScopeAllowed returns null false as the check is executed during the auth request validation
|
||||
func (c *clientCredentialsClient) IsScopeAllowed(scope string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
Reference in New Issue
Block a user