feat(actions): add fields to complement token flow (#5336)

* deprecated `ctx.v1.userinfo`-field in "pre userinfo creation" trigger in favour of `ctx.v1.claims`. The trigger now behaves the same as "pre access token creation"
* added `ctx.v1.claims` to "complement tokens" flow
* added `ctx.v1.grants` to "complement tokens" flow
* document `ctx.v1.getUser()` in "complement tokens" flow

* feat(actions): add getUser() and grant

* map user grants

* map claims

* feat(actions): claims in complement token ctx

* docs(actions): add new fields of complement token

* docs(actions): additions to complement token

* docs(actions): correct field names
This commit is contained in:
Silvan 2023-03-08 15:26:28 +01:00 committed by GitHub
parent 3042d7ef5c
commit 20e4f1ce57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 216 additions and 46 deletions

View File

@ -13,14 +13,21 @@ This trigger is called before userinfo are set in the token or response.
- `ctx` - `ctx`
The first parameter contains the following fields: The first parameter contains the following fields:
- `v1` - `v1`
- `claims` [*Claims*](./objects#claims)
- `getUser()` [*User*](./objects#user)
- `user` - `user`
- `getMetadata()` [*metadataResult*](./objects#metadata-result) - `getMetadata()` [*metadataResult*](./objects#metadata-result)
- `grants` [*UserGrantList*](./objects#user-grant-list)
- `api` - `api`
The second parameter contains the following fields: The second parameter contains the following fields:
- `v1` - `v1`
- `userinfo` - `userinfo`
This function is deprecated, please use `api.v1.claims`
- `setClaim(string, Any)` - `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` - `user`
- `setMetadata(string, Any)` - `setMetadata(string, Any)`
Key of the metadata and any value 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` - `ctx`
The first parameter contains the following fields: The first parameter contains the following fields:
- `v1` - `v1`
- `claims` [*Claims*](./objects#claims)
- `getUser()` [*User*](./objects#user)
- `user` - `user`
- `getMetadata()` [*metadataResult*](./objects#metadata-result) - `getMetadata()` [*metadataResult*](./objects#metadata-result)
- `grants` [*UserGrantList*](./objects#user-grant-list)
- `api` - `api`
The second parameter contains the following fields: The second parameter contains the following fields:
- `v1` - `v1`
- `claims` - `claims`
- `setClaim(string, Any)` - `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)` - `appendLogIntoClaims(string)`
Appends the entry into the claim `urn:zitadel:action:{action.name}:log` the value of the claim is an Array of *string* Appends the entry into the claim `urn:zitadel:action:{action.name}:log` the value of the claim is an Array of *string*
- `user` - `user`

View File

@ -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* - `form` Map *string* of Array of *string*
- `postForm` Map *string* of Array of *string* - `postForm` Map *string* of Array of *string*
- `remoteAddr` *string* - `remoteAddr` *string*
- `headers` Map *string* of Array of *string* - `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*
<ul><li>0: unspecified</li><li>1: active</li><li>2: inactive</li><li>3: removed</li></ul>
- `creationDate` *Date*
- `changeDate` *Date*
- `sequence` *Number*
- `userId` *string*
- `roles` Array of *string*
- `userResourceOwner` *string*
- `userGrantResourceOwner` *string*
- `userGrantResourceOwnerName` *string*
- `projectId` *string*
- `projectName` *string*

View File

@ -67,11 +67,11 @@ Please check below the matrix for an overview where which scope is asserted.
## Custom Claims ## 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 ## 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 | | Claims | Example | Description |
|:--------------------------------------------------|:-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |:--------------------------------------------------|:-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

View File

@ -59,7 +59,16 @@ cookie_secure = false #localdev only false
http_address = "127.0.0.1:4180" #localdev only 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 ## Completion

2
go.mod
View File

@ -57,7 +57,7 @@ require (
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203
github.com/ttacon/libphonenumber v1.2.1 github.com/ttacon/libphonenumber v1.2.1
github.com/zitadel/logging v0.3.4 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 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/google.golang.org/grpc/otelgrpc v0.27.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.27.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.27.0

4
go.sum
View File

@ -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/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 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM=
github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0= 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.9 h1:P7xbgv2501rsW8E0Uj804LMBrabVuZYcstqoFVmgWjA=
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/go.mod h1:2jHMP6o/WK0EmcNJkz+FSpjeqcCuQG9YqqqzKZkfgIE=
github.com/zitadel/saml v0.0.10 h1:cyKd78Vat9vz55S74lggJrXMSqbAPsnJDrPFTPScNYY= 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/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= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=

View File

@ -1,10 +1,13 @@
package object package object
import ( import (
"time"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/zitadel/zitadel/internal/actions" "github.com/zitadel/zitadel/internal/actions"
"github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
) )
type UserGrants struct { type UserGrants struct {
@ -17,6 +20,32 @@ type UserGrant struct {
Roles []string 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 { 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(c *actions.FieldConfig) func(call goja.FunctionCall) goja.Value {
return 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) { func mapObjectToGrant(object *goja.Object, grant *UserGrant) {
for _, key := range object.Keys() { for _, key := range object.Keys() {
switch key { switch key {
@ -50,19 +124,3 @@ func mapObjectToGrant(object *goja.Object, grant *UserGrant) {
panic("projectId not set") 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
}

View File

@ -327,21 +327,19 @@ func (o *OPStorage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSette
} }
} }
if len(roles) == 0 || applicationID == "" { userGrants, projectRoles, err := o.assertRoles(ctx, userID, applicationID, roles)
return o.userinfoFlows(ctx, user.ResourceOwner, userInfo)
}
projectRoles, err := o.assertRoles(ctx, userID, applicationID, roles)
if err != nil { if err != nil {
return err return err
} }
if len(projectRoles) > 0 { if len(projectRoles) > 0 {
userInfo.AppendClaims(ClaimProjectRoles, projectRoles) 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) queriedActions, err := o.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeCustomiseToken, domain.TriggerTypePreUserinfoCreation, resourceOwner, false)
if err != nil { if err != nil {
return err return err
@ -349,6 +347,16 @@ func (o *OPStorage) userinfoFlows(ctx context.Context, resourceOwner string, use
ctxFields := actions.SetContextFields( ctxFields := actions.SetContextFields(
actions.SetFields("v1", 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("user",
actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} { actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} {
return func(goja.FunctionCall) goja.Value { 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) 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) 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("user",
actions.SetFields("setMetadata", func(call goja.FunctionCall) goja.Value { actions.SetFields("setMetadata", func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) != 2 { if len(call.Arguments) != 2 {
@ -480,21 +503,19 @@ func (o *OPStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clie
} }
} }
if len(roles) == 0 || clientID == "" { userGrants, projectRoles, err := o.assertRoles(ctx, userID, clientID, roles)
return o.privateClaimsFlows(ctx, userID, claims)
}
projectRoles, err := o.assertRoles(ctx, userID, clientID, roles)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(projectRoles) > 0 { if len(projectRoles) > 0 {
claims = appendClaim(claims, ClaimProjectRoles, projectRoles) 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) user, err := o.query.GetUserByID(ctx, true, userID, false)
if err != nil { if err != nil {
return nil, err return nil, err
@ -506,6 +527,18 @@ func (o *OPStorage) privateClaimsFlows(ctx context.Context, userID string, claim
ctxFields := actions.SetContextFields( ctxFields := actions.SetContextFields(
actions.SetFields("v1", 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("user",
actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} { actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} {
return func(goja.FunctionCall) goja.Value { 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) 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 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) projectID, err := o.query.ProjectIDFromClientID(ctx, applicationID, false)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID) projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
userIDQuery, err := query.NewUserGrantUserIDSearchQuery(userID) userIDQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
grants, err := o.query.UserGrants(ctx, &query.UserGrantsQueries{ grants, err := o.query.UserGrants(ctx, &query.UserGrantsQueries{
Queries: []query.SearchQuery{projectQuery, userIDQuery}, Queries: []query.SearchQuery{projectQuery, userIDQuery},
}, false) }, false)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
projectRoles := make(map[string]map[string]string) projectRoles := make(map[string]map[string]string)
for _, requestedRole := range requestedRoles { for _, requestedRole := range requestedRoles {
@ -623,7 +659,7 @@ func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID strin
checkGrantedRoles(projectRoles, grant, requestedRole) checkGrantedRoles(projectRoles, grant, requestedRole)
} }
} }
return projectRoles, nil return grants, projectRoles, nil
} }
func (o *OPStorage) assertUserMetaData(ctx context.Context, userID string) (map[string]string, error) { 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 claims[claim] = value
return claims 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)
}
}

View File

@ -18,13 +18,15 @@ import (
) )
type UserGrant struct { type UserGrant struct {
// ID represents the aggregate id (id of the user grant)
ID string ID string
CreationDate time.Time CreationDate time.Time
ChangeDate time.Time ChangeDate time.Time
Sequence uint64 Sequence uint64
Roles database.StringArray Roles database.StringArray
GrantID string // GrantID represents the project grant id
State domain.UserGrantState GrantID string
State domain.UserGrantState
UserID string UserID string
Username string Username string