by Chuanshu Jiang on 6 October 2023
OAuth 2.0 and OpenID Connect are similar protocols commonly used for identity and access management. The purpose of this blog is to provide an in-depth description of the OAuth 2.0 protocol and discuss various security controls required to protect against OAuth-specific attacks which can help you defend and test your applications.
Applications often need to manage resources on behalf of a user. These resources are often owned by a third party, for example, a productivity application might need access to a user’s emails, but the email account is owned by Microsoft. In the early days of the internet, the only way this access could be granted was for the user to directly provide the productivity application with their Microsoft username and password. This was very problematic from a security perspective, because:
OAuth 2.0 is an authorization protocol designed to solve all of these issues. Instead of requiring the user to share their credentials, OAuth allows the third party to issue an access_token on behalf of the user. This access token is usually a Bearer JSON Web Token (JWT) with signed claims representing the duration and level of access granted to the application.
OpenID Connect (OIDC) is an authentication protocol designed for an application to allow a trusted identity provider (IdP), such as Google or Microsoft, to identify a user. OIDC is considered to be built on top of OAuth 2.0, as it uses the OAuth flow to “share” the user’s identity in the form of an id_token.
Although both protocols have become commonplace, their exact implementation differs across providers. The Request for Comments (RFCs) provide skeletal definitions of each protocol, and allow plenty of room for extension. The OAuth protocol also has a history of revisions, mostly due to unique security issues continuously being discovered in the protocol itself.
The following references can be used to read more:
Understanding OAuth 2.0 can be beneficial for anyone involved in web application security. OAuth misconfigurations remain a major security issue in 2023, as demonstrated by Franz Rosen’s 2022 research into postMessages as an OAuth attack vector, and Salt Labs’ recent consecutive discoveries of CVE-2023-28131 and Account takeover on Booking.com.
Let’s continue with a description of the OAuth 2.0 protocol and the various security controls required to protect against OAuth-specific attacks.
The OAuth protocol defines four roles: the end user (or Resource Owner), client, authorization server, and resource server. The high-level interactions between these roles are shown in the image below.

The application that receives the access token is called the client. The term can be misleading as it is unrelated to the common usage of the word, which is synonymous with “front-end”. This can make the idea of a “confidential client” confusing, as a confidential client requires the ability to make server-side requests.
A client should be identified by a unique client_id, and optionally a client_secret. There are two types of clients, distinguished by their ability to securely maintain their client secret.
The authorization server is responsible for identifying the end user and issuing the required tokens for resource access. The authorization server and resource server often belong to the same business entity.
Often, the authorization server will also perform client authentication. This is necessary when an authorization server is shared by many applications (e.g. internal vs third party) that also vary in degrees of trust granted. For example, Google distinguishes between verified and unverified third-party applications, and allow verified applications to access more user data. The verified applications must therefore authenticate with the authorization server, independent of the user whose privileges it wishes to assume.
The difference between public and confidential clients is significant when client authentication is required. As a rule, high-privileged access should only be granted to confidential clients due to the risk of client impersonation.
The following two endpoints must be offered by an authorization server:
myapp.com/auth, or myapp.com/signin.myapp.com/token.The exact purpose of these endpoints requires more context around the information exchanged between the client and authorization server, this will be discussed further in Flows.
An OIDC provider must also host a configuration endpoint at /.well-known/openid-configuration. For example, Google’s OpenID configuration can be found at https://accounts.google.com/.well-known/openid-configuration. This can help during the information gathering phase, as custom extensions must be listed.
The resource server hosts the user resources required by the client. This could be an API that returns private data belonging to the end user. The resource server should be able to verify access tokens issued by the authorization server, usually through a shared secret.
The user-agent is the way the end user communicates with the other services. This is usually assumed to be a web browser, as the OAuth protocol was designed to use existing browser features.
Abstractly, the user-agent needs the ability to:
Although the role of the user-agent is not defined in the OAuth specification, it is important when considering concrete implementations of the protocol. This is particularly true for native applications, where the user-agent can also be the device. The OAuth RFC is lacking in the mobile-specific recommendations. These can instead be found in the best current practice, which advises, among other things, that authorization requests from native applications should only be made through external user-agents.
As previously discussed, the access token is a type of credential that is issued by the authorization server to the client, which then uses it to authorize with the resource server. The access token should explicitly declare its own scope and duration of access. Almost always, the access token will be a JSON web token such as below (example from Auth0).
{
"iss": "https://my-domain.auth0.com/",
"sub": "auth0|123456",
"aud": [
"https://example.com/health-api",
"https://my-domain.auth0.com/userinfo"
],
"azp": "my_client_id",
"exp": 1311281970,
"iat": 1311280970,
"scope": "openid profile read:patients read:admin"
}
The JWT must contain the following claims:
iss (issuer) claim identifies the authorization server issuing the token.sub (subject) claim uniquely identifies the end user as an entity within the authorization server (Note: not the client).aud (audience) claim identifies the intended audience for the token, and must include the authorization server.exp (expiration time) claim declares the time at which the token expires, used to limit the token’s duration of access.An authorization server often issues a refresh_token alongside the access token. This is usually a JWT, but custom formats are not uncommon. The main purpose of the refresh token is for the user to request more access tokens from the authorization server without needing to re-authenticate, allowing for long-term access.
Refresh tokens should only be stored within the client, and only be sent to the authorization server, never to the resource server. Some security considerations include:
If implemented correctly, using refresh tokens is preferable to using a long-term access token, which is more likely to be logged in transit for example.
There are four authorization flows described in the OAuth 2.0 RFC, two of which are not required for this article. The two of concern here are the authorization code flow and the implicit flow, where the authorization code flow is the most commonly used.
The implicit flow is rarely seen in web-facing applications, as it is considered very difficult to implement securely, if not fundamentally insecure. However, it is not only supported by almost all authorization servers, but often active by default, even if it is never used by any clients.
OIDC also defines a hybrid flow, which is a combination of the two. Although this is rarely used by clients, it is sometimes supported on the IdP side by default.
Both the authorization code and implicit flows begin the same way:
The third token exchange step is used only in the authorization code flow and uses the /token endpoint on the authorization server. If the client is a confidential client, this step can be performed server-side, i.e. independent of the user-agent. This is the only scenario where the flow is not entirely visible to the user-agent.

The key differences between the OAuth flows are summarised in this table:
| Flow | Code (Public) | Code (Confidential) | Implicit |
|---|---|---|---|
| Authorization grant contains token | No | No | Yes |
| Authorization grant contains code | Yes | Yes | No |
| /token endpoint used | Yes | Yes | No |
| Token visible to user-agent | Yes | Optional | Yes |
| Client can be authenticated | No | Yes | No |
The authorization request is initiated from the client and lands on the authorization server as shown in the following flow:

The following (compulsory) parameters are then used to provide the authorization server with the information needed to initiate a login session:
codetokenid_tokenNote: response_type=code requests a code to be returned for the authorization code flow, whereas response_type=token and response_type=id_token request a token to be returned in the authorization grant, characteristic of the implicit flow. A combination of the two, such as response_type=code id_token is sometimes also allowed, and corresponds to the hybrid flow.
redirect_uri. It is also required for client authentication for confidential clients.The redirect_uri specifies the callback location on the client application. Valid redirect URIs must be pre-registered in the authorization server, and the supplied value must exactly match a pre-configured option. For example, my application could register the following callback URIs:
https://myapp.com/callbackhttps://myapp.com/callback/oidcSome OAuth and OIDC services permit matching on the hostname only, or allow the use of wildcards and regular expressions, for example:
https://myapp.comhttps://myapp.com/callback/*This is not advisable, and is prohibited by the OIDC specification (section 3.2.2.1). This is because the entire OAuth flow would then act as an open redirector, and an attacker could manipulate a flow so that the secrets land on a page under the attacker’s control, such as one with XSS or an open redirect.scope=["purchases"]. In an OIDC flow, this parameter must contain the value openid.If the scope parameter is to be used by the authorization server, the authorization server must also ensure that the user has permissions to access the scopes requested.The following parameters provide protections against attacks on the OAuth flow (see section OAuth security considerations for more details):
state, but returned as a signed claim within the access token.The authorization grant issues the required credentials from the authorization server as summarised here:

The placement of these credentials is important between the authorization code and implicit flow:
| Flow | Credential placement | Correct |
|---|---|---|
| Code | GET client/callback?code=xxx... |
Yes |
| Code | GET client/callback#code=xxx... |
No |
| Implicit | GET client/callback?access_token=eyJ... |
No |
| Implicit | GET client/callback#access_token=eyJ... |
Yes |
| Hybrid | GET client/callback?code=xxx...&token=eyJ... |
No |
| Hybrid | GET client/callback?code=xxx...#token=eyJ... |
Yes |
| Hybrid | GET client/callback#code=xxx...&token=eyJ... |
No |
Although placing the access token in the URI fragment prevents it from being logged by intermediary services, it is visible to JavaScript running within the browser e.g. browser plugins. This is one of the reasons the implicit flow is considered fundamentally insecure.
The token exchange step is the last step of the authorization code flow, used to exchange the code for an access token:
client_secret, and therefore must be a server-side request.code_verifier must also be included in the token exchange step.
The code returned in the authorization code flow should expire after a single use, i.e. the token exchange request should not be replayable.
The OAuth RFC contains a large section on security considerations, and for good reason. Since OAuth/OIDC is often the “front door” of an application, any flaws in the protocol would have serious impacts on user account security. Many of these issues are minor weaknesses by themselves, but can greatly increase the impact of other issues, such as cross-site scripting (XSS).
Client impersonation can occur when a confidential client exposes its client_secret, allowing a malicious application to authenticate with the authorization server as the high-privileged client. The application could then present a legitimate OAuth login flow to its users, using the compromised client_secret to authenticate with the backend and allowing it to request access to more sensitive user data.
The redirect_uri supplied in the authorization request should ideally be a static, pre-configured value. However, this is not enforced by the specification, and it is not uncommon to see host-based URL matching. In 2013, Egor Homakov called this the Achilles heel of OAuth, after divulging a series of implementation flaws in Facebook’s OAuth service.
For example, if partial redirect_uri matching is performed, an attacker could use the redirect_uri to redirect the authorization grant (containing user secrets) to an arbitrary location on the client application. If stored XSS is found anywhere within the client (e.g. in myapp.com/page-with-xss), the secrets returned within the authorization grant could then be compromised via a 1-click phishing link, such as:
Malicious link:
https://auth.myapp.com?response_type=token&client_id=xxx&redirect_uri=https://myapp.com/page-with-xss
Authorization grant:
GET /page-with-xss#access_token=eyJ...
This also provides a method to circumvent browser protections that would otherwise prevent cross-site scripts from accessing user secrets, as the URL will be accessible to JavaScript running within the page.
Cross-site request forgery is a common weakness in which authenticated users can be manipulated into submitting unwanted actions within an application, for example, submitting GET request parameters via a malicious phishing link. OAuth requests are especially susceptible to CSRF attacks, as the entire flow is typically driven by URL redirects.
In 2012, Igor Homakov describes how a 1-click phishing link can be created to cause a targeted user to enable an attacker’s account as an option for single-sign on (SSO), allowing the attacker to log in to the target’s account.


Applications that used the implicit flow were particularly susceptible to CSRF attacks, as users could be phished into using an application as an attacker-controlled account. Any sensitive data submitted to the application would then be immediately accessible by the attacker.
The state parameter was designed to prevent CSRF attacks by allowing an authorization grant to be associated with a user-agent. The state value must be a random value that is generated dynamically when a user initiates the authorization flow. This value should be stored on the user-agent before the user is redirected to the authorization server. When the user returns with the authorization grant, the state value contained within the grant should be compared against the value stored within the user-agent. If there is a mismatch, an error should be returned by the client.

The state value only protects against CSRF if it is generated uniquely for each user-agent session. Applications that use a fixed or insufficiently random state value are all vulnerable to CSRF attacks.
Recall that authorization codes are returned to the client application within a URL query parameter, so that confidential clients can use the code in a server-side request. However, this causes public clients to be susceptible to authorization code interception, where an attacker eavesdropping on the OAuth dance may be able to steal the access code, which can then be exchanged for an access token without client authentication.
PKCE (pronounced “pixie”) is used to protect against authorization code interception by allowing a token exchange request to be associated with an authorization request. PKCE uses two values:
code_verifier which should be a randomly generated string.code_challenge which should be the SHA256 hash of the code_verifier.The code_challenge should be provided to the authorization server within the authorization request, allowing the authorization server to associate its value with the code issued. The code_verifier should then be included within the token exchange request alongside the access code, and the authorization server should verify its hashed value against the code’s associated code_challenge.
An attacker eavesdropping on the OAuth dance might be able to see the code_challenge and code, but never the code_verfier.
OAuth is everywhere and provides a great convenience. However, this convenience requires a level of complexity within its protocols and implementations. From a security point of view, this complexity is also a weakness, as small and often unassuming misconfigurations can result in serious security consequences. We hope that this article has helped you gain a deeper understanding of the OAuth protocol and its most common configurations that we see in deployment. The security issues and controls described here should be kept in mind when protecting and testing your applications.
Talk to us about penetration testing, security training, or a tailored advisory engagement.
Make an enquiry