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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 1306 additions and 720 deletions

27
cmd/setup/27.go Normal file
View File

@ -0,0 +1,27 @@
package setup
import (
"context"
_ "embed"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
)
var (
//go:embed 27.sql
addSAMLNameIDFormat string
)
type IDPTemplate6SAMLNameIDFormat struct {
dbClient *database.DB
}
func (mig *IDPTemplate6SAMLNameIDFormat) Execute(ctx context.Context, _ eventstore.Event) error {
_, err := mig.dbClient.ExecContext(ctx, addSAMLNameIDFormat)
return err
}
func (mig *IDPTemplate6SAMLNameIDFormat) String() string {
return "26_idp_templates6_add_saml_name_id_format"
}

2
cmd/setup/27.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE IF EXISTS projections.idp_templates6_saml ADD COLUMN IF NOT EXISTS name_id_format SMALLINT;
ALTER TABLE IF EXISTS projections.idp_templates6_saml ADD COLUMN IF NOT EXISTS transient_mapping_attribute_name TEXT;

View File

@ -109,6 +109,7 @@ type Steps struct {
s24AddActorToAuthTokens *AddActorToAuthTokens
s25User11AddLowerFieldsToVerifiedEmail *User11AddLowerFieldsToVerifiedEmail
s26AuthUsers3 *AuthUsers3
s27IDPTemplate6SAMLNameIDFormat *IDPTemplate6SAMLNameIDFormat
}
func MustNewSteps(v *viper.Viper) *Steps {

View File

@ -139,6 +139,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.s24AddActorToAuthTokens = &AddActorToAuthTokens{dbClient: queryDBClient}
steps.s25User11AddLowerFieldsToVerifiedEmail = &User11AddLowerFieldsToVerifiedEmail{dbClient: esPusherDBClient}
steps.s26AuthUsers3 = &AuthUsers3{dbClient: esPusherDBClient}
steps.s27IDPTemplate6SAMLNameIDFormat = &IDPTemplate6SAMLNameIDFormat{dbClient: esPusherDBClient}
err = projection.Create(ctx, projectionDBClient, eventstoreClient, config.Projections, nil, nil, nil)
logging.OnError(err).Fatal("unable to start projections")
@ -190,6 +191,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.s18AddLowerFieldsToLoginNames,
steps.s21AddBlockFieldToLimits,
steps.s25User11AddLowerFieldsToVerifiedEmail,
steps.s27IDPTemplate6SAMLNameIDFormat,
} {
mustExecuteMigration(ctx, eventstoreClient, step, "migration failed")
}

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"
}
},

View File

@ -2,6 +2,7 @@ package admin
import (
"github.com/crewjam/saml"
"github.com/muhlemmer/gu"
idp_grpc "github.com/zitadel/zitadel/internal/api/grpc/idp"
"github.com/zitadel/zitadel/internal/api/grpc/object"
@ -469,24 +470,36 @@ func updateAppleProviderToCommand(req *admin_pb.UpdateAppleProviderRequest) comm
}
func addSAMLProviderToCommand(req *admin_pb.AddSAMLProviderRequest) command.SAMLProvider {
var nameIDFormat *domain.SAMLNameIDFormat
if req.NameIdFormat != nil {
nameIDFormat = gu.Ptr(idp_grpc.SAMLNameIDFormatToDomain(req.GetNameIdFormat()))
}
return command.SAMLProvider{
Name: req.Name,
Metadata: req.GetMetadataXml(),
MetadataURL: req.GetMetadataUrl(),
Binding: bindingToCommand(req.Binding),
WithSignedRequest: req.WithSignedRequest,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
Name: req.Name,
Metadata: req.GetMetadataXml(),
MetadataURL: req.GetMetadataUrl(),
Binding: bindingToCommand(req.Binding),
WithSignedRequest: req.WithSignedRequest,
NameIDFormat: nameIDFormat,
TransientMappingAttributeName: req.GetTransientMappingAttributeName(),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateSAMLProviderToCommand(req *admin_pb.UpdateSAMLProviderRequest) command.SAMLProvider {
var nameIDFormat *domain.SAMLNameIDFormat
if req.NameIdFormat != nil {
nameIDFormat = gu.Ptr(idp_grpc.SAMLNameIDFormatToDomain(req.GetNameIdFormat()))
}
return command.SAMLProvider{
Name: req.Name,
Metadata: req.GetMetadataXml(),
MetadataURL: req.GetMetadataUrl(),
Binding: bindingToCommand(req.Binding),
WithSignedRequest: req.WithSignedRequest,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
Name: req.Name,
Metadata: req.GetMetadataXml(),
MetadataURL: req.GetMetadataUrl(),
Binding: bindingToCommand(req.Binding),
WithSignedRequest: req.WithSignedRequest,
NameIDFormat: nameIDFormat,
TransientMappingAttributeName: req.GetTransientMappingAttributeName(),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}

View File

@ -2,6 +2,7 @@ package idp
import (
"github.com/crewjam/saml"
"github.com/muhlemmer/gu"
"google.golang.org/protobuf/types/known/durationpb"
obj_grpc "github.com/zitadel/zitadel/internal/api/grpc/object"
@ -339,6 +340,21 @@ func azureADTenantTypeToCommand(tenantType idp_pb.AzureADTenantType) azuread.Ten
}
}
func SAMLNameIDFormatToDomain(format idp_pb.SAMLNameIDFormat) domain.SAMLNameIDFormat {
switch format {
case idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_UNSPECIFIED:
return domain.SAMLNameIDFormatUnspecified
case idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_EMAIL_ADDRESS:
return domain.SAMLNameIDFormatEmailAddress
case idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_PERSISTENT:
return domain.SAMLNameIDFormatPersistent
case idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_TRANSIENT:
return domain.SAMLNameIDFormatTransient
default:
return domain.SAMLNameIDFormatUnspecified
}
}
func ProvidersToPb(providers []*query.IDPTemplate) []*idp_pb.Provider {
list := make([]*idp_pb.Provider, len(providers))
for i, provider := range providers {
@ -639,11 +655,17 @@ func appleConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.Appl
}
func samlConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.SAMLIDPTemplate) {
nameIDFormat := idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_PERSISTENT
if template.NameIDFormat.Valid {
nameIDFormat = nameIDToPb(template.NameIDFormat.V)
}
providerConfig.Config = &idp_pb.ProviderConfig_Saml{
Saml: &idp_pb.SAMLConfig{
MetadataXml: template.Metadata,
Binding: bindingToPb(template.Binding),
WithSignedRequest: template.WithSignedRequest,
MetadataXml: template.Metadata,
Binding: bindingToPb(template.Binding),
WithSignedRequest: template.WithSignedRequest,
NameIdFormat: nameIDFormat,
TransientMappingAttributeName: gu.Ptr(template.TransientMappingAttributeName),
},
}
}
@ -662,3 +684,18 @@ func bindingToPb(binding string) idp_pb.SAMLBinding {
return idp_pb.SAMLBinding_SAML_BINDING_UNSPECIFIED
}
}
func nameIDToPb(format domain.SAMLNameIDFormat) idp_pb.SAMLNameIDFormat {
switch format {
case domain.SAMLNameIDFormatUnspecified:
return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_UNSPECIFIED
case domain.SAMLNameIDFormatEmailAddress:
return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_EMAIL_ADDRESS
case domain.SAMLNameIDFormatPersistent:
return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_PERSISTENT
case domain.SAMLNameIDFormatTransient:
return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_TRANSIENT
default:
return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_UNSPECIFIED
}
}

View File

@ -4,6 +4,7 @@ import (
"context"
"github.com/crewjam/saml"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/api/authz"
idp_grpc "github.com/zitadel/zitadel/internal/api/grpc/idp"
@ -462,24 +463,36 @@ func updateAppleProviderToCommand(req *mgmt_pb.UpdateAppleProviderRequest) comma
}
func addSAMLProviderToCommand(req *mgmt_pb.AddSAMLProviderRequest) command.SAMLProvider {
var nameIDFormat *domain.SAMLNameIDFormat
if req.NameIdFormat != nil {
nameIDFormat = gu.Ptr(idp_grpc.SAMLNameIDFormatToDomain(req.GetNameIdFormat()))
}
return command.SAMLProvider{
Name: req.Name,
Metadata: req.GetMetadataXml(),
MetadataURL: req.GetMetadataUrl(),
Binding: bindingToCommand(req.Binding),
WithSignedRequest: req.WithSignedRequest,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
Name: req.Name,
Metadata: req.GetMetadataXml(),
MetadataURL: req.GetMetadataUrl(),
Binding: bindingToCommand(req.Binding),
WithSignedRequest: req.WithSignedRequest,
NameIDFormat: nameIDFormat,
TransientMappingAttributeName: req.GetTransientMappingAttributeName(),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateSAMLProviderToCommand(req *mgmt_pb.UpdateSAMLProviderRequest) command.SAMLProvider {
var nameIDFormat *domain.SAMLNameIDFormat
if req.NameIdFormat != nil {
nameIDFormat = gu.Ptr(idp_grpc.SAMLNameIDFormatToDomain(req.GetNameIdFormat()))
}
return command.SAMLProvider{
Name: req.Name,
Metadata: req.GetMetadataXml(),
MetadataURL: req.GetMetadataUrl(),
Binding: bindingToCommand(req.Binding),
WithSignedRequest: req.WithSignedRequest,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
Name: req.Name,
Metadata: req.GetMetadataXml(),
MetadataURL: req.GetMetadataUrl(),
Binding: bindingToCommand(req.Binding),
WithSignedRequest: req.WithSignedRequest,
NameIDFormat: nameIDFormat,
TransientMappingAttributeName: req.GetTransientMappingAttributeName(),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}

View File

@ -1883,7 +1883,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) {
orgResp := Tester.CreateOrganization(IamCTX, fmt.Sprintf("NotDefaultOrg%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()))
notDefaultOrgIdpID := Tester.AddOrgGenericOAuthProvider(t, CTX, orgResp.OrganizationId)
samlIdpID := Tester.AddSAMLProvider(t, CTX)
samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t, CTX)
samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t, CTX, "")
samlPostIdpID := Tester.AddSAMLPostProvider(t, CTX)
type args struct {
ctx context.Context

View File

@ -229,11 +229,6 @@ func (h *Handler) handleACS(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
sp, err := samlProvider.GetSP()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
intent, err := h.commands.GetActiveIntent(ctx, data.RelayState)
if err != nil {
@ -245,10 +240,10 @@ func (h *Handler) handleACS(w http.ResponseWriter, r *http.Request) {
return
}
session := saml2.Session{
ServiceProvider: sp,
RequestID: intent.RequestID,
Request: r,
session, err := saml2.NewSession(samlProvider, intent.RequestID, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
idpUser, err := session.FetchUser(r.Context())

View File

@ -52,7 +52,7 @@ func TestMain(m *testing.M) {
}
func TestServer_SAMLCertificate(t *testing.T) {
samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t, CTX)
samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t, CTX, "")
oauthIdpID := Tester.AddGenericOAuthProvider(t, CTX)
type args struct {
@ -109,7 +109,7 @@ func TestServer_SAMLCertificate(t *testing.T) {
}
func TestServer_SAMLMetadata(t *testing.T) {
samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t, CTX)
samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t, CTX, "")
oauthIdpID := Tester.AddGenericOAuthProvider(t, CTX)
type args struct {
@ -167,7 +167,7 @@ func TestServer_SAMLMetadata(t *testing.T) {
func TestServer_SAMLACS(t *testing.T) {
userHuman := Tester.CreateHumanUser(CTX)
samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t, CTX)
samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t, CTX, "urn:oid:0.9.2342.19200300.100.1.1") // the username is set in urn:oid:0.9.2342.19200300.100.1.1
externalUserID := "test1"
linkedExternalUserID := "test2"
Tester.CreateUserIDPlink(CTX, userHuman.UserId, linkedExternalUserID, samlRedirectIdpID, linkedExternalUserID)
@ -180,13 +180,15 @@ func TestServer_SAMLACS(t *testing.T) {
assert.NoError(t, err)
type args struct {
ctx context.Context
successURL string
failureURL string
idpID string
username string
intentID string
response string
ctx context.Context
successURL string
failureURL string
idpID string
username string
nameID string
nameIDFormat string
intentID string
response string
}
type want struct {
successful bool
@ -201,12 +203,14 @@ func TestServer_SAMLACS(t *testing.T) {
{
name: "intent invalid",
args: args{
ctx: CTX,
successURL: "https://example.com/success",
failureURL: "https://example.com/failure",
idpID: samlRedirectIdpID,
username: externalUserID,
intentID: "notexisting",
ctx: CTX,
successURL: "https://example.com/success",
failureURL: "https://example.com/failure",
idpID: samlRedirectIdpID,
username: externalUserID,
nameID: externalUserID,
nameIDFormat: string(saml.PersistentNameIDFormat),
intentID: "notexisting",
},
want: want{
successful: false,
@ -217,12 +221,14 @@ func TestServer_SAMLACS(t *testing.T) {
{
name: "response invalid",
args: args{
ctx: CTX,
successURL: "https://example.com/success",
failureURL: "https://example.com/failure",
idpID: samlRedirectIdpID,
username: externalUserID,
response: "invalid",
ctx: CTX,
successURL: "https://example.com/success",
failureURL: "https://example.com/failure",
idpID: samlRedirectIdpID,
username: externalUserID,
nameID: externalUserID,
nameIDFormat: string(saml.PersistentNameIDFormat),
response: "invalid",
},
want: want{
successful: false,
@ -232,11 +238,13 @@ func TestServer_SAMLACS(t *testing.T) {
{
name: "saml flow redirect, ok",
args: args{
ctx: CTX,
successURL: "https://example.com/success",
failureURL: "https://example.com/failure",
idpID: samlRedirectIdpID,
username: externalUserID,
ctx: CTX,
successURL: "https://example.com/success",
failureURL: "https://example.com/failure",
idpID: samlRedirectIdpID,
username: externalUserID,
nameID: externalUserID,
nameIDFormat: string(saml.PersistentNameIDFormat),
},
want: want{
successful: true,
@ -246,11 +254,45 @@ func TestServer_SAMLACS(t *testing.T) {
{
name: "saml flow redirect with link, ok",
args: args{
ctx: CTX,
successURL: "https://example.com/success",
failureURL: "https://example.com/failure",
idpID: samlRedirectIdpID,
username: linkedExternalUserID,
ctx: CTX,
successURL: "https://example.com/success",
failureURL: "https://example.com/failure",
idpID: samlRedirectIdpID,
username: linkedExternalUserID,
nameID: linkedExternalUserID,
nameIDFormat: string(saml.PersistentNameIDFormat),
},
want: want{
successful: true,
user: userHuman.UserId,
},
},
{
name: "saml flow redirect (transient), ok",
args: args{
ctx: CTX,
successURL: "https://example.com/success",
failureURL: "https://example.com/failure",
idpID: samlRedirectIdpID,
username: externalUserID,
nameID: "genericID",
nameIDFormat: string(saml.TransientNameIDFormat),
},
want: want{
successful: true,
user: "",
},
},
{
name: "saml flow redirect with link (transient), ok",
args: args{
ctx: CTX,
successURL: "https://example.com/success",
failureURL: "https://example.com/failure",
idpID: samlRedirectIdpID,
username: linkedExternalUserID,
nameID: "genericID",
nameIDFormat: string(saml.TransientNameIDFormat),
},
want: want{
successful: true,
@ -287,7 +329,7 @@ func TestServer_SAMLACS(t *testing.T) {
relayState = tt.args.intentID
}
callbackURL := http_util.BuildOrigin(Tester.Host(), Tester.Server.Config.ExternalSecure) + "/idps/" + tt.args.idpID + "/saml/acs"
response := createResponse(t, idp, samlRequest, tt.args.username)
response := createResponse(t, idp, samlRequest, tt.args.nameID, tt.args.nameIDFormat, tt.args.username)
//test purposes, use defined response
if tt.args.response != "" {
response = tt.args.response
@ -432,14 +474,16 @@ func getIDP(zitadelBaseURL string, idpIDs []string, user1, user2 string) (*saml.
return &idpServer.IDP, nil
}
func createResponse(t *testing.T, idp *saml.IdentityProvider, req *http.Request, username string) string {
func createResponse(t *testing.T, idp *saml.IdentityProvider, req *http.Request, nameID, nameIDFormat, username string) string {
authnReq, err := saml.NewIdpAuthnRequest(idp, req)
assert.NoError(t, authnReq.Validate())
err = idp.AssertionMaker.MakeAssertion(authnReq, &saml.Session{
CreateTime: time.Now().UTC(),
Index: "",
NameID: username,
CreateTime: time.Now().UTC(),
Index: "",
NameID: nameID,
NameIDFormat: nameIDFormat,
UserName: username,
})
assert.NoError(t, err)
err = authnReq.MakeResponse()

View File

@ -319,12 +319,11 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
l.externalAuthFailed(w, r, authReq, nil, nil, err)
return
}
sp, err := provider.(*saml.Provider).GetSP()
session, err = saml.NewSession(provider.(*saml.Provider), authReq.SAMLRequestID, r)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
return
}
session = &saml.Session{ServiceProvider: sp, RequestID: authReq.SAMLRequestID, Request: r}
case domain.IDPTypeJWT,
domain.IDPTypeLDAP,
domain.IDPTypeUnspecified:
@ -1029,13 +1028,19 @@ func (l *Login) samlProvider(ctx context.Context, identityProvider *query.IDPTem
if err != nil {
return nil, err
}
opts := make([]saml.ProviderOpts, 0, 2)
opts := make([]saml.ProviderOpts, 0, 6)
if identityProvider.SAMLIDPTemplate.WithSignedRequest {
opts = append(opts, saml.WithSignedRequest())
}
if identityProvider.SAMLIDPTemplate.Binding != "" {
opts = append(opts, saml.WithBinding(identityProvider.SAMLIDPTemplate.Binding))
}
if identityProvider.SAMLIDPTemplate.NameIDFormat.Valid {
opts = append(opts, saml.WithNameIDFormat(identityProvider.SAMLIDPTemplate.NameIDFormat.V))
}
if identityProvider.SAMLIDPTemplate.TransientMappingAttributeName != "" {
opts = append(opts, saml.WithTransientMappingAttributeName(identityProvider.SAMLIDPTemplate.TransientMappingAttributeName))
}
opts = append(opts,
saml.WithEntityID(http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), l.externalSecure)+"/idps/"+identityProvider.ID+"/saml/metadata"),
saml.WithCustomRequestTracker(

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/zerrors"
)
@ -110,12 +111,14 @@ type LDAPProvider struct {
}
type SAMLProvider struct {
Name string
Metadata []byte
MetadataURL string
Binding string
WithSignedRequest bool
IDPOptions idp.Options
Name string
Metadata []byte
MetadataURL string
Binding string
WithSignedRequest bool
NameIDFormat *domain.SAMLNameIDFormat
TransientMappingAttributeName string
IDPOptions idp.Options
}
type AppleProvider struct {

View File

@ -6,6 +6,7 @@ import (
"testing"
"github.com/crewjam/saml"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/oidc"
@ -631,6 +632,8 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
[]byte("certificate"),
"",
false,
gu.Ptr(domain.SAMLNameIDFormatUnspecified),
"",
rep_idp.Options{},
)),
),
@ -649,6 +652,8 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
}, []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"),
"",
false,
gu.Ptr(domain.SAMLNameIDFormatUnspecified),
"",
rep_idp.Options{},
)),
),

View File

@ -1726,13 +1726,15 @@ func (wm *AppleIDPWriteModel) GetProviderOptions() idp.Options {
type SAMLIDPWriteModel struct {
eventstore.WriteModel
Name string
ID string
Metadata []byte
Key *crypto.CryptoValue
Certificate []byte
Binding string
WithSignedRequest bool
Name string
ID string
Metadata []byte
Key *crypto.CryptoValue
Certificate []byte
Binding string
WithSignedRequest bool
NameIDFormat *domain.SAMLNameIDFormat
TransientMappingAttributeName string
idp.Options
State domain.IDPState
@ -1759,6 +1761,8 @@ func (wm *SAMLIDPWriteModel) reduceAddedEvent(e *idp.SAMLIDPAddedEvent) {
wm.Certificate = e.Certificate
wm.Binding = e.Binding
wm.WithSignedRequest = e.WithSignedRequest
wm.NameIDFormat = e.NameIDFormat
wm.TransientMappingAttributeName = e.TransientMappingAttributeName
wm.Options = e.Options
wm.State = domain.IDPStateActive
}
@ -1782,6 +1786,12 @@ func (wm *SAMLIDPWriteModel) reduceChangedEvent(e *idp.SAMLIDPChangedEvent) {
if e.WithSignedRequest != nil {
wm.WithSignedRequest = *e.WithSignedRequest
}
if e.NameIDFormat != nil {
wm.NameIDFormat = e.NameIDFormat
}
if e.TransientMappingAttributeName != nil {
wm.TransientMappingAttributeName = *e.TransientMappingAttributeName
}
wm.Options.ReduceChanges(e.OptionChanges)
}
@ -1793,6 +1803,8 @@ func (wm *SAMLIDPWriteModel) NewChanges(
secretCrypto crypto.EncryptionAlgorithm,
binding string,
withSignedRequest bool,
nameIDFormat *domain.SAMLNameIDFormat,
transientMappingAttributeName string,
options idp.Options,
) ([]idp.SAMLIDPChanges, error) {
changes := make([]idp.SAMLIDPChanges, 0)
@ -1818,6 +1830,12 @@ func (wm *SAMLIDPWriteModel) NewChanges(
if wm.WithSignedRequest != withSignedRequest {
changes = append(changes, idp.ChangeSAMLWithSignedRequest(withSignedRequest))
}
if wm.NameIDFormat != nameIDFormat {
changes = append(changes, idp.ChangeSAMLNameIDFormat(nameIDFormat))
}
if wm.TransientMappingAttributeName != transientMappingAttributeName {
changes = append(changes, idp.ChangeSAMLTransientMappingAttributeName(transientMappingAttributeName))
}
opts := wm.Options.Changes(options)
if !opts.IsZero() {
changes = append(changes, idp.ChangeSAMLOptions(opts))
@ -1850,6 +1868,12 @@ func (wm *SAMLIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encryp
if wm.Binding != "" {
opts = append(opts, saml2.WithBinding(wm.Binding))
}
if wm.NameIDFormat != nil {
opts = append(opts, saml2.WithNameIDFormat(*wm.NameIDFormat))
}
if wm.TransientMappingAttributeName != "" {
opts = append(opts, saml2.WithTransientMappingAttributeName(wm.TransientMappingAttributeName))
}
opts = append(opts, saml2.WithCustomRequestTracker(
requesttracker.New(
addRequest,

View File

@ -1763,6 +1763,8 @@ func (c *Commands) prepareAddInstanceSAMLProvider(a *instance.Aggregate, writeMo
cert,
provider.Binding,
provider.WithSignedRequest,
provider.NameIDFormat,
provider.TransientMappingAttributeName,
provider.IDPOptions,
),
}, nil
@ -1811,6 +1813,8 @@ func (c *Commands) prepareUpdateInstanceSAMLProvider(a *instance.Aggregate, writ
c.idpConfigEncryption,
provider.Binding,
provider.WithSignedRequest,
provider.NameIDFormat,
provider.TransientMappingAttributeName,
provider.IDPOptions,
)
if err != nil || event == nil {
@ -1854,6 +1858,8 @@ func (c *Commands) prepareRegenerateInstanceSAMLProviderCertificate(a *instance.
c.idpConfigEncryption,
writeModel.Binding,
writeModel.WithSignedRequest,
writeModel.NameIDFormat,
writeModel.TransientMappingAttributeName,
writeModel.Options,
)
if err != nil || event == nil {

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/repository/instance"
@ -915,6 +916,8 @@ func (wm *InstanceSAMLIDPWriteModel) NewChangedEvent(
secretCrypto crypto.EncryptionAlgorithm,
binding string,
withSignedRequest bool,
nameIDFormat *domain.SAMLNameIDFormat,
transientMappingAttributeName string,
options idp.Options,
) (*instance.SAMLIDPChangedEvent, error) {
changes, err := wm.SAMLIDPWriteModel.NewChanges(
@ -925,6 +928,8 @@ func (wm *InstanceSAMLIDPWriteModel) NewChangedEvent(
secretCrypto,
binding,
withSignedRequest,
nameIDFormat,
transientMappingAttributeName,
options,
)
if err != nil || len(changes) == 0 {

File diff suppressed because it is too large Load Diff

View File

@ -1746,6 +1746,8 @@ func (c *Commands) prepareAddOrgSAMLProvider(a *org.Aggregate, writeModel *OrgSA
cert,
provider.Binding,
provider.WithSignedRequest,
provider.NameIDFormat,
provider.TransientMappingAttributeName,
provider.IDPOptions,
),
}, nil
@ -1794,6 +1796,8 @@ func (c *Commands) prepareUpdateOrgSAMLProvider(a *org.Aggregate, writeModel *Or
c.idpConfigEncryption,
provider.Binding,
provider.WithSignedRequest,
provider.NameIDFormat,
provider.TransientMappingAttributeName,
provider.IDPOptions,
)
if err != nil || event == nil {
@ -1837,6 +1841,8 @@ func (c *Commands) prepareRegenerateOrgSAMLProviderCertificate(a *org.Aggregate,
c.idpConfigEncryption,
writeModel.Binding,
writeModel.WithSignedRequest,
writeModel.NameIDFormat,
writeModel.TransientMappingAttributeName,
writeModel.Options,
)
if err != nil || event == nil {

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/repository/org"
@ -927,6 +928,8 @@ func (wm *OrgSAMLIDPWriteModel) NewChangedEvent(
secretCrypto crypto.EncryptionAlgorithm,
binding string,
withSignedRequest bool,
nameIDFormat *domain.SAMLNameIDFormat,
transientMappingAttributeName string,
options idp.Options,
) (*org.SAMLIDPChangedEvent, error) {
changes, err := wm.SAMLIDPWriteModel.NewChanges(
@ -937,6 +940,8 @@ func (wm *OrgSAMLIDPWriteModel) NewChangedEvent(
secretCrypto,
binding,
withSignedRequest,
nameIDFormat,
transientMappingAttributeName,
options,
)
if err != nil || len(changes) == 0 {

File diff suppressed because it is too large Load Diff

View File

@ -134,3 +134,12 @@ const (
AutoLinkingOptionUsername
AutoLinkingOptionEmail
)
type SAMLNameIDFormat uint8
const (
SAMLNameIDFormatUnspecified SAMLNameIDFormat = iota
SAMLNameIDFormatEmailAddress
SAMLNameIDFormatPersistent
SAMLNameIDFormatTransient
)

View File

@ -1,7 +1,6 @@
package saml
import (
"github.com/crewjam/saml"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
@ -20,8 +19,8 @@ func NewUser() *UserMapper {
return &UserMapper{Attributes: map[string][]string{}}
}
func (u *UserMapper) SetID(id *saml.NameID) {
u.ID = id.Value
func (u *UserMapper) SetID(id string) {
u.ID = id
}
// GetID is an implementation of the [idp.User] interface.

View File

@ -11,6 +11,7 @@ import (
"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/zerrors"
)
@ -26,7 +27,9 @@ type Provider struct {
spOptions *samlsp.Options
binding string
binding string
nameIDFormat saml.NameIDFormat
transientMappingAttributeName string
isLinkingAllowed bool
isCreationAllowed bool
@ -77,6 +80,18 @@ func WithBinding(binding string) ProviderOpts {
}
}
func WithNameIDFormat(format domain.SAMLNameIDFormat) ProviderOpts {
return func(p *Provider) {
p.nameIDFormat = nameIDFormatFromDomain(format)
}
}
func WithTransientMappingAttributeName(attribute string) ProviderOpts {
return func(p *Provider) {
p.transientMappingAttributeName = attribute
}
}
func WithCustomRequestTracker(tracker samlsp.RequestTracker) ProviderOpts {
return func(p *Provider) {
p.requestTracker = tracker
@ -124,6 +139,8 @@ func New(
name: name,
spOptions: &opts,
Certificate: certificate,
// the library uses transient as default, which does not make sense for federating accounts
nameIDFormat: saml.PersistentNameIDFormat,
}
for _, option := range options {
option(provider)
@ -156,10 +173,7 @@ func (p *Provider) GetSP() (*samlsp.Middleware, error) {
if err != nil {
return nil, zerrors.ThrowInternal(err, "SAML-qee09ffuq5", "Errors.Intent.IDPInvalid")
}
// the library uses transient as default, which we currently can't handle (https://github.com/zitadel/zitadel/discussions/7421)
// for the moment we'll use persistent (for those who actually use it from the saml request) and add an option
// later on to specify on the provider: https://github.com/zitadel/zitadel/issues/7743
sp.ServiceProvider.AuthnNameIDFormat = saml.PersistentNameIDFormat
sp.ServiceProvider.AuthnNameIDFormat = p.nameIDFormat
if p.requestTracker != nil {
sp.RequestTracker = p.requestTracker
}
@ -180,3 +194,22 @@ func (p *Provider) BeginAuth(ctx context.Context, state string, _ ...idp.Paramet
state: state,
}, nil
}
func (p *Provider) TransientMappingAttributeName() string {
return p.transientMappingAttributeName
}
func nameIDFormatFromDomain(format domain.SAMLNameIDFormat) saml.NameIDFormat {
switch format {
case domain.SAMLNameIDFormatUnspecified:
return saml.UnspecifiedNameIDFormat
case domain.SAMLNameIDFormatEmailAddress:
return saml.EmailAddressNameIDFormat
case domain.SAMLNameIDFormatPersistent:
return saml.PersistentNameIDFormat
case domain.SAMLNameIDFormatTransient:
return saml.TransientNameIDFormat
default:
return saml.UnspecifiedNameIDFormat
}
}

View File

@ -3,10 +3,12 @@ package saml
import (
"testing"
"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp/providers/saml/requesttracker"
)
@ -20,16 +22,18 @@ func TestProvider_Options(t *testing.T) {
options []ProviderOpts
}
type want struct {
err bool
name string
linkingAllowed bool
creationAllowed bool
autoCreation bool
autoUpdate bool
binding string
withSignedRequest bool
requesttracker samlsp.RequestTracker
entityID string
err bool
name string
linkingAllowed bool
creationAllowed bool
autoCreation bool
autoUpdate bool
binding string
nameIDFormat saml.NameIDFormat
transientMappingAttributeName string
withSignedRequest bool
requesttracker samlsp.RequestTracker
entityID string
}
tests := []struct {
name string
@ -103,10 +107,11 @@ func TestProvider_Options(t *testing.T) {
creationAllowed: false,
autoCreation: false,
autoUpdate: false,
nameIDFormat: saml.PersistentNameIDFormat,
},
},
{
name: "all true",
name: "all set / true",
fields: fields{
name: "saml",
key: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"),
@ -121,18 +126,22 @@ func TestProvider_Options(t *testing.T) {
WithSignedRequest(),
WithCustomRequestTracker(&requesttracker.RequestTracker{}),
WithEntityID("entityID"),
WithNameIDFormat(domain.SAMLNameIDFormatTransient),
WithTransientMappingAttributeName("attribute"),
},
},
want: want{
name: "saml",
linkingAllowed: true,
creationAllowed: true,
autoCreation: true,
autoUpdate: true,
binding: "binding",
withSignedRequest: true,
requesttracker: &requesttracker.RequestTracker{},
entityID: "entityID",
name: "saml",
linkingAllowed: true,
creationAllowed: true,
autoCreation: true,
autoUpdate: true,
binding: "binding",
entityID: "entityID",
nameIDFormat: saml.TransientNameIDFormat,
transientMappingAttributeName: "attribute",
withSignedRequest: true,
requesttracker: &requesttracker.RequestTracker{},
},
},
}
@ -152,6 +161,8 @@ func TestProvider_Options(t *testing.T) {
a.Equal(tt.want.autoCreation, provider.IsAutoCreation())
a.Equal(tt.want.autoUpdate, provider.IsAutoUpdate())
a.Equal(tt.want.binding, provider.binding)
a.Equal(tt.want.nameIDFormat, provider.nameIDFormat)
a.Equal(tt.want.transientMappingAttributeName, provider.transientMappingAttributeName)
a.Equal(tt.want.withSignedRequest, provider.spOptions.SignRequest)
a.Equal(tt.want.requesttracker, provider.requestTracker)
a.Equal(tt.want.entityID, provider.spOptions.EntityID)

View File

@ -17,8 +17,9 @@ var _ idp.Session = (*Session)(nil)
// Session is the [idp.Session] implementation for the SAML provider.
type Session struct {
ServiceProvider *samlsp.Middleware
state string
ServiceProvider *samlsp.Middleware
state string
TransientMappingAttributeName string
RequestID string
Request *http.Request
@ -26,6 +27,19 @@ type Session struct {
Assertion *saml.Assertion
}
func NewSession(provider *Provider, requestID string, request *http.Request) (*Session, error) {
sp, err := provider.GetSP()
if err != nil {
return nil, err
}
return &Session{
ServiceProvider: sp,
TransientMappingAttributeName: provider.TransientMappingAttributeName(),
RequestID: requestID,
Request: request,
}, nil
}
// GetAuth implements the [idp.Session] interface.
func (s *Session) GetAuth(ctx context.Context) (string, bool) {
url, _ := url.Parse(s.state)
@ -56,8 +70,17 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
return nil, zerrors.ThrowInvalidArgument(err, "SAML-nuo0vphhh9", "Errors.Intent.ResponseInvalid")
}
nameID := s.Assertion.Subject.NameID
userMapper := NewUser()
userMapper.SetID(s.Assertion.Subject.NameID)
// use the nameID as default mapping id
userMapper.SetID(nameID.Value)
if nameID.Format == string(saml.TransientNameIDFormat) {
mappingID, err := s.transientMappingID()
if err != nil {
return nil, err
}
userMapper.SetID(mappingID)
}
for _, statement := range s.Assertion.AttributeStatements {
for _, attribute := range statement.Attributes {
values := make([]string, len(attribute.Values))
@ -70,6 +93,21 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
return userMapper, nil
}
func (s *Session) transientMappingID() (string, error) {
for _, statement := range s.Assertion.AttributeStatements {
for _, attribute := range statement.Attributes {
if attribute.Name != s.TransientMappingAttributeName {
continue
}
if len(attribute.Values) != 1 {
return "", zerrors.ThrowInvalidArgument(nil, "SAML-Soij4", "Errors.Intent.MissingSingleMappingAttribute")
}
return attribute.Values[0].Value, nil
}
}
return "", zerrors.ThrowInvalidArgument(nil, "SAML-swwg2", "Errors.Intent.MissingSingleMappingAttribute")
}
type TempResponseWriter struct {
header http.Header
content *bytes.Buffer

File diff suppressed because one or more lines are too long

View File

@ -380,12 +380,13 @@ func (s *Tester) AddSAMLProvider(t *testing.T, ctx context.Context) string {
return id
}
func (s *Tester) AddSAMLRedirectProvider(t *testing.T, ctx context.Context) string {
func (s *Tester) AddSAMLRedirectProvider(t *testing.T, ctx context.Context, transientMappingAttributeName string) string {
ctx = authz.WithInstance(ctx, s.Instance)
id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{
Name: "saml-idp-redirect",
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
Metadata: []byte("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-09-16T09:00:32.986Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
Name: "saml-idp-redirect",
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
Metadata: []byte("<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" validUntil=\"2023-09-16T09:00:32.986Z\" cacheDuration=\"PT48H\" entityID=\"http://localhost:8000/metadata\">\n <IDPSSODescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <KeyDescriptor use=\"signing\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n </KeyDescriptor>\n <KeyDescriptor use=\"encryption\">\n <KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Data xmlns=\"http://www.w3.org/2000/09/xmldsig#\">\n <X509Certificate xmlns=\"http://www.w3.org/2000/09/xmldsig#\">MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=</X509Certificate>\n </X509Data>\n </KeyInfo>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes128-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes192-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#aes256-cbc\"></EncryptionMethod>\n <EncryptionMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p\"></EncryptionMethod>\n </KeyDescriptor>\n <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"http://localhost:8000/sso\"></SingleSignOnService>\n </IDPSSODescriptor>\n</EntityDescriptor>"),
TransientMappingAttributeName: transientMappingAttributeName,
IDPOptions: idp.Options{
IsLinkingAllowed: true,
IsCreationAllowed: true,

View File

@ -155,12 +155,14 @@ type AppleIDPTemplate struct {
}
type SAMLIDPTemplate struct {
IDPID string
Metadata []byte
Key *crypto.CryptoValue
Certificate []byte
Binding string
WithSignedRequest bool
IDPID string
Metadata []byte
Key *crypto.CryptoValue
Certificate []byte
Binding string
WithSignedRequest bool
NameIDFormat sql.Null[domain.SAMLNameIDFormat]
TransientMappingAttributeName string
}
var (
@ -700,6 +702,14 @@ var (
name: projection.SAMLWithSignedRequestCol,
table: samlIdpTemplateTable,
}
SAMLNameIDFormatCol = Column{
name: projection.SAMLNameIDFormatCol,
table: samlIdpTemplateTable,
}
SAMLTransientMappingAttributeNameCol = Column{
name: projection.SAMLTransientMappingAttributeName,
table: samlIdpTemplateTable,
}
)
// IDPTemplateByID searches for the requested id
@ -883,6 +893,8 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
SAMLCertificateCol.identifier(),
SAMLBindingCol.identifier(),
SAMLWithSignedRequestCol.identifier(),
SAMLNameIDFormatCol.identifier(),
SAMLTransientMappingAttributeNameCol.identifier(),
// ldap
LDAPIDCol.identifier(),
LDAPServersCol.identifier(),
@ -997,6 +1009,8 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
var samlCertificate []byte
samlBinding := sql.NullString{}
samlWithSignedRequest := sql.NullBool{}
samlNameIDFormat := sql.Null[domain.SAMLNameIDFormat]{}
samlTransientMappingAttributeName := sql.NullString{}
ldapID := sql.NullString{}
ldapServers := database.TextArray[string]{}
@ -1109,6 +1123,8 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
&samlCertificate,
&samlBinding,
&samlWithSignedRequest,
&samlNameIDFormat,
&samlTransientMappingAttributeName,
// ldap
&ldapID,
&ldapServers,
@ -1237,12 +1253,14 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
}
if samlID.Valid {
idpTemplate.SAMLIDPTemplate = &SAMLIDPTemplate{
IDPID: samlID.String,
Metadata: samlMetadata,
Key: samlKey,
Certificate: samlCertificate,
Binding: samlBinding.String,
WithSignedRequest: samlWithSignedRequest.Bool,
IDPID: samlID.String,
Metadata: samlMetadata,
Key: samlKey,
Certificate: samlCertificate,
Binding: samlBinding.String,
WithSignedRequest: samlWithSignedRequest.Bool,
NameIDFormat: samlNameIDFormat,
TransientMappingAttributeName: samlTransientMappingAttributeName.String,
}
}
if ldapID.Valid {
@ -1370,6 +1388,8 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
SAMLCertificateCol.identifier(),
SAMLBindingCol.identifier(),
SAMLWithSignedRequestCol.identifier(),
SAMLNameIDFormatCol.identifier(),
SAMLTransientMappingAttributeNameCol.identifier(),
// ldap
LDAPIDCol.identifier(),
LDAPServersCol.identifier(),
@ -1489,6 +1509,8 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
var samlCertificate []byte
samlBinding := sql.NullString{}
samlWithSignedRequest := sql.NullBool{}
samlNameIDFormat := sql.Null[domain.SAMLNameIDFormat]{}
samlTransientMappingAttributeName := sql.NullString{}
ldapID := sql.NullString{}
ldapServers := database.TextArray[string]{}
@ -1601,6 +1623,8 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
&samlCertificate,
&samlBinding,
&samlWithSignedRequest,
&samlNameIDFormat,
&samlTransientMappingAttributeName,
// ldap
&ldapID,
&ldapServers,
@ -1728,12 +1752,14 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
}
if samlID.Valid {
idpTemplate.SAMLIDPTemplate = &SAMLIDPTemplate{
IDPID: samlID.String,
Metadata: samlMetadata,
Key: samlKey,
Certificate: samlCertificate,
Binding: samlBinding.String,
WithSignedRequest: samlWithSignedRequest.Bool,
IDPID: samlID.String,
Metadata: samlMetadata,
Key: samlKey,
Certificate: samlCertificate,
Binding: samlBinding.String,
WithSignedRequest: samlWithSignedRequest.Bool,
NameIDFormat: samlNameIDFormat,
TransientMappingAttributeName: samlTransientMappingAttributeName.String,
}
}
if ldapID.Valid {

View File

@ -95,6 +95,8 @@ var (
` projections.idp_templates6_saml.certificate,` +
` projections.idp_templates6_saml.binding,` +
` projections.idp_templates6_saml.with_signed_request,` +
` projections.idp_templates6_saml.name_id_format,` +
` projections.idp_templates6_saml.transient_mapping_attribute_name,` +
// ldap
` projections.idp_templates6_ldap2.idp_id,` +
` projections.idp_templates6_ldap2.servers,` +
@ -220,6 +222,8 @@ var (
"certificate",
"binding",
"with_signed_request",
"name_id_format",
"transient_mapping_attribute_name",
// ldap config
"idp_id",
"servers",
@ -331,6 +335,8 @@ var (
` projections.idp_templates6_saml.certificate,` +
` projections.idp_templates6_saml.binding,` +
` projections.idp_templates6_saml.with_signed_request,` +
` projections.idp_templates6_saml.name_id_format,` +
` projections.idp_templates6_saml.transient_mapping_attribute_name,` +
// ldap
` projections.idp_templates6_ldap2.idp_id,` +
` projections.idp_templates6_ldap2.servers,` +
@ -457,6 +463,8 @@ var (
"certificate",
"binding",
"with_signed_request",
"name_id_format",
"transient_mapping_attribute_name",
// ldap config
"idp_id",
"servers",
@ -608,6 +616,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
@ -756,6 +766,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
@ -902,6 +914,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
@ -1047,6 +1061,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
@ -1191,6 +1207,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
@ -1335,6 +1353,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
@ -1480,6 +1500,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
@ -1624,6 +1646,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
"binding",
false,
domain.SAMLNameIDFormatTransient,
"customAttribute",
// ldap config
nil,
nil,
@ -1674,12 +1698,14 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
IsAutoUpdate: true,
AutoLinking: domain.AutoLinkingOptionUsername,
SAMLIDPTemplate: &SAMLIDPTemplate{
IDPID: "idp-id",
Metadata: []byte("metadata"),
Key: nil,
Certificate: nil,
Binding: "binding",
WithSignedRequest: false,
IDPID: "idp-id",
Metadata: []byte("metadata"),
Key: nil,
Certificate: nil,
Binding: "binding",
WithSignedRequest: false,
NameIDFormat: sql.Null[domain.SAMLNameIDFormat]{V: domain.SAMLNameIDFormatTransient, Valid: true},
TransientMappingAttributeName: "customAttribute",
},
},
},
@ -1770,6 +1796,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
"idp-id",
database.TextArray[string]{"server"},
@ -1934,6 +1962,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
@ -2080,6 +2110,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
@ -2254,6 +2286,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
"idp-id",
database.TextArray[string]{"server"},
@ -2427,6 +2461,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
@ -2574,6 +2610,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
"idp-id-ldap",
database.TextArray[string]{"server"},
@ -2686,6 +2724,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
"binding",
false,
domain.SAMLNameIDFormatTransient,
"customAttribute",
// ldap config
nil,
nil,
@ -2798,6 +2838,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
@ -2910,6 +2952,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
@ -3022,6 +3066,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
@ -3134,6 +3180,8 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
// ldap config
nil,
nil,
@ -3232,12 +3280,14 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
IsAutoUpdate: true,
AutoLinking: domain.AutoLinkingOptionUsername,
SAMLIDPTemplate: &SAMLIDPTemplate{
IDPID: "idp-id-saml",
Metadata: []byte("metadata"),
Key: nil,
Certificate: nil,
Binding: "binding",
WithSignedRequest: false,
IDPID: "idp-id-saml",
Metadata: []byte("metadata"),
Key: nil,
Certificate: nil,
Binding: "binding",
WithSignedRequest: false,
NameIDFormat: sql.Null[domain.SAMLNameIDFormat]{V: domain.SAMLNameIDFormatTransient, Valid: true},
TransientMappingAttributeName: "customAttribute",
},
},
{

View File

@ -161,13 +161,15 @@ const (
ApplePrivateKeyCol = "private_key"
AppleScopesCol = "scopes"
SAMLIDCol = "idp_id"
SAMLInstanceIDCol = "instance_id"
SAMLMetadataCol = "metadata"
SAMLKeyCol = "key"
SAMLCertificateCol = "certificate"
SAMLBindingCol = "binding"
SAMLWithSignedRequestCol = "with_signed_request"
SAMLIDCol = "idp_id"
SAMLInstanceIDCol = "instance_id"
SAMLMetadataCol = "metadata"
SAMLKeyCol = "key"
SAMLCertificateCol = "certificate"
SAMLBindingCol = "binding"
SAMLWithSignedRequestCol = "with_signed_request"
SAMLNameIDFormatCol = "name_id_format"
SAMLTransientMappingAttributeName = "transient_mapping_attribute_name"
)
type idpTemplateProjection struct{}
@ -367,6 +369,8 @@ func (*idpTemplateProjection) Init() *old_handler.Check {
handler.NewColumn(SAMLCertificateCol, handler.ColumnTypeBytes),
handler.NewColumn(SAMLBindingCol, handler.ColumnTypeText, handler.Nullable()),
handler.NewColumn(SAMLWithSignedRequestCol, handler.ColumnTypeBool, handler.Nullable()),
handler.NewColumn(SAMLNameIDFormatCol, handler.ColumnTypeEnum, handler.Nullable()),
handler.NewColumn(SAMLTransientMappingAttributeName, handler.ColumnTypeText, handler.Nullable()),
},
handler.NewPrimaryKey(SAMLInstanceIDCol, SAMLIDCol),
IDPTemplateSAMLSuffix,
@ -1967,6 +1971,20 @@ func (p *idpTemplateProjection) reduceSAMLIDPAdded(event eventstore.Event) (*han
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-9s02m1", "reduce.wrong.event.type %v", []eventstore.EventType{org.SAMLIDPAddedEventType, instance.SAMLIDPAddedEventType})
}
columns := []handler.Column{
handler.NewCol(SAMLIDCol, idpEvent.ID),
handler.NewCol(SAMLInstanceIDCol, idpEvent.Aggregate().InstanceID),
handler.NewCol(SAMLMetadataCol, idpEvent.Metadata),
handler.NewCol(SAMLKeyCol, idpEvent.Key),
handler.NewCol(SAMLCertificateCol, idpEvent.Certificate),
handler.NewCol(SAMLBindingCol, idpEvent.Binding),
handler.NewCol(SAMLWithSignedRequestCol, idpEvent.WithSignedRequest),
handler.NewCol(SAMLTransientMappingAttributeName, idpEvent.TransientMappingAttributeName),
}
if idpEvent.NameIDFormat != nil {
columns = append(columns, handler.NewCol(SAMLNameIDFormatCol, *idpEvent.NameIDFormat))
}
return handler.NewMultiStatement(
&idpEvent,
handler.AddCreateStatement(
@ -1989,15 +2007,7 @@ func (p *idpTemplateProjection) reduceSAMLIDPAdded(event eventstore.Event) (*han
},
),
handler.AddCreateStatement(
[]handler.Column{
handler.NewCol(SAMLIDCol, idpEvent.ID),
handler.NewCol(SAMLInstanceIDCol, idpEvent.Aggregate().InstanceID),
handler.NewCol(SAMLMetadataCol, idpEvent.Metadata),
handler.NewCol(SAMLKeyCol, idpEvent.Key),
handler.NewCol(SAMLCertificateCol, idpEvent.Certificate),
handler.NewCol(SAMLBindingCol, idpEvent.Binding),
handler.NewCol(SAMLWithSignedRequestCol, idpEvent.WithSignedRequest),
},
columns,
handler.WithTableSuffix(IDPTemplateSAMLSuffix),
),
), nil
@ -2490,5 +2500,11 @@ func reduceSAMLIDPChangedColumns(idpEvent idp.SAMLIDPChangedEvent) []handler.Col
if idpEvent.WithSignedRequest != nil {
SAMLCols = append(SAMLCols, handler.NewCol(SAMLWithSignedRequestCol, *idpEvent.WithSignedRequest))
}
if idpEvent.NameIDFormat != nil {
SAMLCols = append(SAMLCols, handler.NewCol(SAMLNameIDFormatCol, *idpEvent.NameIDFormat))
}
if idpEvent.TransientMappingAttributeName != nil {
SAMLCols = append(SAMLCols, handler.NewCol(SAMLTransientMappingAttributeName, *idpEvent.TransientMappingAttributeName))
}
return SAMLCols
}

View File

@ -2774,6 +2774,8 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) {
},
"certificate": `+stringToJSONByte("certificate")+`,
"binding": "binding",
"nameIDFormat": 3,
"transientMappingAttributeName": "customAttribute",
"withSignedRequest": true,
"isCreationAllowed": true,
"isLinkingAllowed": true,
@ -2810,7 +2812,7 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) {
},
},
{
expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request) VALUES ($1, $2, $3, $4, $5, $6, $7)",
expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request, transient_mapping_attribute_name, name_id_format) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
expectedArgs: []interface{}{
"idp-id",
"instance-id",
@ -2819,6 +2821,8 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) {
anyArg{},
"binding",
true,
"customAttribute",
domain.SAMLNameIDFormatTransient,
},
},
},
@ -2842,6 +2846,8 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) {
},
"certificate": `+stringToJSONByte("certificate")+`,
"binding": "binding",
"nameIDFormat": 3,
"transientMappingAttributeName": "customAttribute",
"withSignedRequest": true,
"isCreationAllowed": true,
"isLinkingAllowed": true,
@ -2878,7 +2884,7 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) {
},
},
{
expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request) VALUES ($1, $2, $3, $4, $5, $6, $7)",
expectedStmt: "INSERT INTO projections.idp_templates6_saml (idp_id, instance_id, metadata, key, certificate, binding, with_signed_request, transient_mapping_attribute_name, name_id_format) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
expectedArgs: []interface{}{
"idp-id",
"instance-id",
@ -2887,6 +2893,8 @@ func TestIDPTemplateProjection_reducesSAML(t *testing.T) {
anyArg{},
"binding",
true,
"customAttribute",
domain.SAMLNameIDFormatTransient,
},
},
},

View File

@ -2,6 +2,7 @@ package idp
import (
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors"
)
@ -9,13 +10,15 @@ import (
type SAMLIDPAddedEvent struct {
eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
Name string `json:"name,omitempty"`
Metadata []byte `json:"metadata,omitempty"`
Key *crypto.CryptoValue `json:"key,omitempty"`
Certificate []byte `json:"certificate,omitempty"`
Binding string `json:"binding,omitempty"`
WithSignedRequest bool `json:"withSignedRequest,omitempty"`
ID string `json:"id"`
Name string `json:"name,omitempty"`
Metadata []byte `json:"metadata,omitempty"`
Key *crypto.CryptoValue `json:"key,omitempty"`
Certificate []byte `json:"certificate,omitempty"`
Binding string `json:"binding,omitempty"`
WithSignedRequest bool `json:"withSignedRequest,omitempty"`
NameIDFormat *domain.SAMLNameIDFormat `json:"nameIDFormat,omitempty"`
TransientMappingAttributeName string `json:"transientMappingAttributeName,omitempty"`
Options
}
@ -28,18 +31,22 @@ func NewSAMLIDPAddedEvent(
certificate []byte,
binding string,
withSignedRequest bool,
nameIDFormat *domain.SAMLNameIDFormat,
transientMappingAttributeName string,
options Options,
) *SAMLIDPAddedEvent {
return &SAMLIDPAddedEvent{
BaseEvent: *base,
ID: id,
Name: name,
Metadata: metadata,
Key: key,
Certificate: certificate,
Binding: binding,
WithSignedRequest: withSignedRequest,
Options: options,
BaseEvent: *base,
ID: id,
Name: name,
Metadata: metadata,
Key: key,
Certificate: certificate,
Binding: binding,
WithSignedRequest: withSignedRequest,
NameIDFormat: nameIDFormat,
TransientMappingAttributeName: transientMappingAttributeName,
Options: options,
}
}
@ -67,13 +74,15 @@ func SAMLIDPAddedEventMapper(event eventstore.Event) (eventstore.Event, error) {
type SAMLIDPChangedEvent struct {
eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
Name *string `json:"name,omitempty"`
Metadata []byte `json:"metadata,omitempty"`
Key *crypto.CryptoValue `json:"key,omitempty"`
Certificate []byte `json:"certificate,omitempty"`
Binding *string `json:"binding,omitempty"`
WithSignedRequest *bool `json:"withSignedRequest,omitempty"`
ID string `json:"id"`
Name *string `json:"name,omitempty"`
Metadata []byte `json:"metadata,omitempty"`
Key *crypto.CryptoValue `json:"key,omitempty"`
Certificate []byte `json:"certificate,omitempty"`
Binding *string `json:"binding,omitempty"`
WithSignedRequest *bool `json:"withSignedRequest,omitempty"`
NameIDFormat *domain.SAMLNameIDFormat `json:"nameIDFormat,omitempty"`
TransientMappingAttributeName *string `json:"transientMappingAttributeName,omitempty"`
OptionChanges
}
@ -133,6 +142,18 @@ func ChangeSAMLWithSignedRequest(withSignedRequest bool) func(*SAMLIDPChangedEve
}
}
func ChangeSAMLNameIDFormat(nameIDFormat *domain.SAMLNameIDFormat) func(*SAMLIDPChangedEvent) {
return func(e *SAMLIDPChangedEvent) {
e.NameIDFormat = nameIDFormat
}
}
func ChangeSAMLTransientMappingAttributeName(name string) func(*SAMLIDPChangedEvent) {
return func(e *SAMLIDPChangedEvent) {
e.TransientMappingAttributeName = &name
}
}
func ChangeSAMLOptions(options OptionChanges) func(*SAMLIDPChangedEvent) {
return func(e *SAMLIDPChangedEvent) {
e.OptionChanges = options

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/idp"
)
@ -1016,6 +1017,8 @@ func NewSAMLIDPAddedEvent(
certificate []byte,
binding string,
withSignedRequest bool,
nameIDFormat *domain.SAMLNameIDFormat,
transientMappingAttributeName string,
options idp.Options,
) *SAMLIDPAddedEvent {
return &SAMLIDPAddedEvent{
@ -1032,6 +1035,8 @@ func NewSAMLIDPAddedEvent(
certificate,
binding,
withSignedRequest,
nameIDFormat,
transientMappingAttributeName,
options,
),
}

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/idp"
)
@ -1017,6 +1018,8 @@ func NewSAMLIDPAddedEvent(
certificate []byte,
binding string,
withSignedRequest bool,
nameIDFormat *domain.SAMLNameIDFormat,
transientMappingAttributeName string,
options idp.Options,
) *SAMLIDPAddedEvent {
@ -1034,6 +1037,8 @@ func NewSAMLIDPAddedEvent(
certificate,
binding,
withSignedRequest,
nameIDFormat,
transientMappingAttributeName,
options,
),
}

View File

@ -534,6 +534,7 @@ Errors:
IDPMissing: IDP липсва в заявката
IDPInvalid: IDP невалиден за заявката
ResponseInvalid: Отговорът на IDP е невалиден
MissingSingleMappingAttribute: Не съдържа атрибута за съпоставяне или има повече от една стойност
SuccessURLMissing: В заявката липсва URL адрес за успех
FailureURLMissing: В заявката липсва URL адрес за грешка
StateMissing: В заявката липсва параметър състояние

View File

@ -514,6 +514,7 @@ Errors:
IDPMissing: V požadavku chybí IDP ID
IDPInvalid: IDP je pro požadavek neplatné
ResponseInvalid: Odpověď IDP je neplatná
MissingSingleMappingAttribute: Neobsahuje atribut mapování nebo má více než jednu hodnotu
SuccessURLMissing: V požadavku chybí úspěšná URL
FailureURLMissing: V požadavku chybí URL selhání
StateMissing: V požadavku chybí parametr stavu

View File

@ -516,6 +516,7 @@ Errors:
IDPMissing: IDP ID fehlt im Request
IDPInvalid: IDP ungültig für die Anfrage
ResponseInvalid: IDP-Antwort ist ungültig
MissingSingleMappingAttribute: Enthält das Zuordnungsattribut nicht oder hat mehr als einen Wert
SuccessURLMissing: Success URL fehlt im Request
FailureURLMissing: Failure URL fehlt im Request
StateMissing: State parameter fehlt im Request

View File

@ -516,6 +516,7 @@ Errors:
IDPMissing: IDP ID is missing in the request
IDPInvalid: IDP invalid for the request
ResponseInvalid: IDP response is invalid
MissingSingleMappingAttribute: IDP response does not contain the mapping attribute or has more than one value
SuccessURLMissing: Success URL is missing in the request
FailureURLMissing: Failure URL is missing in the request
StateMissing: State parameter is missing in the request

View File

@ -516,6 +516,7 @@ Errors:
IDPMissing: Falta IDP en la solicitud
IDPInvalid: IDP no válido para la solicitud
ResponseInvalid: La respuesta del IDP no es válida
MissingSingleMappingAttribute: No contiene el atributo de asignación o tiene más de un valor
SuccessURLMissing: Falta la URL de éxito en la solicitud
FailureURLMissing: Falta la URL de error en la solicitud
StateMissing: Falta un parámetro de estado en la solicitud

View File

@ -516,6 +516,7 @@ Errors:
IDPMissing: IDP manquant dans la requête
IDPInvalid: IDP non valide pour la demande
ResponseInvalid: La réponse de l'IDP n'est pas valide
MissingSingleMappingAttribute: Ne contient pas l'attribut de mappage ou a plus d'une valeur
SuccessURLMissing: Success URL absent de la requête
FailureURLMissing: Failure URL absent de la requête
StateMissing: Paramètre d'état manquant dans la requête

View File

@ -516,6 +516,7 @@ Errors:
IDPMissing: IDP mancante nella richiesta
IDPInvalid: IDP non valido per la richiesta
ResponseInvalid: La risposta dell'IDP non è valida
MissingSingleMappingAttribute: Non contiene l'attributo di mapping o ha più di un valore
SuccessURLMissing: URL di successo mancante nella richiesta
FailureURLMissing: URL di errore mancante nella richiesta
StateMissing: parametro di stato mancante nella richiesta

View File

@ -505,6 +505,7 @@ Errors:
IDPMissing: リクエストにIDP IDが含まれていません
IDPInvalid: リクエストのIDPが無効
ResponseInvalid: IDPの回答は無効
MissingSingleMappingAttribute: マッピング属性が含まれていない、または複数の値がある
SuccessURLMissing: リクエストに成功時の URL がありません
FailureURLMissing: リクエストに失敗の URL がありません
StateMissing: リクエストに State パラメータがありません

View File

@ -515,6 +515,7 @@ Errors:
IDPMissing: ID на IDP недостасува во барањето6bg
IDPInvalid: ВРЛ неважечки за барањето
ResponseInvalid: Одговорот на ВРЛ е неважечки
MissingSingleMappingAttribute: не го содржи атрибутот за мапирање или има повеќе од една вредност
SuccessURLMissing: URL за успех недостасува во барањето
FailureURLMissing: URL за неуспех недостасува во барањето
StateMissing: Параметарот State недостасува во барањето

View File

@ -515,6 +515,7 @@ Errors:
IDPMissing: IDP ID ontbreekt in het verzoek
IDPInvalid: IDP ongeldig voor het verzoek
ResponseInvalid: IDP respons is ongeldig
MissingSingleMappingAttribute: Bevat kenmerk toewijzing niet of heeft meer dan één waarde
SuccessURLMissing: Success URL ontbreekt in het verzoek
FailureURLMissing: Failure URL ontbreekt in het verzoek
StateMissing: Staat parameter ontbreekt in het verzoek

View File

@ -516,6 +516,7 @@ Errors:
IDPMissing: Brak identyfikatora IDP w żądaniu
IDPInvalid: IDP nieprawidłowe dla żądania
ResponseInvalid: Odpowiedź IDP jest nieprawidłowa
MissingSingleMappingAttribute: Nie zawiera atrybutu mapowania lub ma więcej niż jedną wartość
SuccessURLMissing: Brak adresu URL powodzenia w żądaniu
FailureURLMissing: Brak adresu URL niepowodzenia w żądaniu
StateMissing: Brak parametru stanu w żądaniu

View File

@ -515,6 +515,7 @@ Errors:
IDPMissing: O ID do IDP está faltando na solicitação
IDPInvalid: IDP inválido para o pedido
ResponseInvalid: A resposta da PDI é inválida
MissingSingleMappingAttribute: Não contém o atributo de mapeamento ou tem mais de um valor
SuccessURLMissing: A URL de sucesso está faltando na solicitação
FailureURLMissing: A URL de falha está faltando na solicitação
StateMissing: O parâmetro de estado está faltando na solicitação

View File

@ -505,6 +505,7 @@ Errors:
NoChallenge: Сеанс без вызова WebAuthN
Intent:
IDPMissing: В запросе отсутствует идентификатор IDP
MissingSingleMappingAttribute: Не содержит атрибут сопоставления или имеет более одного значения
SuccessURLMissing: В запросе отсутствует URL-адрес успешного выполнения
FailureURLMissing: В запросе отсутствует URL-адрес ошибки
StateMissing: В запросе отсутствует параметр State

View File

@ -516,6 +516,7 @@ Errors:
IDPMissing: 请求中缺少IDP ID
IDPInvalid: 请求的 IDP 无效
ResponseInvalid: IDP 响应无效
MissingSingleMappingAttribute: 不包含映射属性或具有多个值
SuccessURLMissing: 请求中缺少成功URL
FailureURLMissing: 请求中缺少失败的URL
StateMissing: 请求中缺少状态参数

View File

@ -6028,31 +6028,28 @@ message AddSAMLProviderRequest {
string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
oneof metadata {
option (validate.required) = true;
// Metadata of the SAML identity provider.
bytes metadata_xml = 2 [
(validate.rules).bytes.max_len = 500000,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Metadata of the SAML identity provider";
}
(validate.rules).bytes.max_len = 500000
];
// Url to the metadata of the SAML identity provider.
string metadata_url = 3 [
(validate.rules).string.max_len = 200,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://test.com/saml/metadata\""
description: "Url to the metadata of the SAML identity provider";
}
];
}
zitadel.idp.v1.SAMLBinding binding = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Binding which defines the type of communication with the identity provider";
}
];
bool with_signed_request = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Boolean which defines if the authentication requests are signed";
}
];
// Binding which defines the type of communication with the identity provider.
zitadel.idp.v1.SAMLBinding binding = 4;
// Boolean which defines if the authentication requests are signed.
bool with_signed_request = 5;
zitadel.idp.v1.Options provider_options = 6;
// Optionally specify the `nameid-format` requested.
optional zitadel.idp.v1.SAMLNameIDFormat name_id_format = 7;
// Optionally specify the name of the attribute, which will be used to map the user
// in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`.
optional string transient_mapping_attribute_name = 8;
}
message AddSAMLProviderResponse {
@ -6063,33 +6060,30 @@ message AddSAMLProviderResponse {
message UpdateSAMLProviderRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
string name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
// Metadata of the SAML identity provider.
oneof metadata {
option (validate.required) = true;
bytes metadata_xml = 3 [
(validate.rules).bytes.max_len = 500000,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Metadata of the SAML identity provider";
}
(validate.rules).bytes.max_len = 500000
];
// Url to the metadata of the SAML identity provider
string metadata_url = 4 [
(validate.rules).string.max_len = 200,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://test.com/saml/metadata\""
description: "Url to the metadata of the SAML identity provider";
}
];
}
zitadel.idp.v1.SAMLBinding binding = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Binding which defines the type of communication with the identity provider";
}
];
bool with_signed_request = 6 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Boolean which defines if the authentication requests are signed";
}
];
// Binding which defines the type of communication with the identity provider.
zitadel.idp.v1.SAMLBinding binding = 5;
// Boolean which defines if the authentication requests are signed
bool with_signed_request = 6;
zitadel.idp.v1.Options provider_options = 7;
// Optionally specify the `nameid-format` requested.
optional zitadel.idp.v1.SAMLNameIDFormat name_id_format = 8;
// Optionally specify the name of the attribute, which will be used to map the user
// in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`.
optional string transient_mapping_attribute_name = 9;
}
message UpdateSAMLProviderResponse {

View File

@ -276,6 +276,13 @@ enum SAMLBinding {
SAML_BINDING_ARTIFACT = 3;
}
enum SAMLNameIDFormat {
SAML_NAME_ID_FORMAT_UNSPECIFIED = 0;
SAML_NAME_ID_FORMAT_EMAIL_ADDRESS = 1;
SAML_NAME_ID_FORMAT_PERSISTENT = 2;
SAML_NAME_ID_FORMAT_TRANSIENT = 3;
}
message ProviderConfig {
Options options = 1;
oneof config {
@ -452,21 +459,17 @@ message LDAPConfig {
}
message SAMLConfig {
bytes metadata_xml = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Metadata of the SAML identity provider";
}
];
zitadel.idp.v1.SAMLBinding binding = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Binding which defines the type of communication with the identity provider";
}
];
bool with_signed_request = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Boolean which defines if the authentication requests are signed";
}
];
// Metadata of the SAML identity provider.
bytes metadata_xml = 1;
// Binding which defines the type of communication with the identity provider.
zitadel.idp.v1.SAMLBinding binding = 2;
// Boolean which defines if the authentication requests are signed.
bool with_signed_request = 3;
// `nameid-format` for the SAML Request.
zitadel.idp.v1.SAMLNameIDFormat name_id_format = 4;
// Optional name of the attribute, which will be used to map the user
// in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`.
optional string transient_mapping_attribute_name = 5;
}
message AzureADConfig {

View File

@ -12767,31 +12767,28 @@ message AddSAMLProviderRequest {
string name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
oneof metadata {
option (validate.required) = true;
// Metadata of the SAML identity provider.
bytes metadata_xml = 2 [
(validate.rules).bytes.max_len = 500000,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Metadata of the SAML identity provider";
}
(validate.rules).bytes.max_len = 500000
];
// Url to the metadata of the SAML identity provider.
string metadata_url = 3 [
(validate.rules).string.max_len = 200,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://test.com/saml/metadata\""
description: "Url to the metadata of the SAML identity provider";
}
];
}
zitadel.idp.v1.SAMLBinding binding = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Binding which defines the type of communication with the identity provider";
}
];
bool with_signed_request = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Boolean which defines if the authentication requests are signed";
}
];
// Binding which defines the type of communication with the identity provider.
zitadel.idp.v1.SAMLBinding binding = 4;
// Boolean which defines if the authentication requests are signed.
bool with_signed_request = 5;
zitadel.idp.v1.Options provider_options = 6;
// Optionally specify the `nameid-format` requested.
optional zitadel.idp.v1.SAMLNameIDFormat name_id_format = 7;
// Optionally specify the name of the attribute, which will be used to map the user
// in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`.
optional string transient_mapping_attribute_name = 8;
}
message AddSAMLProviderResponse {
@ -12802,33 +12799,30 @@ message AddSAMLProviderResponse {
message UpdateSAMLProviderRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
string name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
// Metadata of the SAML identity provider.
oneof metadata {
option (validate.required) = true;
bytes metadata_xml = 3 [
(validate.rules).bytes.max_len = 500000,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Metadata of the SAML identity provider";
}
(validate.rules).bytes.max_len = 500000
];
// Url to the metadata of the SAML identity provider.
string metadata_url = 4 [
(validate.rules).string.max_len = 200,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://test.com/saml/metadata\""
description: "Url to the metadata of the SAML identity provider";
}
];
}
zitadel.idp.v1.SAMLBinding binding = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Binding which defines the type of communication with the identity provider";
}
];
bool with_signed_request = 6 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Boolean which defines if the authentication requests are signed";
}
];
// Binding which defines the type of communication with the identity provider.
zitadel.idp.v1.SAMLBinding binding = 5;
// Boolean which defines if the authentication requests are signed.
bool with_signed_request = 6;
zitadel.idp.v1.Options provider_options = 7;
// Optionally specify the `nameid-format` requested.
optional zitadel.idp.v1.SAMLNameIDFormat name_id_format = 8;
// Optionally specify the name of the attribute, which will be used to map the user
// in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`.
optional string transient_mapping_attribute_name = 9;
}
message UpdateSAMLProviderResponse {