diff --git a/docs/docs/apis/actions/complement-token.md b/docs/docs/apis/actions/complement-token.md index 60f83df36b..92a11f0cb5 100644 --- a/docs/docs/apis/actions/complement-token.md +++ b/docs/docs/apis/actions/complement-token.md @@ -13,14 +13,21 @@ This trigger is called before userinfo are set in the token or response. - `ctx` The first parameter contains the following fields: - `v1` + - `claims` [*Claims*](./objects#claims) + - `getUser()` [*User*](./objects#user) - `user` - `getMetadata()` [*metadataResult*](./objects#metadata-result) + - `grants` [*UserGrantList*](./objects#user-grant-list) - `api` The second parameter contains the following fields: - `v1` - - `userinfo` + - `userinfo` + This function is deprecated, please use `api.v1.claims` - `setClaim(string, Any)` - Key of the claim and any value + Sets any value if the key is not already present. If it's already present there is a message added to `urn:zitadel:iam:action:${action.name}:log` + - `claims` + - `setClaim(string, Any)` + Sets any value if the key is not already present. If it's already present there is a message added to `urn:zitadel:iam:action:${action.name}:log` - `user` - `setMetadata(string, Any)` Key of the metadata and any value @@ -34,14 +41,17 @@ This trigger is called before the claims are set in the access token and the tok - `ctx` The first parameter contains the following fields: - `v1` + - `claims` [*Claims*](./objects#claims) + - `getUser()` [*User*](./objects#user) - `user` - `getMetadata()` [*metadataResult*](./objects#metadata-result) + - `grants` [*UserGrantList*](./objects#user-grant-list) - `api` The second parameter contains the following fields: - `v1` - `claims` - `setClaim(string, Any)` - Sets any value if the key is not already present + Sets any value if the key is not already present. If it's already present there is a message added to `urn:zitadel:iam:action:${action.name}:log` - `appendLogIntoClaims(string)` Appends the entry into the claim `urn:zitadel:action:{action.name}:log` the value of the claim is an Array of *string* - `user` diff --git a/docs/docs/apis/actions/objects.md b/docs/docs/apis/actions/objects.md index f2d4b1fd92..55751b13ff 100644 --- a/docs/docs/apis/actions/objects.md +++ b/docs/docs/apis/actions/objects.md @@ -166,4 +166,44 @@ This object is based on the Golang struct [http.Request](https://pkg.go.dev/net/ - `form` Map *string* of Array of *string* - `postForm` Map *string* of Array of *string* - `remoteAddr` *string* -- `headers` Map *string* of Array of *string* \ No newline at end of file +- `headers` Map *string* of Array of *string* + +## Claims + +This object represents [the claims](../openidoauth/claims) which will be written into the oidc token. + +- `sub` *string* +- `name` *string* +- `email` *string* +- `locale` *string* +- `given_name` *string* +- `family_name` *string* +- `preferred_username` *string* +- `email_verified` *bool* +- `updated_at` *Number* + +Additionally there could additional fields depending on the configuration of your [project](../../guides/manage/console/projects#role-settings) and your [application](../../guides/manage/console/applications#token-settings) + +## user grant list + +This object represents a list of user grant stored in ZITADEL. + +- `count` *Number* +- `sequence` *Number* +- `timestamp` *Date* +- `grants` Array of + - `id` *string* + - `projectGrantId` *string* + The id of the [project grant](../../concepts/usecases/saas#project-grant) + - `state` *Number* + + - `creationDate` *Date* + - `changeDate` *Date* + - `sequence` *Number* + - `userId` *string* + - `roles` Array of *string* + - `userResourceOwner` *string* + - `userGrantResourceOwner` *string* + - `userGrantResourceOwnerName` *string* + - `projectId` *string* + - `projectName` *string* diff --git a/docs/docs/apis/openidoauth/claims.md b/docs/docs/apis/openidoauth/claims.md index 2d0bda35e3..2ee804ad68 100644 --- a/docs/docs/apis/openidoauth/claims.md +++ b/docs/docs/apis/openidoauth/claims.md @@ -67,11 +67,11 @@ Please check below the matrix for an overview where which scope is asserted. ## Custom Claims -> This feature is not yet released +You can add custom claims using the [complement token flow](/docs/apis/actions/complement-token) of the [actions feature](/docs/apis/actions/introduction). ## Reserved Claims -ZITADEL reserves some claims to assert certain data. Please check out the [reserved scopes](scopes#reserved-scopes). +ZITADEL reserves some claims to assert certain data. Please check out the [reserved scopes](scopes#reserved-scopes). | Claims | Example | Description | |:--------------------------------------------------|:-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| diff --git a/docs/docs/examples/identity-proxy/oauth2-proxy.md b/docs/docs/examples/identity-proxy/oauth2-proxy.md index f9c3a89aec..b1a470df5c 100644 --- a/docs/docs/examples/identity-proxy/oauth2-proxy.md +++ b/docs/docs/examples/identity-proxy/oauth2-proxy.md @@ -59,7 +59,16 @@ cookie_secure = false #localdev only false http_address = "127.0.0.1:4180" #localdev only ``` -> This was tested with version `oauth2-proxy v6.1.1 (built with go1.14.2)` +> This was tested with version `oauth2-proxy v7.4.0 (built with go1.20.0)` + +### Check for groups + +If you want oauth2-proxy to check for roles in the tokens you have to add an [action](/docs/apis/actions/introduction) in ZITADEL to [complement the token](/docs/apis/actions/complement-token) according to [this example](https://github.com/zitadel/actions/blob/main/examples/custom_roles.js) and add the following configuration to the config: + +```toml +oidc_groups_claim = "{your_actions_group_key}" +allowed_groups = ["list", "of", "allowed", "roles"] +``` ## Completion diff --git a/go.mod b/go.mod index 8ad5accb8f..e388abc2a6 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,7 @@ require ( github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 github.com/zitadel/logging v0.3.4 - github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.8 + github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.9 github.com/zitadel/saml v0.0.10 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.27.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.27.0 diff --git a/go.sum b/go.sum index a4f28ec522..e2f474e886 100644 --- a/go.sum +++ b/go.sum @@ -1140,8 +1140,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM= github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0= -github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.8 h1:e6sRhY3Lijku8XBzazLoWpJcjO/EniEA7C5UEgiApRY= -github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.8/go.mod h1:2jHMP6o/WK0EmcNJkz+FSpjeqcCuQG9YqqqzKZkfgIE= +github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.9 h1:P7xbgv2501rsW8E0Uj804LMBrabVuZYcstqoFVmgWjA= +github.com/zitadel/oidc/v2 v2.0.0-dynamic-issuer.9/go.mod h1:2jHMP6o/WK0EmcNJkz+FSpjeqcCuQG9YqqqzKZkfgIE= github.com/zitadel/saml v0.0.10 h1:cyKd78Vat9vz55S74lggJrXMSqbAPsnJDrPFTPScNYY= github.com/zitadel/saml v0.0.10/go.mod h1:Hze1/zRN9j1uh7U+89vweP/OwLNO8BLHg3zU1Jtycdg= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= diff --git a/internal/actions/object/user_grant.go b/internal/actions/object/user_grant.go index d95e7d574c..5eb565f986 100644 --- a/internal/actions/object/user_grant.go +++ b/internal/actions/object/user_grant.go @@ -1,10 +1,13 @@ package object import ( + "time" + "github.com/dop251/goja" "github.com/zitadel/zitadel/internal/actions" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" ) type UserGrants struct { @@ -17,6 +20,32 @@ type UserGrant struct { Roles []string } +type userGrantList struct { + Count uint64 + Sequence uint64 + Timestamp time.Time + Grants []*userGrant +} + +type userGrant struct { + Id string + ProjectGrantId string + State domain.UserGrantState + UserGrantResourceOwner string + UserGrantResourceOwnerName string + + CreationDate time.Time + ChangeDate time.Time + Sequence uint64 + + UserId string + UserResourceOwner string + Roles []string + + ProjectId string + ProjectName string +} + func AppendGrantFunc(userGrants *UserGrants) func(c *actions.FieldConfig) func(call goja.FunctionCall) goja.Value { return func(c *actions.FieldConfig) func(call goja.FunctionCall) goja.Value { return func(call goja.FunctionCall) goja.Value { @@ -29,6 +58,51 @@ func AppendGrantFunc(userGrants *UserGrants) func(c *actions.FieldConfig) func(c } } +func UserGrantsFromQuery(c *actions.FieldConfig, userGrants *query.UserGrants) goja.Value { + grantList := &userGrantList{ + Count: userGrants.Count, + Sequence: userGrants.Sequence, + Timestamp: userGrants.Timestamp, + Grants: make([]*userGrant, len(userGrants.UserGrants)), + } + + for i, grant := range userGrants.UserGrants { + grantList.Grants[i] = &userGrant{ + Id: grant.ID, + ProjectGrantId: grant.GrantID, + State: grant.State, + CreationDate: grant.CreationDate, + ChangeDate: grant.ChangeDate, + Sequence: grant.Sequence, + UserId: grant.UserID, + Roles: grant.Roles, + UserResourceOwner: grant.UserResourceOwner, + UserGrantResourceOwner: grant.ResourceOwner, + UserGrantResourceOwnerName: grant.OrgName, + ProjectId: grant.ProjectID, + ProjectName: grant.ProjectName, + } + } + + return c.Runtime.ToValue(grantList) +} + +func UserGrantsToDomain(userID string, actionUserGrants []UserGrant) []*domain.UserGrant { + if actionUserGrants == nil { + return nil + } + userGrants := make([]*domain.UserGrant, len(actionUserGrants)) + for i, grant := range actionUserGrants { + userGrants[i] = &domain.UserGrant{ + UserID: userID, + ProjectID: grant.ProjectID, + ProjectGrantID: grant.ProjectGrantID, + RoleKeys: grant.Roles, + } + } + return userGrants +} + func mapObjectToGrant(object *goja.Object, grant *UserGrant) { for _, key := range object.Keys() { switch key { @@ -50,19 +124,3 @@ func mapObjectToGrant(object *goja.Object, grant *UserGrant) { panic("projectId not set") } } - -func UserGrantsToDomain(userID string, actionUserGrants []UserGrant) []*domain.UserGrant { - if actionUserGrants == nil { - return nil - } - userGrants := make([]*domain.UserGrant, len(actionUserGrants)) - for i, grant := range actionUserGrants { - userGrants[i] = &domain.UserGrant{ - UserID: userID, - ProjectID: grant.ProjectID, - ProjectGrantID: grant.ProjectGrantID, - RoleKeys: grant.Roles, - } - } - return userGrants -} diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 80fa5b61d0..af219bf6be 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -327,21 +327,19 @@ func (o *OPStorage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSette } } - if len(roles) == 0 || applicationID == "" { - return o.userinfoFlows(ctx, user.ResourceOwner, userInfo) - } - projectRoles, err := o.assertRoles(ctx, userID, applicationID, roles) + userGrants, projectRoles, err := o.assertRoles(ctx, userID, applicationID, roles) if err != nil { return err } + if len(projectRoles) > 0 { userInfo.AppendClaims(ClaimProjectRoles, projectRoles) } - return o.userinfoFlows(ctx, user.ResourceOwner, userInfo) + return o.userinfoFlows(ctx, user.ResourceOwner, userGrants, userInfo) } -func (o *OPStorage) userinfoFlows(ctx context.Context, resourceOwner string, userInfo oidc.UserInfoSetter) error { +func (o *OPStorage) userinfoFlows(ctx context.Context, resourceOwner string, userGrants *query.UserGrants, userInfo oidc.UserInfoSetter) error { queriedActions, err := o.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeCustomiseToken, domain.TriggerTypePreUserinfoCreation, resourceOwner, false) if err != nil { return err @@ -349,6 +347,16 @@ func (o *OPStorage) userinfoFlows(ctx context.Context, resourceOwner string, use ctxFields := actions.SetContextFields( actions.SetFields("v1", + actions.SetFields("claims", userinfoClaims(userInfo)), + actions.SetFields("getUser", func(c *actions.FieldConfig) interface{} { + return func(call goja.FunctionCall) goja.Value { + user, err := o.query.GetUserByID(ctx, true, userInfo.GetSubject(), false) + if err != nil { + panic(err) + } + return object.UserFromQuery(c, user) + } + }), actions.SetFields("user", actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} { return func(goja.FunctionCall) goja.Value { @@ -371,6 +379,9 @@ func (o *OPStorage) userinfoFlows(ctx context.Context, resourceOwner string, use return object.UserMetadataListFromQuery(c, metadata) } }), + actions.SetFields("grants", func(c *actions.FieldConfig) interface{} { + return object.UserGrantsFromQuery(c, userGrants) + }), ), ), ) @@ -393,6 +404,18 @@ func (o *OPStorage) userinfoFlows(ctx context.Context, resourceOwner string, use claimLogs = append(claimLogs, entry) }), ), + actions.SetFields("claims", + actions.SetFields("setClaim", func(key string, value interface{}) { + if userInfo.GetClaim(key) == nil { + userInfo.AppendClaims(key, value) + return + } + claimLogs = append(claimLogs, fmt.Sprintf("key %q already exists", key)) + }), + actions.SetFields("appendLogIntoClaims", func(entry string) { + claimLogs = append(claimLogs, entry) + }), + ), actions.SetFields("user", actions.SetFields("setMetadata", func(call goja.FunctionCall) goja.Value { if len(call.Arguments) != 2 { @@ -480,21 +503,19 @@ func (o *OPStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clie } } - if len(roles) == 0 || clientID == "" { - return o.privateClaimsFlows(ctx, userID, claims) - } - projectRoles, err := o.assertRoles(ctx, userID, clientID, roles) + userGrants, projectRoles, err := o.assertRoles(ctx, userID, clientID, roles) if err != nil { return nil, err } + if len(projectRoles) > 0 { claims = appendClaim(claims, ClaimProjectRoles, projectRoles) } - return o.privateClaimsFlows(ctx, userID, claims) + return o.privateClaimsFlows(ctx, userID, userGrants, claims) } -func (o *OPStorage) privateClaimsFlows(ctx context.Context, userID string, claims map[string]interface{}) (map[string]interface{}, error) { +func (o *OPStorage) privateClaimsFlows(ctx context.Context, userID string, userGrants *query.UserGrants, claims map[string]interface{}) (map[string]interface{}, error) { user, err := o.query.GetUserByID(ctx, true, userID, false) if err != nil { return nil, err @@ -506,6 +527,18 @@ func (o *OPStorage) privateClaimsFlows(ctx context.Context, userID string, claim ctxFields := actions.SetContextFields( actions.SetFields("v1", + actions.SetFields("claims", func(c *actions.FieldConfig) interface{} { + return c.Runtime.ToValue(claims) + }), + actions.SetFields("getUser", func(c *actions.FieldConfig) interface{} { + return func(call goja.FunctionCall) goja.Value { + user, err := o.query.GetUserByID(ctx, true, userID, false) + if err != nil { + panic(err) + } + return object.UserFromQuery(c, user) + } + }), actions.SetFields("user", actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} { return func(goja.FunctionCall) goja.Value { @@ -528,6 +561,9 @@ func (o *OPStorage) privateClaimsFlows(ctx context.Context, userID string, claim return object.UserMetadataListFromQuery(c, metadata) } }), + actions.SetFields("grants", func(c *actions.FieldConfig) interface{} { + return object.UserGrantsFromQuery(c, userGrants) + }), ), ), ) @@ -598,24 +634,24 @@ func (o *OPStorage) privateClaimsFlows(ctx context.Context, userID string, claim return claims, nil } -func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID string, requestedRoles []string) (map[string]map[string]string, error) { +func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID string, requestedRoles []string) (*query.UserGrants, map[string]map[string]string, error) { projectID, err := o.query.ProjectIDFromClientID(ctx, applicationID, false) if err != nil { - return nil, err + return nil, nil, err } projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID) if err != nil { - return nil, err + return nil, nil, err } userIDQuery, err := query.NewUserGrantUserIDSearchQuery(userID) if err != nil { - return nil, err + return nil, nil, err } grants, err := o.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery, userIDQuery}, }, false) if err != nil { - return nil, err + return nil, nil, err } projectRoles := make(map[string]map[string]string) for _, requestedRole := range requestedRoles { @@ -623,7 +659,7 @@ func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID strin checkGrantedRoles(projectRoles, grant, requestedRole) } } - return projectRoles, nil + return grants, projectRoles, nil } func (o *OPStorage) assertUserMetaData(ctx context.Context, userID string) (map[string]string, error) { @@ -689,3 +725,18 @@ func appendClaim(claims map[string]interface{}, claim string, value interface{}) claims[claim] = value return claims } + +func userinfoClaims(userInfo oidc.UserInfoSetter) func(c *actions.FieldConfig) interface{} { + return func(c *actions.FieldConfig) interface{} { + marshalled, err := json.Marshal(userInfo) + if err != nil { + panic(err) + } + + claims := make(map[string]interface{}, 10) + if err = json.Unmarshal(marshalled, &claims); err != nil { + panic(err) + } + return c.Runtime.ToValue(claims) + } +} diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index 15df7f5839..2679c5f28f 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -18,13 +18,15 @@ import ( ) type UserGrant struct { + // ID represents the aggregate id (id of the user grant) ID string CreationDate time.Time ChangeDate time.Time Sequence uint64 Roles database.StringArray - GrantID string - State domain.UserGrantState + // GrantID represents the project grant id + GrantID string + State domain.UserGrantState UserID string Username string