diff --git a/console/src/app/modules/machine-keys/machine-keys.component.html b/console/src/app/modules/machine-keys/machine-keys.component.html index 9e24fb2b9e..712bf328ca 100644 --- a/console/src/app/modules/machine-keys/machine-keys.component.html +++ b/console/src/app/modules/machine-keys/machine-keys.component.html @@ -61,7 +61,7 @@ {{ 'USER.MACHINE.EXPIRATIONDATE' | translate }} - {{ key.expirationDate | timestampToDate | localizedDate: 'fromNow' }} + {{ key.expirationDate | timestampToDate | localizedDate: 'EEE dd. MMM YYYY, HH:mm' }} diff --git a/console/src/app/modules/show-key-dialog/show-key-dialog.component.html b/console/src/app/modules/show-key-dialog/show-key-dialog.component.html index a3298c147e..c5b75a0b60 100644 --- a/console/src/app/modules/show-key-dialog/show-key-dialog.component.html +++ b/console/src/app/modules/show-key-dialog/show-key-dialog.component.html @@ -17,6 +17,13 @@

+
+

{{ 'USER.MACHINE.EXPIRATIONDATE' | translate }}

+

+ {{ expirationDate | localizedDate: 'EEE dd. MMM YYYY, HH:mm' }} +

+
+ diff --git a/console/src/app/modules/show-key-dialog/show-key-dialog.component.ts b/console/src/app/modules/show-key-dialog/show-key-dialog.component.ts index e6655ae29b..7466a7d969 100644 --- a/console/src/app/modules/show-key-dialog/show-key-dialog.component.ts +++ b/console/src/app/modules/show-key-dialog/show-key-dialog.component.ts @@ -11,6 +11,7 @@ import { InfoSectionType } from '../info-section/info-section.component'; }) export class ShowKeyDialogComponent { public keyResponse!: AddMachineKeyResponse.AsObject | AddAppKeyResponse.AsObject; + public expirationDate: string = ''; public InfoSectionType: any = InfoSectionType; constructor( @@ -18,6 +19,10 @@ export class ShowKeyDialogComponent { @Inject(MAT_DIALOG_DATA) public data: any, ) { this.keyResponse = data.key; + if (this.keyResponse.keyDetails) { + const keyDetails: { expirationDate: string } = JSON.parse(atob(this.keyResponse.keyDetails.toString())); + this.expirationDate = keyDetails.expirationDate; + } } public saveFile(): void { diff --git a/docs/docs/guides/integrate/private-key-jwt.md b/docs/docs/guides/integrate/private-key-jwt.md index 54288382e9..620293bddf 100644 --- a/docs/docs/guides/integrate/private-key-jwt.md +++ b/docs/docs/guides/integrate/private-key-jwt.md @@ -4,6 +4,7 @@ sidebar_label: Service Users --- This is a guide on how to create service users in ZITADEL. You can read more about users [here](/concepts/structure/users.md). + ## Create a Service User 1. Navigate to Service Users @@ -32,18 +33,22 @@ In this step we will authenticate a service user and receive an access_token to ### 1. Generate a private-public key pair in ZITADEL -Select your service user and in the section KEYS click **New**. Enter an expiration date and click **Add**. Make sure to download the json by clicking **Download**. +Select your service user and in the section KEYS click **New**. Enter an optional expiration date and click **Add**. Make sure to download the json by clicking **Download**. + +:::note +If you specify an expiration date, note that the key will expire at midnight that day +::: ![Create private key](/img/console_serviceusers_new_key.gif) -The downloaded json should look something like outlined below. The value of `key` contains the *private* key for your service account. Please make sure to keep this key securely stored and handle with care. The public key is automatically stored in ZITADEL. +The downloaded json should look something like outlined below. The value of `key` contains the _private_ key for your service account. Please make sure to keep this key securely stored and handle with care. The public key is automatically stored in ZITADEL. ```json { - "type":"serviceaccount", - "keyId":"100509901696068329", - "key":"-----BEGIN RSA PRIVATE KEY----- [...] -----END RSA PRIVATE KEY-----\n", - "userId":"100507859606888466" + "type": "serviceaccount", + "keyId": "100509901696068329", + "key": "-----BEGIN RSA PRIVATE KEY----- [...] -----END RSA PRIVATE KEY-----\n", + "userId": "100507859606888466" } ``` @@ -55,8 +60,8 @@ Header ```json { - "alg": "RS256", - "kid":"100509901696068329" + "alg": "RS256", + "kid": "100509901696068329" } ``` @@ -74,11 +79,11 @@ Payload } ``` -* `iss` represents the requesting party, i.e. the owner of the private key. In this case the value of `userId` from the downloaded JSON. -* `sub` represents the application. Set the value also to the value of `userId` -* `aud` must be ZITADEL's issuing domain -* `iat` is a unix timestamp of the creation signing time of the JWT, e.g. now and must not be older than 1 hour ago -* `exp` is the unix timestamp of expiry of this assertion +- `iss` represents the requesting party, i.e. the owner of the private key. In this case the value of `userId` from the downloaded JSON. +- `sub` represents the application. Set the value also to the value of `userId` +- `aud` must be ZITADEL's issuing domain +- `iat` is a unix timestamp of the creation signing time of the JWT, e.g. now and must not be older than 1 hour ago +- `exp` is the unix timestamp of expiry of this assertion Please refer to [JWT_with_Private_Key](/apis/openidoauth/authn-methods#jwt-with-private-key) in the documentation for further information. @@ -99,11 +104,11 @@ curl --request POST \ If you want to access the ZITADEL API with this access token, you have to add `urn:zitadel:iam:org:project:id:zitadel:aud` to the list of scopes. -* `grant_type` should be set to `urn:ietf:params:oauth:grant-type:jwt-bearer` -* `scope` should contain any [Scopes](/apis/openidoauth/scopes) you want to include, but must include `openid`. For this example, please include `profile` and `email` -* `assertion` is the encoded value of the JWT that was signed with your private key from the prior step +- `grant_type` should be set to `urn:ietf:params:oauth:grant-type:jwt-bearer` +- `scope` should contain any [Scopes](/apis/openidoauth/scopes) you want to include, but must include `openid`. For this example, please include `profile` and `email` +- `assertion` is the encoded value of the JWT that was signed with your private key from the prior step -You should receive a successful response with `access_token`, `token_type` and time to expiry in seconds as `expires_in`. +You should receive a successful response with `access_token`, `token_type` and time to expiry in seconds as `expires_in`. ```bash HTTP/1.1 200 OK @@ -142,11 +147,11 @@ Content-Type: application/json ## Summary -* With service users you can secure machine-to-machine communication -* Because there is no interactive logon, you need to use a JWT signed with your private key to authorize the user -* After successful authorization you can use an access token like for human users +- With service users you can secure machine-to-machine communication +- Because there is no interactive logon, you need to use a JWT signed with your private key to authorize the user +- After successful authorization you can use an access token like for human users Where to go from here: -* Management API -* Securing backend API +- Management API +- Securing backend API diff --git a/internal/command/user_machine_key.go b/internal/command/user_machine_key.go index 1e6ca56f24..d092cc54ef 100644 --- a/internal/command/user_machine_key.go +++ b/internal/command/user_machine_key.go @@ -56,7 +56,7 @@ func (key *MachineKey) Detail() ([]byte, error) { return nil, zerrors.ThrowPreconditionFailed(nil, "KEY-sp2l2m", "Errors.Internal") } if key.Type == domain.AuthNKeyTypeJSON { - return domain.MachineKeyMarshalJSON(key.KeyID, key.PrivateKey, key.AggregateID) + return domain.MachineKeyMarshalJSON(key.KeyID, key.PrivateKey, key.ExpirationDate, key.AggregateID) } return nil, zerrors.ThrowPreconditionFailed(nil, "KEY-dsg52", "Errors.Internal") } diff --git a/internal/domain/machine_key.go b/internal/domain/machine_key.go index ea7777d6da..5be2eb58fe 100644 --- a/internal/domain/machine_key.go +++ b/internal/domain/machine_key.go @@ -42,7 +42,7 @@ func (key *MachineKey) Detail() ([]byte, error) { } func (key *MachineKey) MarshalJSON() ([]byte, error) { - return MachineKeyMarshalJSON(key.KeyID, key.PrivateKey, key.AggregateID) + return MachineKeyMarshalJSON(key.KeyID, key.PrivateKey, key.ExpirationDate, key.AggregateID) } type MachineKeyState int32 @@ -59,16 +59,18 @@ func (f MachineKeyState) Valid() bool { return f >= 0 && f < machineKeyStateCount } -func MachineKeyMarshalJSON(keyID string, privateKey []byte, userID string) ([]byte, error) { +func MachineKeyMarshalJSON(keyID string, privateKey []byte, expirationDate time.Time, userID string) ([]byte, error) { return json.Marshal(struct { - Type string `json:"type"` - KeyID string `json:"keyId"` - Key string `json:"key"` - UserID string `json:"userId"` + Type string `json:"type"` + KeyID string `json:"keyId"` + Key string `json:"key"` + ExpirationDate time.Time `json:"expirationDate"` + UserID string `json:"userId"` }{ - Type: "serviceaccount", - KeyID: keyID, - Key: string(privateKey), - UserID: userID, + Type: "serviceaccount", + KeyID: keyID, + Key: string(privateKey), + ExpirationDate: expirationDate, + UserID: userID, }) }