Stefan Benz 15fd3045e0
feat: add SAML as identity provider (#6454)
* feat: first implementation for saml sp

* fix: add command side instance and org for saml provider

* fix: add query side instance and org for saml provider

* fix: request handling in event and retrieval of finished intent

* fix: add review changes and integration tests

* fix: add integration tests for saml idp

* fix: correct unit tests with review changes

* fix: add saml session unit test

* fix: add saml session unit test

* fix: add saml session unit test

* fix: changes from review

* fix: changes from review

* fix: proto build error

* fix: proto build error

* fix: proto build error

* fix: proto require metadata oneof

* fix: login with saml provider

* fix: integration test for saml assertion

* lint client.go

* fix json tag

* fix: linting

* fix import

* fix: linting

* fix saml idp query

* fix: linting

* lint: try all issues

* revert linting config

* fix: add regenerate endpoints

* fix: translations

* fix mk.yaml

* ignore acs path for user agent cookie

* fix: add AuthFromProvider test for saml

* fix: integration test for saml retrieve information

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
2023-09-29 11:26:14 +02:00

94 lines
2.2 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/errors"
"github.com/zitadel/zitadel/internal/idp"
)
var _ idp.Session = (*Session)(nil)
// Session is the [idp.Session] implementation for the SAML provider.
type Session struct {
ServiceProvider *samlsp.Middleware
state string
RequestID string
Request *http.Request
Assertion *saml.Assertion
}
// 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, errors.ThrowInvalidArgument(nil, "SAML-d09hy0wkex", "Errors.Intent.ResponseInvalid")
}
s.Assertion, err = s.ServiceProvider.ServiceProvider.ParseResponse(s.Request, []string{s.RequestID})
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "SAML-nuo0vphhh9", "Errors.Intent.ResponseInvalid")
}
userMapper := NewUser()
userMapper.SetID(s.Assertion.Subject.NameID)
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
}
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{}),
}
}