Secure OAuth2 : Is Authorization Code Grant Type Secure Enough?
The authorization code grant type is generally considered as the most secure, widely used and commonly recommended grant type within the OAuth2 framework requiring no special emphasis.
The client credential grant type serves a specific purpose, allowing OAuth2 clients (applications) to acquire an access token on their own behalf for use cases that don’t require end-user involvement. Let’s set client credential grant type aside for now. The implicit grant type, despite its simplicity and convenience, comes with significant drawbacks. One key concern is its vulnerability to security threats, such as token leakage through browser history or referral headers due to the direct transmission of access tokens to the client-side application. Additionally, it lacks token refresh capabilities, necessitating clients to request new access tokens upon expiration, potentially disrupting user experience and complicating token management. Moreover, since the implicit grant lacks client authentication, it is susceptible to certain attacks. Similarly, the password grant type, initially introduced as a transitional mechanism from security schemas like BasicAuth, also poses security risks by requiring users to disclose their credentials directly to third-party applications, ultimately undermining one of the key problems OAuth2 aimed to address.
In contrast to implicit and password grant types, the authorization code grant type employs a two-step process that significantly reduces the risk of interception of access tokens. This involves the client obtaining an authorization code from the authorization server after the user has been authenticated, without exposing the user’s credentials to the client. The authorization code is short-lived and can only be used once, mitigating the risk of replay attacks. Additionally, the exchange of the authorization code for an access token occurs through secure back-end communication. This separation of the client’s front-end and the authorization server’s back-end enhances security by minimizing the exposure of tokens. Furthermore, the ability to issue refresh tokens greatly simplifies token management complexity, allowing applications to securely obtain a new access token upon the expiration of the current access token without disturbing the user again. This makes it possible to implement best practices such as short-lived access tokens and long-lived refresh tokens
However, in this post, I’m trying to discuss a crucial question: Despite being the preferred grant type over others, does the authorization code truly offer the highest level of security? Or should we aim to enhance the security measures even further? To answer this, let’s delve into the two-step flow of the authorization code grant type and closely analyze security measures.
Authorization Request
As depicted in the above diagram, authorization code grant type starts with authorization request where an OAuth2 client, which is typically a mobile or a web application, initiates the authorization process by redirecting the user to the authorization server’s authorization endpoint. Typically when a user clicks the login link/button, the application perform the following sequence of activities:
- Constructing an authorization request which involves building a URL with specific parameters. Typically this can be done using in-built features of an underlying framework or SDK used by the application providing the necessary parameters such as client_id, redirect_uri, response_type, scope and state. The authorization request is constructed by appending above parameter values to the URL of authorization endpoint as URL encoded query parameter values.
- In order to redirect the user to the authorization server, the application makes an HTTP GET request to the authorization endpoint of the authorization server along with the authorization request created in the above step.
Typically, you would be able to see the authorization request in the address bar of the browser.
P1 : Integrity protection failure
As you can see in the above diagram, the full authorization request is visible in the browser, which means that the communication between the web browser and the authorization server is not safeguarded against unauthorized modification or tampering. As a result, the authorization request could potentially be altered or manipulated by malicious actors without detection. For example, a malicious actor could forge a request to impersonate the application, or modify the authorization request before it reaches the authorization server. This scenario represents an integrity protection failure in security terms.
Integrity protection failure implies that the integrity of the data being transmitted cannot be guaranteed, which is a fundamental security concern in any communication system specially in the context of OAuth2 client and authorization server communication. OAuth2 requires the use of TLS, which establishes a secure and encrypted transport channel, thereby reducing risks. While this measure may suffice for certain business scenarios, it fails to address the core issue: the inability to apply cryptographic integrity techniques to the authorization request.
P2 — Source Authentication Failure
If we look at the above authorization request again, we can notice a couple of important parameters values. First, client_id represents an unique identifier that helps the authorization server to uniquely identify the particular client. The OAuth2 requires the redirect_uri in the authorization request to be matched with one of the redirect_uri values provided to the authorization server during the client registration. However, none of these parameters help the authorization server to authenticate the client, in other words, there is no assurance that the entity claiming to send the authorization request is actually who they claim to be. This scenario, known as source authentication failure, poses a significant security risk.
In the 2nd step of the authorization code grant type, referred to as the token request flow, confidential clients like server-side web applications must authenticate with the authorization server when making token requests. This authentication step helps mitigate risks. However, when considering the authorization request flow alone, it lacks source authentication. It is also worth noting that client authentication during the token request flow does not apply to public clients such as browser-based applications and mobile applications.
P3 — Confidentiality failure
As we saw in P1, the communication between the client and authorization server is fully visible in the browser, not just authorization requests. Instead the successful response from the authorization server called authorization response which contains the code grant is also visible in the browser. There is no fully acceptable measure to ensure these communications are not monitored by unauthorized parties leading to another issue known as confidentiality failure.
Another possibility is that query string data containing OAuth 2.0 parameters has the potential to unintentionally expose at the web servers. This can occur when the data is logged by web servers or transmitted to other sites through the referrer header.
P4 — Lack of Request as a Reference semantic
“By value” vs “By reference” is a well-known concept used in computer science. In the above authorization request, we embed actual parameter values within the request URL, representing the “by value” semantic. While this is a preferable option in simple scenarios, some application developers may prefer to use “by reference” semantics to tighten security or due to particle limitations such as exceeding the maximum size of the Get request.
OAuth 2.0 Rich Authorization Requests ( OAuth2 RAR) introduced a new parameter called “authorization_details” to be used with the authorization request to facilitate communication of fine-grained authorization data with the authorization server. For example, the following payment initiation request used in Open Banking can be represented as a JSON object and then transmitted to the authorization server using the “authorization_details” parameter. When sending sensitive financial information, due to the fact that all this information is visible in the browser, the developer may prefer to use “by reference” semantics rather than “by value” semantics.
Taking another scenario related to API security, the Resource Indicators for OAuth 2.0 specification defines a new parameter called “resource” so that the client can indicate the target service or resource to which access is required. For example, a typical request can be as follows. However, in a real-world scenario, the resources list can be lengthy and may exceed the maximum size supported by the web browser and web server for HTTP GET requests. In such cases, “by reference” semantics come in very handy.
As we looked at in the above example cases, the lack of “by reference” semantics can also be considered a security or practical limitation of the authorization request depending on the business use case.
Authorization Response
So far we mainly focused on the authorization request part of the flow. Now, let’s move into the authorization response part of the flow.
After the end user is authenticated with the authorization server and has consented to grant access to the particular resource, the authorization server redirects the end user back to the application using the URL mentioned in the redirect_uri parameter of the request, along with the code grant and state parameter.
Similar to the authorization request, the authorization response is also encoded using the URL encoding schema and fully visible in the browser. Thus, some of the issues we discussed in the previous section are applicable to authorization responses as well.
P5 : Server integrity protection failure
When an authorization server returns back to an application with a code grant, it would appear as below in the browser’s address bar.
Essentially, the authorization server appends the code and other parameters such as scope and state into the redirect_uri of the client application as query string values, making it easy for the client to process these parameters. This simplifies the processing of the response and makes it efficient.
On the flip side, there is no mechanism for the client to validate whether the authorization response has been altered or modified during transit, or whether the response has been actually produced by the legitimate authorization server. This can be considered as an integrity protection failure from the client application’s point of view. There is no possibility to employ a cryptographic integrity protection mechanism such as signing or encryption for the authorization response.
P6 — Client confidentiality failure
The code grant is intended to be used exclusively by the client application and ideally should be known only to the client application. However, as seen in the diagram above, the authorization code grant type was designed in such a way that the code grant generated at the authorization server is transmitted to the client application using a web browser as intermediary. This can lead to situations where an attacker may monitor the flow and potentially steal the code, or the code can be leaked to web server logs. This results in a confidentiality failure for the client
P7 — Mixed-up attack
This is a well-known attack related to OAuth2. In this attack, an attacker tricks a client into sending an authorization code to an authorization server controlled by the attacker instead of the intended legitimate authorization server. The client “mixes up” which authorization server it is interacting with. This is primarily because the client can’t distinguish the authorization response from a legitimate authorization server from one sent by a malicious authorization server. This can easily occur in open banking applications because generally each financial service maintains their own authorization server, and the applications usually have to support multiple financial services, hence multiple authorization servers.
In a summery, in this post we have looked at some of the critical security implications related to OAuth2 authorization code grant type specially related to authorization request and response. Before finishing this post, I want to emphasize that my intention in discussing all these issues related to the authorization code grant type is not to discourage its use, but rather to introduce and promote architectural improvements that have been standardized over time to address some of the above discussed limitations. We will discuss these improvements in the next couple of posts.