2023-09-29 11:26:14 +02:00
|
|
|
package saml
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/rsa"
|
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/xml"
|
|
|
|
"net/url"
|
|
|
|
|
|
|
|
"github.com/crewjam/saml"
|
|
|
|
"github.com/crewjam/saml/samlsp"
|
|
|
|
|
|
|
|
"github.com/zitadel/zitadel/internal/idp"
|
2023-12-08 16:30:55 +02:00
|
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
2023-09-29 11:26:14 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
var _ idp.Provider = (*Provider)(nil)
|
|
|
|
|
|
|
|
// Provider is the [idp.Provider] implementation for a generic SAML provider
|
|
|
|
type Provider struct {
|
|
|
|
name string
|
|
|
|
|
|
|
|
requestTracker samlsp.RequestTracker
|
|
|
|
Certificate []byte
|
|
|
|
|
|
|
|
spOptions *samlsp.Options
|
|
|
|
|
|
|
|
binding string
|
|
|
|
|
|
|
|
isLinkingAllowed bool
|
|
|
|
isCreationAllowed bool
|
|
|
|
isAutoCreation bool
|
|
|
|
isAutoUpdate bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type ProviderOpts func(provider *Provider)
|
|
|
|
|
|
|
|
// WithLinkingAllowed allows end users to link the federated user to an existing one.
|
|
|
|
func WithLinkingAllowed() ProviderOpts {
|
|
|
|
return func(p *Provider) {
|
|
|
|
p.isLinkingAllowed = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithCreationAllowed allows end users to create a new user using the federated information.
|
|
|
|
func WithCreationAllowed() ProviderOpts {
|
|
|
|
return func(p *Provider) {
|
|
|
|
p.isCreationAllowed = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithAutoCreation enables that federated users are automatically created if not already existing.
|
|
|
|
func WithAutoCreation() ProviderOpts {
|
|
|
|
return func(p *Provider) {
|
|
|
|
p.isAutoCreation = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithAutoUpdate enables that information retrieved from the provider is automatically used to update
|
|
|
|
// the existing user on each authentication.
|
|
|
|
func WithAutoUpdate() ProviderOpts {
|
|
|
|
return func(p *Provider) {
|
|
|
|
p.isAutoUpdate = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func WithSignedRequest() ProviderOpts {
|
|
|
|
return func(p *Provider) {
|
|
|
|
p.spOptions.SignRequest = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func WithBinding(binding string) ProviderOpts {
|
|
|
|
return func(p *Provider) {
|
|
|
|
p.binding = binding
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func WithCustomRequestTracker(tracker samlsp.RequestTracker) ProviderOpts {
|
|
|
|
return func(p *Provider) {
|
|
|
|
p.requestTracker = tracker
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func WithEntityID(entityID string) ProviderOpts {
|
|
|
|
return func(p *Provider) {
|
|
|
|
p.spOptions.EntityID = entityID
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func New(
|
|
|
|
name string,
|
|
|
|
rootURLStr string,
|
|
|
|
metadata []byte,
|
|
|
|
certificate []byte,
|
|
|
|
key []byte,
|
|
|
|
options ...ProviderOpts,
|
|
|
|
) (*Provider, error) {
|
|
|
|
entityDescriptor := new(saml.EntityDescriptor)
|
|
|
|
if err := xml.Unmarshal(metadata, entityDescriptor); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
keyPair, err := tls.X509KeyPair(certificate, key)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
rootURL, err := url.Parse(rootURLStr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
opts := samlsp.Options{
|
|
|
|
URL: *rootURL,
|
|
|
|
Key: keyPair.PrivateKey.(*rsa.PrivateKey),
|
|
|
|
Certificate: keyPair.Leaf,
|
|
|
|
IDPMetadata: entityDescriptor,
|
|
|
|
SignRequest: false,
|
|
|
|
}
|
|
|
|
provider := &Provider{
|
|
|
|
name: name,
|
|
|
|
spOptions: &opts,
|
|
|
|
Certificate: certificate,
|
|
|
|
}
|
|
|
|
for _, option := range options {
|
|
|
|
option(provider)
|
|
|
|
}
|
|
|
|
return provider, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Provider) Name() string {
|
|
|
|
return p.name
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Provider) IsLinkingAllowed() bool {
|
|
|
|
return p.isLinkingAllowed
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Provider) IsCreationAllowed() bool {
|
|
|
|
return p.isCreationAllowed
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Provider) IsAutoCreation() bool {
|
|
|
|
return p.isAutoCreation
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Provider) IsAutoUpdate() bool {
|
|
|
|
return p.isAutoUpdate
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Provider) GetSP() (*samlsp.Middleware, error) {
|
|
|
|
sp, err := samlsp.New(*p.spOptions)
|
|
|
|
if err != nil {
|
2023-12-08 16:30:55 +02:00
|
|
|
return nil, zerrors.ThrowInternal(err, "SAML-qee09ffuq5", "Errors.Intent.IDPInvalid")
|
2023-09-29 11:26:14 +02:00
|
|
|
}
|
2024-04-10 12:05:07 +02:00
|
|
|
// 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
|
2023-09-29 11:26:14 +02:00
|
|
|
if p.requestTracker != nil {
|
|
|
|
sp.RequestTracker = p.requestTracker
|
|
|
|
}
|
2024-02-05 15:45:15 +01:00
|
|
|
if p.binding != "" {
|
|
|
|
sp.Binding = p.binding
|
|
|
|
}
|
2023-09-29 11:26:14 +02:00
|
|
|
return sp, nil
|
|
|
|
}
|
|
|
|
|
2023-11-13 10:25:26 +02:00
|
|
|
func (p *Provider) BeginAuth(ctx context.Context, state string, _ ...idp.Parameter) (idp.Session, error) {
|
2023-09-29 11:26:14 +02:00
|
|
|
m, err := p.GetSP()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Session{
|
|
|
|
ServiceProvider: m,
|
|
|
|
state: state,
|
|
|
|
}, nil
|
|
|
|
}
|