Keith Casey, an API Problem Solver at Okta, covers the basics of OAuth 2.0 and OpenID Connect to help you build applications that are secure, reliable, and protect your systems and data the way you expect.
If you're interested in being a part of our next series, fill out this short form and we'll get in touch with you for our next run.
In the last few years, I’ve worked with dozens of companies to understand their needs, goals, and design on how they’ll use OAuth within their systems. Throughout those projects, I’ve found good news and bad news.
The good news is the tools and supporting libraries are steadily getting better, both in terms of ongoing support and security overall. As a result, mistakes that were common just a few years ago are steadily disappearing. This is a major win because if we start with better, more secure tools, we’ll build more secure software by default.
The bad news is there are still too many easy ways to build systems that look secure and seem secure but leak user information, application data, or inadvertently encourage bad security practices in downstream applications.
Therefore, let’s talk about some of those common mistakes and how we can improve security from day one.
OpenID Connect vs OAuth 2.0
The first step to making our applications more secure is understanding what problems our tools are designed to solve.
The OAuth 2.0 Framework describes overarching patterns for granting authorization but does not define how to actually perform authentication. The application using OAuth constructs a specific request for permissions to a third party system - usually called an Identity Provider (IdP) - which handles the authentication process and returns an Access Token representing success. The IdP may require additional factors such as SMS or email but that is entirely outside the scope of OAuth. Finally, the contents and structure of that Access Token are undefined by default. This ambiguity guarantees that Identity Providers will build incompatible systems.
Luckily, OpenID Connect or OIDC brings some sanity to the madness. It is an OAuth extension which adds and strictly defines an ID Token for returning user information. Now when we log in with our Identity Provider, it can return specific fields that our applications can expect and handle. The important thing to remember is that OIDC is just a special, simplified case of OAuth, not a replacement. It uses the same terminology and concepts.
This is where we need to understand the use case: When we use “Log in with $favorite_social_provider” are we using that provider to establish our identity or are we granting the application access to interact with the data? There’s a radical difference between importing my Twitter account name and profile information versus sending direct messages as me. As developers, we have to understand the difference and only request the permissions we need.
Scopes & Access Control
Scopes - or the permissions we request - are the next most common mistake. Fundamentally, scopes are how we define and apply fine-grained access control to individual resources, information, or actions available in our system.
Within the OpenID Connect specification, the scopes are defined as
phone and each grants access to that specific information. If you use any scope beyond those, you’re beyond the OIDC specification and back into general OAuth and this is where it gets complicated.
OAuth itself does not define naming conventions, relationships, or access for particular scopes. In practical terms, it means that an
admin scope means different things to different systems, if it means anything at all. On the positive side, it means companies like Google can make highly structured and predictable OAuth Scopes. On the negative side, it means Github has scopes called
user:email to grant read only access to user profile. The same problem happened in Android where granting the Facebook application access to your contact list included your SMS and call history.
Unfortunately, too many of our applications are designed and built around a simple is_admin boolean instead of fine-grained access control. This gives users too much access to too many things they don’t need or shouldn’t have. We need to lean towards Google favoring fine-grained access control with specific, consistent naming. It’s more work up front for us but the power and flexibility for us and our users is well worth it.
Next, we need to think carefully about claims. A claim is simply the name/value pair embedded within our Access and ID Tokens. For example, you might have a user_id or email claim so downstream applications can use them to create profiles or make decisions. The confusing part is that the OAuth Core specification doesn’t introduce the concept of claims or even include the word claim. This is useful because we can define however we need. Unfortunately, the challenge is that people will define them however they need. The JWT specification (RFC 7519) introduces the concept and defines a basic structure but still doesn’t set any conventions for names, structures, etc.
Using OpenID Connect protects us quite a bit. It defines a simple set of claims for user details such as name, address, and similar. If we only use those and limit access according to the proper scopes, the user knows what information they are sharing and applications know how to use it.
Alternatively, as we add extra claims, the natural desire is to have a user-unique identifier. If we’re thoughtful about it, we use an obfuscated primary key that has no meaning outside our system. If we’re not careful, that could be a customer identifier, employee number, or even a Social Security Number. This is the same situation where Facebook is struggling with the implications of sharing too much information about a user’s network via their API.
We have to be thoughtful and consider the consequences of the information we put into our tokens. We should never include data “just in case” and instead wait for specific use cases that we choose to support. Anything else is risky at best and irresponsible at worst.
Grant Types - When and Why
While I jumped straight into scopes and claims, the other most common mistake is related to the specific OAuth grant types or flows. The four grant types - Authorization Code, Implicit, Resource Owner Password, and Client Credential - define how an application can retrieve tokens from your OAuth server and are used in different use cases.
The Authorization Code flow is the most powerful and most secure by default. When the application redirects the user to the Identity Provider to authenticate, the IdP passes back a short-lived, one-time use authorization code. The application uses the authorization code to retrieve the Access Token.
The important part is twofold: First, by the time the user sees the authorization code, it’s already been consumed and therefore can’t be used again. Second, the Access Token is kept by the application in the backend. Assuming the application is built securely, a malicious user has to find another way to attack it.
Since it’s quite likely that the user could interact with the token(s), it’s important that our use cases reflect that. If we have a banking app, allowing the send_wire_transfers_to_russia scope may be a bad idea unless we have additional factors baked into our authentication process to validate that the right user is using it. The next time you lose your phone, you’ll appreciate that.
As a result, this is often used for OpenID Connect scenarios where a user wants to provide trusted profile information to a third party but not necessarily access or permissions to other systems. Since the underlying concepts are the same and the implementation looks very similar, it’s most of the benefit for the same effort.
Resource Owner Password
Compared to the previous grant types, Resource Owner Password makes me nervous. With both the Authorization Code and Implicit flows, the application redirects the user to the Identity Provider to submit their username and password. As a result, the application never sees their credentials. With the Resource Owner Password flow, the application itself accepts the credentials and submits them on behalf of the user.
If the application is malicious or even just poorly developed, it could store those credentials and compromise the user’s information. Therefore, you should only use this if you’re building applications for your users to interact with your legacy systems. For example, a bank may implement this for an internal employee portal.
But remember: Fundamentally, you’re training users to put their credentials into applications they may not trust which is a bad habit at best and a security risk at all times.
The Client Credential grant type is designed exclusively for backend server to server operations. Think of it as a server’s username and password. Conceptually, it’s not far from how your application connects to other backend systems such as your database or Twilio. The benefit is that your OAuth provider can return configuration information or other details within the token itself.
Finally, since there’s not a user involved, it doesn’t support OpenID Connect.
While this entire post has been about OAuth, you’ve probably noticed that I didn’t include any code. The reason for that is simple: The design decisions are still more important.
If your scopes are too broad or your claims include sensitive information or you implement the wrong flow for the environment, the best libraries in the world won’t protect you. Your users’ information will be compromised, your applications will be vulnerable, and your company will suffer the consequences. Alternatively, if you understand the use cases for your software and what your users are trying to accomplish, your software will be better, more secure, and you can limit the “We’re sorry..” emails to your customers.
For Further Reading
From here we can keep going into the protocols, individual libraries, or great books on the subject. If you want to understand how the individual RFCs fit together, check out Aaron Parecki’s book OAuth2.0 Simplified and his accompanying Map of OAuth 2.0 Specs. It’s my go to cheatsheet on links to the key specs and which one covers what. All of the above inspired my guide for Recommended Practices for API Access Management which details good design and development principles for OAuth Clients, Authorization Servers, API gateways, and your applications.