Authentication broken on ASP.Net Core and Safari on iOS 12 / Mojave (take 2)

A recent change in Webkit has broken authentication in ASP.Net Core 2 MVC apps that use the OpenIdConnect and Cookies middleware (like in all of our quickstarts and samples). In particular, after authenticating you might see:

  • a redirect loop, where the application redirects to Auth0 and Auth0 redirects back to the application
  • right after the authentication the application still thinks that the user is not authenticated until the page is refreshed.

Cause

  • The Cookies middleware uses a samesite=lax policy for the session cookie (which is a good thing, security-wise). This cookie is set at the end of the authentication flow (on the callback processing handler).

  • WebKit’s interpretation of spec for the SameSite policy causes Safari to block the session cookie set by the middleware on the immediate request after the authentication (usually the ReturnUrl) if the authentication flow includes a POST, like

    • A user typing credentials on a login form
    • Auth0 returning the results to the application (because the OIDC middleware asks for the response to be POSTed back to the application).

A full flow looks like this:

  1. Visit site, access some protected resource (or trigger the login manually).
  2. Application redirects to the OIDC provider (Auth0) asking for an authentication.
  3. User completes all the authentication prompts.
  4. Auth0 sends back the result to the app by returning HTML to the browser that does a POST request.
  5. The OpenIdConnect middleware (at /signin-auth0 ) validates the response, creates a login ticket. The Cookie middleware sets the identity cookie with a samesite=lax policy.
  6. The middleware redirects to the protected resource (or to the home page).
  7. The middleware checks for the identity cookie, which is blocked by WebKit. Since it’s missing, return to step 2.

This issue is being discussed on WebKit’s bug tracker at 188165 – iOS 12 Safari breaks ASP.NET Core 2.1 OIDC authentication.

(Note that this problem is not exclusive to Auth0, it affects any OIDC flow).

See a workaround in the response below.

Workaround

UPDATE: see next message for a simpler solution.

The OpenID Connect middleware redirects to the ReturnUrl after processing the callback (on the /signin-auth0 path on our examples). A workaround is to put an additional “stop” step before redirecting back to the ReturnUrl. This step is not a 302 redirect, but rather an HTML page that makes the request to the final URL (mostly transparent for the user, except for the additional redirection).

We’ll make this step in the /Account/Continue route. Add this action in the AccountController:

/// <summary>
/// This view is invoked by the OpenIDConnect middleware after setting the authentication ticket
/// as an intermediate step to make sure that the session cookie is sent
/// to the returnUrl request in Safari iOS 12/Mojave.
/// It is marked as AllowAnonymous because this request will look as
/// unauthenticated on Safari.
/// </summary>
[AllowAnonymous]
public IActionResult Continue(string returnUrl = "/")
{
    ViewBag.returnUrl = Url.IsLocalUrl(returnUrl) ? returnUrl : "/";
    return View();
}

and create this Continue.cshtml view in the /Views/Account folder:

@{
    Layout = null;
 }
 <html>
  <head>
    <title>Working...</title>
  </head>
  <body>
    <noscript>
    <a href="@ViewBag.ReturnUrl">Script is disabled. Click Submit to continue.</a>
    </noscript>
    <script language="javascript" type="text/javascript">
      window.setTimeout(function() {
        window.location = "@ViewBag.ReturnUrl";
      }, 0);
    </script>
  </body>
</html>

Note that the HTML simply redirects the user back to the intended location.

Finally, add this handler to the OnTicketReceived event of the existing OpenIDConnect middleware configuration in Startup.cs:

// in Startup.cs' ConfigureServices method.
.AddOpenIdConnect("Auth0", options => {
    [...]
    options.Events = new OpenIdConnectEvents
    {
        [...],
        OnTicketReceived = (context) =>
        {
            // stop by `/Account/Continue` instead of going directly to the ReturnUri
            // to work around Safari's issues with SameSite=lax session cookies not being
            // returned on the final redirect of the authentication flow.
            context.ReturnUri = "/Account/Continue?returnUrl="+ System.Net.WebUtility.UrlEncode(context.ReturnUri ?? "/");
            return Task.CompletedTask;
        }

If you don’t want to penalize all users with this additional step for this Safari quirk, you can try to put additional logic on the OnTicketReceived handler and analyze the context.Request.UserAgent so that the context.ReturnUri modification is only applied when requests come from these newer versions of Safari.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.

UPDATE: While I haven’t tested it, Brock Allen provided a much cleaner solution here: Same-site cookies, ASP.NET Core, and external authentication providers | brockallen (specifically, the code under " The fix specifically for ASP.NET Core"). If using that code, remember to switch signin-oidc to signin-auth0 in the path check. I.e.:

public void Configure(IApplicationBuilder app)
{
   app.Use(async (ctx, next) =>
   {
      await next();

      if (ctx.Request.Path == "/signin-auth0" && 
          ctx.Response.StatusCode == 302)
      {
          var location = ctx.Response.Headers["location"];
          ctx.Response.StatusCode = 200;
          var html = $@"
             <html><head>
                <meta http-equiv='refresh' content='0;url={location}' />
             </head></html>";
          await ctx.Response.WriteAsync(html);
      }
   }
   
   [...]
}
1 Like