English
  • authentication
  • authorization
  • oauth
  • openid-connect
  • oidc
  • application
  • api

Secure cloud-based applications with OAuth 2.0 and OpenID Connect

A complete guide for securing your cloud applications with OAuth 2.0 and OpenID Connect and how to offer a great user experience with authentication and authorization.

Gao
Gao
Founder

Introduction

Cloud-based applications are the trend nowadays. While the application type varies (web, mobile, desktop, etc.), they all have a cloud backend that provides services like storage, computing, and databases. Most of the time, these applications need to authenticate users and authorize them to access certain resources.

While homegrown authentication and authorization mechanisms are possible, security has become one of the top concerns when developing cloud applications. Luckily, our industry has battle-tested standards like OAuth 2.0 and OpenID Connect to help us implement secure authentication and authorization.

This post has the following assumptions:

  1. You have a basic understanding of application development (web, mobile, or any other type).
  2. You've heard about the concepts of authentication and authorization.
  3. You've heard about OAuth 2.0 and OpenID Connect.

Yes, "heard" is sufficient for both 2 and 3. This post will use real-world examples to explain concepts and illustrate the process with diagrams. Let's get started!

OAuth 2.0 vs. OpenID Connect

If you are familiar with OAuth 2.0 and OpenID Connect, you can also continue reading because we will cover some real-world examples in this section; if you are new to these standards, it's also safe to continue as we will introduce them in a simple way.

OAuth 2.0

OAuth 2.0 is an authorization framework that allows an application to obtain limited access to protected resources on another application on behalf of a user or the application itself. Most of popular services like Google, Facebook, and GitHub use OAuth 2.0 for social login (e.g., "Sign in with Google").

For example, you have a web application MyApp that wants to access the user's Google Drive. Instead of asking the user to share their Google Drive credentials, MyApp can use OAuth 2.0 to request access to Google Drive on behalf of the user. Here's a simplified flow:

In this flow, MyApp never sees the user's Google Drive credentials. Instead, it receives an access token from Google that allows it to access Google Drive on behalf of the user.

In OAuth 2.0 terms, MyApp is the client, Google is both the authorization server and the resource server for simplicity. In the real world, we often have separate authorization and resource servers to offer a single sign-on (SSO) experience. For example, Google is the authorization server and it may have multiple resource servers like Google Drive, Gmail, and YouTube.

Note that the actual authorization flow is more complex than this. OAuth 2.0 has different grant types, scopes, and other concepts that you should be aware of. Let's put that aside for now and move on to OpenID Connect.

OpenID Connect (OIDC)

OAuth 2.0 is great for authorization, but you may notice that it doesn't have a way to identify the user (i.e., authentication). OpenID Connect is an identity layer on top of OAuth 2.0 that adds authentication capabilities.

In the example above, MyApp needs to know who the user is before initiating the authorization flow. Note that there are two users involved here: the user of MyApp and the user of Google Drive. In this case, MyApp needs to know the user of its own application.

Let's see a straightforward example, assuming users can sign in to MyApp using username and password:

Since we are authenticating the user of our own application, usually there's no need to ask for permission like what Google did in the OAuth 2.0 flow. In the meantime, we need something that can identify the user. OpenID Connect introduces the concepts like ID token and userinfo endpoint to help us with that.

You may notice that the identity provider (IdP) is a new standalone participant in the flow. It is the same as the authorization server in OAuth 2.0, but for better clarity, we use the term IdP to show that it is responsible for user authentication and identity management.

When your business grows, you may have multiple applications that share the same user database. Just like OAuth 2.0, OpenID Connect allows you to have a single authorization server that can authenticate users for multiple applications. If the user is already signed in to one application, they don't need to enter their credentials again when another application redirects them to the IdP. The flow can be done automatically without user interaction. This is called single sign-on (SSO).

Again, this is a highly simplified flow and there are more details under the hood. For now let's move on to the next section to prevent information overload.

Application types

In the previous section, we used web applications as examples while the world is more diverse than that. To an identity provider, the exact programming language, framework, or platform you use doesn't really matter. In practice, one notable difference is if the application is a public client or a private (trusted) client:

  • Public client: A client that cannot keep its credentials confidential, which means the resource owner (user) can access them. For example, a web application running in a browser (e.g. single-page application).
  • Private client: A client that has the ability to keep its credentials confidential without exposing it to (resource owners) users. For example, a web application running on a server (e.g. server-side web application) or an API service.

With this in mind, let's see how OAuth 2.0 and OpenID Connect can be used in different application types.

"Application" and "client" can be used interchangeably in the context of this post.

Web applications running on a server

The application runs on a server and serves HTML pages to users. Many popular web frameworks like Express.js, Django, and Ruby on Rails fall into this category; and backend-for-frontend (BFF) frameworks like Next.js and Nuxt.js are also included. These applications have the following characteristics:

  1. Since a server only allows private access (there's no way for public users to see the server's code or credentials), it is considered a private client.
  2. The overall user authentication flow is the same as the one we discussed in the "OpenID Connect" section.
  3. The application can use the ID token issued by the identity provider (i.e., the OpenID Connect provider) to identify the user and display user-specific content.
  4. To keep the application secure, the application usually use the authorization code flow for user authentication and to obtain tokens.

In the meantime, the application may need to access other internal API services in a microservices architecture; or it is a monolithic application that needs access control for different parts of the application. We'll discuss this in the "Protect your API" section.

Single-page applications (SPAs)

The application runs in a user's browser and communicates with the server via APIs. React, Angular, and Vue.js are popular frameworks for building SPAs. These applications have the following characteristics:

  1. Since the application's code is visible to the public, it is considered a public client.
  2. The overall user authentication flow is the same as the one we discussed in the "OpenID Connect" section.
  3. The application can use the ID token issued by the identity provider (i.e., the OpenID Connect provider) to identify the user and display user-specific content.
  4. To keep the application secure, the application usually use the authorization code flow with PKCE (Proof Key for Code Exchange) for user authentication and to obtain tokens.

Usually, SPAs need to access other API services for data fetching and updating. We'll discuss this in the "Protect your API" section.

Mobile applications

The application runs on a mobile device (iOS, Android, etc.) and communicates with the server via APIs. In most cases, theses applications have the same characteristics as SPAs.

Machine-to-machine (M2M) applications

Machine-to-machine applications are clients that run on a server (machine) and communicate with other servers. These applications have the following characteristics:

  1. Like web applications running on a server, M2M applications are private clients.
  2. The application may not need to identify the user; instead, it needs to authenticate itself to access other services.
  3. The application can use the access token issued by the identity provider (i.e., the OAuth 2.0 provider) to access other services.
  4. To keep the application secure, the application usually use the client credentials flow to obtain access tokens.

When accessing other services, the application may need to provide the access token in the request header. We'll discuss this in the "Protect your API" section.

Applications running on IoT devices

The application runs on an IoT device (e.g., smart home devices, wearables, etc.) and communicates with the server via APIs. These applications have the following characteristics:

  1. Depending on the device's capability, it can be a public or private client.
  2. The overall authentication flow may be different from the one we discussed in the "OpenID Connect" section according to the device's capability. For example, some devices may not have a screen for users to enter their credentials.
  3. If the device doesn't need to identify the user, it may not need to use ID tokens or userinfo endpoints; instead, it can be treated as a machine-to-machine (M2M) application.
  4. To keep the application secure, the application may use the authorization code flow with PKCE (Proof Key for Code Exchange) for user authentication and obtain tokens or the client credentials flow to obtain access tokens.

When communicating with the server, the device may need to provide the access token in the request header. We'll discuss this in the "Protect your API" section.

Protect your API

With OpenID Connect, it's possible to identify the user and fetch user-specific data via ID tokens or userinfo endpoints. This process is called authentication. But you probably don't want to expose all your resources to all authenticated users, for example, only administrators can access the user management page.

This is where authorization comes into play. Remember that OAuth 2.0 is an authorization framework, and OpenID Connect is an identity layer on top of OAuth 2.0; which means you can also use OAuth 2.0 when OpenID Connect is already in place.

Let's recall the example we used in the "OAuth 2.0" section: MyApp wants to access the user's Google Drive. It's not practical to let MyApp access all the user's files in Google Drive. Instead, MyApp should explicitly claim what it wants to access (e.g., read-only access to files in a specific folder). In OAuth 2.0 terms, this is called a scope.

You may see the term "permission" used interchangeably with "scope" in the context of OAuth 2.0 as sometimes "scope" is ambiguous for non-technical users.

When the user grants access to MyApp, the authorization server issues an access token with the requested scope. The access token is then sent to the resource server (Google Drive) to access the user's files.

Naturally, we can replace Google Drive with our own API services. For example, MyApp needs to access the OrderService to fetch the user's order history. This time, since user authentication is already done by the identity provider and both MyApp and OrderService are under our control, we can skip asking the user to grant access; MyApp can directly send the request to OrderService with the access token issued by the identity provider.

The access token may contain a read:order scope to indicate that the user can read their order history.

Now, let's say the user accidentally enters an admin page URL in the browser. Since the user is not an admin, there's no admin scope in the access token. The OrderService will reject the request and return an error message.

In this case, the OrderService may return a 403 Forbidden status code to indicate that the user is not authorized to access the admin page.

For machine-to-machine (M2M) applications, no user is involved in the process. Applications can directly request access tokens from the identity provider and use them to access other services. The same concept applies: the access token contains the necessary scopes to access the resources.

Authorization design

We can see two important things to consider when designing authorization to protect your API services:

  1. Scopes: Define what the client can access. Scopes can be fine-grained (e.g., read:order, write:order) or more general (e.g., order) depending on your requirements.
  2. Access control: Define who can have specific scopes. For example, only administrators can have the admin scope.

Regarding access control, some popular approaches are:

  • Role-based access control (RBAC): Assign roles to users and define what roles can access what resources. For example, an administrator role can access the admin page.
  • Attribute-based access control (ABAC): Define policies based on attributes (e.g., user's department, location, etc.) and make access control decisions based on these attributes. For example, a user from the "Engineering" department can access the engineering page.

It's worth mentioning that for the both approaches, the standard way for verifying access control is to check the access token's scopes, instead of roles or attributes. Roles and attributes can be very dynamic and scopes are more static which makes much easier to manage.

For detailed information about access control, you can refer to RBAC and ABAC: The access control models you should know.

Access tokens

Although we've mentioned the term "access token" many times, we haven't discussed how to get one. In OAuth 2.0, an access token is issued by the authorization server (identity provider) after a successful authorization flow.

Let's have a closer look at the Google Drive example and assume we are using the authorization code flow:

There are some important steps in the flow for access token issuance:

  • Step 2 (Redirect to Google): MyApp redirects the user to Google with an authorization request. Typically, this request includes the following information:
    • Which client (MyApp) is initiating the request
    • What scopes MyApp is requesting
    • Where Google should redirect the user after the authorization is done
  • Step 5 (Ask for permission to access Google Drive): Google asks the user to grant access to MyApp. The user can choose to grant or deny access. This step is called consent.
  • Step 7 (Redirect to MyApp with authorization data): This step is newly introduced in the diagram. Rather than returning the access token directly, Google returns an one-time authorization code to MyApp for a more secure exchange. This code is used to obtain the access token.
  • Step 8 (Use authorization code to exchange for access token): This is also a new step. MyApp sends the authorization code to Google to exchange for an access token. As an identity provider, Google will compose the context of the request and decide whether to issue an access token:
    • The client (MyApp) is who it claims to be
    • The user has granted access to the client
    • The user is who they claim to be
    • The user has the necessary scopes
    • The authorization code is valid and not expired

The above example assumes that the authorization server (identity provider) and the resource server are the same (Google). If they are separate, taking the example of MyApp and OrderService, the flow will be like this:

In this flow, the authorization server (IdP) issues both an ID token and an access token to MyApp (step 8). The ID token is used to identify the user, and the access token is used to access other services like OrderService. Since both MyApp and OrderService are first-party services, usually they don't ask the user to grant access; instead, they rely on the access control in the identity provider to determine whether the user can access the resources (i.e. whether the access token contains the necessary scopes).

Finally, let's see how the access token is used in machine-to-machine (M2M) applications. Since no user is involved in the process and the application is trusted, it can directly request an access token from the identity provider:

Access control can still be applied here. To OrderService, it doesn't matter who the user is or which application is requesting the data; it only cares about the access token and the scopes it contains.

Access tokens are usually encoded as JSON Web Tokens (JWT). To learn more about JWT, you can refer to What is JSON Web Token (JWT)?.

Resource indicators

OAuth 2.0 introduces the concept of scopes for access control. However, you may quickly realize that scopes are not enough:

  • OpenID Connect defines a set of standard scopes like openid, offline_access and profile. It may be confusing to mix these standard scopes with your custom scopes.
  • You may have multiple API services that share the same scope name but have different meanings.

One common solution is to add suffixes (or prefixes) to the scope names to indicate the resource (API service). For example, read:order and read:product are more clear than read and read. An OAuth 2.0 extension RFC 8707 introduces a new parameter resource to indicate the resource server that the client wants to access.

In reality, API services are usually defined by URLs, so it's more natural to use URLs as resource indicators. For example, the OrderService API can be represented as:

As you can see, the parameter brings the convenience of using the actual resource URLs in the authorization requests and access tokens. It's worth mentioning that the RFC 8707 may not be implemented by all identity providers. You should check the documentation of your identity provider to see if it supports the resource parameter (Logto supports it).

In a nutshell, the resource parameter can be used in authorization requests and access tokens to indicate the resource that the client wants to access.

There's no restriction on the accessibility of the resource indicators, i.e. the resource indicator doesn't need to be a real URL that points to an API service. Thus the name "resource indicator" properly reflects its role in the authorization process. You can use virtual URLs to represent resources you want to protect. For example, you can define a virtual URL https://api.example.com/admin in your monolithic application to represent the resource that only administrators can access.

Bring it all together

At this point, we've covered the basics of OAuth 2.0 and OpenID Connect, and how to use them in different application types and scenarios. Although we've discussed them separately, you can combine them according to your business requirements. The overall architecture can be as simple as this:

Or a bit more complex:

As your application grows, you will see identity provider (IdP) plays a critical role in the architecture; but it doesn't relate to your business goals directly. While it's a great idea to offload it to a reliable vendor, we need to choose the identity provider wisely. A good identity provider can greatly simplify the process, reduce the development effort, and save you from potential security pitfalls.

Closing notes

For modern cloud applications, identity provider (or authorization server) is the central place for user authentication, identity management, and access control. While we discussed a lot concepts in this post, there are still many nuances to consider when implementing such a system. If you are interested in learning more, you can browse our blog for more in-depth articles.