Blog

The complete guide to OAuth 2.0

Learn everything you need to know about OAuth: what it is, what problem it solves, and how it works.


OAuth is everywhere. Whether you are aware of it or not, we all use it daily in our online lives.

In this article, we will see what it is, what problem it solves, and how it works. We will also clarify the terminology, all the different OAuth flows, and when to use what.

What is OAuth?

OAuth stands for Open Authorization and is a standard designed to allow an application to access resources hosted by other apps. This is done either on behalf of the user (by getting their explicit approval) or by allowing the website or app to obtain access on its own behalf (by using its own credentials). We will focus on the first case since it’s the most common online.

OAuth 2.0 replaced OAuth 1.0 in 2012 and has been the de facto industry standard ever since. Whenever you see OAuth in this article, you can assume we are talking about OAuth 2.0. To learn more about the differences between the two, see OAuth vs. OAuth 2: differences + what you need to know.

The problem OAuth solves

OAuth solves the problem of allowing users to grant to other apps access to their accounts without sharing their password with the application or website.

For example, let’s assume you are using Todoist to log your todos and Google Calendar for your appointments. If you want to have an overview of your day, you have to go to two different places: check Todoist for your todos and Google Calendar for your appointments. If you want to continue using this setup but hate cross-checking different apps and want to see an overview on a single page, OAuth is the solution. You can connect Todoist to your Google Calendar and have your todos appear in your calendar view. You would use an OAuth flow to connect the two: allow Todoist to connect to Google Calendar on your behalf and create entries. And all this without sharing your Google password with Todoist. Neat huh?

Even if you didn’t know what OAuth is until today, you have probably used it more than once. The sample dialog box below might look familiar: You are John Doe, and you are using Super App, which wants access to your email information and appointments list from another app. Super App shows you this dialog box, and you can choose whether to authorize sharing this information or not.

Understanding the terminology

Something that trips up many people who try to understand OAuth is the terminology introduced by the spec and used by the industry. It is, therefore, important to clarify these terms before we dive deeper into how OAuth works.

Roles

The OAuth framework specifies specific roles that participate in the authorization process. The industry heavily uses this terminology, so it’s important to understand what is what.

  • Resource owner: the user authorizing an application to access their resources in another application. If you, as a user, authorize app X to access your Google Calendar, you are the resource owner (because you own the resource, i.e., your calendar).
  • Resource server: the server hosting the protected resources that another application wants to access. In the same example, if you authorize app X to access your Google Calendar, the Google Calendar API is the resource server (where the protected resource, i.e., your calendar, resides).
  • Authorization server: the server that handles all the communication between the app that wants to access protected resources and the app that has the resources. The authorization server authenticates the user, gets their consent for sharing resources, and issues tokens that the app can use to ask for them.
  • Client: the application that wants to access resources in another app. Clients can be either confidential or public, and as we will see later, this dictates which OAuth flow they should use.
    • Confidential clients: Apps that can hold sensitive information (i.e., server-side apps).
    • Public clients: Apps that cannot keep secrets. These include native apps, which can be decompiled, and single-page apps, which have their entire source code available to the browser.

Mapping this terminology to the example we saw earlier, we can see the following:

  • John Doe is the resource owner.
  • Super App is the client.
  • The app displaying this dialog box is the resource server.
  • Read your email and read your appointments are the scopes that the app wants to be granted.

Tokens and other credentials

  • Access token: A special credential in the form of a string, that is issued for an application and allows it to access an API. The access token is issued by the authorization server, and sent to the application after a user authenticates and allows access to the resources. The application then can use the access token to access the resources they want (and the token permits) from the target API. The token informs the API that the app (the bearer of the token) has been authorized to access specific resources. Access tokens are usually JWTs and contain a payload in JSON format. They are short-lived and usually expire after a few minutes.
  • Refresh token: Asking the user to re-authenticate every time an access token expires is not the best UX. That’s when refresh tokens come into the picture. A refresh token is also a special credential in form of a string, issued by the authorization server and sent to an application alongside an access token. When the access token expires, the application can use the refresh token to get a new access token from the authorization server, without user involvement. Refresh tokens live longer (usually a few days) but they also expire at some point. That’s when the user will have to re-authenticate.
  • ID token: This is a trick entry and congratulations if you caught it! OAuth does not use ID tokens. You might see articles out there that talk about them but ID tokens are simply not part of the spec. The spec that does use ID tokens is OpenID Connect (OIDC), an authentication layer built on top of OAuth. These two specs are used so often together that many people think that ID tokens are part of OAuth.
  • Authorization code: A credential, in the form of a string, issued by an authorization server during some of the OAuth 2.0 flows. After the user authenticates and provides consent, the authorization server generates an authorization code and sends it back to the application. The application then makes another request to the authorization server, asking to exchange the authorization code for a token. This happens during the Authorization Code and the PKCE grants.
  • JWT: A JSON Web Token (JWT) is an open standard for securely transmitting information between different systems as signed JSON objects. JWTs, which is pronounced “jots”, are commonly used in the OAuth 2.0 and OIDC specs. To learn more about their format and how they are used see What are JSON Web Tokens (JWT) used for?.
  • Client ID: A public identifier for your application as issued by the authorization server. It’s sent from the application to the authorization server during any OAuth flow so the server can know which app they are talking with.
  • Client secret: This is another value issued by the authorization server, but unlike the client ID, the client secret is confidential. In essence it’s your application’s password. It should be stored safely and handled only by server-side applications (i.e. confidential clients).

Scopes

Scopes are used to limit an application’s access to a user’s account. An application can ask one or more scopes and the user will have to review them and agree to grant them. The access token that will be issued will contain the granted scopes so the resource server knows what the bearer is allowed to access. If, for example, an application wants to read your email and your appointments (same as in the screenshot we saw earlier), then as a user you would see a list like the following in the authorization request screen:

  • Read your email
  • Read your appointments

If you choose to accept then the application will get an access token that will contain something like that: scopes: {read-email, read-calendar}. The API that will receive the access token will read this and know that it should only provide this info, regardless of what the application might ask to retrieve.

Let’s see another example. The GitHub API offers these available scopes. If you want to delete a repository then your access token must include the delete_repo scope.

Flows

OAuth comes in different flavors (which we call authorization grants or flows) depending on the application type and the use case. Every OAuth flow’s purpose is to get an access token but they way they go about it differs. The most common flows are:

  • Authorization code: Used by server-side apps
  • Authorization code with Proof Key for Code Exchange (PKCE): Used by client-side apps (can, and is recommended to, be used by server-apps as well)
  • Client credentials: Machine-to-machine communication.

There are two other flows that are best avoided and we will not cover them in this article:

  • Resource owner password credentials: In this flow, the user’s credentials are shared directly with the application, and the client uses these credentials to get an access token from the authorization server each time it is needed. It’s not recommended because it involves the application handling the user's password.
  • Implicit: Used by client-side apps. In this flow, the access token is returned directly to the app, without going through the authorization code exchange. It’s not recommended due to the inherent risks of returning access tokens in an HTTP redirect without any confirmation that it has been received by the client. For more see Why you should stop using the OAuth implicit grant.

Authorization code grant

The authorization code grant is used by apps that run on a server. This flow uses the client secret, which can only be made available to apps that can securely hold secrets. Let’s see now how the Authorization code grant works:

  1. The user initiates the flow by doing something like clicking a button (hosted by the app) to connect and share their Google Calendar.
  2. The app sends an authorization request to the authorization server.
  3. The authorization server redirects the user to authenticate and agree with granting the app the permissions it asked for (e.g., view your Google Calendar).
  4. Upon successful authentication, the user grants permission.
  5. The authorization server generates an authorization code and sends it back to the app, as the response of the authorization request initiated in step 2.
  6. The app sends a new request to the authorization server, providing as input the authorization code and asking for an access token in return.
  7. The authorization server verifies the code and generates an access token (and optionally a refresh token).
  8. With the access token, the app can request resources from the resource server. In the Google Calendar example, the app would send a request to the Google API, using the provided access token, and asking for the user’s calendar info.
  9. The resource server validates the access token and provides the requested data.

Let’s see now how you would implement the authorization code grant with WorkOS.

First, you need to initiate the authorization request (step 2 in the flow above). This can be done with AuthKit or, if you are using your own UI, by calling the WorkOS API directly using an SDK. The request looks like this:

	
import { WorkOS } from '@workos-inc/node';

const workos = new WorkOS('sk_example_123456789');

const authorizationUrl = workos.userManagement.getAuthorizationUrl({
  connectionId: 'conn_01E4ZCR3C56J083X43JQXF3JK5',
  clientId: 'client_123456789',
  redirectUri: 'https://your-app.com/callback',
  state: 'dj1kUXc0dzlXZ1hjUQ==',
});
	

Let’s look into some of the parameters:

  • The input to new WorkOS() is your API key, which you can find in the dashboard as WORKOS_API_KEY.
  • The connectionId is the identifier of the WorkOS connection the user should use to authenticate.
    • If you use AuthKit, it will take care of detecting the user’s authentication method and direct them to the corresponding login flow (e.g., username/password or SSO).
    • Otherwise, you’ll have to specify the user’s connection (connectionId), organization (organizationId), or OAuth provider (provider) as a parameter. For example, to log a user using Github, set provider: 'GitHubOAuth'.
  • The clientId is your app’s identifier, which you can find in the dashboard as WORKOS_CLIENT_ID.
  • The redirectUri is the URL where you want to redirect the user after they complete the authentication process. For this to work you must add this URL in the Redirects page on the dashboard.
  • The state is an arbitrary string that can be used to help restore application state between redirects. It’s an optional parameter. If included, the redirect URI received from WorkOS will contain the exact state value that was passed

For more details on the endpoint check Get an authorization URL.

After the user authenticates, WorkOS redirects them to the redirectUri including in the query string the authorization code and, optionally, the state. The URL would look like this:

	
https://your-app.com/redirect?code=g0FGFmNjVmOWIkTGf2PLk4FTYyFGU5&state=dj1kUXc0dzlXZ1hjUQ==
	

The next, and final, step is to exchange the code with a token and the user’s profile (step 6 in the flow above). The request looks like this:

	
import { WorkOS } from '@workos-inc/node';

const workos = new WorkOS('sk_example_123456789');

const { user, access_token, refresh_token } = await workos.userManagement.authenticateWithCode({
  clientId: 'client_123456789',
  code: '01E2RJ4C05B52KKZ8FSRDAP23J',
});
	

The response will include the user info, an access token, and a refresh token:

	
{
  "user": {
    "object": "user",
    "id": "user_01E4ZCR3C56J083X43JQXF3JK5",
    "email": "marcelina.davis@example.com",
    "first_name": "Marcelina",
    "last_name": "Davis",
    "email_verified": true,
    "profile_picture_url": "https://workoscdn.com/images/v1/123abc",
    "created_at": "2021-06-25T19:07:33.155Z",
    "updated_at": "2021-06-25T19:07:33.155Z"
  },
  "organization_id": "org_01H945H0YD4F97JN9MATX7BYAG",
  "access_token": "eyJhb.nNzb19vaWRjX2tleV9.lc5Uk4yWVk5In0",
  "refresh_token": "yAjhKk123NLIjdrBdGZPf8pLIDvK",
  "impersonator": {
    "email": "admin@foocorp.com",
    "reason": "Investigating an issue with the customer's account."
  }
}
	

For more details on this endpoint, see Authenticate with code.

PKCE

For public clients, who cannot securely hold secrets and therefore cannot have the client secret, the PKCE flow is used instead the authorization code grant. For more details on this distinction and why it happens see The challenge with frontend apps.

The PKCE flow introduces three new parameters: the code verifier, the code challenge, and the code challenge method.

  • The code verifier is a cryptographically random string the application creates when a user requests to log in. It’s important not to hard-code this value and to generate a new one for each authorization request. If not, you are exposing yourself to replay attacks.
  • The code challenge is a hash of the code verifier.
  • The code challenge method is the hashing algorithm used to generate the code challenge. This should be SHA-256.

Let’s see now how the PKCE flow works from start to finish:

  1. The user initiates the flow by doing something like clicking a button (hosted by the app) to connect and share their Google Calendar or sign in with Google.
  2. The app generates the code verifier, stores it locally, hashes it using the code challenge method (SHA-256) to generate a code challenge, and sends an authorization request to the authorization server. The request includes the code challenge and the code challenge method.
  3. The authorization server stores the code challenge and redirects the user to authenticate and agree with granting the app the permissions it asked for (e.g., view your Google Calendar).
  4. Upon successful authentication, the user grants permission.
  5. The authorization server generates an authorization code and sends it back to the app, as the response of the authorization request initiated in step 2.
  6. The app sends a new request to the authorization server, providing as input the authorization code, and the code verifier generated in the first step, and asking for an access token in return.
  7. The authorization server verifies the code, hashes the code verifier, and compares it to the stored code challenge. If they match, an access token is issued; otherwise, an error occurs. A refresh token is also optionally generated.
  8. With the access token, the app can request resources from the resource server. In the Google Calendar example, the app would send a request to the Google API, using the provided access token, and asking for the user’s calendar info.

Let’s see now how you would implement the authorization code grant with WorkOS.

First, your app needs to generate the code verifier, store it locally, and hash it using the code challenge method (SHA-256) to generate the code challenge. Once this is done, you can initiate the authorization request (step 2 in the flow above). This can be done with AuthKit or, if you are using your own UI, by calling the WorkOS API directly using an SDK. The request looks like this:

	
import { WorkOS } from '@workos-inc/node';

const workos = new WorkOS('sk_example_123456789');

const authorizationUrl = workos.userManagement.getAuthorizationUrl({
  connectionId: 'conn_01E4ZCR3C56J083X43JQXF3JK5',
  clientId: 'client_123456789',
  redirectUri: 'https://your-app.com/callback',
  state: 'dj1kUXc0dzlXZ1hjUQ==',
  codeChallenge: 'rSihM8NWOVbzdOnZXkIgTUI_JDWKe3fDsZpwte7xjd8',
  codeChallengeMethod: 'S256',
});
	

We described most of the parameters in the previous section. The new ones are the following:

  • The codeChallenge is the one the app generated. The code verified and code challenge have to be freshly generated with each request, they should not be reused under any circumstances.
  • The codeChallengeMethod which is always set to S256.

After the user authenticates, WorkOS redirects them to the redirectUri including in the query string the authorization code and, optionally, the state. The URL would look like this:

	
https://your-app.com/redirect?code=g0FGFmNjVmOWIkTGf2PLk4FTYyFGU5&state=dj1kUXc0dzlXZ1hjUQ==
	

The next, and final, step is to exchange the code with a token and the user’s profile (step 6 in the flow above). The request looks like this:

	
import { WorkOS } from '@workos-inc/node';

const workos = new WorkOS('sk_example_123456789');

const { user } = await workos.userManagement.authenticateWithCode({
  clientId: 'client_123456789',
  code: '01E2RJ4C05B52KKZ8FSRDAP23J',
  codeVerifier: 'EHzBfHKnwlZdpWmlYDjs',
});
	

In this request, besides the code, you must send the codeVerifier too, so the authorization server can has it and compare it to the stored code challenge from step 2. If the two values do not match you will get an error response.

The response will include the user info, an access token, and a refresh token, same as we saw earlier. For more details and code samples, see Authenticate with code.

Client credentials

This flow is used for machine-to-machine communication, when a user is not present. In this case the application (always server-side) wants to access their own resources, not on behalf of a user. This flow is very simple. The application sends a request to the authorization server that contains the following information:

  • grant_type (= client_credentials)
  • client_id
  • client_secret
	
POST /oauth2/token HTTP/1.1 
Host: authorization-server.com 
Content-Type: application/x-www-form-urlencoded 

grant_type=client_credentials&client_id=your_client_id&client_secret=your_client_secret&scope=api.read%20api.write
	

The authorization server returns an access token that the application can now use to access the resource server. Basically, the application uses its own username and password to get an access token.

What about authentication?

OAuth is not an authentication protocol, it only does authorization. If you use OAuth for authorization you will have to use something else for authentication and more often that not this is OpenID Connect (OIDC).OpenID Connect (OIDC) is an authentication layer built on top of OAuth and is commonly used for Single Sign-On (SSO), which allows users to authenticate once and gain access to multiple apps without re-authenticating. Its purpose is to allow websites and applications to verify the identity of users without needing to manage and store sensitive details like passwords. Instead, apps rely on a trusted third-party identity provider, like Google and Facebook, to confirm if someone is who they claim to be.

OAuth specs

There are several OAuth specs. You don’t need to read them all but we will list some of the most important in this section in case you want to.

This list is not exhaustive. There are several more specialized OAuth standards like the RFC 7591 - OAuth 2.0 Dynamic Client Registration Protocol and the RFC 8628 - OAuth 2.0 Device Authorization Grant that covers a grant designed specifically for clients like televisions or other appliances.

OAuth best practices

  • Use the proper OAuth flow for your application type.
  • Handle tokens and other sensitive information, like client credentials, safely. For server-side apps that store tokens, encrypt them at rest and ensure that your data store is not publicly accessible to the Internet. For client-side and native apps use PKCE.
  • Use short-lived tokens to minimize exposure of resources. Access token shouldn’t live more than a few minutes, and refresh tokens more than a few days.
  • Use HTTPS.
  • Use token encryption.
  • Use limited scope of access. Don’t request access to data, unless it is essential for the core functionality of your app. Request only the specific scopes that are needed for a task. Always select the smallest, most limited scopes possible. This is known as incremental authorization.
  • When requesting multiple scopes at once, users may not grant all OAuth scopes you have requested. They might choose to share their email but not more than that. Your app should handle the denial of scopes by disabling relevant functionality.
  • Use token revocation if possible. This allows the user or the authorization server to invalidate a token before it expires, and is used in case a token is compromised.
  • Don’t use the Implicit grant.
  • Don’t use the Resource Owner grant unless the client is 100% trusted.
  • Always validate the tokens before you use them. For more on this see JWT validation: how-to and best libraries to use.

For more info on best practices see OAuth 2.0 Security Best Current Practice.

Last but not least, don’t reinvent the wheel, unless you absolutely have to. There are many providers out there that have solved OAuth already and have implemented authorization servers using best practices. Some are open source, others not.

OAuth might seem simpler after you read this article, but there are many details that can get wrong, and they can cost you and your users dearly. An example of what happens when you make even minor mistakes when it comes to SSO - even with 10 years of open source devs looking at it - is the Ruby SAML CVE-2024-45409 vulnerability.

For more details on how complex building authentication and authorization on your own can be see our Build vs Buy series.

Implement OAuth with WorkOS

You can easily implement OAuth in your app using WorkOS as the authorization server. You can do it using the API directly if you want to build your own login box, or use AuthKit.

  • Get started fast: With SDKs in every popular language, easy-to-follow documentation, and Slack-based support, you can implement SSO in minutes rather than weeks.
  • Support every protocol: With OAuth 2.0 integrations to popular providers like Google and Microsoft, compatibility with every major IdP, and full support for custom SAML/OIDC connections, WorkOS can support any enterprise customer out of the box.
  • Avoid the back-and-forth: WorkOS’s Admin Portal takes the pain out of onboarding your customers’ IT teams and configuring your app to work with their identity provider.
  • Pricing that makes sense: Unlike competitors who price by monthly active users, WorkOS charges a flat rate for each company you onboard — whether they bring 10 or 10,000 SSO users to your app.

Sign up for WorkOS and start building today.

In this article

This site uses cookies to improve your experience. Please accept the use of cookies on this site. You can review our cookie policy here and our privacy policy here. If you choose to refuse, functionality of this site will be limited.