mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-15 12:27:59 +00:00
e57a9b57c8
# 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>
132 lines
3.5 KiB
Go
132 lines
3.5 KiB
Go
package saml
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/crewjam/saml"
|
|
"github.com/crewjam/saml/samlsp"
|
|
|
|
"github.com/zitadel/zitadel/internal/idp"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
var _ idp.Session = (*Session)(nil)
|
|
|
|
// Session is the [idp.Session] implementation for the SAML provider.
|
|
type Session struct {
|
|
ServiceProvider *samlsp.Middleware
|
|
state string
|
|
TransientMappingAttributeName string
|
|
|
|
RequestID string
|
|
Request *http.Request
|
|
|
|
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)
|
|
resp := NewTempResponseWriter()
|
|
|
|
request := &http.Request{
|
|
URL: url,
|
|
}
|
|
s.ServiceProvider.HandleStartAuthFlow(
|
|
resp,
|
|
request.WithContext(ctx),
|
|
)
|
|
|
|
if location := resp.Header().Get("Location"); location != "" {
|
|
return idp.Redirect(location)
|
|
}
|
|
return idp.Form(resp.content.String())
|
|
}
|
|
|
|
// FetchUser implements the [idp.Session] interface.
|
|
func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
|
|
if s.RequestID == "" || s.Request == nil {
|
|
return nil, zerrors.ThrowInvalidArgument(nil, "SAML-d09hy0wkex", "Errors.Intent.ResponseInvalid")
|
|
}
|
|
|
|
s.Assertion, err = s.ServiceProvider.ServiceProvider.ParseResponse(s.Request, []string{s.RequestID})
|
|
if err != nil {
|
|
return nil, zerrors.ThrowInvalidArgument(err, "SAML-nuo0vphhh9", "Errors.Intent.ResponseInvalid")
|
|
}
|
|
|
|
nameID := s.Assertion.Subject.NameID
|
|
userMapper := NewUser()
|
|
// 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))
|
|
for i := range attribute.Values {
|
|
values[i] = attribute.Values[i].Value
|
|
}
|
|
userMapper.Attributes[attribute.Name] = values
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
func (w *TempResponseWriter) Header() http.Header {
|
|
return w.header
|
|
}
|
|
|
|
func (w *TempResponseWriter) Write(content []byte) (int, error) {
|
|
return w.content.Write(content)
|
|
}
|
|
|
|
func (w *TempResponseWriter) WriteHeader(statusCode int) {}
|
|
|
|
func NewTempResponseWriter() *TempResponseWriter {
|
|
return &TempResponseWriter{
|
|
header: map[string][]string{},
|
|
content: bytes.NewBuffer([]byte{}),
|
|
}
|
|
}
|