diff --git a/cmd/start/start.go b/cmd/start/start.go index c89f4361e..e424a3d90 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -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 diff --git a/console/src/app/modules/providers/provider-next/provider-next.service.ts b/console/src/app/modules/providers/provider-next/provider-next.service.ts index 8b0ed0406..245571e69 100644 --- a/console/src/app/modules/providers/provider-next/provider-next.service.ts +++ b/console/src/app/modules/providers/provider-next/provider-next.service.ts @@ -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`, }, ]), ); diff --git a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.ts b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.ts index eede73dc8..b0741c7b3 100644 --- a/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.ts +++ b/console/src/app/modules/providers/provider-saml-sp/provider-saml-sp.component.ts @@ -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`, }, { diff --git a/docs/docs/guides/integrate/identity-providers/azure-ad-saml.mdx b/docs/docs/guides/integrate/identity-providers/azure-ad-saml.mdx index 75d93bcfd..236488431 100644 --- a/docs/docs/guides/integrate/identity-providers/azure-ad-saml.mdx +++ b/docs/docs/guides/integrate/identity-providers/azure-ad-saml.mdx @@ -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" diff --git a/docs/docs/guides/integrate/identity-providers/linkedin_oauth.mdx b/docs/docs/guides/integrate/identity-providers/linkedin_oauth.mdx index 4475735b2..5709c4e32 100644 --- a/docs/docs/guides/integrate/identity-providers/linkedin_oauth.mdx +++ b/docs/docs/guides/integrate/identity-providers/linkedin_oauth.mdx @@ -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 diff --git a/docs/docs/guides/integrate/identity-providers/okta_saml.mdx b/docs/docs/guides/integrate/identity-providers/okta_saml.mdx index 88d9976bd..49eb8e6c4 100644 --- a/docs/docs/guides/integrate/identity-providers/okta_saml.mdx +++ b/docs/docs/guides/integrate/identity-providers/okta_saml.mdx @@ -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 diff --git a/docs/static/img/guides/azure_app_redirects.png b/docs/static/img/guides/azure_app_redirects.png new file mode 100644 index 000000000..4368b4a2f Binary files /dev/null and b/docs/static/img/guides/azure_app_redirects.png differ diff --git a/docs/static/img/guides/azure_app_register.png b/docs/static/img/guides/azure_app_register.png new file mode 100644 index 000000000..f553c76b6 Binary files /dev/null and b/docs/static/img/guides/azure_app_register.png differ diff --git a/docs/static/img/guides/azure_app_registration.png b/docs/static/img/guides/azure_app_registration.png index 641fb918e..578666492 100644 Binary files a/docs/static/img/guides/azure_app_registration.png and b/docs/static/img/guides/azure_app_registration.png differ diff --git a/docs/static/img/guides/github_oauth_app_registration.png b/docs/static/img/guides/github_oauth_app_registration.png index 4ffd7528f..4350ea5e2 100644 Binary files a/docs/static/img/guides/github_oauth_app_registration.png and b/docs/static/img/guides/github_oauth_app_registration.png differ diff --git a/docs/static/img/guides/gitlab_app_id_secret.png b/docs/static/img/guides/gitlab_app_id_secret.png index 4d1e818a8..c5495e102 100644 Binary files a/docs/static/img/guides/gitlab_app_id_secret.png and b/docs/static/img/guides/gitlab_app_id_secret.png differ diff --git a/docs/static/img/guides/gitlab_app_registration.png b/docs/static/img/guides/gitlab_app_registration.png index 36e0c90f7..3cec036e7 100644 Binary files a/docs/static/img/guides/gitlab_app_registration.png and b/docs/static/img/guides/gitlab_app_registration.png differ diff --git a/docs/static/img/guides/google_oauth_app_registration.png b/docs/static/img/guides/google_oauth_app_registration.png index 7493379a4..1008388ba 100644 Binary files a/docs/static/img/guides/google_oauth_app_registration.png and b/docs/static/img/guides/google_oauth_app_registration.png differ diff --git a/docs/static/img/guides/zitadel_apple_create_provider.png b/docs/static/img/guides/zitadel_apple_create_provider.png index a4836ef02..0b030ce71 100644 Binary files a/docs/static/img/guides/zitadel_apple_create_provider.png and b/docs/static/img/guides/zitadel_apple_create_provider.png differ diff --git a/docs/static/img/guides/zitadel_azure_provider2.png b/docs/static/img/guides/zitadel_azure_provider2.png index 1e9069882..7651f1a18 100644 Binary files a/docs/static/img/guides/zitadel_azure_provider2.png and b/docs/static/img/guides/zitadel_azure_provider2.png differ diff --git a/docs/static/img/guides/zitadel_azure_saml_provider_urls.png b/docs/static/img/guides/zitadel_azure_saml_provider_urls.png new file mode 100644 index 000000000..585a96beb Binary files /dev/null and b/docs/static/img/guides/zitadel_azure_saml_provider_urls.png differ diff --git a/docs/static/img/guides/zitadel_generic_oidc_create_provider.png b/docs/static/img/guides/zitadel_generic_oidc_create_provider.png index 2e2abd58e..4ca8b9a55 100644 Binary files a/docs/static/img/guides/zitadel_generic_oidc_create_provider.png and b/docs/static/img/guides/zitadel_generic_oidc_create_provider.png differ diff --git a/docs/static/img/guides/zitadel_github_create_provider.png b/docs/static/img/guides/zitadel_github_create_provider.png index c551c0de2..2687affc3 100644 Binary files a/docs/static/img/guides/zitadel_github_create_provider.png and b/docs/static/img/guides/zitadel_github_create_provider.png differ diff --git a/docs/static/img/guides/zitadel_gitlab_create_provider.png b/docs/static/img/guides/zitadel_gitlab_create_provider.png index b79ec247a..2d1571946 100644 Binary files a/docs/static/img/guides/zitadel_gitlab_create_provider.png and b/docs/static/img/guides/zitadel_gitlab_create_provider.png differ diff --git a/docs/static/img/guides/zitadel_google_create_provider.png b/docs/static/img/guides/zitadel_google_create_provider.png index 3bd005d03..ab2ebbb6b 100644 Binary files a/docs/static/img/guides/zitadel_google_create_provider.png and b/docs/static/img/guides/zitadel_google_create_provider.png differ diff --git a/docs/static/img/guides/zitadel_okta_saml_provider_urls.png b/docs/static/img/guides/zitadel_okta_saml_provider_urls.png new file mode 100644 index 000000000..a2c1e736d Binary files /dev/null and b/docs/static/img/guides/zitadel_okta_saml_provider_urls.png differ diff --git a/docs/static/img/guides/zitadel_saml_provider_urls.png b/docs/static/img/guides/zitadel_saml_provider_urls.png deleted file mode 100644 index 7a955958f..000000000 Binary files a/docs/static/img/guides/zitadel_saml_provider_urls.png and /dev/null differ diff --git a/internal/api/idp/idp.go b/internal/api/idp/idp.go index acf835d02..0209e0337 100644 --- a/internal/api/idp/idp.go +++ b/internal/api/idp/idp.go @@ -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) { diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index fc0deff91..ab0d98b99 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -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, )