Introduction
This article will explain what OAuth2 and OpenID are and how they work from a client perspective. There are many OAuth2 libraries in various languages and for different types of applications, but many are incomplete, have horrible documentation, and require the developer to take a fairly active part in implementation, so it’s necessary to have a good understanding of what’s really going on.
- OAuth2 is an open source standard for implementing a RESTful API, JSON, and the HTTPS protocol to authorize and authenticate users.
- OpenID Connect (OIDC) is an extension to OAuth2. It provides access to data fields after authentication. The fields can contain almost anything, such as a user’s name, email address, street address, subscription status, etc. It gets a little confusing since many utility packages identify themselves as “OIDC.” This simply means that they are an OAuth2 package that also supports the OpenID extension.
- Proof Key for Code Exchange (PKCE) is another extension to OAuth2. In short, it’s a token that allows all applications to use the most secure form of OAuth2 no matter which type of environment they’re in.
There are two main use cases for OAuth2: First, as a way for users to safely log in to their favorite online resource, such as Spotify, Reddit, or Mastodon through your application, while giving your application permission to read and write data to and from the resource on behalf of the user. And second, as a way for your application to implement an identity as a service provider (IDaaS). An IDaaS allows your application to support users and user authentication, while the IDaaS takes care the hard work of authenticating users and safely storing user identities. Do your homework though, since some IDaaS sell user data to marketing companies, which may be a deal breaker.
General Terminology
Let’s get some important terminology out of the way. You should have a basic understanding of these four concepts.
- Authorization – The process of establishing which services are available to an application. For example a protected resource might contain an email service and a storage service, but the application is only granted authorization to read from the email service.
- Authentication – The process of verifying a user’s identity using authentication factors such as a username and password.
- Access Token – A token that allows an application to access a protected resource. Obtaining an access token is what OAuth2 is all about.
- Federated Authentication – A strong agreement between organizations about how to authenticate users. OAuth2 is a form of federated authentication. Another, older form, that you may still run across is SAML. It’s an XML based authorization and authentication standard.
Roles
A role is an entity that plays a function in the OAuth2 authentication and authorization process.
- Resource Server – A server that hosts protected user data. The resource requires that each data request contain an access token.
- Resource Owner – The user.
- Client – The entity that requests data from the resource server. Your application.
- Authorization Server – A server that issues tokens to the client on behalf of the resource server. This server is key to using OAuth2 since it’s where your application makes all of its HTTPS authorization and authentication requests. There are three endpoints on the authorization server; authorize, token, and revoke. We’ll get into what each one of these does in the following sections.
Flows
The process of OAuth2 authorization and authentication is called a flow. The flow consists of a series of HTTPS requests, redirects, and responses between your application and the authorization server. It results in the authorization server returning an access token which allows your application to make requests to the resource server on behalf of the user.
There are currently two main types of OAuth2 flows, authorization code flow and implicit flow. The authorization code flow is the safest. In the past, this flow could only be used in protected environments such as servers since it relied on a secret token. Web applications don’t have the ability to keep secrets, so they were stuck using the less secure implicit flow. However, the PKCE extension makes it possible to use the authorization code flow with any application, no matter the environment. In fact, the implicit flow will be removed from the next OAuth version. It’s now considered best practice to only use the authorization code flow.
In the spirit of best practice and future proofing, this article will outline the authorization code flow with PKCE.
Before You Begin
You need to register your application with the resource that you want to use. Registration provides you with a client ID and a client secret. Note that we’ll only be using the client ID in the flow that I’ll describe.
You’ll also need to add an endpoint from your application to the authorization server’s allow list. This endpoint handles a big piece of the flow; processing the access token. I’ll explain more about that in the next section.
Authorization Code Flow
1. Authorize Request
We’re off to the races. It all starts when a user clicks a “sign-in” button or an equivalent user action. The action must direct the client to the authorization server’s authorize endpoint. This step has two main purposes. First, to identify your application to the server, and second, to let the server know which flow you’re using. For a PKCE authorization code flow, the server expects five query parameters. Some servers may support or even require additional parameters.
- client_id – Identifies your application to the server. You get it from registering your application with the resource.
- code_challenge – This parameter supports the PKCE extension. Most OIDC packages handle this parameter for you behind the scenes. If not, your application needs to create a “cryptographically secure random number” each time the flow is run. The application must store this number for the duration of the flow (you’ll use it later). Next, the application must create a SHA256 hash from the number. You use the hash as the value for this parameter. If your application is written in JavaScript, you can use the SubtleCrypto API to accomplish these tasks (this API is complex and only supported in the latest browsers). Otherwise, there are several crypto packages available for various languages, frameworks, and environments.
- redirect_uri – The endpoint where the client is eventually redirected after it gets an authorization code from the authorization server (a few steps away in the flow). You must set up a function at this endpoint for handling the redirect. The endpoint you specify here must also be added to the authorization server’s allow list.
- response_type – Specifies which type of token we’re requesting. In this part of the flow we’re requesting an authorization code, so the value is code.
- scope – A space separated list of the requested permissions. The list will vary from one resource to the next. If you’re requesting access to OpenID fields, you’ll need to include openid in the list.
Here is an example of an authorize request:
https://auth.someauthenicationserver.com/v2/oauth/authorize?client_id=12345&redirect_uri=https%3A%2F%2Fmysite.com%2Fmyhandler&scope=email%20name%20favorite_pet%20openid&response_type=code&code_challenge=12345
2. Login Redirect
If the authorization server recognizes your application, we’re on to the second step in the flow; user authentication. The authorization server redirects the client to a branded sign-in page on the authorization server. This is where the user logs in to their account and is shown a list of the permissions that your application requested. The user must both successfully log in to their account and click an “allow” button that indicates that they approve of the requested permissions.
3. Authorize Redirect
Next the client is redirected to the endpoint that you specified in the redirect_uri parameter. The authorization server adds query parameters to the URL, so your application endpoint must be built to handle these parameters.
If the user successfully logged in and approved all of the permissions that your application requested, the redirect URL will contain a code parameter. This is the authorization code and it’s used by your application in the next step.
If the user failed to log in or did not approve all of the permissions, the redirect URL will contain error and error_description parameters. If this happens, the ride is over and your application should display an error to the user.
4. Access Token Request
This is the last step! Assuming the user successfully logged in and your application received an authorization code, you can now make a regular old POST request to the authorization server’s token endpoint to get the coveted access token. The server expects five parameters in the body of the request.
- client_id – The same value that was used in the first request.
- code – The authorization code that your received in the previous step.
- code_verifier – This is the “cryptographically secure random number” that you created in the first step. NOT the hashed version of it.
- grant_type – Specifies the type of token that you’re using as a credential. In this case, you’re using an authorization code, so the value is authorization_code.
- response_type – Specifies which type of token you’re requesting. For this last step you’re requesting an access token and, if you want to access OpenID fields, an ID token. For OpenID the value is token id_token, otherwise it’s just token.
If the request is successful, the server returns a JSON object similar to this.
{
"access_token": "AYjcyMzY3ZDhiNmJkNTY",
"refresh_token": "RjY2NjM5NzA2OWJjuE7c",
"token_type": "Bearer",
"expires_at": 1681424154
}
Your user and application are now officially logged in to the resource! The access_token property is what you’ll use in the Authorization header in all requests to the resource server.
You’ll want to store this object in a semi-permanent location so that your application continues to have access to it. If your application is client side, it’s best to store it in the browser’s local storage, that way, if the user decides to refresh the page or open your application in a new tab, you can still retrieve the data without having to make the user re-login.
Refresh Token
The access token is short lived (usually 1 to 2 hours) and, depending on how long the user keeps a single session open in your application, it may need to be renewed, otherwise your application’s resource requests will unexpectedly fail since they would now be using an invalid access token.
You may have noticed that the authorization server returned an expires_at property and a refresh_token property along with the access token. The expires_at property is the datetime when the access token becomes invalid. Your application must make a request for a new access token before the current one expires. The refresh_token property is used as a credential when making the request for a new access token.
The request for a new access token is a POST request to the authorization server’s token endpoint. The server expects three parameters in the body of the request.
- client_id – The same value that was used in the first request.
- grant_type – Specifies the type of token that you’re using as a credential. In this case, you’re using a refresh token, so the value is refresh_token.
- refresh_token – The refresh token.
The authorization server returns a JSON object just like the one from the Access Token Request. Your application must immediately replace the current and now invalid access token with the new one before making another request from the resource server.
Log Out
When the user decides to log out, your application makes a POST request with two parameters to the authorization server’s revoke endpoint. This action invalidates the current access token. Your application should then indicate to the user that they are now logged out.
- client_id – The same value that was used in the first request.
- access_token – The current access token.
Conclusion
OAuth2 is incredibly powerful since it allows your application to access a user’s personal data. Unfortunately, the standard is not implemented identically by all resources, nor do they all support the complete feature set. Read the resource’s documentation carefully before beginning your project so you’ll know the limitations sooner rather than later.
Lastly, since the OAuth2 flow is dependent on several HTTPS requests, comprehensive error handling is essential. At any point in the flow, a request may fail, and your application must be able to gracefully handle it, by either making another attempt or notifying the user that an error occurred. Happy coding!