feat(console): device code (#5771)

* 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>
This commit is contained in:
Max Peintner 2023-05-11 10:18:14 +02:00 committed by GitHub
parent 35a0977663
commit 2dc016ea3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 212 additions and 46 deletions

View File

@ -30,19 +30,23 @@
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="app-specs cnsl-secondary-text"> <div class="app-specs cnsl-secondary-text">
<div class="row" *ngIf="isOIDC && method && method.responseType !== undefined"> <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> <span>{{ 'APP.OIDC.RESPONSE.' + method.responseType.toString() | translate }}</span>
</div> </div>
<div class="row" *ngIf="isOIDC && method.grantType !== undefined"> <div class="row" *ngIf="isOIDC && method.grantType !== undefined">
<span>{{ 'APP.GRANT' | translate }}</span> <span class="row-entry">{{ 'APP.GRANT' | translate }}</span>
<span>{{ 'APP.OIDC.GRANT.' + method.grantType.toString() | translate }}</span> <span
><span class="space" *ngFor="let grant of method.grantType">{{
'APP.OIDC.GRANT.' + grant.toString() | translate
}}</span></span
>
</div> </div>
<div class="row" *ngIf="isOIDC && method.authMethod !== undefined"> <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> <span>{{ 'APP.OIDC.AUTHMETHOD.' + method.authMethod.toString() | translate }}</span>
</div> </div>
<div class="row" *ngIf="!isOIDC && method.apiAuthMethod !== undefined"> <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> <span>{{ 'APP.API.AUTHMETHOD.' + method.apiAuthMethod.toString() | translate }}</span>
</div> </div>
</div> </div>

View File

@ -155,7 +155,11 @@
white-space: nowrap; white-space: nowrap;
} }
:first-child { .space {
margin-left: 0.5rem;
}
.row-entry {
margin-right: 1rem; margin-right: 1rem;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@ -14,7 +14,7 @@ export interface RadioItemAuthType {
prefix: string; prefix: string;
background: string; background: string;
responseType?: OIDCResponseType; responseType?: OIDCResponseType;
grantType?: OIDCGrantType; grantType?: OIDCGrantType[];
authMethod?: OIDCAuthMethodType; authMethod?: OIDCAuthMethodType;
apiAuthMethod?: APIAuthMethodType; apiAuthMethod?: APIAuthMethodType;
recommended?: boolean; recommended?: boolean;

View File

@ -58,13 +58,9 @@
</form> </form>
</mat-step> </mat-step>
<!-- skip for native OIDC and SAML applications --> <!-- skip for SAML applications -->
<mat-step <mat-step
*ngIf=" *ngIf="appType?.value?.createType === AppCreateType.OIDC || appType?.value?.createType === AppCreateType.API"
(appType?.value?.createType === AppCreateType.OIDC &&
appType?.value.oidcAppType !== OIDCAppType.OIDC_APP_TYPE_NATIVE) ||
appType?.value?.createType === AppCreateType.API
"
[stepControl]="secondFormGroup" [stepControl]="secondFormGroup"
[editable]="true" [editable]="true"
> >
@ -93,9 +89,11 @@
</div> </div>
</form> </form>
</mat-step> </mat-step>
<!-- show redirect step only for OIDC apps --> <!-- 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> <ng-template matStepLabel>{{ 'APP.OIDC.REDIRECTSECTION' | translate }}</ng-template>
<p class="step-title">{{ 'APP.OIDC.REDIRECTTITLE' | translate }}</p> <p class="step-title">{{ 'APP.OIDC.REDIRECTTITLE' | translate }}</p>
@ -431,7 +429,13 @@
</ng-container> </ng-container>
</div> </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"> <div class="formfield full-width">
<cnsl-redirect-uris <cnsl-redirect-uris
class="redirect-section" class="redirect-section"

View File

@ -32,6 +32,7 @@ import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog
import { import {
BASIC_AUTH_METHOD, BASIC_AUTH_METHOD,
CODE_METHOD, CODE_METHOD,
DEVICE_CODE_METHOD,
getPartialConfigFromAuthMethod, getPartialConfigFromAuthMethod,
IMPLICIT_METHOD, IMPLICIT_METHOD,
PKCE_METHOD, PKCE_METHOD,
@ -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_AUTHORIZATION_CODE, checked: true, disabled: false },
{ type: OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT, checked: false, disabled: true }, { 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_REFRESH_TOKEN, checked: false, disabled: true },
{ type: OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE, checked: false, disabled: true },
]; ];
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE]; public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
@ -163,7 +165,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
switch (this.appType?.value.oidcAppType) { switch (this.appType?.value.oidcAppType) {
case OIDCAppType.OIDC_APP_TYPE_NATIVE: case OIDCAppType.OIDC_APP_TYPE_NATIVE:
this.authMethods = [PKCE_METHOD]; this.authMethods = [PKCE_METHOD, DEVICE_CODE_METHOD];
// automatically set to PKCE and skip step // automatically set to PKCE and skip step
this.oidcAppRequest.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE]); this.oidcAppRequest.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE]);
@ -473,6 +475,13 @@ export class AppCreateComponent implements OnInit, OnDestroy {
return this.form.get('grantTypesList'); 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 { get formappType(): AbstractControl | null {
return this.form.get('appType'); return this.form.get('appType');
} }
@ -480,9 +489,6 @@ export class AppCreateComponent implements OnInit, OnDestroy {
get formMetadataUrl(): AbstractControl | null { get formMetadataUrl(): AbstractControl | null {
return this.form.get('metadataUrl'); return this.form.get('metadataUrl');
} }
// get formapplicationType(): AbstractControl | null {
// return this.form.get('applicationType');
// }
get authMethodType(): AbstractControl | null { get authMethodType(): AbstractControl | null {
return this.form.get('authMethodType'); return this.form.get('authMethodType');

View File

@ -46,6 +46,7 @@ import {
BASIC_AUTH_METHOD, BASIC_AUTH_METHOD,
CODE_METHOD, CODE_METHOD,
CUSTOM_METHOD, CUSTOM_METHOD,
DEVICE_CODE_METHOD,
getAuthMethodFromPartialConfig, getAuthMethodFromPartialConfig,
getPartialConfigFromAuthMethod, getPartialConfigFromAuthMethod,
IMPLICIT_METHOD, IMPLICIT_METHOD,
@ -89,6 +90,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public oidcGrantTypes: OIDCGrantType[] = [ public oidcGrantTypes: OIDCGrantType[] = [
OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT, OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT,
OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN, OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN,
]; ];
public oidcAppTypes: OIDCAppType[] = [ public oidcAppTypes: OIDCAppType[] = [
@ -274,13 +276,24 @@ export class AppDetailComponent implements OnInit, OnDestroy {
if (this.app.oidcConfig) { if (this.app.oidcConfig) {
this.getAuthMethodOptions('OIDC'); this.getAuthMethodOptions('OIDC');
this.settingsList = [ if (
{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' }, this.app.oidcConfig.grantTypesList.length === 1 &&
{ id: 'token', i18nKey: 'APP.TOKEN' }, this.app.oidcConfig.grantTypesList[0] === OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE
{ id: 'redirect-uris', i18nKey: 'APP.OIDC.REDIRECTSECTIONTITLE' }, ) {
{ id: 'additional-origins', i18nKey: 'APP.ADDITIONALORIGINS' }, this.settingsList = [
{ id: 'urls', i18nKey: 'APP.URLS' }, { 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.initialAuthMethod = this.authMethodFromPartialConfig({ oidc: this.app.oidcConfig });
this.currentAuthMethod = this.initialAuthMethod; this.currentAuthMethod = this.initialAuthMethod;
@ -381,7 +394,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
if (type === 'OIDC') { if (type === 'OIDC') {
switch (this.app?.oidcConfig?.appType) { switch (this.app?.oidcConfig?.appType) {
case OIDCAppType.OIDC_APP_TYPE_NATIVE: case OIDCAppType.OIDC_APP_TYPE_NATIVE:
this.authMethods = [PKCE_METHOD, CUSTOM_METHOD]; this.authMethods = [PKCE_METHOD, DEVICE_CODE_METHOD, CUSTOM_METHOD];
break; break;
case OIDCAppType.OIDC_APP_TYPE_WEB: case OIDCAppType.OIDC_APP_TYPE_WEB:
this.authMethods = [PKCE_METHOD, CODE_METHOD, PK_JWT_METHOD, POST_METHOD]; this.authMethods = [PKCE_METHOD, CODE_METHOD, PK_JWT_METHOD, POST_METHOD];

View File

@ -16,10 +16,11 @@ export const CODE_METHOD: RadioItemAuthType = {
prefix: 'CODE', prefix: 'CODE',
background: 'linear-gradient(40deg, rgb(25 105 143) 30%, rgb(23 95 129))', background: 'linear-gradient(40deg, rgb(25 105 143) 30%, rgb(23 95 129))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE, 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, authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC,
recommended: false, recommended: false,
}; };
export const PKCE_METHOD: RadioItemAuthType = { export const PKCE_METHOD: RadioItemAuthType = {
key: 'PKCE', key: 'PKCE',
titleI18nKey: 'APP.AUTHMETHODS.PKCE.TITLE', titleI18nKey: 'APP.AUTHMETHODS.PKCE.TITLE',
@ -28,10 +29,11 @@ export const PKCE_METHOD: RadioItemAuthType = {
prefix: 'PKCE', prefix: 'PKCE',
background: 'linear-gradient(40deg, #059669 30%, #047857)', background: 'linear-gradient(40deg, #059669 30%, #047857)',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE, 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, authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
recommended: true, recommended: true,
}; };
export const POST_METHOD: RadioItemAuthType = { export const POST_METHOD: RadioItemAuthType = {
key: 'POST', key: 'POST',
titleI18nKey: 'APP.AUTHMETHODS.POST.TITLE', titleI18nKey: 'APP.AUTHMETHODS.POST.TITLE',
@ -40,10 +42,11 @@ export const POST_METHOD: RadioItemAuthType = {
prefix: 'POST', prefix: 'POST',
background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))', background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE, 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, authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
notRecommended: true, notRecommended: true,
}; };
export const PK_JWT_METHOD: RadioItemAuthType = { export const PK_JWT_METHOD: RadioItemAuthType = {
key: 'PK_JWT', key: 'PK_JWT',
titleI18nKey: 'APP.AUTHMETHODS.PK_JWT.TITLE', titleI18nKey: 'APP.AUTHMETHODS.PK_JWT.TITLE',
@ -52,11 +55,12 @@ export const PK_JWT_METHOD: RadioItemAuthType = {
prefix: 'JWT', prefix: 'JWT',
background: 'linear-gradient(40deg, rgb(70 77 145) 30%, rgb(58 65 124))', background: 'linear-gradient(40deg, rgb(70 77 145) 30%, rgb(58 65 124))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE, 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, authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
apiAuthMethod: APIAuthMethodType.API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, apiAuthMethod: APIAuthMethodType.API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
// recommended: true, // recommended: true,
}; };
export const BASIC_AUTH_METHOD: RadioItemAuthType = { export const BASIC_AUTH_METHOD: RadioItemAuthType = {
key: 'BASIC', key: 'BASIC',
titleI18nKey: 'APP.AUTHMETHODS.BASIC.TITLE', titleI18nKey: 'APP.AUTHMETHODS.BASIC.TITLE',
@ -65,7 +69,7 @@ export const BASIC_AUTH_METHOD: RadioItemAuthType = {
prefix: 'BASIC', prefix: 'BASIC',
background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))', background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE, 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, authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
apiAuthMethod: APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC, apiAuthMethod: APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC,
}; };
@ -78,11 +82,24 @@ export const IMPLICIT_METHOD: RadioItemAuthType = {
prefix: 'IMP', prefix: 'IMP',
background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))', background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_ID_TOKEN, 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, authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
notRecommended: true, 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 = { export const CUSTOM_METHOD: RadioItemAuthType = {
key: 'CUSTOM', key: 'CUSTOM',
titleI18nKey: 'APP.AUTHMETHODS.CUSTOM.TITLE', titleI18nKey: 'APP.AUTHMETHODS.CUSTOM.TITLE',
@ -112,6 +129,15 @@ export function getPartialConfigFromAuthMethod(authMethod: string):
}, },
}; };
return config; 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: case PKCE_METHOD.key:
config = { config = {
oidc: { oidc: {
@ -211,6 +237,38 @@ export function getAuthMethodFromPartialConfig(config: {
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST, 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([ const pkjwt = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE], [OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE], [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
@ -245,6 +303,15 @@ export function getAuthMethodFromPartialConfig(config: {
case postWithRefresh: case postWithRefresh:
return POST_METHOD.key; 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: case pkjwt:
return PK_JWT_METHOD.key; return PK_JWT_METHOD.key;
case pkjwtWithRefresh: case pkjwtWithRefresh:

View File

@ -1965,7 +1965,8 @@
"GRANT": { "GRANT": {
"0": "Authorization Code", "0": "Authorization Code",
"1": "Implicit", "1": "Implicit",
"2": "Refresh Token" "2": "Refresh Token",
"3": "Device Code"
}, },
"AUTHMETHOD": { "AUTHMETHOD": {
"0": "Basic", "0": "Basic",
@ -2056,6 +2057,10 @@
"TITLE": "Implicit", "TITLE": "Implicit",
"DESCRIPTION": "Erhalte die Token direkt vom authorize Endpoint" "DESCRIPTION": "Erhalte die Token direkt vom authorize Endpoint"
}, },
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "Autorisieren Sie das Gerät auf einem Computer oder Smartphone."
},
"CUSTOM": { "CUSTOM": {
"TITLE": "Custom", "TITLE": "Custom",
"DESCRIPTION": "Deine Konfiguration entspricht keiner anderen Option." "DESCRIPTION": "Deine Konfiguration entspricht keiner anderen Option."

View File

@ -1962,7 +1962,8 @@
"GRANT": { "GRANT": {
"0": "Authorization Code", "0": "Authorization Code",
"1": "Implicit", "1": "Implicit",
"2": "Refresh Token" "2": "Refresh Token",
"3": "Device Code"
}, },
"AUTHMETHOD": { "AUTHMETHOD": {
"0": "Basic", "0": "Basic",
@ -2053,6 +2054,10 @@
"TITLE": "Implicit", "TITLE": "Implicit",
"DESCRIPTION": "Get the tokens directly from the authorization endpoint" "DESCRIPTION": "Get the tokens directly from the authorization endpoint"
}, },
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "Authorize the device on a computer or smartphone."
},
"CUSTOM": { "CUSTOM": {
"TITLE": "Custom", "TITLE": "Custom",
"DESCRIPTION": "Your setting doesn't correspond to any other option." "DESCRIPTION": "Your setting doesn't correspond to any other option."

View File

@ -1962,7 +1962,8 @@
"GRANT": { "GRANT": {
"0": "Código de autorización", "0": "Código de autorización",
"1": "Implícito", "1": "Implícito",
"2": "Token de refresco" "2": "Token de refresco",
"3": "Device Code"
}, },
"AUTHMETHOD": { "AUTHMETHOD": {
"0": "Básico", "0": "Básico",
@ -2053,6 +2054,10 @@
"TITLE": "Implícita", "TITLE": "Implícita",
"DESCRIPTION": "Obtén los tokens directamente del endpoint de autorización" "DESCRIPTION": "Obtén los tokens directamente del endpoint de autorización"
}, },
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "Autorizar el dispositivo en una computadora o teléfono."
},
"CUSTOM": { "CUSTOM": {
"TITLE": "Personalizada", "TITLE": "Personalizada",
"DESCRIPTION": "Tu configuración no se corresponde con alguna de las otras opciones." "DESCRIPTION": "Tu configuración no se corresponde con alguna de las otras opciones."

View File

@ -1966,7 +1966,8 @@
"GRANT": { "GRANT": {
"0": "Code d'autorisation", "0": "Code d'autorisation",
"1": "Implicite", "1": "Implicite",
"2": "Rafraîchir le jeton" "2": "Rafraîchir le jeton",
"3": "Device Code"
}, },
"AUTHMETHOD": { "AUTHMETHOD": {
"0": "Basic", "0": "Basic",
@ -2045,6 +2046,10 @@
"TITLE": "Implicite", "TITLE": "Implicite",
"DESCRIPTION": "Obtenir les jetons directement à partir du point final d'autorisation" "DESCRIPTION": "Obtenir les jetons directement à partir du point final d'autorisation"
}, },
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "Autoriser l'appareil sur un ordinateur ou un smartphone."
},
"CUSTOM": { "CUSTOM": {
"TITLE": "Personnalisé", "TITLE": "Personnalisé",
"DESCRIPTION": "Votre paramètre ne correspond à aucune autre option." "DESCRIPTION": "Votre paramètre ne correspond à aucune autre option."

View File

@ -1967,7 +1967,8 @@
"GRANT": { "GRANT": {
"0": "Authorization Code", "0": "Authorization Code",
"1": "Implicit", "1": "Implicit",
"2": "Refresh Token" "2": "Refresh Token",
"3": "Device Code"
}, },
"AUTHMETHOD": { "AUTHMETHOD": {
"0": "Basic", "0": "Basic",
@ -2058,6 +2059,10 @@
"TITLE": "Implicit", "TITLE": "Implicit",
"DESCRIPTION": "Ottenere i token direttamente dall'endpoint di autorizzazione" "DESCRIPTION": "Ottenere i token direttamente dall'endpoint di autorizzazione"
}, },
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "Autorizza il dispositivo su un computer o uno smartphone."
},
"CUSTOM": { "CUSTOM": {
"TITLE": "Custom", "TITLE": "Custom",
"DESCRIPTION": "La tua impostazione non corrisponde a nessun'altra opzione." "DESCRIPTION": "La tua impostazione non corrisponde a nessun'altra opzione."

View File

@ -1957,7 +1957,8 @@
"GRANT": { "GRANT": {
"0": "Authorization Code", "0": "Authorization Code",
"1": "Implicit", "1": "Implicit",
"2": "Refresh Token" "2": "Refresh Token",
"3": "Device Code"
}, },
"AUTHMETHOD": { "AUTHMETHOD": {
"0": "Basic", "0": "Basic",
@ -2048,6 +2049,10 @@
"TITLE": "Implicit", "TITLE": "Implicit",
"DESCRIPTION": "認証エンドポイントから直接トークンを取得します。" "DESCRIPTION": "認証エンドポイントから直接トークンを取得します。"
}, },
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "コンピューターまたはスマートフォンでデバイスを認証します。"
},
"CUSTOM": { "CUSTOM": {
"TITLE": "Custom", "TITLE": "Custom",
"DESCRIPTION": "設定は他のオプションに対応していません。" "DESCRIPTION": "設定は他のオプションに対応していません。"

View File

@ -1966,7 +1966,8 @@
"GRANT": { "GRANT": {
"0": "Kod autoryzacyjny", "0": "Kod autoryzacyjny",
"1": "Implicite", "1": "Implicite",
"2": "Token odświeżający" "2": "Token odświeżający",
"3": "Device Code"
}, },
"AUTHMETHOD": { "AUTHMETHOD": {
"0": "Podstawowy", "0": "Podstawowy",
@ -2057,6 +2058,10 @@
"TITLE": "Implicit", "TITLE": "Implicit",
"DESCRIPTION": "Pobierz tokeny bezpośrednio z punktu autoryzacyjnego" "DESCRIPTION": "Pobierz tokeny bezpośrednio z punktu autoryzacyjnego"
}, },
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "Autoryzuj urządzenie na komputerze lub smartfonie."
},
"CUSTOM": { "CUSTOM": {
"TITLE": "Niestandardowy", "TITLE": "Niestandardowy",
"DESCRIPTION": "Twoje ustawienie nie odpowiada żadnej innej opcji." "DESCRIPTION": "Twoje ustawienie nie odpowiada żadnej innej opcji."

View File

@ -1965,7 +1965,8 @@
"GRANT": { "GRANT": {
"0": "Authorization Code", "0": "Authorization Code",
"1": "Implicit", "1": "Implicit",
"2": "Refresh Token" "2": "Refresh Token",
"3": "Device Code"
}, },
"AUTHMETHOD": { "AUTHMETHOD": {
"0": "Basic", "0": "Basic",
@ -2044,6 +2045,10 @@
"TITLE": "Implicit", "TITLE": "Implicit",
"DESCRIPTION": "直接从授权端点获取令牌" "DESCRIPTION": "直接从授权端点获取令牌"
}, },
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "在计算机或智能手机上授权设备。"
},
"CUSTOM": { "CUSTOM": {
"TITLE": "Custom", "TITLE": "Custom",
"DESCRIPTION": "您的设置与任何其他选项都不对应。" "DESCRIPTION": "您的设置与任何其他选项都不对应。"

View File

@ -2,7 +2,8 @@ import { Apps, ensureProjectExists, ensureProjectResourceDoesntExist } from '../
import { Context } from 'support/commands'; import { Context } from 'support/commands';
const testProjectName = 'e2eprojectapplication'; const testProjectName = 'e2eprojectapplication';
const testAppName = 'e2eappundertest'; const testPKCEAppName = 'e2eapppkcetest';
const testDEVICECODEAppName = 'e2eappdevicecodetest';
describe('applications', () => { describe('applications', () => {
beforeEach(() => { beforeEach(() => {
@ -17,15 +18,15 @@ describe('applications', () => {
beforeEach(`ensure it doesn't exist already`, () => { beforeEach(`ensure it doesn't exist already`, () => {
cy.get<Context>('@ctx').then((ctx) => { cy.get<Context>('@ctx').then((ctx) => {
cy.get<string>('@projectId').then((projectId) => { cy.get<string>('@projectId').then((projectId) => {
ensureProjectResourceDoesntExist(ctx.api, projectId, Apps, testAppName); ensureProjectResourceDoesntExist(ctx.api, projectId, Apps, testPKCEAppName);
cy.visit(`/projects/${projectId}`); cy.visit(`/projects/${projectId}`);
}); });
}); });
}); });
it('add app', () => { it('add web pkce app', () => {
cy.get('[data-e2e="app-card-add"]').should('be.visible').click(); cy.get('[data-e2e="app-card-add"]').should('be.visible').click();
cy.get('[formcontrolname="name"]').focus().type(testAppName); cy.get('[formcontrolname="name"]').focus().type(testPKCEAppName);
cy.get('[for="WEB"]').click(); cy.get('[for="WEB"]').click();
cy.get('[data-e2e="continue-button-nameandtype"]').click(); cy.get('[data-e2e="continue-button-nameandtype"]').click();
cy.get('[for="PKCE"]').should('be.visible').click(); cy.get('[for="PKCE"]').should('be.visible').click();
@ -43,6 +44,33 @@ describe('applications', () => {
}); });
}); });
describe('add native device code app', () => {
beforeEach(`ensure it doesn't exist already`, () => {
cy.get<Context>('@ctx').then((ctx) => {
cy.get<string>('@projectId').then((projectId) => {
ensureProjectResourceDoesntExist(ctx.api, projectId, Apps, testDEVICECODEAppName);
cy.visit(`/projects/${projectId}`);
});
});
});
it('add device code app', () => {
cy.get('[data-e2e="app-card-add"]').should('be.visible').click();
cy.get('[formcontrolname="name"]').focus().type(testDEVICECODEAppName);
cy.get('[for="N"]').click();
cy.get('[data-e2e="continue-button-nameandtype"]').click();
cy.get('[for="DEVICECODE"]').should('be.visible').click();
cy.get('[data-e2e="continue-button-authmethod"]').click();
cy.get('[data-e2e="create-button"]').click();
cy.get('[id*=overlay]').should('exist');
cy.shouldConfirmSuccess();
const expectClientId = new RegExp(`^.*[0-9]+\\@${testProjectName}.*$`);
cy.get('[data-e2e="client-id-copy"]').click();
cy.contains('[data-e2e="client-id"]', expectClientId);
cy.clipboardMatches(expectClientId);
});
});
describe('edit app', () => { describe('edit app', () => {
it('should configure an application to enable dev mode'); it('should configure an application to enable dev mode');
it('should configure an application to put user roles and info inside id token'); it('should configure an application to put user roles and info inside id token');