mirror of
https://github.com/zitadel/zitadel.git
synced 2025-11-02 03:38:46 +00:00
fix(console): Add login v2 url to identity providers (#10583)
# Which Problems Are Solved
When using login V2 the Callback URL for an Identity Provider is
different. When following the guideance in the console and using Login
V2 users will use the wrong callback url.
<img width="1234" height="323" alt="grafik"
src="https://github.com/user-attachments/assets/8632ecf2-d9e4-4e3b-8940-2bf80baab8df"
/>
# How the Problems Are Solved
I have added the correct Login V2 url to the identity providers and
updated our docs.
<img width="628" height="388" alt="grafik"
src="https://github.com/user-attachments/assets/2dd4f4f9-d68f-4605-a52e-2e51069da10e"
/>
# Additional Changes
Small refactorings and porting some components over to ChangeDetection
OnPush
# Additional Context
Replace this example with links to related issues, discussions, discord
threads, or other sources with more context.
Use the Closing #issue syntax for issues that are resolved with this PR.
- Closes #10461
---------
Co-authored-by: Max Peintner <max@caos.ch>
(cherry picked from commit 5cde52148f)
This commit is contained in:
@@ -2,11 +2,11 @@
|
|||||||
<div class="cnsl-cr-secondary-text" [ngStyle]="{ minWidth: labelMinWidth }">{{ label }}</div>
|
<div class="cnsl-cr-secondary-text" [ngStyle]="{ minWidth: labelMinWidth }">{{ label }}</div>
|
||||||
<button
|
<button
|
||||||
class="cnsl-cr-copy"
|
class="cnsl-cr-copy"
|
||||||
[disabled]="copied === value"
|
[disabled]="copied() === value"
|
||||||
[matTooltip]="(copied !== value ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
|
[matTooltip]="(copied() !== value ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
|
||||||
cnslCopyToClipboard
|
cnslCopyToClipboard
|
||||||
[valueToCopy]="value"
|
[valueToCopy]="value"
|
||||||
(copiedValue)="copied = $event"
|
(copiedValue)="copied.set($event)"
|
||||||
>
|
>
|
||||||
{{ value }}
|
{{ value }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, Input } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
import { CopyToClipboardModule } from '../../directives/copy-to-clipboard/copy-to-clipboard.module';
|
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -11,11 +11,12 @@ import { CopyToClipboardModule } from '../../directives/copy-to-clipboard/copy-t
|
|||||||
templateUrl: './copy-row.component.html',
|
templateUrl: './copy-row.component.html',
|
||||||
styleUrls: ['./copy-row.component.scss'],
|
styleUrls: ['./copy-row.component.scss'],
|
||||||
imports: [CommonModule, TranslateModule, MatButtonModule, MatTooltipModule, CopyToClipboardModule],
|
imports: [CommonModule, TranslateModule, MatButtonModule, MatTooltipModule, CopyToClipboardModule],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class CopyRowComponent {
|
export class CopyRowComponent {
|
||||||
@Input({ required: true }) public label = '';
|
@Input({ required: true }) public label!: string;
|
||||||
@Input({ required: true }) public value = '';
|
@Input({ required: true }) public value!: string;
|
||||||
@Input() public labelMinWidth = '';
|
@Input() public labelMinWidth = '';
|
||||||
|
|
||||||
public copied = '';
|
protected readonly copied = signal('');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="card" [ngClass]="{ nomargin: nomargin, stretch: stretch, warn: warn }" data-e2e="app-card">
|
<div class="card" [ngClass]="{ nomargin: nomargin, warn: warn }" data-e2e="app-card">
|
||||||
<div *ngIf="title || description" class="header" [ngClass]="{ 'bottom-margin': expanded }">
|
<div *ngIf="title || description" class="header" [ngClass]="{ 'bottom-margin': expanded }">
|
||||||
<div *ngIf="title" class="row">
|
<div *ngIf="title" class="row">
|
||||||
<h2 class="title" data-e2e="app-card-title">{{ title }}</h2>
|
<h2 class="title" data-e2e="app-card-title">{{ title }}</h2>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
{{ description }}
|
{{ description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content" *ngIf="expanded" [@openClose]="animate">
|
<div class="card-content" *ngIf="expanded">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,10 +46,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.stretch {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-content {
|
.card-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,26 +1,15 @@
|
|||||||
import { animate, style, transition, trigger } from '@angular/animations';
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||||
import { Component, Input } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-card',
|
selector: 'cnsl-card',
|
||||||
templateUrl: './card.component.html',
|
templateUrl: './card.component.html',
|
||||||
styleUrls: ['./card.component.scss'],
|
styleUrls: ['./card.component.scss'],
|
||||||
animations: [
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
trigger('openClose', [
|
|
||||||
transition(':enter', [
|
|
||||||
style({ height: '0', opacity: 0 }),
|
|
||||||
animate('150ms ease-in-out', style({ height: '*', opacity: 1 })),
|
|
||||||
]),
|
|
||||||
transition(':leave', [animate('150ms ease-in-out', style({ height: '0', opacity: 0 }))]),
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class CardComponent {
|
export class CardComponent {
|
||||||
@Input() public expanded: boolean = true;
|
|
||||||
@Input() public warn: boolean = false;
|
|
||||||
@Input() public title: string = '';
|
@Input() public title: string = '';
|
||||||
@Input() public description: string = '';
|
@Input() public description: string = '';
|
||||||
@Input() public animate: boolean = false;
|
@Input() public expanded: boolean = true;
|
||||||
@Input() public nomargin?: boolean = false;
|
@Input() public warn: boolean = false;
|
||||||
@Input() public stretch: boolean = false;
|
@Input() public nomargin: boolean = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
<div
|
<div
|
||||||
class="info-section-row"
|
class="info-section-row"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
info: type === 'INFO',
|
info: type === infoSectionType.INFO,
|
||||||
warn: type === 'WARN',
|
warn: type === infoSectionType.WARN,
|
||||||
alert: type === 'ALERT',
|
alert: type === infoSectionType.ALERT,
|
||||||
success: type === 'SUCCESS',
|
|
||||||
fit: fitWidth,
|
fit: fitWidth,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<i *ngIf="type === 'INFO'" class="icon las la-info"></i>
|
<i *ngIf="type === infoSectionType.INFO" class="icon las la-info"></i>
|
||||||
<i *ngIf="type === 'WARN'" class="icon las la-exclamation"></i>
|
<i *ngIf="type === infoSectionType.WARN" class="icon las la-exclamation"></i>
|
||||||
<i *ngIf="type === 'ALERT'" class="icon las la-exclamation"></i>
|
<i *ngIf="type === infoSectionType.ALERT" class="icon las la-exclamation"></i>
|
||||||
<i *ngIf="type === 'SUCCESS'" class="icon las la-check-circle"></i>
|
|
||||||
|
|
||||||
<div class="info-section-content">
|
<div class="info-section-content">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
|
|||||||
@@ -40,15 +40,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.success {
|
|
||||||
background-color: map-get($background, successinfosection);
|
|
||||||
color: map-get($foreground, successinfosection);
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: map-get($foreground, successinfosection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.warn {
|
&.warn {
|
||||||
background-color: map-get($background, warninfosection);
|
background-color: map-get($background, warninfosection);
|
||||||
color: map-get($foreground, warninfosection);
|
color: map-get($foreground, warninfosection);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||||
|
|
||||||
export enum InfoSectionType {
|
export enum InfoSectionType {
|
||||||
INFO = 'INFO',
|
INFO = 'INFO',
|
||||||
SUCCESS = 'SUCCESS',
|
|
||||||
WARN = 'WARN',
|
WARN = 'WARN',
|
||||||
ALERT = 'ALERT',
|
ALERT = 'ALERT',
|
||||||
}
|
}
|
||||||
@@ -11,8 +10,11 @@ export enum InfoSectionType {
|
|||||||
selector: 'cnsl-info-section',
|
selector: 'cnsl-info-section',
|
||||||
templateUrl: './info-section.component.html',
|
templateUrl: './info-section.component.html',
|
||||||
styleUrls: ['./info-section.component.scss'],
|
styleUrls: ['./info-section.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class InfoSectionComponent {
|
export class InfoSectionComponent {
|
||||||
@Input() type: InfoSectionType = InfoSectionType.INFO;
|
@Input() type: InfoSectionType = InfoSectionType.INFO;
|
||||||
@Input() fitWidth: boolean = false;
|
@Input() fitWidth: boolean = false;
|
||||||
|
|
||||||
|
protected readonly infoSectionType = InfoSectionType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Component, Injector, Type } from '@angular/core';
|
|||||||
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
|
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
|
||||||
import { MatChipInputEvent } from '@angular/material/chips';
|
import { MatChipInputEvent } from '@angular/material/chips';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { BehaviorSubject, Observable, take } from 'rxjs';
|
import { BehaviorSubject, take } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
AddAppleProviderRequest as AdminAddAppleProviderRequest,
|
AddAppleProviderRequest as AdminAddAppleProviderRequest,
|
||||||
GetProviderByIDRequest as AdminGetProviderByIDRequest,
|
GetProviderByIDRequest as AdminGetProviderByIDRequest,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
|
||||||
export interface CopyUrl {
|
export interface CopyUrl {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -10,17 +10,16 @@ export interface CopyUrl {
|
|||||||
selector: 'cnsl-provider-next',
|
selector: 'cnsl-provider-next',
|
||||||
templateUrl: './provider-next.component.html',
|
templateUrl: './provider-next.component.html',
|
||||||
styleUrls: ['./provider-next.component.scss'],
|
styleUrls: ['./provider-next.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ProviderNextComponent {
|
export class ProviderNextComponent {
|
||||||
@Input() copyUrls?: CopyUrl[] | null;
|
@Input() copyUrls: CopyUrl[] | null = null;
|
||||||
@Input() autofillLink?: string | null;
|
@Input() autofillLink: string | null = null;
|
||||||
@Input() activateLink?: string | null;
|
@Input({ required: true }) activateLink!: string | null;
|
||||||
@Input() configureProvider?: boolean | null;
|
@Input({ required: true }) configureProvider!: boolean;
|
||||||
@Input() configureTitle?: string;
|
@Input({ required: true }) configureTitle!: string;
|
||||||
@Input() configureDescription?: string;
|
@Input({ required: true }) configureDescription!: string;
|
||||||
@Input() configureLink?: string;
|
@Input() configureLink?: string;
|
||||||
@Input() expanded?: boolean;
|
@Input({ required: true }) expanded!: boolean;
|
||||||
@Output() activate = new EventEmitter<void>();
|
@Output() activate = new EventEmitter<void>();
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,45 @@
|
|||||||
import { Injectable, Injector, Type } from '@angular/core';
|
import { Injectable, Injector, Type } from '@angular/core';
|
||||||
import { BehaviorSubject, combineLatestWith, from, Observable, of, shareReplay, switchMap, take } from 'rxjs';
|
import { BehaviorSubject, combineLatestWith, defer, from, Observable, of, shareReplay, switchMap, take } from 'rxjs';
|
||||||
import { filter, map, tap } from 'rxjs/operators';
|
import { catchError, filter, map, timeout } from 'rxjs/operators';
|
||||||
import { EnvironmentService } from '../../../services/environment.service';
|
import { EnvironmentService } from 'src/app/services/environment.service';
|
||||||
import { CopyUrl } from './provider-next.component';
|
import { CopyUrl } from './provider-next.component';
|
||||||
import { ManagementService } from '../../../services/mgmt.service';
|
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||||
import { AdminService } from '../../../services/admin.service';
|
import { AdminService } from 'src/app/services/admin.service';
|
||||||
import { IDPOwnerType } from '../../../proto/generated/zitadel/idp_pb';
|
import { IDPOwnerType } from 'src/app/proto/generated/zitadel/idp_pb';
|
||||||
import { ToastService } from '../../../services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
import { Data, ParamMap } from '@angular/router';
|
import { Data, ParamMap } from '@angular/router';
|
||||||
import { LoginPolicyService } from '../../../services/login-policy.service';
|
import { LoginPolicyService } from 'src/app/services/login-policy.service';
|
||||||
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
|
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
|
||||||
|
import { NewFeatureService } from 'src/app/services/new-feature.service';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class ProviderNextService {
|
export class ProviderNextService {
|
||||||
|
private readonly loginV2BaseUri$: Observable<string | undefined>;
|
||||||
constructor(
|
constructor(
|
||||||
private env: EnvironmentService,
|
private readonly env: EnvironmentService,
|
||||||
private toast: ToastService,
|
private readonly toast: ToastService,
|
||||||
private loginPolicySvc: LoginPolicyService,
|
private readonly loginPolicySvc: LoginPolicyService,
|
||||||
private injector: Injector,
|
private readonly injector: Injector,
|
||||||
) {}
|
private readonly featureService: NewFeatureService,
|
||||||
|
) {
|
||||||
|
this.loginV2BaseUri$ = this.getLoginV2BaseUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLoginV2BaseUri(): Observable<string | undefined> {
|
||||||
|
return defer(() => this.featureService.getInstanceFeatures()).pipe(
|
||||||
|
timeout(1000),
|
||||||
|
// we try to load the features if this fails or takes too long we just assume loginV2 is not available
|
||||||
|
catchError(() => of({ loginV2: undefined })),
|
||||||
|
map((features) => features?.loginV2?.baseUri),
|
||||||
|
// we only try this once as the backup plan is not too bad
|
||||||
|
// and in most cases this will work
|
||||||
|
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||||
|
takeUntilDestroyed(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
service(routeData: Observable<Data>): Observable<ManagementService | AdminService> {
|
service(routeData: Observable<Data>): Observable<ManagementService | AdminService> {
|
||||||
return routeData.pipe(
|
return routeData.pipe(
|
||||||
@@ -82,11 +101,18 @@ export class ProviderNextService {
|
|||||||
|
|
||||||
callbackUrls(): Observable<CopyUrl[]> {
|
callbackUrls(): Observable<CopyUrl[]> {
|
||||||
return this.env.env.pipe(
|
return this.env.env.pipe(
|
||||||
map((env) => [
|
combineLatestWith(this.loginV2BaseUri$),
|
||||||
|
map(([env, loginV2BaseUri]) => [
|
||||||
{
|
{
|
||||||
label: 'ZITADEL Callback URL',
|
label: 'Login V1 Callback URL',
|
||||||
url: `${env.issuer}/ui/login/login/externalidp/callback`,
|
url: `${env.issuer}/ui/login/login/externalidp/callback`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Login V2 Callback URL',
|
||||||
|
// if we don't have a loginV2BaseUri we provide a placeholder url so the user knows what to fill in
|
||||||
|
// this is not ideal but better than nothing
|
||||||
|
url: loginV2BaseUri ? `${loginV2BaseUri}idps/callback` : '{LOGIN V2 Hostname}/idps/callback',
|
||||||
|
},
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<cnsl-provider-next
|
<cnsl-provider-next
|
||||||
[configureProvider]="exists$ | async"
|
[configureProvider]="(exists$ | async) === true"
|
||||||
[configureTitle]="'DESCRIPTIONS.SETTINGS.IDPS.SAML.TITLE' | translate"
|
[configureTitle]="'DESCRIPTIONS.SETTINGS.IDPS.SAML.TITLE' | translate"
|
||||||
[configureDescription]="'DESCRIPTIONS.SETTINGS.IDPS.SAML.DESCRIPTION' | translate"
|
[configureDescription]="'DESCRIPTIONS.SETTINGS.IDPS.SAML.DESCRIPTION' | translate"
|
||||||
configureLink="https://zitadel.com/docs/guides/integrate/identity-providers/mocksaml"
|
configureLink="https://zitadel.com/docs/guides/integrate/identity-providers/mocksaml"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { animate, style, transition, trigger } from '@angular/animations';
|
|
||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
@@ -21,15 +20,6 @@ import { PasswordComplexityValidatorFactoryService } from 'src/app/services/pass
|
|||||||
selector: 'cnsl-org-create',
|
selector: 'cnsl-org-create',
|
||||||
templateUrl: './org-create.component.html',
|
templateUrl: './org-create.component.html',
|
||||||
styleUrls: ['./org-create.component.scss'],
|
styleUrls: ['./org-create.component.scss'],
|
||||||
animations: [
|
|
||||||
trigger('openClose', [
|
|
||||||
transition(':enter', [
|
|
||||||
style({ height: '0', opacity: 0 }),
|
|
||||||
animate('150ms ease-in-out', style({ height: '*', opacity: 1 })),
|
|
||||||
]),
|
|
||||||
transition(':leave', [animate('150ms ease-in-out', style({ height: '0', opacity: 0 }))]),
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class OrgCreateComponent {
|
export class OrgCreateComponent {
|
||||||
public orgForm: UntypedFormGroup = this.fb.group({
|
public orgForm: UntypedFormGroup = this.fb.group({
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ import TestSetup from './_test_setup.mdx';
|
|||||||
2. Add your App Name, your Company Page and a Logo
|
2. Add your App Name, your Company Page and a Logo
|
||||||
3. Add "Sign In with LinkedIn using OpenID Connect" by clicking "Request access"
|
3. Add "Sign In with LinkedIn using OpenID Connect" by clicking "Request access"
|
||||||
4. Go to the Auth Settings of the App and add the following URL to the "Authorized redirect URLs"
|
4. Go to the Auth Settings of the App and add the following URL to the "Authorized redirect URLs"
|
||||||
- `{your_domain}/ui/login/login/externalidp/callback`
|
- Login V1: `${CUSTOM_DOMAIN}/ui/login/login/externalidp/callback`
|
||||||
- Example redirect url for the domain `https://acme.zitadel.cloud` would look like this: `https://acme.zitadel.cloud/ui/login/login/externalidp/callback`
|
- Example redirect url for the domain `https://acme.zitadel.cloud` would look like this: `https://acme.zitadel.cloud/ui/login/login/externalidp/callback`
|
||||||
|
- Login V2: `{LOGINGV2_DOMAIN}/idps/callback`
|
||||||
|
- In this case the url would look like this: `https://acme.zitadel.cloud/idps/callback`
|
||||||
5. Verify the app as your company
|
5. Verify the app as your company
|
||||||
6. In the Auth - OAuth 2.0 scopes section you should see `openid`, `profile` and `email` listed
|
6. In the Auth - OAuth 2.0 scopes section you should see `openid`, `profile` and `email` listed
|
||||||
7. Save Client ID and Primary Client Secret from the Application credentials
|
7. Save Client ID and Primary Client Secret from the Application credentials
|
||||||
|
|||||||
Reference in New Issue
Block a user