From 5b4c64d740e3663d990c2892a66ac93edbc3f28e Mon Sep 17 00:00:00 2001 From: Fabi <38692350+fgerschwiler@users.noreply.github.com> Date: Thu, 26 Aug 2021 13:38:13 +0200 Subject: [PATCH] feat: add user metadata scope and claim (#2268) * feat: add user metadata scope and claime * docs: scope * docs: metadata base 64 encoded * docs: metadata base 64 encoded * docs: metadata base 64 encoded --- docs/docs/apis/openidoauth/scopes.md | 1 + internal/api/oidc/client.go | 37 +++++++++++++++---- internal/api/oidc/client_converter.go | 3 ++ .../eventsourcing/eventstore/user.go | 15 +++++++- internal/auth/repository/user.go | 2 + 5 files changed, 49 insertions(+), 9 deletions(-) diff --git a/docs/docs/apis/openidoauth/scopes.md b/docs/docs/apis/openidoauth/scopes.md index 05f57dad1..f17e9e348 100644 --- a/docs/docs/apis/openidoauth/scopes.md +++ b/docs/docs/apis/openidoauth/scopes.md @@ -28,5 +28,6 @@ In addition to the standard compliant scopes we utilize the following scopes. | urn:zitadel:iam:org:domain:primary:{domainname} | `urn:zitadel:iam:org:domain:primary:acme.ch` | When requesting this scope **ZITADEL** will enforce that the user is a member of the selected organization. If the organization does not exist a failure is displayed | | urn:zitadel:iam:role:{rolename} | | | | `urn:zitadel:iam:org:project:id:{projectid}:aud` | ZITADEL's Project id is `urn:zitadel:iam:org:project:id:69234237810729019:aud` | By adding this scope, the requested projectid will be added to the audience of the access and id token | +| urn:zitadel:iam:user:metadata | `urn:zitadel:iam:user:metadata` | By adding this scope, the metadata of the user will be included in the token. The values are base64 encoded. | > If access to ZITADEL's API's is needed with a service user the scope `urn:zitadel:iam:org:project:id:69234237810729019:aud` needs to be used with the JWT Profile request diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 96e3ceee3..151082b81 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -2,6 +2,7 @@ package oidc import ( "context" + "encoding/base64" "strings" "github.com/caos/oidc/pkg/oidc" @@ -22,14 +23,10 @@ import ( ) const ( - scopeOpenID = "openid" - scopeProfile = "profile" - scopeEmail = "email" - scopePhone = "phone" - scopeAddress = "address" - ScopeProjectRolePrefix = "urn:zitadel:iam:org:project:role:" ClaimProjectRoles = "urn:zitadel:iam:org:project:roles" + ScopeUserMetaData = "urn:zitadel:iam:user:metadata" + ClaimUserMetaData = ScopeUserMetaData oidcCtx = "oidc" ) @@ -186,6 +183,13 @@ func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo oidc.Use } } } + userMetaData, err := o.assertUserMetaData(ctx, userID) + if err != nil { + return err + } + if len(userMetaData) > 0 { + userInfo.AppendClaims(ClaimUserMetaData, userMetaData) + } if len(roles) == 0 || applicationID == "" { return nil @@ -197,7 +201,6 @@ func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo oidc.Use if len(projectRoles) > 0 { userInfo.AppendClaims(ClaimProjectRoles, projectRoles) } - return nil } @@ -243,6 +246,13 @@ func (o *OPStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clie if len(projectRoles) > 0 { claims = appendClaim(claims, ClaimProjectRoles, projectRoles) } + userMetaData, err := o.assertUserMetaData(ctx, userID) + if err != nil { + return nil, err + } + if len(userMetaData) > 0 { + claims = appendClaim(claims, ClaimUserMetaData, userMetaData) + } return claims, err } @@ -264,6 +274,19 @@ func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID strin return projectRoles, nil } +func (o *OPStorage) assertUserMetaData(ctx context.Context, userID string) (map[string]string, error) { + metaData, err := o.repo.SearchUserMetadata(ctx, userID) + if err != nil { + return nil, err + } + + userMetaData := make(map[string]string) + for _, md := range metaData.Result { + userMetaData[md.Key] = base64.RawURLEncoding.EncodeToString(md.Value) + } + return userMetaData, nil +} + func checkGrantedRoles(roles map[string]map[string]string, grant *grant_model.UserGrantView, requestedRole string) { for _, grantedRole := range grant.RoleKeys { if requestedRole == grantedRole { diff --git a/internal/api/oidc/client_converter.go b/internal/api/oidc/client_converter.go index f23ca9dfd..16797b12e 100644 --- a/internal/api/oidc/client_converter.go +++ b/internal/api/oidc/client_converter.go @@ -106,6 +106,9 @@ func (c *Client) IsScopeAllowed(scope string) bool { if strings.HasPrefix(scope, authreq_model.ProjectIDScope) { return true } + if strings.HasPrefix(scope, ScopeUserMetaData) { + return true + } for _, allowedScope := range c.allowedScopes { if scope == allowedScope { return true diff --git a/internal/auth/repository/eventsourcing/eventstore/user.go b/internal/auth/repository/eventsourcing/eventstore/user.go index f155c4f60..fba74a3b9 100644 --- a/internal/auth/repository/eventsourcing/eventstore/user.go +++ b/internal/auth/repository/eventsourcing/eventstore/user.go @@ -307,16 +307,27 @@ func (repo *UserRepo) GetMyMetadataByKey(ctx context.Context, key string) (*doma return iam_model.MetadataViewToDomain(data), nil } +func (repo *UserRepo) SearchUserMetadata(ctx context.Context, userID string) (*domain.MetadataSearchResponse, error) { + req := new(domain.MetadataSearchRequest) + return repo.searchUserMetadata(userID, "", req) +} + func (repo *UserRepo) SearchMyMetadata(ctx context.Context, req *domain.MetadataSearchRequest) (*domain.MetadataSearchResponse, error) { ctxData := authz.GetCtxData(ctx) err := req.EnsureLimit(repo.SearchLimit) if err != nil { return nil, err } + return repo.searchUserMetadata(ctxData.UserID, ctxData.ResourceOwner, req) +} + +func (repo *UserRepo) searchUserMetadata(userID, resourceOwner string, req *domain.MetadataSearchRequest) (*domain.MetadataSearchResponse, error) { sequence, sequenceErr := repo.View.GetLatestUserSequence() logging.Log("EVENT-N9fsd").OnError(sequenceErr).Warn("could not read latest user sequence") - req.AppendAggregateIDQuery(ctxData.UserID) - req.AppendResourceOwnerQuery(ctxData.ResourceOwner) + req.AppendAggregateIDQuery(userID) + if resourceOwner != "" { + req.AppendResourceOwnerQuery(resourceOwner) + } metadata, count, err := repo.View.SearchMetadata(req) if err != nil { return nil, err diff --git a/internal/auth/repository/user.go b/internal/auth/repository/user.go index 768e27911..0f9c573c8 100644 --- a/internal/auth/repository/user.go +++ b/internal/auth/repository/user.go @@ -21,6 +21,8 @@ type UserRepository interface { MachineKeyByID(ctx context.Context, keyID string) (*key_model.AuthNKeyView, error) SearchUsers(ctx context.Context, request *model.UserSearchRequest) (*model.UserSearchResponse, error) + + SearchUserMetadata(ctx context.Context, userID string) (*domain.MetadataSearchResponse, error) } type myUserRepo interface {