feat(saml): allow setting nameid-format and alternative mapping for transient format (#7979)

# Which Problems Are Solved

ZITADEL currently always uses
`urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` in SAML requests,
relying on the IdP to respect that flag and always return a peristent
nameid in order to be able to map the external user with an existing
user (idp link) in ZITADEL.
In case the IdP however returns a
`urn:oasis:names:tc:SAML:2.0:nameid-format:transient` (transient)
nameid, the attribute will differ between each request and it will not
be possible to match existing users.

# How the Problems Are Solved

This PR adds the following two options on SAML IdP:
- **nameIDFormat**: allows to set the nameid-format used in the SAML
Request
- **transientMappingAttributeName**: allows to set an attribute name,
which will be used instead of the nameid itself in case the returned
nameid-format is transient

# Additional Changes

To reduce impact on current installations, the `idp_templates6_saml`
table is altered with the two added columns by a setup job. New
installations will automatically get the table with the two columns
directly.
All idp unit tests are updated to use `expectEventstore` instead of the
deprecated `eventstoreExpect`.

# Additional Context

Closes #7483
Closes #7743

---------

Co-authored-by: peintnermax <max@caos.ch>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
This commit is contained in:
Livio Spring
2024-05-23 07:04:07 +02:00
committed by GitHub
parent 12be21a3ff
commit e57a9b57c8
58 changed files with 1306 additions and 720 deletions

View File

@@ -1,7 +1,7 @@
.option-form {
display: grid;
grid-template-columns: 1fr;
max-width: 400px;
max-width: 500px;
padding-bottom: 1rem;
.checkbox-desc {

View File

@@ -70,6 +70,28 @@
</button>
</div>
<div *ngIf="showOptional">
<div class="transient-info">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.SAML.NAMEIDFORMAT' | translate }}</cnsl-label>
<mat-select formControlName="nameIdFormat" [compareWith]="compareNameIDFormat">
<mat-option *ngFor="let nameIdFormat of nameIDFormatValues" [value]="nameIdFormat">{{
nameIdFormat
}}</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-info-section>
<div>
<p class="transient-info-desc">{{ 'IDP.SAML.TRANSIENTMAPPINGATTRIBUTENAME_DESC' | translate }}</p>
</div>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.SAML.TRANSIENTMAPPINGATTRIBUTENAME' | translate }}</cnsl-label>
<input cnslInput formControlName="transientMappingAttributeName" />
</cnsl-form-field>
</cnsl-info-section>
</div>
<cnsl-provider-options
[initialOptions]="provider?.config?.options"
(optionsChanged)="options = $event"

View File

@@ -1,3 +1,12 @@
.metadata-xml {
min-height: 200px;
}
.transient-info {
max-width: 500px;
.transient-info-desc {
margin-top: 0;
margin-bottom: 0.5rem;
}
}

View File

@@ -1,6 +1,12 @@
import { Component, Injector, Type } from '@angular/core';
import { Location } from '@angular/common';
import { AutoLinkingOption, Options, Provider, SAMLBinding } from '../../../proto/generated/zitadel/idp_pb';
import {
AutoLinkingOption,
Options,
Provider,
SAMLBinding,
SAMLNameIDFormat,
} from '../../../proto/generated/zitadel/idp_pb';
import { AbstractControl, FormGroup, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
import { ManagementService } from '../../../services/mgmt.service';
@@ -46,6 +52,7 @@ export class ProviderSamlSpComponent {
// DEPRECATED: use service$ instead
private service!: ManagementService | AdminService;
bindingValues: string[] = Object.keys(SAMLBinding);
nameIDFormatValues: string[] = Object.keys(SAMLNameIDFormat);
public justCreated$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public justActivated$ = new BehaviorSubject<boolean>(false);
@@ -118,6 +125,8 @@ export class ProviderSamlSpComponent {
metadataUrl: new UntypedFormControl('', []),
binding: new UntypedFormControl(this.bindingValues[0], [requiredValidator]),
withSignedRequest: new UntypedFormControl(true, [requiredValidator]),
nameIdFormat: new UntypedFormControl(SAMLNameIDFormat.SAML_NAME_ID_FORMAT_PERSISTENT, []),
transientMappingAttributeName: new UntypedFormControl('', []),
},
atLeastOneIsFilled('metadataXml', 'metadataUrl'),
);
@@ -196,8 +205,12 @@ export class ProviderSamlSpComponent {
req.setWithSignedRequest(this.withSignedRequest?.value);
// @ts-ignore
req.setBinding(SAMLBinding[this.binding?.value]);
// @ts-ignore
req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat?.value]);
req.setTransientMappingAttributeName(this.transientMapping?.value);
req.setProviderOptions(this.options);
console.log(req);
this.loading = true;
this.service
.updateSAMLProvider(req)
@@ -229,6 +242,11 @@ export class ProviderSamlSpComponent {
// @ts-ignore
req.setBinding(SAMLBinding[this.binding?.value]);
req.setWithSignedRequest(this.withSignedRequest?.value);
if (this.nameIDFormat) {
// @ts-ignore
req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat.value]);
}
req.setTransientMappingAttributeName(this.transientMapping?.value);
this.loading = true;
this.service
.addSAMLProvider(req)
@@ -279,6 +297,14 @@ export class ProviderSamlSpComponent {
return false;
}
compareNameIDFormat(value: string, index: number) {
console.log(value, index);
if (value) {
return value === Object.keys(SAMLNameIDFormat)[index];
}
return false;
}
private get name(): AbstractControl | null {
return this.form.get('name');
}
@@ -298,4 +324,12 @@ export class ProviderSamlSpComponent {
private get withSignedRequest(): AbstractControl | null {
return this.form.get('withSignedRequest');
}
private get nameIDFormat(): AbstractControl | null {
return this.form.get('nameIdFormat');
}
private get transientMapping(): AbstractControl | null {
return this.form.get('transientMappingAttributeName');
}
}

View File

@@ -50,7 +50,7 @@
.formfield {
display: block;
max-width: 400px;
max-width: 500px;
&.pwd {
display: none;
@@ -132,7 +132,7 @@
}
.string-list-component-wrapper {
max-width: 400px;
max-width: 500px;
}
.identity-provider-content {
@@ -144,7 +144,7 @@
}
.identity-provider-2-col {
max-width: 400px;
max-width: 500px;
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 1rem;
@@ -160,7 +160,7 @@
.flex-line {
display: flex;
align-items: flex-start;
max-width: 400px;
max-width: 500px;
.formfield {
flex: 1;

View File

@@ -2044,7 +2044,7 @@
"DESCRIPTION": "Enter the credentials for your Apple Provider"
},
"SAML": {
"TITLE": "Sign in with Saml SP",
"TITLE": "Sign in with SAML SP",
"DESCRIPTION": "Enter the credentials for your SAML Provider"
}
},
@@ -2177,7 +2177,10 @@
"METADATAXML": "Metadata Xml",
"METADATAURL": "Metadata URL",
"BINDING": "Binding",
"SIGNEDREQUEST": "Signed Request"
"SIGNEDREQUEST": "Signed Request",
"NAMEIDFORMAT": "NameID Format",
"TRANSIENTMAPPINGATTRIBUTENAME": "Custom Mapping Attribute Name",
"TRANSIENTMAPPINGATTRIBUTENAME_DESC": "Alternative attribute name to map the user in case the `nameid-format` returned is `transient`, e.g. `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`"
},
"TOAST": {
"SAVED": "Successfully saved.",

View File

@@ -2044,7 +2044,7 @@
"DESCRIPTION": "Voer de inloggegevens in voor uw Apple Provider"
},
"SAML": {
"TITLE": "Log in met Saml SP",
"TITLE": "Log in met SAML SP",
"DESCRIPTION": "Voer de inloggegevens in voor uw SAML Provider"
}
},