After implementing "Configure IdP-Initiated SAML Sign-on to OIDC Apps", user has to SSO twice when IdP-initiated

I have followed the steps from Configure IdP-Initiated SAML Sign-on to OIDC Apps

With the following settings:

  • At Google Workspace

    • ACS: https://tenant.eu.auth0.com/login/callback?connection=xyz-google&organization=org_XYZ
    • Start URL (a.k.a RelayState): https://xyz.com
    • Entity ID: urn:auth0:tenant:connection
    • Name ID format: UNSPECIFIED
  • At Auth0 (as SAML Enterprise connection)

    • Sign in URL: https://accounts.google.com/o/saml2/idp?idpid=xyz
    • Certificate uploaded
    • User ID Attribute: https://accounts.google.com/o/saml2?idpid=xyz
    • Debug mode: ON
    • Accepting unsolicited login requests
    • Response protocol: OpenID Connect
    • Query string set with redirect_uri containing the custom login endpoint with the SAML connection name
    • HRD is set to the top domain used by Google Workspace
    • The application itself activated under the SAML connection settings and has the custom login endpoint as Allowed Callback URL

In Auth0, I have also updated the request template:

<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    AssertionConsumerServiceUrl="@@AssertionConsumerServiceUrl@@"
    Destination="@@Destination@@"
    ID="@@ID@@"
    ProviderName="@@ProviderName@@"
    LoginHint="@@LoginHint@@"
    IssueInstant="@@IssueInstant@@"
    ProtocolBinding="@@ProtocolBinding@@" Version="2.0">
    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">@@Issuer@@</saml:Issuer>
</samlp:AuthnRequest>

In DotNet I have the following two endpoints:

		[HttpGet]
		public async Task Login(
			[FromQuery] string return_to = "/",
			[FromQuery] string? connection = null,
			[FromQuery] string? organization = null,
			[FromQuery] string? invitation = null,
			[FromQuery] string? error = null,
			[FromQuery] string? error_description = null,
			[FromQuery] string? state = null
		) {
			var builder = new LoginAuthenticationPropertiesBuilder().WithRedirectUri(return_to);

			if (!String.IsNullOrEmpty(connection)) builder.WithParameter("connection", connection);
			if (!String.IsNullOrEmpty(organization)) builder.WithOrganization(organization);
			if (!String.IsNullOrEmpty(invitation)) builder.WithInvitation(invitation);

			await HttpContext.ChallengeAsync(Auth0Constants.AuthenticationScheme, builder.Build());
		}

		[HttpGet]
		public Task LoginSAML(
			[FromQuery] string connection,
			[FromQuery] string? error = null,
			[FromQuery] string? error_description = null,
			[FromQuery] string? state = null
		) => Login(connection: connection, error: error, error_description: error_description, state: state);

And in Startup.cs:

	.AddAuthentication(opt => {
		opt.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
		opt.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
		opt.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
	})
	.AddCookie(opt => {
		opt.SlidingExpiration = true;
		opt.LoginPath = "/login";
		opt.LogoutPath = "/logout";
		opt.ReturnUrlParameter = "return_to";
		opt.Cookie.Name = ".auth";
		opt.Cookie.Domain = isDev ? "localhost" : "xyz.com";
		opt.Cookie.HttpOnly = true;
		opt.Cookie.SameSite = SameSiteMode.None;
		opt.Cookie.SecurePolicy = CookieSecurePolicy.Always;
		opt.Cookie.IsEssential = true;
	})
	.AddOpenIdConnect(Auth0Constants.AuthenticationScheme, opt => {
		opt.SaveTokens = true;
		opt.UseTokenLifetime = true;
		opt.Authority = builder.Configuration["Auth0:Domain"];
		opt.ClientId = builder.Configuration["Auth0:ClientId"];
		opt.ClientSecret = builder.Configuration["Auth0:ClientSecret"];
		opt.ResponseType = OpenIdConnectResponseType.Code;
		opt.CallbackPath = new PathString("/auth/callback");
		opt.ClaimsIssuer = Auth0Constants.AuthenticationScheme;

		opt.Scope.Clear();
		opt.Scope.Add("openid");

		opt.Events = new OpenIdConnectEvents {
			OnRedirectToIdentityProvider = ctx => {
				var connection = String.Empty;
				var organization = String.Empty;
				var invitation = String.Empty;

				if (ctx.Properties.Items.TryGetValue(
					Auth0AuthenticationParameters.Parameter("connection"),
					out connection
				)) ctx.ProtocolMessage.Parameters.Add(
					"connection",
					connection
				);

				if (ctx.Properties.Items.TryGetValue(
					Auth0AuthenticationParameters.Organization,
					out organization
				)) ctx.ProtocolMessage.Parameters.Add(
					"organization",
					organization
				);

				if (ctx.Properties.Items.TryGetValue(
					Auth0AuthenticationParameters.Invitation,
					out invitation
				)) ctx.ProtocolMessage.Parameters.Add(
					"invitation",
					invitation
				);

				return Task.CompletedTask;
			}
		};
	})

I think I have followed the documentation to a tee.

With all this, when the user performs an IdP-initiated login, the user has to login twice to Google Workspace. Is this correct or am I doing something wrong? Am I doing something wrong in DotNet with custom SAML login endpoint perhaps?

Here are the different endpoints being called:

  1. Googles SSO page
  2. User logs in
  3. Redirects to https://tenant.eu.auth0.com/login/callback?connection=xyz-google&organization=org_XYZ with form data SAMLResponse and RelayState=https://xyz.com
  4. Redirects to https://tenant.eu.auth0.com/authorize/resume?state=XYZ
  5. Redirects to https://xyz.com/auth/login/saml?connection=xyz-google&code=XYZ&state=https%3A%2F%2Fxyz.com ← this is the custom login endpoint
  6. Redirects to https://tenant.eu.auth0.com/authorize?client_id=XYZ&redirect_uri=https%3A%2F%2Fxyz.com%2Fauth%2Fcallback&response_type=code&scope=openid&code_challenge=XYZ&code_challenge_method=S256&response_mode=form_post&nonce=XYZ&connection=xyz-google&state=XYZ&x-client-SKU=ID_NET6_0&x-client-ver=6.25.1.0
  7. Redirects back to Googles SSO page
  8. Redirects again to https://tenant.eu.auth0.com/login/callback?connection=xyz-google&organization=org_XYZ with form data SAMLResponse and RelayState but this time RelayState is some hashed string
  9. Redirects again to https://tenant.eu.auth0.com/authorize/resume?state=123 with different state query string
  10. Redirect to https://xyz.com/auth/callback with form data code and state
  11. Redirect to application, logged in

Hi @jay.zahiri

I am sorry to hear about the issue that you are facing with IdP initiated SAML.

I believe there might be some kind of misconfiguration which makes the user be redirected twice once authenticating with SSO through Google Workspace.

Make sure that under the SAML connection’s IdP Initiated SSO settings you have everything set properly.

Under Default Application, the application selected should be the one you want the user to be redirected to once the SAML response is validated. Also, inside the Query String make sure that the redirect_uri you are using does not redirect the user to a different URL than the intended one.

If you have configured everything properly, then you application should redirect from step 3 directly to Google Workspace where they would be logged in.

Otherwise, I would advise to visit this Knowledge Article which explains how to implement a IdP-initiated SAML flow or review our Documentation on this matter.

If you have any further questions on this matter, please feel free to leave a reply!

Kind Regards,
Nik