revert: "feat(IDP): use single callback endpoint"
This reverts commit e126ccc9aa
.
# Which Problems Are Solved
#8295 introduced the possibility to handle idps on a single callback,
but broke current setups.
# How the Problems Are Solved
- Revert the change until a proper solution is found. Revert is needed
as docs were also changed.
# Additional Changes
None.
# Additional Context
- relates to #8295
@ -425,6 +425,8 @@ func startAPIs(
|
||||
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
|
||||
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle))
|
||||
|
||||
apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, config.ExternalSecure, instanceInterceptor.Handler))
|
||||
|
||||
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost, login.EndpointSAMLACS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -488,8 +490,6 @@ func startAPIs(
|
||||
apis.RegisterHandlerOnPrefix(login.HandlerPrefix, l.Handler())
|
||||
apis.HandleFunc(login.EndpointDeviceAuth, login.RedirectDeviceAuthToPrefix)
|
||||
|
||||
apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, config.ExternalSecure, instanceInterceptor.Handler, login.IDPCallbackRedirect))
|
||||
|
||||
// After OIDC provider so that the callback endpoint can be used
|
||||
if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil {
|
||||
return nil, err
|
||||
|
@ -85,7 +85,7 @@ export class ProviderNextService {
|
||||
map((env) => [
|
||||
{
|
||||
label: 'ZITADEL Callback URL',
|
||||
url: `${env.issuer}/idps/callback`,
|
||||
url: `${env.issuer}/ui/login/login/externalidp/callback`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
@ -84,7 +84,11 @@ export class ProviderSamlSpComponent {
|
||||
downloadable: true,
|
||||
},
|
||||
{
|
||||
label: 'ZITADEL ACS',
|
||||
label: 'ZITADEL ACS Login Form',
|
||||
url: `${environment.issuer}/ui/login/login/externalidp/saml/acs`,
|
||||
},
|
||||
{
|
||||
label: 'ZITADEL ACS Intent API',
|
||||
url: `${idpBase}/acs`,
|
||||
},
|
||||
{
|
||||
|
@ -72,7 +72,7 @@ Now we configure the identity provider on ZITADEL.
|
||||
|
||||
After you created the SAML provider in ZITADEL, you can copy the URLs you need to configure in your Entra ID application.
|
||||
|
||||
![Azure SAML App URLs](/img/guides/zitadel_saml_provider_urls.png)
|
||||
![Azure SAML App URLs](/img/guides/zitadel_azure_saml_provider_urls.png)
|
||||
|
||||
1. Go to Microsoft Entra > Manage > Single sign-on
|
||||
2. Edit the "Basic SAML Configuration"
|
||||
|
@ -23,8 +23,8 @@ import TestSetup from './_test_setup.mdx';
|
||||
2. Add your App Name, your Company Page and a Logo
|
||||
3. Add "Sign In with LinkedIn using OpenID Connect" by clicking "Request access"
|
||||
4. Go to the Auth Settings of the App and add the following URL to the "Authorized redirect URLs"
|
||||
- `{your_domain}/idps/callback`
|
||||
- Example redirect url for the domain `https://acme.zitadel.cloud` would look like this: `https://acme.zitadel.cloud/idps/callback`
|
||||
- `{your_domain}/ui/login/login/externalidp/callback`
|
||||
- Example redirect url for the domain `https://acme.zitadel.cloud` would look like this: `https://acme.zitadel.cloud/ui/login/login/externalidp/callback`
|
||||
5. Verify the app as your company
|
||||
6. In the Auth - OAuth 2.0 scopes section you should see `openid`, `profile` and `email` listed
|
||||
7. Save Client ID and Primary Client Secret from the Application credentials
|
||||
|
@ -35,7 +35,7 @@ As an alternative you can add the SAML identity provider through the API, either
|
||||
|
||||
After you created the SAML Provider in ZITADEL, you can copy the URLs you need to configure in your OKTA application.
|
||||
|
||||
![OKTA SAML App URLs](/img/guides/zitadel_saml_provider_urls.png)
|
||||
![OKTA SAML App URLs](/img/guides/zitadel_okta_saml_provider_urls.png)
|
||||
|
||||
## OKTA Configuration
|
||||
|
||||
|
BIN
docs/static/img/guides/azure_app_redirects.png
vendored
Normal file
After Width: | Height: | Size: 127 KiB |
BIN
docs/static/img/guides/azure_app_register.png
vendored
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
docs/static/img/guides/azure_app_registration.png
vendored
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 327 KiB |
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 116 KiB |
BIN
docs/static/img/guides/gitlab_app_id_secret.png
vendored
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 103 KiB |
BIN
docs/static/img/guides/gitlab_app_registration.png
vendored
Before Width: | Height: | Size: 339 KiB After Width: | Height: | Size: 253 KiB |
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 46 KiB |
BIN
docs/static/img/guides/zitadel_azure_provider2.png
vendored
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 45 KiB |
BIN
docs/static/img/guides/zitadel_azure_saml_provider_urls.png
vendored
Normal file
After Width: | Height: | Size: 95 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 44 KiB |
BIN
docs/static/img/guides/zitadel_okta_saml_provider_urls.png
vendored
Normal file
After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 98 KiB |
@ -9,11 +9,13 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/form"
|
||||
@ -51,13 +53,13 @@ const (
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
commands *command.Commands
|
||||
queries *query.Queries
|
||||
parser *form.Parser
|
||||
encryptionAlgorithm crypto.EncryptionAlgorithm
|
||||
callbackURL func(ctx context.Context) string
|
||||
samlRootURL func(ctx context.Context, idpID string) string
|
||||
loginUICallbackRedirect func(w http.ResponseWriter, r *http.Request, state string) bool
|
||||
commands *command.Commands
|
||||
queries *query.Queries
|
||||
parser *form.Parser
|
||||
encryptionAlgorithm crypto.EncryptionAlgorithm
|
||||
callbackURL func(ctx context.Context) string
|
||||
samlRootURL func(ctx context.Context, idpID string) string
|
||||
loginSAMLRootURL func(ctx context.Context) string
|
||||
}
|
||||
|
||||
type externalIDPCallbackData struct {
|
||||
@ -89,22 +91,27 @@ func SAMLRootURL(externalSecure bool) func(ctx context.Context, idpID string) st
|
||||
}
|
||||
}
|
||||
|
||||
func LoginSAMLRootURL(externalSecure bool) func(ctx context.Context) string {
|
||||
return func(ctx context.Context) string {
|
||||
return http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), externalSecure) + login.HandlerPrefix + login.EndpointSAMLACS
|
||||
}
|
||||
}
|
||||
|
||||
func NewHandler(
|
||||
commands *command.Commands,
|
||||
queries *query.Queries,
|
||||
encryptionAlgorithm crypto.EncryptionAlgorithm,
|
||||
externalSecure bool,
|
||||
instanceInterceptor func(next http.Handler) http.Handler,
|
||||
loginUICallbackRedirect func(w http.ResponseWriter, r *http.Request, state string) bool,
|
||||
) http.Handler {
|
||||
h := &Handler{
|
||||
commands: commands,
|
||||
queries: queries,
|
||||
parser: form.NewParser(),
|
||||
encryptionAlgorithm: encryptionAlgorithm,
|
||||
callbackURL: CallbackURL(externalSecure),
|
||||
loginUICallbackRedirect: loginUICallbackRedirect,
|
||||
samlRootURL: SAMLRootURL(externalSecure),
|
||||
commands: commands,
|
||||
queries: queries,
|
||||
parser: form.NewParser(),
|
||||
encryptionAlgorithm: encryptionAlgorithm,
|
||||
callbackURL: CallbackURL(externalSecure),
|
||||
samlRootURL: SAMLRootURL(externalSecure),
|
||||
loginSAMLRootURL: LoginSAMLRootURL(externalSecure),
|
||||
}
|
||||
|
||||
router := mux.NewRouter()
|
||||
@ -182,6 +189,22 @@ func (h *Handler) handleMetadata(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
metadata := sp.ServiceProvider.Metadata()
|
||||
|
||||
for i, spDesc := range metadata.SPSSODescriptors {
|
||||
spDesc.AssertionConsumerServices = append(
|
||||
spDesc.AssertionConsumerServices,
|
||||
saml.IndexedEndpoint{
|
||||
Binding: saml.HTTPPostBinding,
|
||||
Location: h.loginSAMLRootURL(ctx),
|
||||
Index: len(spDesc.AssertionConsumerServices) + 1,
|
||||
}, saml.IndexedEndpoint{
|
||||
Binding: saml.HTTPArtifactBinding,
|
||||
Location: h.loginSAMLRootURL(ctx),
|
||||
Index: len(spDesc.AssertionConsumerServices) + 2,
|
||||
},
|
||||
)
|
||||
metadata.SPSSODescriptors[i] = spDesc
|
||||
}
|
||||
|
||||
buf, _ := xml.MarshalIndent(metadata, "", " ")
|
||||
w.Header().Set("Content-Type", "application/samlmetadata+xml")
|
||||
_, err = w.Write(buf)
|
||||
@ -195,9 +218,6 @@ func (h *Handler) handleACS(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
data := parseSAMLRequest(r)
|
||||
|
||||
if h.loginUICallbackRedirect(w, r, data.RelayState) {
|
||||
return
|
||||
}
|
||||
provider, err := h.getProvider(ctx, data.IDPID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
@ -252,9 +272,6 @@ func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if h.loginUICallbackRedirect(w, r, data.State) {
|
||||
return
|
||||
}
|
||||
intent, err := h.commands.GetActiveIntent(ctx, data.State)
|
||||
if err != nil {
|
||||
if zerrors.IsNotFound(err) {
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/crewjam/saml/samlsp"
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
@ -14,8 +13,8 @@ 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"
|
||||
idp_api "github.com/zitadel/zitadel/internal/api/idp"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
@ -38,8 +37,6 @@ import (
|
||||
const (
|
||||
queryIDPConfigID = "idpConfigID"
|
||||
tmplExternalNotFoundOption = "externalnotfoundoption"
|
||||
|
||||
prefixLoginState = "LOGIN_"
|
||||
)
|
||||
|
||||
type externalIDPData struct {
|
||||
@ -101,36 +98,6 @@ type externalRegisterFormData struct {
|
||||
TermsConfirm bool `schema:"terms-confirm"`
|
||||
}
|
||||
|
||||
// IDPCallbackRedirect checks if the state parameter was set by the login UI
|
||||
// and redirects the request to the login UI idp callback handlers (GET and POST).
|
||||
// The function is used in the /idps/callback endpoint to distinguish between requests
|
||||
// starte in the login UI and IdP intents.
|
||||
func IDPCallbackRedirect(w http.ResponseWriter, r *http.Request, state string) bool {
|
||||
state, loginUI := authRequestIDFromState(state)
|
||||
if !loginUI {
|
||||
return false
|
||||
}
|
||||
callback := EndpointExternalLoginCallback
|
||||
if r.Method == http.MethodPost {
|
||||
callback = EndpointExternalLoginCallbackFormPost
|
||||
}
|
||||
target := gu.PtrCopy(r.URL)
|
||||
target.Path = HandlerPrefix + callback
|
||||
q := target.Query()
|
||||
q.Set("state", state)
|
||||
target.RawQuery = q.Encode()
|
||||
http.Redirect(w, r, target.String(), http.StatusTemporaryRedirect)
|
||||
return true
|
||||
}
|
||||
|
||||
func stateFromAuthRequest(authRequestID string) string {
|
||||
return prefixLoginState + authRequestID
|
||||
}
|
||||
|
||||
func authRequestIDFromState(state string) (string, bool) {
|
||||
return strings.CutPrefix(state, prefixLoginState)
|
||||
}
|
||||
|
||||
// handleExternalLoginStep is called as nextStep
|
||||
func (l *Login) handleExternalLoginStep(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, selectedIDPID string) {
|
||||
for _, idp := range authReq.AllowedExternalIDPs {
|
||||
@ -220,8 +187,7 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
|
||||
return
|
||||
}
|
||||
params := l.sessionParamsFromAuthRequest(r.Context(), authReq, identityProvider.ID)
|
||||
state := stateFromAuthRequest(authReq.ID)
|
||||
session, err := provider.BeginAuth(r.Context(), state, params...)
|
||||
session, err := provider.BeginAuth(r.Context(), authReq.ID, params...)
|
||||
if err != nil {
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
@ -693,7 +659,7 @@ func (l *Login) handleExternalNotFoundOptionCheck(w http.ResponseWriter, r *http
|
||||
data := new(externalNotFoundOptionFormData)
|
||||
authReq, err := l.ensureAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -995,7 +961,7 @@ func (l *Login) googleProvider(ctx context.Context, identityProvider *query.IDPT
|
||||
return google.New(
|
||||
identityProvider.GoogleIDPTemplate.ClientID,
|
||||
secret,
|
||||
idp_api.CallbackURL(l.externalSecure)(ctx),
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.GoogleIDPTemplate.Scopes,
|
||||
)
|
||||
}
|
||||
@ -1014,7 +980,7 @@ func (l *Login) oidcProvider(ctx context.Context, identityProvider *query.IDPTem
|
||||
identityProvider.OIDCIDPTemplate.Issuer,
|
||||
identityProvider.OIDCIDPTemplate.ClientID,
|
||||
secret,
|
||||
idp_api.CallbackURL(l.externalSecure)(ctx),
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.OIDCIDPTemplate.Scopes,
|
||||
openid.DefaultMapper,
|
||||
opts...,
|
||||
@ -1044,7 +1010,7 @@ func (l *Login) oauthProvider(ctx context.Context, identityProvider *query.IDPTe
|
||||
AuthURL: identityProvider.OAuthIDPTemplate.AuthorizationEndpoint,
|
||||
TokenURL: identityProvider.OAuthIDPTemplate.TokenEndpoint,
|
||||
},
|
||||
RedirectURL: idp_api.CallbackURL(l.externalSecure)(ctx),
|
||||
RedirectURL: l.baseURL(ctx) + EndpointExternalLoginCallback,
|
||||
Scopes: identityProvider.OAuthIDPTemplate.Scopes,
|
||||
}
|
||||
return oauth.New(
|
||||
@ -1062,7 +1028,6 @@ func (l *Login) samlProvider(ctx context.Context, identityProvider *query.IDPTem
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rootURL := idp_api.SAMLRootURL(l.externalSecure)(ctx, identityProvider.ID)
|
||||
opts := make([]saml.ProviderOpts, 0, 6)
|
||||
if identityProvider.SAMLIDPTemplate.WithSignedRequest {
|
||||
opts = append(opts, saml.WithSignedRequest())
|
||||
@ -1077,12 +1042,11 @@ func (l *Login) samlProvider(ctx context.Context, identityProvider *query.IDPTem
|
||||
opts = append(opts, saml.WithTransientMappingAttributeName(identityProvider.SAMLIDPTemplate.TransientMappingAttributeName))
|
||||
}
|
||||
opts = append(opts,
|
||||
saml.WithEntityID(rootURL+"saml/metadata"),
|
||||
saml.WithEntityID(http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), l.externalSecure)+"/idps/"+identityProvider.ID+"/saml/metadata"),
|
||||
saml.WithCustomRequestTracker(
|
||||
requesttracker.New(
|
||||
func(ctx context.Context, state, samlRequestID string) error {
|
||||
func(ctx context.Context, authRequestID, samlRequestID string) error {
|
||||
useragent, _ := http_mw.UserAgentIDFromCtx(ctx)
|
||||
authRequestID, _ := authRequestIDFromState(state)
|
||||
return l.authRepo.SaveSAMLRequestID(ctx, authRequestID, samlRequestID, useragent)
|
||||
},
|
||||
func(ctx context.Context, authRequestID string) (*samlsp.TrackedRequest, error) {
|
||||
@ -1100,7 +1064,7 @@ func (l *Login) samlProvider(ctx context.Context, identityProvider *query.IDPTem
|
||||
))
|
||||
return saml.New(
|
||||
identityProvider.Name,
|
||||
rootURL,
|
||||
l.baseURL(ctx)+EndpointExternalLogin+"/",
|
||||
identityProvider.SAMLIDPTemplate.Metadata,
|
||||
identityProvider.SAMLIDPTemplate.Certificate,
|
||||
key,
|
||||
@ -1124,7 +1088,7 @@ func (l *Login) azureProvider(ctx context.Context, identityProvider *query.IDPTe
|
||||
identityProvider.Name,
|
||||
identityProvider.AzureADIDPTemplate.ClientID,
|
||||
secret,
|
||||
idp_api.CallbackURL(l.externalSecure)(ctx),
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.AzureADIDPTemplate.Scopes,
|
||||
opts...,
|
||||
)
|
||||
@ -1138,7 +1102,7 @@ func (l *Login) githubProvider(ctx context.Context, identityProvider *query.IDPT
|
||||
return github.New(
|
||||
identityProvider.GitHubIDPTemplate.ClientID,
|
||||
secret,
|
||||
idp_api.CallbackURL(l.externalSecure)(ctx),
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.GitHubIDPTemplate.Scopes,
|
||||
)
|
||||
}
|
||||
@ -1152,7 +1116,7 @@ func (l *Login) githubEnterpriseProvider(ctx context.Context, identityProvider *
|
||||
identityProvider.Name,
|
||||
identityProvider.GitHubIDPTemplate.ClientID,
|
||||
secret,
|
||||
idp_api.CallbackURL(l.externalSecure)(ctx),
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.GitHubEnterpriseIDPTemplate.AuthorizationEndpoint,
|
||||
identityProvider.GitHubEnterpriseIDPTemplate.TokenEndpoint,
|
||||
identityProvider.GitHubEnterpriseIDPTemplate.UserEndpoint,
|
||||
@ -1168,7 +1132,7 @@ func (l *Login) gitlabProvider(ctx context.Context, identityProvider *query.IDPT
|
||||
return gitlab.New(
|
||||
identityProvider.GitLabIDPTemplate.ClientID,
|
||||
secret,
|
||||
idp_api.CallbackURL(l.externalSecure)(ctx),
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.GitLabIDPTemplate.Scopes,
|
||||
)
|
||||
}
|
||||
@ -1183,7 +1147,7 @@ func (l *Login) gitlabSelfHostedProvider(ctx context.Context, identityProvider *
|
||||
identityProvider.GitLabSelfHostedIDPTemplate.Issuer,
|
||||
identityProvider.GitLabSelfHostedIDPTemplate.ClientID,
|
||||
secret,
|
||||
idp_api.CallbackURL(l.externalSecure)(ctx),
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.GitLabSelfHostedIDPTemplate.Scopes,
|
||||
)
|
||||
}
|
||||
@ -1197,7 +1161,7 @@ func (l *Login) appleProvider(ctx context.Context, identityProvider *query.IDPTe
|
||||
identityProvider.AppleIDPTemplate.ClientID,
|
||||
identityProvider.AppleIDPTemplate.TeamID,
|
||||
identityProvider.AppleIDPTemplate.KeyID,
|
||||
idp_api.CallbackURL(l.externalSecure)(ctx),
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallbackFormPost,
|
||||
privateKey,
|
||||
identityProvider.AppleIDPTemplate.Scopes,
|
||||
)
|
||||
|