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:
    • Start URL (a.k.a RelayState):
    • Entity ID: urn:auth0:tenant:connection
    • Name ID format: UNSPECIFIED
  • At Auth0 (as SAML Enterprise connection)

    • Sign in URL:
    • Certificate uploaded
    • User ID Attribute:
    • 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"
    ProtocolBinding="@@ProtocolBinding@@" Version="2.0">
    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">@@Issuer@@</saml:Issuer>

In DotNet I have the following two endpoints:

		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());

		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" : "";
		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.Events = new OpenIdConnectEvents {
			OnRedirectToIdentityProvider = ctx => {
				var connection = String.Empty;
				var organization = String.Empty;
				var invitation = String.Empty;

				if (ctx.Properties.Items.TryGetValue(
					out connection
				)) ctx.ProtocolMessage.Parameters.Add(

				if (ctx.Properties.Items.TryGetValue(
					out organization
				)) ctx.ProtocolMessage.Parameters.Add(

				if (ctx.Properties.Items.TryGetValue(
					out invitation
				)) ctx.ProtocolMessage.Parameters.Add(

				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 with form data SAMLResponse and RelayState=
  4. Redirects to
  5. Redirects to ← this is the custom login endpoint
  6. Redirects to
  7. Redirects back to Googles SSO page
  8. Redirects again to with form data SAMLResponse and RelayState but this time RelayState is some hashed string
  9. Redirects again to with different state query string
  10. Redirect to with form data code and state
  11. Redirect to application, logged in