feat: add ZITADEL project id scope (#4146)

* feat: add ZITADEL project id scope

* update documentation

* documentation

* fix scopes

* change to lowercase
This commit is contained in:
Livio Spring 2022-08-09 09:45:59 +02:00 committed by GitHub
parent 1b5c8677ab
commit 02d2032790
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 71 additions and 73 deletions

View File

@ -6,13 +6,13 @@ ZITADEL supports the usage of scopes as way of requesting information from the I
## Standard Scopes
| Scopes | Example | Description |
|:---------------|:-----------------|--------------------------------------------------------------------------------|
| openid | `openid` | When using openid connect this is a mandatory scope |
| profile | `profile` | Optional scope to request the profile of the subject |
| email | `email` | Optional scope to request the email of the subject |
| address | `address` | Optional scope to request the address of the subject |
| offline_access | `offline_access` | Optional scope to request a refresh_token (only possible when using code flow) |
| Scopes | Description |
|:---------------|--------------------------------------------------------------------------------|
| openid | When using openid connect this is a mandatory scope |
| profile | Optional scope to request the profile of the subject |
| email | Optional scope to request the email of the subject |
| address | Optional scope to request the address of the subject |
| offline_access | Optional scope to request a refresh_token (only possible when using code flow) |
## Custom Scopes
@ -22,14 +22,13 @@ ZITADEL supports the usage of scopes as way of requesting information from the I
In addition to the standard compliant scopes we utilize the following scopes.
| Scopes | Example | Description |
|:-------------------------------------------------|:-------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| urn:zitadel:iam:org:project:role:{rolename} | `urn:zitadel:iam:org:project:role:user` | By using this scope a client can request the claim urn:zitadel:iam:roles:rolename} to be asserted when possible. As an alternative approach you can enable all roles to be asserted from the [project](../../guides/manage/console/projects) a client belongs to. |
| 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. |
| 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. |
| urn:zitadel:iam:org:idp:id:{idp_id} | `urn:zitadel:iam:org:idp:id:76625965177954913` | By adding this scope the user will directly be redirected to the identity provider to authenticate. Make sure you also send the primary domain scope if a custom login policy is configured. Otherwise the system will not be able to identify the identity provider. |
> 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
| Scopes | Example | Description |
|:--------------------------------------------------|:-------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `urn:zitadel:iam:org:project:role:{rolename}` | `urn:zitadel:iam:org:project:role:user` | By using this scope a client can request the claim urn:zitadel:iam:roles:rolename} to be asserted when possible. As an alternative approach you can enable all roles to be asserted from the [project](../../guides/manage/console/projects) a client belongs to. |
| `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` | `urn:zitadel:iam:org:project:id:69234237810729019:aud` | By adding this scope, the requested projectid will be added to the audience of the access token |
| `urn:zitadel:iam:org:project:id:zitadel:aud` | `urn:zitadel:iam:org:project:id:zitadel:aud` | By adding this scope, the ZITADEL project ID will be added to the audience of the access 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. |
| `urn:zitadel:iam:org:idp:id:{idp_id}` | `urn:zitadel:iam:org:idp:id:76625965177954913` | By adding this scope the user will directly be redirected to the identity provider to authenticate. Make sure you also send the primary domain scope if a custom login policy is configured. Otherwise the system will not be able to identify the identity provider. |

View File

@ -31,11 +31,14 @@ go get github.com/zitadel/zitadel-go/v2
### Create example client
Create a new go file with the content below. This will create a client for the management api and call its `GetMyOrg` function.
The SDK will make sure you will have access to the API by retrieving a Bearer Token using JWT Profile with the provided scopes (`openid` and `urn:zitadel:iam:org:project:id:{projectID}:aud`).
Make sure to fill the vars `issuer`, `api`, `projectID `and `orgID`
The SDK will make sure you will have access to the API by retrieving a Bearer Token using JWT Profile with the provided scopes (`openid` and `urn:zitadel:iam:org:project:id:zitadel:aud`).
Make sure to fill the vars `issuer` and `api`.
The issuer and api is the domain of your instance you can find it on the instance detail in the ZITADEL Cloud Customer Portal or in the ZITADEL Console.
The projectID you will find in the ZITADEL project in the first organization of your instance and the orgID on the first organization.
:::note
The issuer will require the protocol (`https://` and `http://`) and you will only have to specify a port if they're not default (443 for https and 80 for http). The API will always require a port, but no protocol.
:::
```go
package main
@ -54,43 +57,44 @@ import (
)
var (
issuer = flag.String("issuer", "", "issuer of your ZITADEL instance (in the form: https://<instance>.zitadel.cloud or https://<yourdomain>)")
api = flag.String("api", "", "gRPC endpoint of your ZITADEL instance (in the form: <instance>.zitadel.cloud:443 or <yourdomain>:443)")
projectID = flag.String("projectID", "", "ZITADEL projectID in your instance")
orgID = flag.String("orgID", "", "orgID to set for overwrite example")
issuer = flag.String("issuer", "", "issuer of your ZITADEL instance (in the form: https://<instance>.zitadel.cloud or https://<yourdomain>)")
api = flag.String("api", "", "gRPC endpoint of your ZITADEL instance (in the form: <instance>.zitadel.cloud:443 or <yourdomain>:443)")
)
func main() {
flag.Parse()
flag.Parse()
client, err := management.NewClient(
*issuer,
*api,
[]string{oidc.ScopeOpenID, zitadel.ScopeProjectID(*projectID)},
//create a client for the management api providing:
//- issuer (e.g. https://acme-dtfhdg.zitadel.cloud)
//- api (e.g. acme-dtfhdg.zitadel.cloud:443)
//- scopes (including the ZITADEL project ID),
//- a JWT Profile token source (e.g. path to your key json), if not provided, the file will be read from the path set in env var ZITADEL_KEY_PATH
client, err := management.NewClient(
*issuer,
*api,
[]string{oidc.ScopeOpenID, zitadel.ScopeZitadelAPI()},
)
if err != nil {
log.Fatalln("could not create client", err)
}
defer func() {
err := client.Connection.Close()
if err != nil {
log.Println("could not close grpc connection", err)
}
}()
ctx := context.Background()
resp, err := client.GetMyOrg(ctx, &pb.GetMyOrgRequest{})
if err != nil {
log.Fatalln("call failed: ", err)
}
log.Printf("%s was created on: %s", resp.Org.Name, resp.Org.Details.CreationDate.AsTime())
respOverwrite, err := client.GetMyOrg(middleware.SetOrgID(ctx, *orgID), &pb.GetMyOrgRequest{})
if err != nil {
log.Fatalln("call failed: ", err)
}
log.Printf("%s was created on: %s", respOverwrite.Org.Name, respOverwrite.Org.Details.CreationDate.AsTime())
if err != nil {
log.Fatalln("could not create client", err)
}
defer func() {
err := client.Connection.Close()
if err != nil {
log.Println("could not close grpc connection", err)
}
}()
ctx := context.Background()
//call ZITADEL and print the name and creation date of your organisation
//the call was successful if no error occurred
resp, err := client.GetMyOrg(ctx, &pb.GetMyOrgRequest{})
if err != nil {
log.Fatalln("call failed: ", err)
}
log.Printf("%s was created on: %s", resp.Org.Name, resp.Org.Details.CreationDate.AsTime())
}
```
#### Key JSON

View File

@ -41,14 +41,14 @@ With the encoded JWT from the prior step, you will need to craft a POST request
To access the ZITADEL APIs you need the ZITADEL Project ID in the audience of your token.
This is possible by sending a custom scope for the audience. More about [Custom Scopes](../../apis/openidoauth/scopes)
Use the scope `urn:zitadel:iam:org:project:id:{projectid}:aud` to include the project id in your audience
Use the scope `urn:zitadel:iam:org:project:id:zitadel:aud` to include the ZITADEL project id in your audience
```bash
curl --request POST \
--url {your_domain}/oauth/v2/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
--data scope='openid profile email urn:zitadel:iam:org:project:id:69234237810729019:aud' \
--data scope='openid profile email urn:zitadel:iam:org:project:id:zitadel:aud' \
--data assertion=eyJ0eXAiOiJKV1QiL...
```
@ -75,7 +75,7 @@ With this token you are allowed to access the [ZITADEL APIs](../../apis/introduc
* Grant a user for ZITADEL
* Because there is no interactive logon, you need to use a JWT signed with your private key to authorize the user
* With a custom scope (`urn:zitadel:iam:org:project:id:{projectid}:aud`) you can access ZITADEL APIs
* With a custom scope (`urn:zitadel:iam:org:project:id:zitadel:aud`) you can access ZITADEL APIs
Where to go from here:

View File

@ -263,7 +263,7 @@ func (c *Commands) addUserToken(ctx context.Context, userWriteModel *UserWriteMo
return nil, nil, caos_errs.ThrowNotFound(nil, "COMMAND-1d6Gg", "Errors.User.NotFound")
}
audience = domain.AddAudScopeToAudience(audience, scopes)
audience = domain.AddAudScopeToAudience(ctx, audience, scopes)
preferredLanguage := ""
existingHuman, err := c.getHumanWriteModelByID(ctx, userWriteModel.AggregateID, userWriteModel.ResourceOwner)

View File

@ -151,19 +151,6 @@ func (a *AuthRequest) AppendAudIfNotExisting(aud string) {
a.Audience = append(a.Audience, aud)
}
func (a *AuthRequest) GetScopeProjectIDsForAud() []string {
projectIDs := make([]string, 0)
switch request := a.Request.(type) {
case *AuthRequestOIDC:
for _, scope := range request.Scopes {
if strings.HasPrefix(scope, ProjectIDScope) && strings.HasSuffix(scope, AudSuffix) {
projectIDs = append(projectIDs, strings.TrimSuffix(strings.TrimPrefix(scope, ProjectIDScope), AudSuffix))
}
}
}
return projectIDs
}
func (a *AuthRequest) GetScopeOrgPrimaryDomain() string {
switch request := a.Request.(type) {
case *AuthRequestOIDC:

View File

@ -4,6 +4,7 @@ const (
OrgDomainPrimaryScope = "urn:zitadel:iam:org:domain:primary:"
OrgDomainPrimaryClaim = "urn:zitadel:iam:org:domain:primary"
ProjectIDScope = "urn:zitadel:iam:org:project:id:"
ProjectIDScopeZITADEL = "zitadel"
AudSuffix = ":aud"
SelectIDPScope = "urn:zitadel:iam:org:idp:id:"
)

View File

@ -1,9 +1,11 @@
package domain
import (
"context"
"strings"
"time"
"github.com/zitadel/zitadel/internal/api/authz"
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
@ -20,11 +22,16 @@ type Token struct {
PreferredLanguage string
}
func AddAudScopeToAudience(audience, scopes []string) []string {
func AddAudScopeToAudience(ctx context.Context, audience, scopes []string) []string {
for _, scope := range scopes {
if strings.HasPrefix(scope, ProjectIDScope) && strings.HasSuffix(scope, AudSuffix) {
audience = append(audience, strings.TrimSuffix(strings.TrimPrefix(scope, ProjectIDScope), AudSuffix))
if !(strings.HasPrefix(scope, ProjectIDScope) && strings.HasSuffix(scope, AudSuffix)) {
continue
}
projectID := strings.TrimSuffix(strings.TrimPrefix(scope, ProjectIDScope), AudSuffix)
if projectID == ProjectIDScopeZITADEL {
projectID = authz.GetInstance(ctx).ProjectID()
}
audience = append(audience, projectID)
}
return audience
}