IdP-initiated SAML sign-in to OIDC applications

Problem Statement

An OIDC application requires IdP-initiated SSO from a SAML Identity Provider.

A common pattern is to use Auth0 to enable an OIDC application to work with a SAML IdP. This poses a special challenge for OIDC applications as OpenID Connect (OIDC) does not support the concept of an IdP-Initiated flow. So while Auth0 offers the possibility of translating a SAML IdP-Initiated flow (from a SAML connection) into an OIDC response for an application, any application that properly implements the OIDC/OAuth2 protocol will reject an unsolicited response. Auth0 strongly recommends starting login flow at the application in all cases (e.g. a portal can simply link to the application’s /login endpoint directly instead of starting sign-in at the IdP). If this is impossible, the following document provides guidance on a workaround.

Symptoms

Failure to complete sign-in to an OIDC application. This usually manifests in one of two ways:

  • The OIDC application rejects the unsolicited sign-in attempt
  • The OIDC application expects an access_token in JWT format and returns an error when presented with an opaque access_token.

Causes

  • Any application that properly implements the OIDC/OAuth2 protocol will reject an unsolicited response from an identity provider
  • Auth0 will only return an opaque (non-JWT) access_token from idp-initiated SAML flows

Solution

Notes:

  1. This method will complete an IdP-initiated SAML login, immediately followed by a seamless (transparent to the user) SP-initiated SAML login, and return an OIDC response to the application.
    1. This provides protection against CSRF attacks
    2. User will not see a login prompt because they have a session from the idp-initiated login they just completed
    3. User may see a consent prompt–this is expected
  2. This solution requires the application to implement and host a custom route handler or redirect. It should be accessible through a URL like: https://example.com/startlogin
  3. The route handler should be able to accept a querystring parameter and generate an authentication request (usually leveraging an OIDC library). An example is provided at the end of this document.

Configuration:

  1. Create an Application to represent the OIDC application in Auth0. Applications in Auth0
  2. Add the application’s custom route handler URL to Allowed Callback URLs for the application.
  3. Create a SAML Enterprise connection in Auth0 and configure it to work with the SAML IdP. The details of this aren’t covered in this document. Ignore the “Idp-initiated SSO” tab for now.
  4. Enable the SAML connection for the application created in Step 1
  5. In the SAML enterprise connection configuration. click the IdP-Initiated SSO tab. Under Default Application select the application from step 1
  6. For Response Protocol, select OpenID Connect
  7. For Query String, construct a querystring parameter:
    1. set redirect_uri to the custom route handler, plus a querystring indicating the name of the SAML connection (indeed, this is a querystring inside a querystring). This entire URL should be URL-encoded.
    2. For the example route handler above, and the SAML connection called “samlidp1” the Query String would be: redirect_uri=https%3A%2F%2Fexample.com%2Fstartlogin%3Fconnection%3Dsamlidp1
    3. Screenshot showing a correct configuration:

  1. Note that the IdP-initiated callback (ACS) URL for a SAML connection follows this pattern: https://mytenant.auth0.com/login/callback?connection=MY_CONNECTION_NAME
  2. Determine how to start an IdP-initiated flow with the IdP. This varies by IdP and will involve passing the URL from step 8 to the IdP. A common pattern is to use the RelayState parameter. So if the IdP’s login URL is https://samlidp.auth0.com/saml and the name of the Auth0 connection (from step 2 above) is mySamlConn then the IdP-initiated URL, using the URL from step 8, might look like:
    1. https://idp.example.com/saml?RelayState=https://mytenant.auth0.com/login/callback?connection=mySamlConn
    2. This will vary by IdP. Don’t assume a particular IdP uses the above pattern.

Flow

This is a sequence diagram showing the entire sign-in flow. Certain steps are highlighted in detail later.

Notes:

  • Starting in (8) this is simply a regular OIDC sign-in flow, with an SP-initiated interaction with the SAML IdP.
  • This example uses the implicit flow for the sake of simplicity, but the application can request any supported OIDC flow in step 8 (authorize code flow, PKCE, etc.).
  • In (7) the application simply “throws away” the response from Auth0, reads the connection parameter from the request, and creates an internal state for the authentication transaction.
  • (4) through (15) require no interaction from the user (with the possible exception of a consent prompt from Auth0).
  • In (15) the application reads the state parameter returned in the response from Auth0. This state parameter must match the internal state generated in (7). This is a critical part of the OIDC security model.

Route Handler

Generally the route handler should have a different URL than the standard “login” endpoint. For simplicity, this endpoint can and probably should call the same login() method as the /login endpoint. The existing login() method may need to be updated to accept and relay the connection parameter to auth Auth0 tenant.

Assuming the pattern above, the route handler will parse the connection name from the query string, then pass it to the login() method, which will generate an appropriate /authorize request including the redirect_uri to the application and a connection parameter (the connection parameter is key here and this won’t work without it).

Assuming an application has a /login route that calls the following login() method:

(This examples uses Auth0’s auth0-spa-js SDK)

const router = 
  "/": () => showContent("content-home"),
  "/profile": () =>
    requireAuth(() => showContent("content-profile"), "/profile"),
  "/login": () => login()
};

const login = async (targetUrl) => {
  try {
    const options = {
      redirect_uri: window.location.origin
    };

    if (targetUrl) {
      options.appState = { targetUrl };
    }
    await auth0.loginWithRedirect(options);
  } catch (err) {
    console.log("Log in failed", err);
  }
};

The modification would look like this:

const router = {
  "/": () => showContent("content-home"),
  "/profile": () =>
    requireAuth(() => showContent("content-profile"), "/profile"),
  "/login": () => login(),
  "/startlogin": () => startlogin()
};

//new method to start login from idp-initiated callback
const startlogin = async () => {
  console.log(window.location.href)
  let myURL = new URL(window.location.href);
  let conn = myURL.searchParams.get("connection");
  return  login(null, conn);
}

/**
 * Starts the authentication flow
 */
const login = async (targetUrl, connection) => {
  try {
    console.log("Logging in", targetUrl);

    const options = {
      redirect_uri: window.location.origin,
    };

    if (connection) {
      options.connection = connection;
    }

    if (targetUrl) {
      options.appState = { targetUrl };
    }

    await auth0.loginWithRedirect(options);
  } catch (err) {
    console.log("Log in failed", err);
  }
};

Any implementation should pass a state value to the value to /authorize for CSRF protection, and validate the state in the response from the identity provider.

5 Likes