The Implicit Flow of OpenID Connect is useful for performing delegated access to resources when the client is not controlled - for example, a javascript-based Single Page App (SPA).

A very basic and naive explanation of how this flow works:

  1. User loads the SPA
  2. User tries to access a protected resource (e.g. a WebAPI via an AJAX request), and is given a 401 in return
  3. SPA code detects the 401, and realises the user needs to be authorized to access it.
  4. SPA constructs a request to the Identity Provider (e.g. IdentityServer 3.0), and POSTs it over HTTPS
  5. User is presented with a log-in screen by the Identity Provider, and logs in
  6. User is presented with a consent form, in the style of "Site XYZ wants to access your name, email" etc, and ticks "Sure, take all my info"
  7. The Identity Provider responds with a 302, redirecting the user back to the SPA, including an identity_token and usually an access_token. Something like this:
HTTP/1.1 302 Found  
Content-Length: 0  

The user then sees the browser redirect back to the SPA, and boom! They're logged in.

I've been struggling for a while to understand how the heck passing tokens around in a URL, as in the Implicit Flow of OpenID Connect can be considered secure. Even secured with TLS, only the body of the request/response is encrypted - URLs are plain for all to see.

What I was missing was a part of the URI Spec around how fragments (the name for the bit after the hash #) should be handled:

the fragment identifier is not used in the scheme-specific processing of a URI; instead, the fragment identifier is separated from the rest of the URI prior to a dereference, and thus the identifying information within the fragment itself is dereferenced solely by the user agent, regardless of the URI scheme

Now. What the heck does that mean?

"Dereference" in this context means retrieve the value referenced by a pointer - our URI is the pointer; the resource it represents is the value. "User agent" is the user's browser, and the scheme we're talking about is HTTP. So we can paraphrase the above as:

The fragment is not used as part of a HTTP request. Instead, the fragment is separated from the rest of the URI before the request is made to the server. Any information in the fragment is used purely by the browser.

We can see this when we compare a request in browser to what we see over the network in Fiddler:

Request for a resource in Chrome with a URI Fragment pointing to a specific section

Resulting HTTP request traced in Fiddler - no URI Fragment included in the request

So. How can Implicit Flow be secure?

It all hinges on the whole flow occurring over TLS. No plain HTTP here, or you're asking for trouble!

Looking at the handover points between the various systems involved:

  1. Request from SPA to Identity Provider: all values in the querystring are non-sensitive
  2. User interactions with Identity Provider: username/password and consent all sent in request body, so encrypted by TLS.
  3. Handover from Identity Provider back to SPA: this is a 302 redirect in response to the consent form being submitted by the user. The "Location" header in this redirect contains the pointer back to the SPA with the id_token and access_token in the URI. HTTP Headers are encrypted under TLS.
  4. Processing of redirect by Browser: Browser receives the tokens in the fragment, but doesn't send them when it fires the request to the SPA - but once the JS loads, the fragment is available client-side only for processing by the SPA's Javascript OAuth handler.
  5. The access token is usually stored sandboxed in the browser's local storage, and any requests to the API are done using the Authorization header - again, requests must be encrypted with TLS to ensure the acccess_token is kept private.

Any other considerations?

Yep. This scheme relies heavily on compliance of the browser to the specifications. It requires that the browser complies with the URI Spec around keeping the fragment private and not including it in HTTP requests.

It relies on the security of the browser's implementation of local storage (if that's what's used to persist the access_token), and relies on the token only being used either in the Authorization header, or as part of the request body - never in the query string!