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*
+
- 0: unspecified
- 1: active
- 2: inactive
- 3: removed
+ - `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