Ember OAuth2 Deep Dive: Silent Authentication

Token-based authentication poses some gnarly challenges for JavaScript single page apps. Because your application code and state lives on the user's machine, you can't use an OAuth2 grant that lets your application directly fetch a new token - if the credentials needed to act on your user's behalf indefinitely were available to your application, they would be a prime target for attackers. Consequently, single page applications should use the implicit grant flow. Using this flow, any time you want to refresh your user's token, your user's browser ("user agent" to use the OAuth2 jargon) must visit the sign-on service ("authorization server").

Shuttling your user back and forth from an external site to re-authenticate without feeling obtrusive is tricky. The ideal is to implement "Silent Authentication" where your user's browser repeats the OAuth dance so quickly or invisibly that the user doesn't notice. The key challenge is that redirecting away from your application destroys its state. So you either need to

  1. Persist vital application state across redirects.
  2. Not redirect at all.

We'll look at both approaches. Throughout, I'll leverage Ember Simple Auth (ESA) and sometimes Torii, two libraries I profile in a previous post.

0: Terminology: OAuth2 and OpenID Connect

You really shouldn't use vanilla OAuth2 for authentication. OAuth2 is an authorization protocol; it provides a way for a user to grant permissions for specific operations to your application. OpenID Connect (OIDC) is a widely used authentication protocol that layers on top of OAuth2; OIDC lets your user prove to your application that they are who they claim to be. You can conceivably implement your own homespun authentication scheme on top of OAuth2, but you shouldn't. My team uses Keycloak as our OIDC server. Auth0 and Google Sign-In implement OIDC as well, while Facebook login does not. For the rest of the article, I'll assume we're integrating with OIDC, even though many of the libraries only make reference to OAuth2.

1: Preserving State Across the Redirect

If you're adding authentication to an Ember app, Ember Simple Auth is well worth a look. You'll notice that it provides an OAuth2ImplicitGrantAuthenticator. The authenticator gets triggered when the OIDC server redirects to your Ember app after your user has authenticated. What happens before then is up to you. The simplest approach is the one used in the addon's example application. When your user hits the login button, window.redirect to your sign-on service.

Before you redirect away, you'll need to store some state that will survive the redirect. At minimum, you'll need to store state and nonce parameters that the implicit grant spec requires to protect against replay attacks. Using localStorage, our code might look like:

const redirectUri = `${window.location.origin}/oauth2callback`;  
const state = randomString();  
const nonce = randomString();  
localStorage.setItem('state', state);  
localStorage.setItem('nonce', nonce);  
window.location.replace(`https://my.signon.service?` +  
    `client_id=${clientId}` +
    `&redirect_uri=${redirectUri}` +
    `&response_type=token` +
    `&state=${state}` +
    `&nonce=${nonce}`
  );
}

where randomString is a function that uses a cryptographic random number generator, as illustrated in this example.

Then, you should override the OAuth2ImplicitGrantAuthenticator's authenticate method to check that the nonce and state returned by the sign-on service match the ones that you stuffed into localStorage.

That approach might be good enough for your initial login. But what happens when your user's token is close to expiration? You'll want your user to go back to the sign-on service so they can return to your app with a fresh token. Whatever sign-on service you're using should drop a token on the user's browser such that after their first successful login, the service should recognize them and quickly redirect back to the application on subsequent redirects (as long as you don't exceed an inactivity time limit on your sign-in service). This happens so quickly that many users won't notice what's happened.

Even if the re-authentication is "silent", your users will definitely notice if they've been kicked back to the landing page after the redirect flow has completed. So in addition to storing state and nonce across redirects, you should also store the URL your user was on or trying to access before the redirect. Then after the redirect, you can pull the URL from localStorage and drop your user off there.

In an app that uses this approach, I check if the user's token is close to expiration in the beforeModel hook of a base AuthenticatedRoute class. If the token is getting stale, I use the transition parameter as follows:

let url = transition.intent.url;  
if (!url) {  
  const {intent} = transition;
  url = this.router.generate(intent.name, ...intent.contexts);
}
// omitted: use transition.queryParams to tack on query params if applicable
localStorage.setItem('attemptedTransition', url);  
transition.abort();  
// Now do the redirect

This approach is straightforward to implement (if a tad janky), but it has some major drawbacks. If your app has important state that isn't serializable into the URL, then this approach is a nonstarter. At minimum, your Ember Data cache will be cold. Also, this approach sneakily inserts the sign-on service and your app's callback route into your user's history. They'll be in for a surprise if they try to hit the back button immediately after a token refresh.

The root of these problems is redirecting away from your application. So what if we don't redirect away?

2. Performing the Redirect in a Hidden iframe

Instead of redirecting the user away from your page, you could prod them through the implicit grant flow in a hidden iframe. That way, you don't need to redirect away from your app and discard your application state. If you take this approach you have two implementation paths: leverage Torii's nice popup and iframe handling or roll your own mechanism to spin up an iframe, pass along the access token, and tear it down. We'll look at the Torii approach first.

Using Torii

Sticking with Ember Simple Auth, instead of the OAuth2ImplicitGrantAuthenticator, you could try the ToriiAuthenticator, which wraps the Torii library. Torii is more focused on social login than ESA. As such, it provides some nice functionality for your user to carry out OAuth2 login in a popup or iframe. There is some error-prone logic needed to do that, so it's nice to not have to implement it yourself! If you notice the user's token is getting stale and initiate the redirect, the iframe should be able to do its business without any input from the user.

But what happens if, say, the user has cleared their cookies or you didn't open the iframe before an inactivity time limit? By default, if the server does not recognize the user, it will display a login form. If you're trying to implement silent auth, you do not want this behavior. The sign-in form will wait around in an invisible iframe until the end of time - what a tragedy! This is where the prompt query parameter comes in handy. It is an optional parameter in the OpenID Connect spec that is widely implemented. Your OIDC server probably accepts the prompt parameter in its URL query arguments for the page where your user authenticates. If you specify prompt=none in your query arguments, then the OIDC server will immediately redirect with a 400 error if it doesn't recognize the user. Then your application can handle the error and spawn a visible iframe or popup for your user to resupply their credentials.

You could make two Torii providers for the two types of authentication you want to perform: one provider that creates a popup for your user to enter their credentials for initial authentication, and one that uses prompt=none in a hidden iframe for refreshing the user's access token.

Using a Homespun Solution

Torii is a great tool, but there are always tradeoffs. In an application I maintained that used Ember Simple Auth and Torii in tandem, I found it cumbersome to configure all the fiddly bits I needed to make them play nice together. And now my app needs boilerplate for both libraries. The mental overhead of understanding the ins and outs of both libraries might outweigh the cost of just implementing the iframe redirect logic yourself. On the Ember Slack, @pauln shared a skeleton implementation that demonstrates this and integrates with the ESA OAuth2ImplicitGrantAuthenticator.

For me, one of the most interesting problems to solve if you're going this route is what happens after a successful redirect within the iframe. Somehow, the iframe needs to send a message back to the parent with the access token it received from the OIDC server. You could redirect to your application at a special callback route, but that would require your whole application to initialize just so that it can send the message to the parent and destroy itself (This can also present security concerns.) The best practice (encouraged by Torii since v 0.9.0) is to use an HTML page separate from your SPA whose only job is to send that message. Torii uses localStorage to send the message to the parent, the Auth0.js library uses window.postMessage, and @pauln's demonstration gist attaches a callback to window for the child iframe to call from window.parent.

Next Steps

If you are using the implicit grant flow in an SPA, a silent auth implementation that fetches fresh tokens from a hidden iframe is the gold standard for usability. Currently, Ember developers who use Ember Simple Auth face a choice between integrating another large auth library or implementing some tricky child window logic themselves. It would be great if they didn't need to make this choice! I'm thinking about building on @pauln's gist to make a silent auth addon that would play well with ESA.