2023-09-29 11:26:14 +02:00
|
|
|
package saml
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2025-06-30 11:07:33 -04:00
|
|
|
"encoding/base64"
|
2024-10-11 08:04:15 +01:00
|
|
|
"errors"
|
2023-09-29 11:26:14 +02:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2025-07-31 17:12:26 +02:00
|
|
|
"strings"
|
2025-05-02 13:44:24 +02:00
|
|
|
"time"
|
2023-09-29 11:26:14 +02:00
|
|
|
|
2025-06-30 11:07:33 -04:00
|
|
|
"github.com/beevik/etree"
|
2023-09-29 11:26:14 +02:00
|
|
|
"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.Session = (*Session)(nil)
|
|
|
|
|
|
|
|
// Session is the [idp.Session] implementation for the SAML provider.
|
|
|
|
type Session struct {
|
2024-05-23 07:04:07 +02:00
|
|
|
ServiceProvider *samlsp.Middleware
|
|
|
|
state string
|
|
|
|
TransientMappingAttributeName string
|
2023-09-29 11:26:14 +02:00
|
|
|
|
|
|
|
RequestID string
|
|
|
|
Request *http.Request
|
|
|
|
|
|
|
|
Assertion *saml.Assertion
|
|
|
|
}
|
|
|
|
|
2024-05-23 07:04:07 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-09-29 11:26:14 +02:00
|
|
|
// GetAuth implements the [idp.Session] interface.
|
2025-06-30 11:07:33 -04:00
|
|
|
func (s *Session) GetAuth(ctx context.Context) (idp.Auth, error) {
|
|
|
|
url, err := url.Parse(s.state)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-09-29 11:26:14 +02:00
|
|
|
request := &http.Request{
|
|
|
|
URL: url,
|
|
|
|
}
|
2025-06-30 11:07:33 -04:00
|
|
|
return s.auth(request.WithContext(ctx))
|
2023-09-29 11:26:14 +02:00
|
|
|
}
|
|
|
|
|
2025-02-26 13:20:47 +01:00
|
|
|
// PersistentParameters implements the [idp.Session] interface.
|
|
|
|
func (s *Session) PersistentParameters() map[string]any {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-09-29 11:26:14 +02:00
|
|
|
// FetchUser implements the [idp.Session] interface.
|
|
|
|
func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
|
|
|
|
if s.RequestID == "" || s.Request == nil {
|
2023-12-08 16:30:55 +02:00
|
|
|
return nil, zerrors.ThrowInvalidArgument(nil, "SAML-d09hy0wkex", "Errors.Intent.ResponseInvalid")
|
2023-09-29 11:26:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
s.Assertion, err = s.ServiceProvider.ServiceProvider.ParseResponse(s.Request, []string{s.RequestID})
|
|
|
|
if err != nil {
|
2024-10-11 08:04:15 +01:00
|
|
|
invalidRespErr := new(saml.InvalidResponseError)
|
|
|
|
if errors.As(err, &invalidRespErr) {
|
2024-12-03 11:38:28 +01:00
|
|
|
return nil, zerrors.ThrowInvalidArgument(invalidRespErr.PrivateErr, "SAML-ajl3irfs", "Errors.Intent.ResponseInvalid")
|
2024-10-11 08:04:15 +01:00
|
|
|
}
|
2023-12-08 16:30:55 +02:00
|
|
|
return nil, zerrors.ThrowInvalidArgument(err, "SAML-nuo0vphhh9", "Errors.Intent.ResponseInvalid")
|
2023-09-29 11:26:14 +02:00
|
|
|
}
|
|
|
|
|
2025-07-31 17:12:26 +02:00
|
|
|
userMapper := NewUser()
|
2024-10-03 10:17:33 +02:00
|
|
|
// nameID is required, but at least in ADFS it will not be sent unless explicitly configured
|
|
|
|
if s.Assertion.Subject == nil || s.Assertion.Subject.NameID == nil {
|
2025-07-31 17:12:26 +02:00
|
|
|
if strings.TrimSpace(s.TransientMappingAttributeName) == "" {
|
|
|
|
return nil, zerrors.ThrowInvalidArgument(err, "SAML-EFG32", "Errors.Intent.MissingTransientMappingAttributeName")
|
|
|
|
}
|
|
|
|
// workaround to use the transient mapping attribute when the subject / nameID are missing (e.g. in ADFS, Shibboleth)
|
2024-05-23 07:04:07 +02:00
|
|
|
mappingID, err := s.transientMappingID()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
userMapper.SetID(mappingID)
|
2025-07-31 17:12:26 +02:00
|
|
|
} else {
|
|
|
|
nameID := 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)
|
|
|
|
}
|
2024-05-23 07:04:07 +02:00
|
|
|
}
|
2025-07-31 17:12:26 +02:00
|
|
|
|
2023-09-29 11:26:14 +02:00
|
|
|
for _, statement := range s.Assertion.AttributeStatements {
|
|
|
|
for _, attribute := range statement.Attributes {
|
|
|
|
values := make([]string, len(attribute.Values))
|
|
|
|
for i := range attribute.Values {
|
|
|
|
values[i] = attribute.Values[i].Value
|
|
|
|
}
|
|
|
|
userMapper.Attributes[attribute.Name] = values
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return userMapper, nil
|
|
|
|
}
|
|
|
|
|
2025-05-02 13:44:24 +02:00
|
|
|
func (s *Session) ExpiresAt() time.Time {
|
|
|
|
if s.Assertion == nil || s.Assertion.Conditions == nil {
|
|
|
|
return time.Time{}
|
|
|
|
}
|
|
|
|
return s.Assertion.Conditions.NotOnOrAfter
|
|
|
|
}
|
|
|
|
|
2024-05-23 07:04:07 +02:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2025-06-30 11:07:33 -04:00
|
|
|
// auth is a modified copy of the [samlsp.Middleware.HandleStartAuthFlow] method.
|
|
|
|
// Instead of writing the response to the http.ResponseWriter, it returns the auth request as an [idp.Auth].
|
|
|
|
// In case of an error, it returns the error directly and does not write to the response.
|
|
|
|
func (s *Session) auth(r *http.Request) (idp.Auth, error) {
|
|
|
|
if r.URL.Path == s.ServiceProvider.ServiceProvider.AcsURL.Path {
|
|
|
|
// should never occur, but was handled in the original method, so we keep it here
|
|
|
|
return nil, zerrors.ThrowInvalidArgument(nil, "SAML-Eoi24", "don't wrap Middleware with RequireAccount")
|
|
|
|
}
|
2023-09-29 11:26:14 +02:00
|
|
|
|
2025-06-30 11:07:33 -04:00
|
|
|
var binding, bindingLocation string
|
|
|
|
if s.ServiceProvider.Binding != "" {
|
|
|
|
binding = s.ServiceProvider.Binding
|
|
|
|
bindingLocation = s.ServiceProvider.ServiceProvider.GetSSOBindingLocation(binding)
|
|
|
|
} else {
|
|
|
|
binding = saml.HTTPRedirectBinding
|
|
|
|
bindingLocation = s.ServiceProvider.ServiceProvider.GetSSOBindingLocation(binding)
|
|
|
|
if bindingLocation == "" {
|
|
|
|
binding = saml.HTTPPostBinding
|
|
|
|
bindingLocation = s.ServiceProvider.ServiceProvider.GetSSOBindingLocation(binding)
|
|
|
|
}
|
|
|
|
}
|
2023-09-29 11:26:14 +02:00
|
|
|
|
2025-06-30 11:07:33 -04:00
|
|
|
authReq, err := s.ServiceProvider.ServiceProvider.MakeAuthenticationRequest(bindingLocation, binding, s.ServiceProvider.ResponseBinding)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
relayState, err := s.ServiceProvider.RequestTracker.TrackRequest(nil, r, authReq.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-09-29 11:26:14 +02:00
|
|
|
|
2025-06-30 11:07:33 -04:00
|
|
|
if binding == saml.HTTPRedirectBinding {
|
|
|
|
redirectURL, err := authReq.Redirect(relayState, &s.ServiceProvider.ServiceProvider)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return idp.Redirect(redirectURL.String())
|
|
|
|
}
|
|
|
|
if binding == saml.HTTPPostBinding {
|
|
|
|
doc := etree.NewDocument()
|
|
|
|
doc.SetRoot(authReq.Element())
|
|
|
|
reqBuf, err := doc.WriteToBytes()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
encodedReqBuf := base64.StdEncoding.EncodeToString(reqBuf)
|
|
|
|
return idp.Form(authReq.Destination,
|
|
|
|
map[string]string{
|
|
|
|
"SAMLRequest": encodedReqBuf,
|
|
|
|
"RelayState": relayState,
|
|
|
|
})
|
2023-09-29 11:26:14 +02:00
|
|
|
}
|
2025-06-30 11:07:33 -04:00
|
|
|
return nil, zerrors.ThrowInvalidArgument(nil, "SAML-Eoi24", "Errors.Intent.Invalid")
|
2023-09-29 11:26:14 +02:00
|
|
|
}
|