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 <[email protected]>
Co-authored-by: Stefan Benz <[email protected]>
This commit is contained in:
Livio Spring
2024-05-23 05:04:07 +00:00
committed by GitHub
co-authored by peintnermax Stefan Benz
parent 12be21a3ff
commit e57a9b57c8
58 changed files with 1306 additions and 720 deletions
+9 -6
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 {
+5
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{},
)),
),
+31 -7
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,
+6
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 {
+5
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
+6
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 {
+5
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