Files
zitadel/console/src/app/pages/projects/apps/app-detail/app-detail.component.html
T
Livio SpringandGitHub d527a1c824 feat(console): specify login UI for SAML apps (#9486)
# Which Problems Are Solved

Users were not yet able to specify (and test) the new login UI or
self-hosted login UI for SAML applications through Console.

# How the Problems Are Solved

Added the configuration for SAML apps (as already available for OIDC) in
Console.

# Additional Changes

None

# Additional Context

- closes #9354
2025-03-12 12:37:43 +00:00

534 lines
22 KiB
HTML

<cnsl-top-view
title="{{ app?.name }}"
[hasActions]="isZitadel === false && (['project.app.write:' + projectId, 'project.app.write'] | hasRole | async)"
docLink="https://zitadel.com/docs/guides/manage/console/projects"
[sub]="app?.oidcConfig ? ('APP.OIDC.APPTYPE.' + app?.oidcConfig?.appType | translate) : app?.apiConfig ? 'API' : 'SAML'"
[isActive]="app?.state === AppState.APP_STATE_ACTIVE"
[isInactive]="app?.state === AppState.APP_STATE_INACTIVE"
stateTooltip="{{ 'APP.PAGES.DETAIL.STATE.' + app?.state | translate }}"
>
<ng-template topActions cnslHasRole [hasRole]="['project.app.write:' + projectId, 'project.app.write']">
<div
class="regen-secret"
*ngIf="isZitadel === false && this.app?.oidcConfig && (currentAuthMethod === 'CODE' || currentAuthMethod === 'POST')"
>
<button type="button" [disabled]="!canWrite" mat-menu-item (click)="regenerateOIDCClientSecret()">
{{ 'APP.OIDC.REGENERATESECRET' | translate }}
</button>
</div>
<div class="regen-secret" *ngIf="isZitadel === false && this.app?.apiConfig && currentAuthMethod === 'BASIC'">
<button [disabled]="!canWrite" mat-menu-item (click)="regenerateAPIClientSecret()">
{{ 'APP.API.REGENERATESECRET' | translate }}
</button>
</div>
<button *ngIf="isZitadel === false" mat-menu-item (click)="openNameDialog()" aria-label="Edit project name">
{{ 'ACTIONS.RENAME' | translate }}
</button>
<button
mat-menu-item
*ngIf="isZitadel === false && app?.state !== AppState.APP_STATE_INACTIVE"
(click)="changeState(AppState.APP_STATE_INACTIVE)"
>
{{ 'ACTIONS.DEACTIVATE' | translate }}
</button>
<button
mat-menu-item
*ngIf="isZitadel === false && app?.state === AppState.APP_STATE_INACTIVE"
(click)="changeState(AppState.APP_STATE_ACTIVE)"
>
{{ 'ACTIONS.REACTIVATE' | translate }}
</button>
<ng-template cnslHasRole [hasRole]="['project.app.delete:' + projectId, 'project.app.delete']">
<button
*ngIf="isZitadel === false"
mat-menu-item
matTooltip="{{ 'APP.PAGES.DELETE' | translate }}"
(click)="deleteApp()"
>
<span [style.color]="'var(--warn)'">{{ 'APP.PAGES.DELETE' | translate }}</span>
</button>
</ng-template>
</ng-template>
<span *ngIf="errorMessage" class="app-err-container">{{ errorMessage }}</span>
<cnsl-info-row topContent *ngIf="app" [app]="app"></cnsl-info-row>
</cnsl-top-view>
<div class="max-width-container">
<cnsl-info-section *ngIf="isNew()" class="problem" [type]="InfoSectionType.INFO">
<div class="jumptoproject-row">
<span class="jumptoproject">{{ 'APP.PAGES.JUMPTOPROJECT' | translate }}</span>
<a [routerLink]="['/projects', projectId]" color="primary" mat-raised-button>{{ 'ACTIONS.CONFIGURE' | translate }}</a>
</div>
</cnsl-info-section>
<div
class="compliance"
*ngIf="app && app.oidcConfig && app.oidcConfig.complianceProblemsList && app.oidcConfig.complianceProblemsList?.length"
>
<h2 class="compliance-title">{{ 'APP.COMPLIANCE' | translate }}</h2>
<cnsl-info-section class="problem" [type]="InfoSectionType.ALERT">
<ul style="margin: 0">
<li style="margin: 0 0 0.5rem 0" *ngFor="let problem of app.oidcConfig?.complianceProblemsList || []">
{{ problem.localizedMessage }}
</li>
</ul>
</cnsl-info-section>
</div>
<div class="app-main-content">
<cnsl-meta-layout>
<cnsl-sidenav [(ngModel)]="currentSetting" [settingsList]="settingsList">
<ng-container *ngIf="currentSetting === 'configuration'">
<cnsl-card *ngIf="oidcForm && app?.oidcConfig" title=" {{ 'APP.OIDC.TITLE' | translate }}">
<div
class="current-auth-method"
*ngIf="currentRadioItemAuthType"
[ngStyle]="{ background: currentRadioItemAuthType.background }"
>
<h3>{{ currentRadioItemAuthType.titleI18nKey | translate }}</h3>
<p>{{ currentRadioItemAuthType.descI18nKey | translate }}</p>
<span class="fill-space"></span>
<button mat-icon-button (click)="changeAuthMethod()"><i class="las la-pen"></i></button>
</div>
<form [formGroup]="oidcForm" (ngSubmit)="saveOIDCApp()">
<div class="app-oidc-content">
<div class="app-oidc-grid">
<cnsl-form-field class="app-formfield">
<cnsl-label>{{ 'APP.OIDC.CLIENTID' | translate }}</cnsl-label>
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<cnsl-form-field class="app-formfield">
<cnsl-label>{{ 'APP.TYPE' | translate }}</cnsl-label>
<mat-select formControlName="appType">
<mat-option *ngFor="let type of oidcAppTypes" [value]="type">
{{ 'APP.OIDC.APPTYPE.' + type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="app-formfield">
<cnsl-label>{{ 'APP.OIDC.RESPONSETYPE' | translate }}</cnsl-label>
<mat-select formControlName="responseTypesList" multiple>
<mat-option *ngFor="let type of oidcResponseTypes" [value]="type">
{{ 'APP.OIDC.RESPONSE.' + type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="app-formfield">
<cnsl-label>{{ 'APP.AUTHMETHOD' | translate }}</cnsl-label>
<mat-select formControlName="authMethodType">
<mat-option *ngFor="let type of oidcAuthMethodType" [value]="type">
{{ 'APP.OIDC.AUTHMETHOD.' + type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="app-formfield">
<cnsl-label>{{ 'APP.OIDC.GRANTTYPE' | translate }}</cnsl-label>
<mat-select formControlName="grantTypesList" multiple>
<mat-option *ngFor="let grant of oidcGrantTypes" [value]="grant">
{{ 'APP.OIDC.GRANT.' + grant | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<mat-checkbox
color="primary"
class="rt"
(change)="toggleRefreshToken($event)"
[disabled]="!this.grantTypesList?.value.includes(OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE)"
[checked]="this.grantTypesList?.value.includes(OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN)"
>
{{ 'APP.OIDC.REFRESHTOKEN' | translate }}
</mat-checkbox>
<mat-checkbox
*ngIf="oidcLoginV2"
color="primary"
class="rt"
[formControl]="oidcLoginV2"
name="loginV2"
matTooltip="{{ 'APP.LOGINV2DESC' | translate }}"
>
{{ 'APP.LOGINV2.USEV2' | translate }}
</mat-checkbox>
<cnsl-form-field class="app-formfield">
<cnsl-label>{{ 'APP.LOGINV2.BASEURL' | translate }}</cnsl-label>
<input cnslInput formControlName="loginV2BaseURL" />
</cnsl-form-field>
</div>
</div>
<div class="btn-container">
<button
class="submit-button"
type="submit"
color="primary"
[disabled]="oidcForm.invalid || !canWrite"
mat-raised-button
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
</cnsl-card>
<cnsl-card *ngIf="samlForm && app?.samlConfig" title="{{ 'APP.SAML.CONFIGSECTION' | translate }}">
<h3 class="title cnsl-secondary-text">
{{ 'APP.SAML.CHOOSEMETADATASOURCE' | translate }}
</h3>
<div class="metadata-option">
<span class="saml-config-option">{{ 'APP.SAML.METADATAOPT1' | translate }}</span>
<form [formGroup]="samlForm">
<cnsl-form-field class="formfield">
<input cnslInput formControlName="metadataUrl" placeholder="https://" />
</cnsl-form-field>
</form>
</div>
<div class="metadata-option">
<span class="saml-config-option">{{ 'APP.SAML.METADATAOPT2' | translate }}</span>
<input
#xmlFileInput
style="display: none"
class="file-input"
type="file"
accept="text/xml,application/xml"
(change)="onDropXML($any($event.target).files)"
/>
<button class="upload-button" mat-stroked-button (click)="$event.preventDefault(); xmlFileInput.click()">
{{ 'APP.SAML.UPLOAD' | translate }}
</button>
</div>
<div class="metadata-option">
<span class="saml-config-option">{{ 'APP.SAML.METADATAOPT3' | translate }}</span>
<form [formGroup]="samlForm">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'APP.SAML.ENTITYID' | translate }}</cnsl-label>
<input cnslInput formControlName="entityId" placeholder="https://" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'APP.SAML.ACSURL' | translate }}</cnsl-label>
<input cnslInput formControlName="acsURL" placeholder="https://" />
</cnsl-form-field>
</form>
</div>
<div
class="saml-xml"
[ngClass]="{ disabled: !!metadataUrl?.value }"
*ngIf="decodedBase64 && app && app.samlConfig && !app.samlConfig.metadataUrl"
>
<ngx-codemirror
[(ngModel)]="decodedBase64"
[disabled]="!!metadataUrl?.value"
[options]="{
lineNumbers: true,
theme: 'material',
mode: 'application/xml',
}"
></ngx-codemirror>
</div>
<div class="saml-login-version">
<form [formGroup]="samlForm">
<mat-checkbox
*ngIf="samlLoginV2"
color="primary"
class="rt"
[formControl]="samlLoginV2"
name="loginV2"
matTooltip="{{ 'APP.LOGINV2DESC' | translate }}"
>
{{ 'APP.LOGINV2.USEV2' | translate }}
</mat-checkbox>
<cnsl-form-field class="app-formfield">
<cnsl-label>{{ 'APP.LOGINV2.BASEURL' | translate }}</cnsl-label>
<input cnslInput formControlName="loginV2BaseURL" />
</cnsl-form-field>
</form>
</div>
<div class="btn-container">
<button
class="submit-button"
type="submit"
color="primary"
(click)="saveSAMLApp()"
[disabled]="samlForm.invalid || !canWrite"
mat-raised-button
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</cnsl-card>
</ng-container>
<ng-container *ngIf="currentSetting === 'token'">
<cnsl-card *ngIf="oidcTokenForm && app?.oidcConfig" title=" {{ 'APP.OIDC.TOKENSECTIONTITLE' | translate }}">
<form [formGroup]="oidcTokenForm" (ngSubmit)="saveOIDCApp()">
<cnsl-form-field class="app-formfield">
<cnsl-label>{{ 'APP.OIDC.TOKENTYPE' | translate }}</cnsl-label>
<mat-select formControlName="accessTokenType">
<mat-option *ngFor="let type of oidcTokenTypes" [value]="type">
{{ 'APP.OIDC.TOKENTYPE' + type | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<mat-checkbox
*ngIf="accessTokenType?.value === OIDCTokenType.OIDC_TOKEN_TYPE_JWT"
class="full-width"
style="margin-top: 1.5rem"
formControlName="accessTokenRoleAssertion"
color="primary"
>
{{ 'APP.OIDC.ACCESSTOKENROLEASSERTION' | translate }}</mat-checkbox
>
<cnsl-info-section class="full-width app-desc">
<span>{{ 'APP.OIDC.ACCESSTOKENROLEASSERTION_DESCRIPTION' | translate }}</span>
</cnsl-info-section>
<mat-checkbox
class="full-width"
style="margin-top: 1.5rem"
formControlName="idTokenRoleAssertion"
color="primary"
>
{{ 'APP.OIDC.IDTOKENROLEASSERTION' | translate }}</mat-checkbox
>
<cnsl-info-section class="full-width app-desc">
<span>{{ 'APP.OIDC.IDTOKENROLEASSERTION_DESCRIPTION' | translate }}</span>
</cnsl-info-section>
<mat-checkbox
class="full-width"
style="margin-top: 1.5rem"
formControlName="idTokenUserinfoAssertion"
color="primary"
>
{{ 'APP.OIDC.IDTOKENUSERINFOASSERTION' | translate }}</mat-checkbox
>
<cnsl-info-section class="full-width app-desc">
<span>{{ 'APP.OIDC.IDTOKENUSERINFOASSERTION_DESCRIPTION' | translate }}</span>
</cnsl-info-section>
<p class="clockskew-title cnsl-secondary-text">ClockSkew</p>
<!-- TODO: The 'tickInterval' property no longer exists -->
<mat-slider
color="primary"
class="clockskew-slider"
thumbLabel
[displayWith]="formatClockSkewLabel"
min="0"
[step]="1"
max="5"
><input matSliderThumb formControlName="clockSkewSeconds" />
</mat-slider>
<cnsl-info-section class="full-width app-desc">
<span>{{ 'APP.OIDC.CLOCKSKEW' | translate }}</span>
</cnsl-info-section>
<div class="btn-container">
<button
class="submit-button"
type="submit"
color="primary"
[disabled]="oidcTokenForm.invalid || !canWrite"
mat-raised-button
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
</cnsl-card>
</ng-container>
<ng-container *ngIf="currentSetting === 'redirect-uris'">
<cnsl-card title=" {{ 'APP.OIDC.REDIRECTSECTIONTITLE' | translate }}">
<cnsl-info-section *ngIf="appType?.value === OIDCAppType.OIDC_APP_TYPE_NATIVE">
<div class="dev-col">
<span>{{ 'APP.OIDC.REDIRECTDESCRIPTIONNATIVE' | translate }}</span>
<mat-slide-toggle
*ngIf="devMode"
color="primary"
class="devmode"
[formControl]="devMode"
name="devMode"
matTooltip="{{ 'APP.OIDC.DEVMODEDESC' | translate }}"
>
{{ 'APP.OIDC.DEVMODE' | translate }}
</mat-slide-toggle>
</div>
</cnsl-info-section>
<cnsl-info-section
*ngIf="
appType?.value === OIDCAppType.OIDC_APP_TYPE_WEB || appType?.value === OIDCAppType.OIDC_APP_TYPE_USER_AGENT
"
>
<div class="dev-col">
<span>{{ 'APP.OIDC.REDIRECTDESCRIPTIONWEB' | translate }}</span>
<mat-slide-toggle
*ngIf="devMode"
color="primary"
class="devmode"
[formControl]="devMode"
name="devMode"
matTooltip="{{ 'APP.OIDC.DEVMODEDESC' | translate }}"
>
{{ 'APP.OIDC.DEVMODE' | translate }}
</mat-slide-toggle>
</div>
</cnsl-info-section>
<mat-checkbox
*ngIf="skipNativeAppSuccessPage && appType?.value === OIDCAppType.OIDC_APP_TYPE_NATIVE"
class="full-width"
style="margin-top: 1.5rem"
[formControl]="skipNativeAppSuccessPage"
color="primary"
>
{{ 'APP.OIDC.SKIPNATIVEAPPSUCCESSPAGE' | translate }}</mat-checkbox
>
<cnsl-info-section *ngIf="appType?.value === OIDCAppType.OIDC_APP_TYPE_NATIVE" class="full-width app-desc">
<span>{{ 'APP.OIDC.SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION' | translate }}</span>
</cnsl-info-section>
<cnsl-redirect-uris
*ngIf="appType?.value !== undefined"
class="redirect-section"
[disabled]="!canWrite"
[devMode]="!!devMode?.value"
[(ngModel)]="redirectUrisList"
title="{{ 'APP.OIDC.REDIRECT' | translate }}"
[isNative]="appType?.value === OIDCAppType.OIDC_APP_TYPE_NATIVE"
>
</cnsl-redirect-uris>
<cnsl-redirect-uris
*ngIf="appType?.value !== undefined"
class="redirect-section"
[disabled]="!canWrite"
[devMode]="!!devMode?.value"
[(ngModel)]="postLogoutRedirectUrisList"
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
[isNative]="appType?.value === OIDCAppType.OIDC_APP_TYPE_NATIVE"
>
</cnsl-redirect-uris>
<div class="btn-container">
<button class="submit-button" color="primary" (click)="saveOIDCApp()" [disabled]="!canWrite" mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</cnsl-card>
</ng-container>
<ng-container *ngIf="currentSetting === 'additional-origins'">
<cnsl-card title=" {{ 'APP.ADDITIONALORIGINS' | translate }}">
<p class="app-desc cnsl-secondary-text">{{ 'APP.ADDITIONALORIGINSDESC' | translate }}</p>
<cnsl-additional-origins
*ngIf="additionalOriginsList && appType && appType?.value !== undefined"
class="app-input"
[canWrite]="canWrite"
[getValues]="requestRedirectValuesSubject$"
(changedUris)="additionalOriginsListChanged($event)"
[urisList]="additionalOriginsList"
title="{{ 'APP.ORIGINS' | translate }}"
>
</cnsl-additional-origins>
<div class="btn-container">
<button class="submit-button" (click)="saveOIDCApp()" color="primary" [disabled]="!canWrite" mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</cnsl-card>
</ng-container>
<ng-container *ngIf="currentSetting === 'urls'">
<cnsl-card title=" {{ 'APP.URLS' | translate }}">
<cnsl-info-section *ngIf="issuer$ | async as issuer">
<div
class="link"
[innerHtml]="'APP.OIDC.WELLKNOWN' | translate: { url: issuer + '/.well-known/openid-configuration' }"
></div>
</cnsl-info-section>
<ng-container *ngIf="app?.apiConfig && apiURLs$ | async as apiURLs">
<cnsl-copy-row
labelMinWidth="220px"
*ngFor="let apiUrl of apiURLs"
[label]="apiUrl[0]"
[value]="apiUrl[1]"
></cnsl-copy-row>
</ng-container>
<ng-container *ngIf="app?.samlConfig && samlURLs$ | async as samlMap">
<cnsl-copy-row
labelMinWidth="220px"
*ngIf="samlMap['samlCertificateURL'] as url"
[label]="'APP.SAML.CERTIFICATE' | translate"
[value]="url"
>
<a class="download-button" mat-stroked-button [href]="url" download>{{ 'ACTIONS.DOWNLOAD' | translate }}</a>
</cnsl-copy-row>
<cnsl-copy-row
labelMinWidth="220px"
*ngIf="samlMap['samlSSO'] as url"
label="Single Sign On (SSO)"
[value]="url"
></cnsl-copy-row>
<cnsl-copy-row
labelMinWidth="220px"
*ngIf="samlMap['samlSLO'] as url"
label="Single Logout (SLO)"
[value]="url"
></cnsl-copy-row>
</ng-container>
<ng-container *ngIf="app?.oidcConfig && wellknownURLs$ | async as wellKnownURLs">
<ng-container *ngFor="let wellknown of wellKnownURLs">
<cnsl-copy-row labelMinWidth="220px" [label]="wellknown[0]" [value]="wellknown[1]"></cnsl-copy-row>
</ng-container>
</ng-container>
</cnsl-card>
</ng-container>
<ng-container *ngIf="currentSetting === 'configuration'">
<cnsl-card
*ngIf="initialAuthMethod === 'PK_JWT' && projectId && app && app.id"
title="{{ 'USER.MACHINE.KEYSTITLE' | translate }}"
description="{{ 'USER.MACHINE.KEYSDESC' | translate }}"
>
<cnsl-client-keys [projectId]="projectId" [appId]="app.id"></cnsl-client-keys>
<div *ngIf="apiForm && app?.apiConfig" class="btn-container">
<button
class="submit-button"
(click)="saveAPIApp()"
color="primary"
[disabled]="apiForm.invalid || !canWrite"
mat-raised-button
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</cnsl-card>
</ng-container>
</cnsl-sidenav>
<div metainfo>
<cnsl-changes *ngIf="app" [changeType]="ChangeType.APP" [id]="app.id" [secId]="projectId"></cnsl-changes>
</div>
</cnsl-meta-layout>
</div>
</div>