Skip to content

Commit

Permalink
feat(console): device code (#5771)
Browse files Browse the repository at this point in the history
* feat: device code

* device code, create stepper

* rm logs

* app setup with device code

* remove redirects if grant type is device code only

* add device code app e2e

---------

Co-authored-by: Fabi <fabienne.gerschwiler@gmail.com>
Co-authored-by: Elio Bischof <elio@zitadel.com>
  • Loading branch information
3 people committed May 11, 2023
1 parent 35a0977 commit 2dc016e
Show file tree
Hide file tree
Showing 16 changed files with 212 additions and 46 deletions.
Expand Up @@ -30,19 +30,23 @@
<span class="fill-space"></span>
<div class="app-specs cnsl-secondary-text">
<div class="row" *ngIf="isOIDC && method && method.responseType !== undefined">
<span>{{ 'APP.OIDC.RESPONSETYPE' | translate }}</span>
<span class="row-entry">{{ 'APP.OIDC.RESPONSETYPE' | translate }}</span>
<span>{{ 'APP.OIDC.RESPONSE.' + method.responseType.toString() | translate }}</span>
</div>
<div class="row" *ngIf="isOIDC && method.grantType !== undefined">
<span>{{ 'APP.GRANT' | translate }}</span>
<span>{{ 'APP.OIDC.GRANT.' + method.grantType.toString() | translate }}</span>
<span class="row-entry">{{ 'APP.GRANT' | translate }}</span>
<span
><span class="space" *ngFor="let grant of method.grantType">{{
'APP.OIDC.GRANT.' + grant.toString() | translate
}}</span></span
>
</div>
<div class="row" *ngIf="isOIDC && method.authMethod !== undefined">
<span>{{ 'APP.AUTHMETHOD' | translate }}</span>
<span class="row-entry">{{ 'APP.AUTHMETHOD' | translate }}</span>
<span>{{ 'APP.OIDC.AUTHMETHOD.' + method.authMethod.toString() | translate }}</span>
</div>
<div class="row" *ngIf="!isOIDC && method.apiAuthMethod !== undefined">
<span>{{ 'APP.AUTHMETHOD' | translate }}</span>
<span class="row-entry">{{ 'APP.AUTHMETHOD' | translate }}</span>
<span>{{ 'APP.API.AUTHMETHOD.' + method.apiAuthMethod.toString() | translate }}</span>
</div>
</div>
Expand Down
Expand Up @@ -155,7 +155,11 @@
white-space: nowrap;
}

:first-child {
.space {
margin-left: 0.5rem;
}

.row-entry {
margin-right: 1rem;
overflow: hidden;
text-overflow: ellipsis;
Expand Down
Expand Up @@ -14,7 +14,7 @@ export interface RadioItemAuthType {
prefix: string;
background: string;
responseType?: OIDCResponseType;
grantType?: OIDCGrantType;
grantType?: OIDCGrantType[];
authMethod?: OIDCAuthMethodType;
apiAuthMethod?: APIAuthMethodType;
recommended?: boolean;
Expand Down
Expand Up @@ -58,13 +58,9 @@ <h1>{{ 'APP.PAGES.CREATE_DESC_TITLE' | translate }}</h1>
</form>
</mat-step>

<!-- skip for native OIDC and SAML applications -->
<!-- skip for SAML applications -->
<mat-step
*ngIf="
(appType?.value?.createType === AppCreateType.OIDC &&
appType?.value.oidcAppType !== OIDCAppType.OIDC_APP_TYPE_NATIVE) ||
appType?.value?.createType === AppCreateType.API
"
*ngIf="appType?.value?.createType === AppCreateType.OIDC || appType?.value?.createType === AppCreateType.API"
[stepControl]="secondFormGroup"
[editable]="true"
>
Expand Down Expand Up @@ -93,9 +89,11 @@ <h1>{{ 'APP.PAGES.CREATE_DESC_TITLE' | translate }}</h1>
</div>
</form>
</mat-step>

<!-- show redirect step only for OIDC apps -->
<mat-step *ngIf="appType?.value?.createType === AppCreateType.OIDC" [editable]="true">
<mat-step
*ngIf="appType?.value?.createType === AppCreateType.OIDC && authMethod?.value !== 'DEVICECODE'"
[editable]="true"
>
<ng-template matStepLabel>{{ 'APP.OIDC.REDIRECTSECTION' | translate }}</ng-template>

<p class="step-title">{{ 'APP.OIDC.REDIRECTTITLE' | translate }}</p>
Expand Down Expand Up @@ -431,7 +429,13 @@ <h1>{{ 'APP.PAGES.CREATE_DESC_TITLE' | translate }}</h1>
</ng-container>
</div>

<div class="content" *ngIf="formappType?.value?.createType === AppCreateType.OIDC">
<div
class="content"
*ngIf="
formappType?.value?.createType === AppCreateType.OIDC &&
!(oidcAppRequest.toObject().appType === OIDCAppType.OIDC_APP_TYPE_NATIVE && grantTypesListContainsOnlyDeviceCode)
"
>
<div class="formfield full-width">
<cnsl-redirect-uris
class="redirect-section"
Expand Down
Expand Up @@ -32,6 +32,7 @@ import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog
import {
BASIC_AUTH_METHOD,
CODE_METHOD,
DEVICE_CODE_METHOD,
getPartialConfigFromAuthMethod,
IMPLICIT_METHOD,
PKCE_METHOD,
Expand Down Expand Up @@ -112,6 +113,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
{ type: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, checked: true, disabled: false },
{ type: OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT, checked: false, disabled: true },
{ type: OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN, checked: false, disabled: true },
{ type: OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE, checked: false, disabled: true },
];

public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
Expand Down Expand Up @@ -163,7 +165,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {

switch (this.appType?.value.oidcAppType) {
case OIDCAppType.OIDC_APP_TYPE_NATIVE:
this.authMethods = [PKCE_METHOD];
this.authMethods = [PKCE_METHOD, DEVICE_CODE_METHOD];

// automatically set to PKCE and skip step
this.oidcAppRequest.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE]);
Expand Down Expand Up @@ -473,16 +475,20 @@ export class AppCreateComponent implements OnInit, OnDestroy {
return this.form.get('grantTypesList');
}

get grantTypesListContainsOnlyDeviceCode(): boolean {
return (
this.oidcAppRequest.toObject().grantTypesList.length === 1 &&
this.oidcAppRequest.toObject().grantTypesList[0] === OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE
);
}

get formappType(): AbstractControl | null {
return this.form.get('appType');
}

get formMetadataUrl(): AbstractControl | null {
return this.form.get('metadataUrl');
}
// get formapplicationType(): AbstractControl | null {
// return this.form.get('applicationType');
// }

get authMethodType(): AbstractControl | null {
return this.form.get('authMethodType');
Expand Down
Expand Up @@ -46,6 +46,7 @@ import {
BASIC_AUTH_METHOD,
CODE_METHOD,
CUSTOM_METHOD,
DEVICE_CODE_METHOD,
getAuthMethodFromPartialConfig,
getPartialConfigFromAuthMethod,
IMPLICIT_METHOD,
Expand Down Expand Up @@ -89,6 +90,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public oidcGrantTypes: OIDCGrantType[] = [
OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT,
OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN,
];
public oidcAppTypes: OIDCAppType[] = [
Expand Down Expand Up @@ -274,13 +276,24 @@ export class AppDetailComponent implements OnInit, OnDestroy {
if (this.app.oidcConfig) {
this.getAuthMethodOptions('OIDC');

this.settingsList = [
{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' },
{ id: 'token', i18nKey: 'APP.TOKEN' },
{ id: 'redirect-uris', i18nKey: 'APP.OIDC.REDIRECTSECTIONTITLE' },
{ id: 'additional-origins', i18nKey: 'APP.ADDITIONALORIGINS' },
{ id: 'urls', i18nKey: 'APP.URLS' },
];
if (
this.app.oidcConfig.grantTypesList.length === 1 &&
this.app.oidcConfig.grantTypesList[0] === OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE
) {
this.settingsList = [
{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' },
{ id: 'token', i18nKey: 'APP.TOKEN' },
{ id: 'urls', i18nKey: 'APP.URLS' },
];
} else {
this.settingsList = [
{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' },
{ id: 'token', i18nKey: 'APP.TOKEN' },
{ id: 'redirect-uris', i18nKey: 'APP.OIDC.REDIRECTSECTIONTITLE' },
{ id: 'additional-origins', i18nKey: 'APP.ADDITIONALORIGINS' },
{ id: 'urls', i18nKey: 'APP.URLS' },
];
}

this.initialAuthMethod = this.authMethodFromPartialConfig({ oidc: this.app.oidcConfig });
this.currentAuthMethod = this.initialAuthMethod;
Expand Down Expand Up @@ -381,7 +394,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
if (type === 'OIDC') {
switch (this.app?.oidcConfig?.appType) {
case OIDCAppType.OIDC_APP_TYPE_NATIVE:
this.authMethods = [PKCE_METHOD, CUSTOM_METHOD];
this.authMethods = [PKCE_METHOD, DEVICE_CODE_METHOD, CUSTOM_METHOD];
break;
case OIDCAppType.OIDC_APP_TYPE_WEB:
this.authMethods = [PKCE_METHOD, CODE_METHOD, PK_JWT_METHOD, POST_METHOD];
Expand Down
79 changes: 73 additions & 6 deletions console/src/app/pages/projects/apps/authmethods.ts
Expand Up @@ -16,10 +16,11 @@ export const CODE_METHOD: RadioItemAuthType = {
prefix: 'CODE',
background: 'linear-gradient(40deg, rgb(25 105 143) 30%, rgb(23 95 129))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
grantType: [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC,
recommended: false,
};

export const PKCE_METHOD: RadioItemAuthType = {
key: 'PKCE',
titleI18nKey: 'APP.AUTHMETHODS.PKCE.TITLE',
Expand All @@ -28,10 +29,11 @@ export const PKCE_METHOD: RadioItemAuthType = {
prefix: 'PKCE',
background: 'linear-gradient(40deg, #059669 30%, #047857)',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
grantType: [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
recommended: true,
};

export const POST_METHOD: RadioItemAuthType = {
key: 'POST',
titleI18nKey: 'APP.AUTHMETHODS.POST.TITLE',
Expand All @@ -40,10 +42,11 @@ export const POST_METHOD: RadioItemAuthType = {
prefix: 'POST',
background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
grantType: [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
notRecommended: true,
};

export const PK_JWT_METHOD: RadioItemAuthType = {
key: 'PK_JWT',
titleI18nKey: 'APP.AUTHMETHODS.PK_JWT.TITLE',
Expand All @@ -52,11 +55,12 @@ export const PK_JWT_METHOD: RadioItemAuthType = {
prefix: 'JWT',
background: 'linear-gradient(40deg, rgb(70 77 145) 30%, rgb(58 65 124))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
grantType: [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
apiAuthMethod: APIAuthMethodType.API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
// recommended: true,
};

export const BASIC_AUTH_METHOD: RadioItemAuthType = {
key: 'BASIC',
titleI18nKey: 'APP.AUTHMETHODS.BASIC.TITLE',
Expand All @@ -65,7 +69,7 @@ export const BASIC_AUTH_METHOD: RadioItemAuthType = {
prefix: 'BASIC',
background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
grantType: [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
apiAuthMethod: APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC,
};
Expand All @@ -78,11 +82,24 @@ export const IMPLICIT_METHOD: RadioItemAuthType = {
prefix: 'IMP',
background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_ID_TOKEN,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT,
grantType: [OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT],
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
notRecommended: true,
};

export const DEVICE_CODE_METHOD: RadioItemAuthType = {
key: 'DEVICECODE',
titleI18nKey: 'APP.AUTHMETHODS.DEVICECODE.TITLE',
descI18nKey: 'APP.AUTHMETHODS.DEVICECODE.DESCRIPTION',
disabled: false,
prefix: 'DEVICECODE',
background: 'linear-gradient(40deg, rgb(56 189 248) 30%, rgb(14 165 233))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE],
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC,
recommended: false,
};

export const CUSTOM_METHOD: RadioItemAuthType = {
key: 'CUSTOM',
titleI18nKey: 'APP.AUTHMETHODS.CUSTOM.TITLE',
Expand Down Expand Up @@ -112,6 +129,15 @@ export function getPartialConfigFromAuthMethod(authMethod: string):
},
};
return config;
case DEVICE_CODE_METHOD.key:
config = {
oidc: {
responseTypesList: [OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
grantTypesList: [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE],
authMethodType: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
},
};
return config;
case PKCE_METHOD.key:
config = {
oidc: {
Expand Down Expand Up @@ -211,6 +237,38 @@ export function getAuthMethodFromPartialConfig(config: {
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
]);

const deviceCode = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);

const deviceCodeWithCode = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[
OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE,
// OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN,
],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);

const deviceCodeWithCodeAndRefresh = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[
OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN,
],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);

const deviceCodeWithRefresh = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE, OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);

const pkjwt = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
Expand Down Expand Up @@ -245,6 +303,15 @@ export function getAuthMethodFromPartialConfig(config: {
case postWithRefresh:
return POST_METHOD.key;

case deviceCode:
return DEVICE_CODE_METHOD.key;
case deviceCodeWithCode:
return DEVICE_CODE_METHOD.key;
case deviceCodeWithRefresh:
return DEVICE_CODE_METHOD.key;
case deviceCodeWithCodeAndRefresh:
return DEVICE_CODE_METHOD.key;

case pkjwt:
return PK_JWT_METHOD.key;
case pkjwtWithRefresh:
Expand Down
7 changes: 6 additions & 1 deletion console/src/assets/i18n/de.json
Expand Up @@ -1965,7 +1965,8 @@
"GRANT": {
"0": "Authorization Code",
"1": "Implicit",
"2": "Refresh Token"
"2": "Refresh Token",
"3": "Device Code"
},
"AUTHMETHOD": {
"0": "Basic",
Expand Down Expand Up @@ -2056,6 +2057,10 @@
"TITLE": "Implicit",
"DESCRIPTION": "Erhalte die Token direkt vom authorize Endpoint"
},
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "Autorisieren Sie das Gerät auf einem Computer oder Smartphone."
},
"CUSTOM": {
"TITLE": "Custom",
"DESCRIPTION": "Deine Konfiguration entspricht keiner anderen Option."
Expand Down

1 comment on commit 2dc016e

@vercel
Copy link

@vercel vercel bot commented on 2dc016e May 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./

docs-zitadel.vercel.app
docs-git-main-zitadel.vercel.app
zitadel-docs.vercel.app

Please sign in to comment.