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

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