OAuth2 and OpenID Connect Essentials for Web Developers (Part 2)

This is the second part of a series about OAuth2 and OpenID Connect. Read
Part 1 here
.

In the first part of this series, we covered how a user can use OAuth2 to sign in to an
Authorization Server, verify their identity, and request authorization to do something.
Authorization may be requested for the user itself, or for some service to whom the user delegates
access (a user may delegate a 3rd party like Trello to have limited access to their Google name and
email address, for example).

Eventually, somebody has to make a request to a Resource Server to access some data or perform an
API operation of some sort. How the Resource Server determines whether to permit an incoming
request will be the subject of this article.

Using Permission: Making Authorized Requests

A successful authorization request results in a user (or delegated service) obtaining an access
token of some sort from an Authorization Server that can be used to authorize a request to a
Resource Server. So let's switch perspective to the Resource Server, which has just received a
request.

How does the Resource Server know if it can even trust that request?

Trusting Incoming Requests

Resource Servers need to take some precautions to enforce authorization, lest they trust any
well-formed request. First and foremost, a Resource Server must verify that the request includes an
accepted means of authorization (often a Bearer token in an
Authorization header), verify that the token itself is
well-formed, and respond with the appropriate HTTP status code if the request is incomplete or
malformed.

If the token is a JWT token, the recipient must perform some validation before relying upon it.
Specifically, it must check that the JWT token has not been forged, that it was issued by a trusted
Authorization Server, and that it has not yet expired. This is done by verifying the
signature
and validating standard claims.

Depending on the algorithm used to sign the JWT token, the recipient needs some information from the
Authorization Server: either a shared secret or a public key. Note that signing key pairs may be
rotated, so the Resource Server might need to fetch updated public signing keys from time to time.
Authorization Servers often publish these in a standardized format at a well-known
URL, which is further standardized in the OpenID Connect
specification
.

Determining Authorization

Once the Resource Server is able to trust a token, it can:

  • Parse additional information from the token that identifies the session, user, roles, or
    authorized scopes.
  • Look up (or request) any additional information it needs to determine authorization. More on this
    in a moment.
  • Check permissions to see if the requested operation should be allowed.

It has been a rather lengthy process of requesting authorization and of doing some basic validations
of the request that comes in to the Resource Server, but now it is finally time to perform the
step that one typically thinks about when thinking of "authorization": deciding who gets to do what.

How does a Resource Server know whether to allow a request? It has to look at (the body of) the
token. As with authentication, authorization can be described as stateful or stateless,
depending upon whether any additional information needs to have been stored earlier and looked up
now to make a decision. Stateful authorization schemes may look up permissions based upon the
identity or role(s) contained in the token. Stateless authorization schemes often rely on one or
more scope values to identify what resource(s) the bearer may access and what the bearer may do
with those resources. As an example of the latter case, there are different scopes to grant access
to different parts of your Facebook user profile.

At long last, the Resource Server knows whether the request is authorized, and it can attempt to do
the requested operation. Its response is now determined by the outcome of attempting the operation,
not on whether the request is authorized.

Identifying Users with Their Permission: OpenID Connect

At long last, it is time to return to the topic where I started this long journey: OpenID
Connect
. Just what does it add on top of OAuth2? In the words of its authors:

OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. It enables
Clients to verify the identity of the End-User based on the authentication performed by an
Authorization Server, as well as to obtain basic profile information about the End-User in an
interoperable and REST-like manner.

So the big idea is that OpenID Connect defines a standardized resource (the user profile) that
users can authorize and grant access to with OAuth2. This is achieved by adding new
scopes
(such as openid and profile) that result in an additional token
being issued (an id_token) about the user. This token–and a new
/userinfo endpoint–can be used to obtain information about the user, in
a standardized format.

There's also an extra Hybrid flow that returns tokens and an
authorization code in the same response. This is useful for applications that need some user
information (ID tokens), the ability to make its own API calls (access tokens), and something else
from a 3rd party (authorization codes) and want to get all that information at the same time.

Finally, there's a practical matter that makes an OpenID Connect more convenient to use: the
Discovery Endpoint. In a nutshell, it's a JSON document in a
standardized format at a standardized path that tells you a few things about the server:

  • its endpoints: Why configure several addresses to the same server, when you can configure one
    address and use it to find all its other endpoints? This just makes it easier to figure out what
    URL to use to request authorization, tokens, and userinfo. It also enables one Identity Provider
    to delegate authorization to another, which could be used to facilitate migrating users from one
    Identity Provider to another without having to re-configure and re-start components en-masse.
  • its public signing keys: When JWT tokens are signed with a public-private key pair, Resource
    Servers need a way to handle when these keys are rotated. They can find the new keys with this
    document, rather than using a separate configuration for them.

OAuth2 and OpenID Connect in Practice

Now that you know how it all works, now what do you do? You probably still need to write some
code and make the magic happen.

Who Needs To Do What

Here's a rough breakdown of what you will need to implement or configure on each component that
participates in an OAuth2 workflow.

Clients like web applications have work to do, depending upon the workflow that is used:

  • Clients initiating the implicit grant/flow need to implement an authorization callback route of
    some sort and provide this address as the redirect_uri when it requests authorization. This
    route needs to parse the requested information from the callback request.
  • Clients initiating the code grant/flow need to add a state parameter when requesting authorization
    and verify that the state at the end of the workflow is what was sent in the initial request.
    Note that clients often need to request redirection to the delegated service, not the client
    itself.

Each Service or API endpoint that receives authorization codes needs to:

  • implement a route to parse the authorization code that was requested on behalf of the user.
  • make a request to the Authorization Server to exchange the authorization code for token(s).
  • implement a callback route to parse any ID or access tokens returned from the Authorization
    Server.

Each Resource Server or API Endpoint needs to:

  • keep in touch with the Authorization Server, so that any public signing keys are updated after a
    key rotation.
  • verify any JWT tokens it receives and validate its claims, before relying upon it.
  • parse the body any token it receives and use its fields (often scope) to determine whether the
    request should be authorized.

Finally, the Authorization Server needs to be configured with an app client for each client_id
that will be used in authorization requests. Each app client defines:

  • the kind(s) of grants—authorization codes or tokens—that may be issued for valid authorization
    requests.
  • which scopes may be requested, for each client.
  • (OpenID Connect) which user attributes will be released for openid and profile scopes in the
    form of ID tokens.
  • a whitelist of valid callback addresses, to which to release authorization codes and/or tokens.
    Note that Authorization Servers typically only accept callback addresses that are on HTTPS or to
    an app (myapp://auth/callback)
    . Some services like AWS Cognito make an exception for addresses
    that start with http://localhost, to support development of an application or service that is
    running on a developer's machine.

Tools and Techniques

There are a number of tools out there to help developers who are making or debugging OAuth requests:

  • Chrome devtools (or similar) can be used to log auth-related HTTP requests. Make sure to
    select "Preserve Log" in the Network tab, as there will be a lot of redirects. Further inspection
    can reveal Authorization headers, cookie-related headers, and query parameters.
  • jwt.io can be used to inspect JWT tokens.
  • ngrok can be used to route
    traffic through a publicly accessible Internet address back to your local machine, in case you are
    integrating a local Authorization Server with a remote web server, or vice versa.
  • OAuth Debugger can be used to make well-formed authorization requests for
    OAuth2 grants and to inspect responses from the Authorization Server.
  • OpenID Connect Debugger can be used to make well-formed authorization
    requests using OpenID Connect flows and to inspect responses from the Authorization Server.

Conclusion

Looking back to my motivating situation (adding a new, protected service to an existing web
architecture), it's no wonder it took a while to learn all of this. This touches upon a number of
topics such as establishing trust and identity, verifying communication, and defining a system for
requesting, delegating, and verifying authorization. On top of that, there is a considerable variety
in where applications run (web or mobile), how they are rendered (single page apps and server-based
apps), and how their services are composed (monoliths and microservices). Creating a standard that
works in all of those situations must have been quite a task!

However, while the concepts and workflows described here are genuinely complex, it is possible for
web developers (who are not necessarily security experts) to make progress and use these
technologies effectively. The key is to take it one step at a time and to take a bit of time to
understand each step as you go, without getting overwhelmed by the minutiae and hype surrounding
the latest library or service.

Acknowledgements

I gratefully acknowledge Brad Ediger and Colin Jones for their contributions on the finer points of
authentication and JWT tokens, respectively, and to Brad Ediger, Heather You, and Stacey Boeke for
their thoughtful and detailed reviews of earlier drafts of this article.

Source: 8th Light