mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 04:57:33 +00:00
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>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/crewjam/saml/samlsp"
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@@ -27,6 +29,8 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
|
||||
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/saml"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/saml/requesttracker"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
@@ -43,6 +47,9 @@ type externalIDPCallbackData struct {
|
||||
State string `schema:"state"`
|
||||
Code string `schema:"code"`
|
||||
|
||||
RelayState string `schema:"RelayState"`
|
||||
Method string `schema:"Method"`
|
||||
|
||||
// Apple returns a user on first registration
|
||||
User string `schema:"user"`
|
||||
}
|
||||
@@ -167,6 +174,8 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
|
||||
provider, err = l.appleProvider(r.Context(), identityProvider)
|
||||
case domain.IDPTypeLDAP:
|
||||
provider, err = l.ldapProvider(r.Context(), identityProvider)
|
||||
case domain.IDPTypeSAML:
|
||||
provider, err = l.samlProvider(r.Context(), identityProvider)
|
||||
case domain.IDPTypeUnspecified:
|
||||
fallthrough
|
||||
default:
|
||||
@@ -183,7 +192,17 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, session.GetAuthURL(), http.StatusFound)
|
||||
|
||||
content, redirect := session.GetAuth(r.Context())
|
||||
if redirect {
|
||||
http.Redirect(w, r, content, http.StatusFound)
|
||||
return
|
||||
}
|
||||
_, err = w.Write([]byte(content))
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleExternalLoginCallbackForm handles the callback from a IDP with form_post.
|
||||
@@ -195,6 +214,7 @@ func (l *Login) handleExternalLoginCallbackForm(w http.ResponseWriter, r *http.R
|
||||
l.renderLogin(w, r, nil, err)
|
||||
return
|
||||
}
|
||||
r.Form.Add("Method", http.MethodPost)
|
||||
http.Redirect(w, r, HandlerPrefix+EndpointExternalLoginCallback+"?"+r.Form.Encode(), 302)
|
||||
}
|
||||
|
||||
@@ -207,6 +227,15 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
|
||||
l.renderLogin(w, r, nil, err)
|
||||
return
|
||||
}
|
||||
if data.State == "" {
|
||||
data.State = data.RelayState
|
||||
}
|
||||
// workaround because of CSRF on external identity provider flows
|
||||
if data.Method == http.MethodPost {
|
||||
r.Method = http.MethodPost
|
||||
r.PostForm = r.Form
|
||||
}
|
||||
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID)
|
||||
if err != nil {
|
||||
@@ -284,6 +313,18 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
session = &apple.Session{Session: &openid.Session{Provider: provider.(*apple.Provider).Provider, Code: data.Code}, UserFormValue: data.User}
|
||||
case domain.IDPTypeSAML:
|
||||
provider, err = l.samlProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
l.externalAuthFailed(w, r, authReq, nil, nil, err)
|
||||
return
|
||||
}
|
||||
sp, err := provider.(*saml.Provider).GetSP()
|
||||
if err != nil {
|
||||
l.externalAuthFailed(w, r, authReq, nil, nil, err)
|
||||
return
|
||||
}
|
||||
session = &saml.Session{ServiceProvider: sp, RequestID: authReq.SAMLRequestID, Request: r}
|
||||
case domain.IDPTypeJWT,
|
||||
domain.IDPTypeLDAP,
|
||||
domain.IDPTypeUnspecified:
|
||||
@@ -881,6 +922,49 @@ func (l *Login) oauthProvider(ctx context.Context, identityProvider *query.IDPTe
|
||||
)
|
||||
}
|
||||
|
||||
func (l *Login) samlProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*saml.Provider, error) {
|
||||
key, err := crypto.Decrypt(identityProvider.SAMLIDPTemplate.Key, l.idpConfigAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts := make([]saml.ProviderOpts, 0, 2)
|
||||
if identityProvider.SAMLIDPTemplate.WithSignedRequest {
|
||||
opts = append(opts, saml.WithSignedRequest())
|
||||
}
|
||||
if identityProvider.SAMLIDPTemplate.Binding != "" {
|
||||
opts = append(opts, saml.WithBinding(identityProvider.SAMLIDPTemplate.Binding))
|
||||
}
|
||||
opts = append(opts,
|
||||
saml.WithEntityID(http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), l.externalSecure)+"/idps/"+identityProvider.ID+"/saml/metadata"),
|
||||
saml.WithCustomRequestTracker(
|
||||
requesttracker.New(
|
||||
func(ctx context.Context, authRequestID, samlRequestID string) error {
|
||||
useragent, _ := http_mw.UserAgentIDFromCtx(ctx)
|
||||
return l.authRepo.SaveSAMLRequestID(ctx, authRequestID, samlRequestID, useragent)
|
||||
},
|
||||
func(ctx context.Context, authRequestID string) (*samlsp.TrackedRequest, error) {
|
||||
useragent, _ := http_mw.UserAgentIDFromCtx(ctx)
|
||||
auhRequest, err := l.authRepo.AuthRequestByID(ctx, authRequestID, useragent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &samlsp.TrackedRequest{
|
||||
SAMLRequestID: auhRequest.SAMLRequestID,
|
||||
Index: authRequestID,
|
||||
}, nil
|
||||
},
|
||||
),
|
||||
))
|
||||
return saml.New(
|
||||
identityProvider.Name,
|
||||
l.baseURL(ctx)+EndpointExternalLogin+"/",
|
||||
identityProvider.SAMLIDPTemplate.Metadata,
|
||||
identityProvider.SAMLIDPTemplate.Certificate,
|
||||
key,
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
func (l *Login) azureProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*azuread.Provider, error) {
|
||||
secret, err := crypto.DecryptString(identityProvider.AzureADIDPTemplate.ClientSecret, l.idpConfigAlg)
|
||||
if err != nil {
|
||||
|
@@ -126,7 +126,7 @@ func createCSRFInterceptor(cookieName string, csrfCookieKey []byte, externalSecu
|
||||
}
|
||||
// ignore form post callback
|
||||
// it will redirect to the "normal" callback, where the cookie is set again
|
||||
if r.URL.Path == EndpointExternalLoginCallbackFormPost && r.Method == http.MethodPost {
|
||||
if (r.URL.Path == EndpointExternalLoginCallbackFormPost || r.URL.Path == EndpointSAMLACS) && r.Method == http.MethodPost {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ const (
|
||||
EndpointExternalLogin = "/login/externalidp"
|
||||
EndpointExternalLoginCallback = "/login/externalidp/callback"
|
||||
EndpointExternalLoginCallbackFormPost = "/login/externalidp/callback/form"
|
||||
EndpointSAMLACS = "/login/externalidp/saml/acs"
|
||||
EndpointJWTAuthorize = "/login/jwt/authorize"
|
||||
EndpointJWTCallback = "/login/jwt/callback"
|
||||
EndpointLDAPLogin = "/login/ldap"
|
||||
@@ -73,6 +74,8 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M
|
||||
router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointExternalLoginCallbackFormPost, login.handleExternalLoginCallbackForm).Methods(http.MethodPost)
|
||||
router.HandleFunc(EndpointSAMLACS, login.handleExternalLoginCallback).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointSAMLACS, login.handleExternalLoginCallbackForm).Methods(http.MethodPost)
|
||||
router.HandleFunc(EndpointJWTAuthorize, login.handleJWTRequest).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointJWTCallback, login.handleJWTCallback).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointPasswordlessLogin, login.handlePasswordlessVerification).Methods(http.MethodPost)
|
||||
|
Reference in New Issue
Block a user