feat: add resource owner scope / claim (#2274)

* feat: add resource owner scope / claime

* fix: private claimes

* fix: private claims

* fix: add claim description

* Update claims.md

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
Fabi 2021-08-31 11:49:31 +02:00 committed by GitHub
parent c884a11f1b
commit 31a91a0039
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 119 additions and 45 deletions

View File

@ -5,31 +5,35 @@ title: Claims
ZITADEL asserts claims on different places according to the corresponding specifications or project and clients settings.
Please check below the matrix for an overview where which scope is asserted.
| Claims | Userinfo | Introspection | ID Token | Access Token |
|:------------------------------------------------|:---------------|----------------|---------------------------------------------|--------------------------------------|
| acr | No | No | Yes | No |
| address | When requested | When requested | When requested amd response_type `id_token` | No |
| amr | No | No | Yes | No |
| aud | No | No | Yes | When JWT |
| auth_time | No | No | Yes | No |
| azp | No | No | Yes | When JWT |
| email | When requested | When requested | When requested amd response_type `id_token` | No |
| email_verified | When requested | When requested | When requested amd response_type `id_token` | No |
| exp | No | No | Yes | When JWT |
| family_name | When requested | When requested | When requested amd response_type `id_token` | No |
| gender | When requested | When requested | When requested amd response_type `id_token` | No |
| given_name | When requested | When requested | When requested amd response_type `id_token` | No |
| iat | No | No | Yes | When JWT |
| iss | No | No | Yes | When JWT |
| locale | When requested | When requested | When requested amd response_type `id_token` | No |
| name | When requested | When requested | When requested amd response_type `id_token` | No |
| nonce | No | No | Yes | No |
| phone | When requested | When requested | When requested amd response_type `id_token` | No |
| phone_verified | When requested | When requested | When requested amd response_type `id_token` | No |
| preferred_username (username when Introspect ) | When requested | When requested | Yes | No |
| sub | Yes | Yes | Yes | When JWT |
| urn:zitadel:iam:org:domain:primary:{domainname} | When requested | When requested | When requested | When JWT and requested |
| urn:zitadel:iam:org:project:roles:{rolename} | When requested | When requested | When requested or configured | When JWT and requested or configured |
| Claims | Userinfo | Introspection | ID Token | Access Token |
|:--------------------------------------------------|:---------------|----------------|---------------------------------------------|--------------------------------------|
| acr | No | No | Yes | No |
| address | When requested | When requested | When requested amd response_type `id_token` | No |
| amr | No | No | Yes | No |
| aud | No | No | Yes | When JWT |
| auth_time | No | No | Yes | No |
| azp | No | No | Yes | When JWT |
| email | When requested | When requested | When requested amd response_type `id_token` | No |
| email_verified | When requested | When requested | When requested amd response_type `id_token` | No |
| exp | No | No | Yes | When JWT |
| family_name | When requested | When requested | When requested amd response_type `id_token` | No |
| gender | When requested | When requested | When requested amd response_type `id_token` | No |
| given_name | When requested | When requested | When requested amd response_type `id_token` | No |
| iat | No | No | Yes | When JWT |
| iss | No | No | Yes | When JWT |
| locale | When requested | When requested | When requested amd response_type `id_token` | No |
| name | When requested | When requested | When requested amd response_type `id_token` | No |
| nonce | No | No | Yes | No |
| phone | When requested | When requested | When requested amd response_type `id_token` | No |
| phone_verified | When requested | When requested | When requested amd response_type `id_token` | No |
| preferred_username (username when Introspect ) | When requested | When requested | Yes | No |
| sub | Yes | Yes | Yes | When JWT |
| urn:zitadel:iam:org:domain:primary:{domainname} | When requested | When requested | When requested | When JWT and requested |
| urn:zitadel:iam:org:project:roles:{rolename} | When requested | When requested | When requested or configured | When JWT and requested or configured |
| urn:zitadel:iam:user:metadata | When requested | When requested | When requested | When JWT and requested |
| urn:zitadel:iam:user:resourceowner:id | When requested | When requested | When requested | When JWT and requested |
| urn:zitadel:iam:user:resourceowner:name | When requested | When requested | When requested | When JWT and requested |
| urn:zitadel:iam:user:resourceowner:primary_domain | When requested | When requested | When requested | When JWT and requested |
## Standard Claims
@ -64,8 +68,12 @@ Please check below the matrix for an overview where which scope is asserted.
ZITADEL reserves some claims to assert certain data.
| Claims | Example | Description |
|:------------------------------------------------|:-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| urn:zitadel:iam:org:domain:primary:{domainname} | `{"urn:zitadel:iam:org:domain:primary": "acme.ch"}` | This claim represents the primary domain of the organization the user belongs to. |
| urn:zitadel:iam:org:project:roles:{rolename} | `{"urn:zitadel:iam:org:project:roles": [ {"user": {"id1": "acme.zitade.ch", "id2": "caos.ch"} } ] }` | When roles are asserted, ZITADEL does this by providing the `id` and `primaryDomain` below the role. This gives you the option to check in which organization a user has the role. |
| urn:zitadel:iam:roles:{rolename} | TBA | TBA |
| Claims | Example | Description |
|:--------------------------------------------------|:-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| urn:zitadel:iam:org:domain:primary:{domainname} | `{"urn:zitadel:iam:org:domain:primary": "acme.ch"}` | This claim represents the primary domain of the organization the user belongs to. |
| urn:zitadel:iam:org:project:roles:{rolename} | `{"urn:zitadel:iam:org:project:roles": [ {"user": {"id1": "acme.zitade.ch", "id2": "caos.ch"} } ] }` | When roles are asserted, ZITADEL does this by providing the `id` and `primaryDomain` below the role. This gives you the option to check in which organization a user has the role. |
| urn:zitadel:iam:roles:{rolename} | TBA | TBA |
| urn:zitadel:iam:user:metadata | `{"urn:zitadel:iam:user:metadata": [ {"key": "VmFsdWU=" } ] }` | The metadata claim will include all metadata of a user. The values are base64 encoded. |
| urn:zitadel:iam:user:resourceowner:id | `{"urn:zitadel:iam:user:resourceowner:id": "orgid"}` | This claim represents the id of the resource owner organisation of the user. |
| urn:zitadel:iam:user:resourceowner:name | `{"urn:zitadel:iam:user:resourceowner:name": "ACME"}` | This claim represents the name of the resource owner organisation of the user. |
| urn:zitadel:iam:user:resourceowner:primary_domain | `{"urn:zitadel:iam:user:resourceowner:primary_domain": "acme.ch"}` | This claim represents the primary domain of the resource owner organisation of the user. |

View File

@ -29,5 +29,6 @@ In addition to the standard compliant scopes we utilize the following scopes.
| 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. |
| urn:zitadel:iam:user:resourceowner | `urn:zitadel:iam:user:resourceowner` | By adding this scope, the resourceowner (id, name, primary_domain) of the user will be included in the token. |
> 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

View File

@ -27,6 +27,8 @@ const (
ClaimProjectRoles = "urn:zitadel:iam:org:project:roles"
ScopeUserMetaData = "urn:zitadel:iam:user:metadata"
ClaimUserMetaData = ScopeUserMetaData
ScopeResourceOwner = "urn:zitadel:iam:user:resourceowner"
ClaimResourceOwner = ScopeResourceOwner + ":"
oidcCtx = "oidc"
)
@ -174,6 +176,23 @@ func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo oidc.Use
continue
}
userInfo.SetAddress(oidc.NewUserInfoAddress(user.StreetAddress, user.Locality, user.Region, user.PostalCode, user.Country, ""))
case ScopeUserMetaData:
userMetaData, err := o.assertUserMetaData(ctx, userID)
if err != nil {
return err
}
if len(userMetaData) > 0 {
userInfo.AppendClaims(ClaimUserMetaData, userMetaData)
}
case ScopeResourceOwner:
resourceOwnerClaims, err := o.assertUserResourceOwner(ctx, userID)
if err != nil {
return err
}
for claim, value := range resourceOwnerClaims {
userInfo.AppendClaims(claim, value)
}
default:
if strings.HasPrefix(scope, ScopeProjectRolePrefix) {
roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix))
@ -183,14 +202,6 @@ 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
}
@ -230,6 +241,24 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection
func (o *OPStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
roles := make([]string, 0)
for _, scope := range scopes {
switch scope {
case ScopeUserMetaData:
userMetaData, err := o.assertUserMetaData(ctx, userID)
if err != nil {
return nil, err
}
if len(userMetaData) > 0 {
claims = appendClaim(claims, ClaimUserMetaData, userMetaData)
}
case ScopeResourceOwner:
resourceOwnerClaims, err := o.assertUserResourceOwner(ctx, userID)
if err != nil {
return nil, err
}
for claim, value := range resourceOwnerClaims {
claims = appendClaim(claims, claim, value)
}
}
if strings.HasPrefix(scope, ScopeProjectRolePrefix) {
roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix))
} else if strings.HasPrefix(scope, model.OrgDomainPrimaryScope) {
@ -246,13 +275,6 @@ 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
}
@ -287,6 +309,18 @@ func (o *OPStorage) assertUserMetaData(ctx context.Context, userID string) (map[
return userMetaData, nil
}
func (o *OPStorage) assertUserResourceOwner(ctx context.Context, userID string) (map[string]string, error) {
resourceOwner, err := o.repo.OrgByUserID(ctx, userID)
if err != nil {
return nil, err
}
return map[string]string{
ClaimResourceOwner + "id": resourceOwner.AggregateID,
ClaimResourceOwner + "name": resourceOwner.Name,
ClaimResourceOwner + "primary_domain": resourceOwner.PrimaryDomain,
}, nil
}
func checkGrantedRoles(roles map[string]map[string]string, grant *grant_model.UserGrantView, requestedRole string) {
for _, grantedRole := range grant.RoleKeys {
if requestedRole == grantedRole {

View File

@ -109,6 +109,9 @@ func (c *Client) IsScopeAllowed(scope string) bool {
if strings.HasPrefix(scope, ScopeUserMetaData) {
return true
}
if strings.HasPrefix(scope, ScopeResourceOwner) {
return true
}
for _, allowedScope := range c.allowedScopes {
if scope == allowedScope {
return true

View File

@ -17,6 +17,7 @@ import (
iam_model "github.com/caos/zitadel/internal/iam/repository/view/model"
key_model "github.com/caos/zitadel/internal/key/model"
key_view_model "github.com/caos/zitadel/internal/key/repository/view/model"
org_model "github.com/caos/zitadel/internal/org/repository/view/model"
"github.com/caos/zitadel/internal/telemetry/tracing"
"github.com/caos/zitadel/internal/user/model"
usr_view "github.com/caos/zitadel/internal/user/repository/view"
@ -307,6 +308,18 @@ func (repo *UserRepo) GetMyMetadataByKey(ctx context.Context, key string) (*doma
return iam_model.MetadataViewToDomain(data), nil
}
func (repo *UserRepo) OrgByUserID(ctx context.Context, userID string) (*domain.Org, error) {
user, err := repo.View.UserByID(userID)
if err != nil {
return nil, err
}
org, err := repo.View.OrgByID(user.ResourceOwner)
if err != nil {
return nil, err
}
return org_model.OrgToDomain(org), nil
}
func (repo *UserRepo) SearchUserMetadata(ctx context.Context, userID string) (*domain.MetadataSearchResponse, error) {
req := new(domain.MetadataSearchRequest)
return repo.searchUserMetadata(userID, "", req)

View File

@ -23,6 +23,7 @@ type UserRepository interface {
SearchUsers(ctx context.Context, request *model.UserSearchRequest) (*model.UserSearchResponse, error)
SearchUserMetadata(ctx context.Context, userID string) (*domain.MetadataSearchResponse, error)
OrgByUserID(ctx context.Context, userID string) (*domain.Org, error)
}
type myUserRepo interface {

View File

@ -2,6 +2,7 @@ package model
import (
"encoding/json"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
"time"
@ -55,6 +56,19 @@ func OrgToModel(org *OrgView) *org_model.OrgView {
}
}
func OrgToDomain(org *OrgView) *domain.Org {
return &domain.Org{
ObjectRoot: es_models.ObjectRoot{
AggregateID: org.ID,
ChangeDate: org.ChangeDate,
CreationDate: org.CreationDate,
Sequence: org.Sequence,
},
Name: org.Name,
PrimaryDomain: org.Domain,
}
}
func OrgsToModel(orgs []*OrgView) []*org_model.OrgView {
modelOrgs := make([]*org_model.OrgView, len(orgs))