Achieving SSO between native iOS apps, and between iOS <-> web SPA

Much like this previous community post (Sharing authentication between native app & two websites), we are trying to provide SSO for our mobile native applications & web SPAs (using Auth0 of course) like so:

  • A user can open then log into native-app1, and then on the same device open native-app2 and be automatically logged in (SSO’d)

  • A user can navigate to app1.mycompany.com using their mobile device’s default browser, login, and then open native-app1 and be automatically logged in (SSO’d)

  • A user can open then log into native-app1, and then on the same device navigate to app1.mycompany.com using their mobile device’s default browser, and be automatically logged in (SSO’d)

We have already achieved SSO between our web SPAs (on a single device) and this works well

We already having achieved ‘forever login’ using Auth0 (via Refresh tokens) for our native apps (iOS & Android) and this works well, but does not provide SSO whatsoever

We believe that our ‘SSO goals’ above could be effectively achieved for our Android-native apps <-> web SPAs.

We have been led to believe so far (both by reading and by direct experience) that we cannot achieve our ‘SSO goals’ above for our iOS apps, because of the prompt behavior that results from using iOS’ ASWebAuthenticationSession class as part of navigating a user to our (auth0) Hosted Login Page. (see this issue in the ‘AppAuth-iOS’ Github repo here as a background: Impact of iOS 11 no longer providing shared cookies between Safari, Safari View Controller instances · Issue #120 · openid/AppAuth-iOS · GitHub)

Specifically with iOS, if/when we try to achieve SSO by calling the /authorize API (via Auth0.swift cocoa pod, 1.14.1) to silently retrieve a fresh access_token (via appending the query param “prompt=none”), the user receives a ‘double prompt’ whereby they see this OS dialog twice, thus having a pretty terrible experience:

" ‘AppName’ Wants to use ‘mycompany.com’ to Sign In"

They see this >1 times, because the (native iOS) app, using the ‘ASWebAuthenticationSession’ class (via Auth0.swift), must make >1 HTTP calls to our Auth0 service endpoint to allow the user to visit our Hosted Login Page, as part of an SSO flow. (first to try to get an access_token for an existing SSO session, second to take then to the HLP if no existing session)

So, this post is really just a check-in to ask these questions:

  1. Does anyone reading this post know of any existing way to circumvent this prompt appearing >1 times?

  2. Do you Auth0, have any plans to allow us to only call Auth0 via the ‘ASWebAuthenticationSession’ class 1 time to get a fresh access_token via SSO or receive back our HLP if no existing SSO session? (e.g. allowing us to pass ‘prompt=onlyIfNeeded’)

  3. Has Apple made any hints that they may be changing how this prompt behavior works in order to facilitate achieving SSO for native (iOS) apps? (i.e. allowing one prompt to cover >1 HTTP requests?)

  4. Does anyone know of a viable, alternative method for achieving SSO (using OAuth2/OIDC) for a native iOS app besides how we are trying to achieve it? (effectively, we are trying to accomplish SSO in the same way that we have already achieved it amongst our web SPAs)

Hi @Joe_Tillotson

They see this >1 times, because the (native iOS) app, using the ‘ASWebAuthenticationSession’ class (via Auth0.swift), must make >1 HTTP calls to our Auth0 service endpoint to allow the user to visit our Hosted Login Page, as part of an SSO flow. (first to try to get an access_token for an existing SSO session, second to take then to the HLP if no existing session)

If your native iOS app needs to authenticate the user (because it’s the first time the user uses the app, or the refresh token is invalid), it should do only one, regular /authorize request (no need to use /prompt=none in a request from a native app).

If the user already has a session and your tenant has “Enabled Seamless SSO” turned on in the Advanced Settings (or if you don’t see the option, meaning that your tenant has that enabled by default) then Auth0 will return the token result back to the application, without any prompt for the user.

I believe this will get you the result you are after. If you turn that on, a regular /authorize request will do the equivalent of “prompt only if needed”.

Thank you very much for this reply Nicolas. I definitely do not (yet) have a full understanding of what turning on “Enable seamless SSO” signifies vis-a-vis the flow between our app<->Auth0. I am used to the query param “prompt=none” being sent (in the call to /authorize) if/when using the auth0-js NPM package in a web (SPA) app.

A follow-up question. You mentioned in your answer:

“If your native iOS app needs to authenticate the user (because it’s the first time the user uses the app, or the refresh token is invalid),…”

Today we use refresh tokens and have (already) support for ‘forever login’ (as I mentioned above). It seems to me that if we instead switch to supporting SSO (which has a max session-length of 30 days) then that obviates the need/usage of refresh tokens? But, maybe I’m thinking about this incorrectly?

IOW: if I am using refresh tokens (which never ‘expire’), and I never require my users of App-A to re-authenticate, then if/when they install-and-first-use App-B, there will be no SSO session (very possibly, because their origial SSO session will have expired a ‘long time ago’) that will auto-authenticate the user at that point. Thus, it seems to me that ‘mixing’ refresh tokens with SSO is a bad match because SSO sessions don’t ‘live forever’. Do you have any thoughts on that?

In general, native apps tend to rely on refresh token flows and thus avoid the login experience pretty much forever. Currently Auth0 SSO sessions will expire after a maximum of 3 days of inactivity, so that might not be a good experience for native app users.

As a general approach, I’d say use refresh tokens on native apps (where the refresh token can be securely stored) and rely on the SSO session in SPA (using prompt=none if you don’t want to refresh the page when requesting a new token).

Traditionally, Auth0 always had SSO sessions but an /authorize request showed you the login screen even if the user was already authenticated. Lock would show you the “Last time you logged in with …” button that allowed you to effectively use the existing session.
Turning on “Enable Seamless SSO” (which is turned on by default and can’t be turned off on new tenants) will cause the login screen to be directly skipped if the session in place is sufficient for a regular /authorize request.

So when Seamless SSO is enabled, /authorize will show the user a prompt (login, consent, mfa) only if needed. Otherwise it returns back to the application.

/authorize with prompt=none guarantees that there will be no user interaction (and an error will be returned if user input is needed). checkSession from Auth0.js use prompt=none in combination with a hidden IFRAME to refresh a token without altering the current state in a SPA (without refreshing the page, as the regular /authorize does).

/authorize with prompt=login will force the login prompt. This is useful when the user clicks on something like “I’m not this person” or “Log in with a different account”.

Thank you Nicolas, that all makes sense and I appreciate the feedback and answers to my questions.

/authorize with prompt=login will force the login prompt. This is useful when the user clicks on something like “I’m not this person” or “Log in with a different account”.

A bit of feedback for Auth0 (not for you in particular): within our web SPAs we use (as you can imagine) auth0-js. We also force our users to have verified email addresses. So as I can imagine many Auth0 customers do, we have a simple custom rule that ensures users have a verified email address, and if not we return an ‘unauthorized error’ to the app via this custom rule. Because this ‘returns control’ to the application, our applications must recognize this problem (error) and, rather than providing a UI for this situation within each of our many apps, we instead have each app simply redirect the user back to our HLP, passing this error as a query param. So within our SPA app, that looks something like this:

// 'auth0' is declared above via auth0-js
function login () {
    ...
    this.auth0.authorize({errorDesc: 'email_not_verified', prompt: 'login'});
}

It seems somewhat dissonant to require this ‘prompt’ param whenever the app ‘is sure’ it is time to take the user to our HLP. If any app forgets this extra parameter, the user will be in a ‘loop’ between our app & auth0, because though they have a valid SSO session, our custom rules will continually return an error (if/when the user’s email is not yet verified).

I guess I’m really complaining that an authentication that ‘fails’ because of a custom rule still creates an SSO session (now that ‘Enable seamless sso’ is the default for new tenants).

Hi Joe

Did you have any success in implementation SSO between iOS native app and Web SPA? We are currently facing the same issue and it seems from Apples documentation that session cookies cannot be shared between ASWebAuthenticationSession and the web browser: Apple Developer Documentation

Perhaps I am seeing it from a from perspective?

Best,
Nam