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

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

Kyle Krull
Kyle Krull

October 08, 2019

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.