A brief OAuth security recap

When it comes to OAuth, it is crucial to prioritize security and fraud protection. One can never be too careful in safeguarding sensitive information. How well-versed are you in the protective measures employed by OAuth? Does your system adhere to the open standard of OAuth? Are you mindful of the potential risks that may arise during the implementation of the user authentication flow? Let's briefly recap what we have learned about OAuth.
Simeng
SimengDeveloper
June 15, 202315 min read
A brief OAuth security recap

Introduction

Back a few days, an interesting OAuth vulnerability article just hit us. A-new-oauth-vulnerability-that-may-impact-hundreds-of-online-services by the SALT lab. This specific post highlights vulnerability discovered in Expo, a widely used framework for implementing OAuth and other functionalities. It specifically addresses a vulnerability in the expo-auth-session library, which has been assigned and properly resolved already.

If you have an interest in OAuth or are working on a CIAM-related product like us, we highly recommend giving this article a read. It's quite inspiring and provides helpful insights. These white-hat reports serve as a reminder of how even the simplest feature can cause vulnerabilities. When it comes to cyber security and authorization, we can never be too careful in ensuring the security and privacy of user information. If this article captures your attention, I believe you will strongly agree with us.

It reminds me of the time when we first started. We spent a lot of time learning and researching the details of the OAuth and OIDC protocols. It was painful and tedious, but the benefits were immense. While maybe not every individual on our team is an OAuth expert, everyone is committed to putting in continuous effort toward security and meticulousness. With those dedicated efforts, the Logto product has evolved into what it is today.

Thanks to this great opportunity, we would like to refresh our memory on some of the security details of OAuth here.

A peek of OAuth Authorization Code Flow

OAuth 2.0 provides various authorization flows catering to different client types and requirements. These include the Implicit Flow, Client Credentials Flow, Resource Owner Password Credentials Flow, and Device Authorization Flow. However, the Authorization Code Flow stands out as the most secure and widely used. Unlike other flows, it separates user authentication from the client application and involves exchanging an Authorization Code for tokens. This approach provides an additional layer of security, as the sensitive tokens are never exposed to the client. Additionally, the Authorization Code Flow supports server-side token management, making it suitable for web applications that require robust security and enhanced control over user access.

Here is a simplest Authorization Code Flow diagram:

ResourceProviderAuthServerClientResourceProviderAuthServerClientResourceOwnerVisit Client App1. Redirect User to AuthServer for authentication2. User login3. Authenticate and grant auth code4. Redirect User to the client callback URI with auth code5. Exchange for access_token with given auth code and client credentials6. return access_token & refresh_token7. Access resource with given access_tokenResourceOwner

Let's take a look at the two most crucial requests made in the Authorization Code Grant flow and the seemingly trivial fragments within them but play a critical role in protecting against fraud.

The authorization endpoint:

GET /authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&scope=openid%20profile&state=YOUR_STATE_VALUE&code_challenge=YOUR_CODE_CHALLENGE HTTP/1.1

The token exchange endpoint:

POST /token HTTP/1.1
Host: authorization-server.com
Authorization: Basic YOUR_CLIENT_ID:CLIENT_SECRET // encoded
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=AUTHORIZATION_CODE&redirect_uri=YOUR_REDIRECT_URI&code_verifier=YOUR_CODE_VERIFIER

Client Credentials

In OAuth, Client Credentials refer to the credentials used by a client application to authenticate and identify itself to the authorization server. These credentials are obtained during the client registration process and are used to validate the client's identity when making requests to the authorization server. (You may find your client credential in Logto’s Admin console when you have your application first registered. )

Client credentials are typically composed of two components:

  1. Client ID: A unique identifier assigned to the client application by the authorization server. It is a public value that is generally not considered sensitive.
  2. Client Secret: A confidential and securely stored value known only to the client and the authorization server. It serves as a form of authentication for the client application and is used to verify the client's identity when making requests to the authorization server.

As you may find, The Client ID and Client Secret combination is used during the token request to authenticate the client and obtain Access Tokens.

The client credentials play a vital role in ensuring the security of the OAuth flow. They help the authorization server verify the authenticity of client applications and control access to protected resources. It's important to handle client credentials securely and protect them from unauthorized access. Logto categorize the client applications by two different security levels:

  • Confidential clients: These include server-rendered web applications and machine-to-machine (M2M) applications. In the case of confidential clients, all authorization-related credentials, including client credentials, are securely stored on the server side. Additionally, all intermediate exchange requests are encrypted to ensure the confidentiality of data. The risk of client credentials leakage for confidential clients is very low, making them inherently more secure. Therefore, confidential clients are treated with a higher security level by default. In the token exchange flow, presenting Client Secret is a MUST.
  • Public clients: These include single-page web applications (SPA) and native applications. For public clients, the client credentials are typically hard coded on the client side, such as within a JavaScript package or an app package in a native platform. The risk of credential leakage is higher compared to confidential clients due to the inherent exposure of client credentials in the client-side code. In the token exchange flow, presenting Client Secret is OPTIONAL. Logto won’t trust those credentials from a public client by default.

State

In the OAuth flow, the state parameter is a randomly generated value that is included in the authorization request sent by the client to the authorization server. Its purpose is to maintain the state or context of the client's request throughout the authorization process.

The state parameter acts as a security measure to prevent cross-site request forgery (CSRF) attacks. When the authorization server redirects the user back to the client application after authentication and authorization, it includes the same state value in the response. The client application MUST compare this value with the original state value it sent in the authorization request.

By verifying the state parameter, the client can ensure that the response received from the authorization server corresponds to the initial request it made. This helps prevent attacks where an attacker tries to trick the client into accepting a response intended for another user or application.

Take the following CSRF attack example in a fictional use case:

CSRF attack: Fraud to bind social account - problem

Protected ResourceWhatsApp AuthServerACME clientProtected ResourceWhatsApp AuthServerACME clientAttacker DO NOT exchange for access_token with given authorization_code DELIBERATELY.User hajacked. DO NOT exchange for access_token with given authorization_code IN TIME.Attacker induces User to click the link to https://acme.com/client/callback?code=codeAUser's ACME Account is bound to Attacker's WhatsApp account.Attacker can social-login User's ACME account via Attacker's WhatsApp account NOW!AttackerUserRequest to bind Attacker's WhatsApp account1. Redirect User to AuthServerLoad AuthServer for authentication with redirect_uri = https://acme.com/client/callback2. authorization_code = codeA for Attacker's WhatsApp accountRedirect to ACME client redirect_uriwith authorization_code = codeALogin via username & passwordAuthenticateRequest to bind User's WhatsApp account1. Redirect User to AuthServerLoad AuthServer for authentication with redirect_uri = https://acme.com/client/callback2. authorization_code = codeBRedirect to ACME client redirect_uri with authorization_code = codeBRedirect to ACME client redirect_uri with authorization_code = codeA3. Exchange for access_token with given authorization_code = codeA4. Return access_token for User's ACME account bound to Attacker's WhatsApp accountLogin via Attacker's WhatsApp account1. Redirect User to AuthServerLoad AuthServer for authentication with redirect_uri = https://acme.com/client/callback2. authorization_code = codeC for Attacker's WhatsApp accountRedirect to ACME client redirect_uri with authorization_code = codeC3. Exchange for access_token with given authorization_code = codeC4. Return access_token for User's ACME account bound with Attacker's WhatsApp accountDo something bad: e.g. access User's private dataLoad ProtectedResource for User's private data with access_tokenUser's private dataAttackerUser

With a proper state validation mechanism, the client can detect the attack and prevent the user from being redirected to the attacker's website:

CSRF attack: Fraud to bind social account - solution

WhatsApp AuthServerACME clientWhatsApp AuthServerACME clientAttacker DO NOT exchange for access_token with given authorization_code DELIBERATELY.User hajacked. DO NOT exchange for access_token with given authorization code IN TIME.Attacker induces User to click the link to https://acme.com/client/callback?code=codeA&state=atateAAbort as the state DOES NOT match.State protects the User's account from hajacked by the AttackerAttackerUserRequest to bind Attacker's WhatsApp account1. Redirect User to AuthServerLoad AuthServer for authentication with redirect_uri = https://acme.com/client/callback2. authorization_code = codeA for Attacker's WhatsApp accountRedirect to ACME client redirect_uriwith authorization_code = codeALogin via username & passwordAuthenticateRequest to bind User's WhatsApp accountGenerate random state = stateB and save in storage1. Redirect User to AuthServer with state = stateBLoad AuthServer for authentication with redirect_uri = https://acme.com/client/callback and ?state=stateB2. authorization_code = codeB and state = stateBRedirect to ACME client redirect_uri with authorization_code = codeB and state = stateBRedirect to ACME client redirect_uri with authorization_code = codeA and state = stateALoad original state = stateB from storage and compare with current state = stateAExchange for access_tokenAttackerUser

PKCE

As mentioned earlier, public clients such as SPA web apps and native applications carry a higher risk of auth credential leakage, including the Authorization Code issued by the authorization server.

PKCE stands for Proof Key for Code Exchange. It is an extension to the OAuth 2.0 Authorization Code Flow that enhances the security of public clients.

PKCE was introduced to mitigate the risk of an attacker intercepting the Authorization Code and exchanging it for an Access Token without the client's knowledge. This type of attack, known as an Authorization Code interception attack, is more prevalent in environments where the client application cannot securely store a client secret.

To implement PKCE, the client application generates a random Code Verifier and derives a Code Challenge from it using a specific hashing algorithm (usually SHA-256). The Code Challenge is included in the initial authorization request sent to the authorization server.

When the authorization server issues the Authorization Code, the client application includes the original Code Verifier in the token request. The server verifies that the Code Verifier matches the stored Code Challenge and only then issues the Access Token.

AuthServerClientAppAuthServerClientAppResourceOwner1. Access ClientCreate random code_verifier(v) Create code_challenge(c) = sha256(v)Redirect with generated code_challenge(c)2. Request for authentication with code_challenge(c)store code_challenge(c)Redirect to loginLogin with credentials3. Authenticate and return authorization_code(t)4. Request for access_token with client_id, code_verifier(v), and authorization_code(t)Verify client_id, sha256(code_verifier(v)) ==== code_challenge(c) and code(t)5. return access_token, refresh_tokenResourceOwnerPKCE diagram

By using PKCE, the client application ensures that the Authorization Code alone is not sufficient to obtain an Access Token. This mechanism adds an extra layer of security to the authorization flow, especially for public clients where the storage of Client Secrets is challenging.

Logto uses PKCE as the ONLY authorization flow for all the public client typed applications. However, PKCE and be omitted for those confidential clients.

Redirect URI

A Redirect URI(Uniform Resource Identifier) is a specific endpoint or URL that the authorization server redirects the user back to after the authentication and authorization process in OAuth.

During the OAuth flow, the client application includes a Redirect URI as part of the initial authorization request. This URI serves as the callback URL where the user will be redirected to after successfully authenticating and granting permissions to the client.

Once the user completes the authentication process, the authorization server generates a response that includes an Authorization Code, and redirects the user back to the specified Redirect URI.

The validation of Redirect URI is an essential step in ensuring the security and integrity of the OAuth flow. It involves verifying that the Redirect URI used in the authorization request and subsequent redirections is valid and trusted.

Let’s head back and take a look at the original OAuth vulnerability report. (The following section is referenced from the original post)

When the user clicks “login with facebook” using the Mobile APP in Expo Go, it redirects to the user to the following link:

https://auth.expo.io/@moreisless3/me321/start?authUrl=https://www.facebook.com/v6.0/dialog/oauth?code_challenge=...&display=popup&auth_nonce=...&code_challenge_method=S256&redirect_uri=https://auth.expo.io/@moreisless3/me321&client_id=3287341734837076&response_type=code,token&state=gBpzi0quEg&scope=public_profile,email&returnUrl=exp://192.168.14.41:19000/--/expo-auth-session

In the response, auth.expo.io set the following cookie: ru=exp://192.168.14.41:19000/--/expo-auth-session. The value RU will be later used as a Return Url in step 5. It then shows the user a confirmation message, and if the user approves - it redirects him to the Facebook login to continue the authentication flow

This page reads the query parameter “returnUrl” and sets the cookie accordingly.

Let’s change the returnUrl to hTTps://attacker.com (https is not allowed, so I tried to insert capitals letters and it worked), which sets the RU (Return Url)in the cookie to https://attacker.com.

In the above case, discard of the original redirect_uri params, Expo introduced a new param called returnUrl without proper validation. This oversight provided an opportunity for attackers to gain access to the Authorization Code returned by Facebook. For more details, please refer to the original post.

Redirect URI validation serves several important purposes:

  1. Preventing phishing attacks: By validating the Redirect URI, the authorization server ensures that the user is redirected back to a trusted and authorized endpoint. This helps prevent attackers from redirecting users to malicious or unauthorized locations.
  2. Protecting against open redirects: Open redirects are vulnerabilities that can be exploited to redirect users to malicious websites. By validating the Redirect URI, the authorization server can ensure that the redirect stays within the boundaries of the authorized domain or set of trusted domains.
  3. Ensuring correct routing of authorization responses: Validating the Redirect URI helps guarantee that the authorization server redirects the user back to the intended client application. It ensures that the response, such as an Authorization Code or Access Token, is delivered to the correct destination.

In Logto, redirect_uri registration is mandatory for all types of applications. We compare and match the value received against the ones registered in the Logto server. That includes any custom search parameters. If an authorization request fails the validation due to a missing, invalid, or mismatching redirect_uri value, an invalid Redirect URI error will be returned to the registered redirect_urion file.

Summary

Due to their intricate and nuanced nature, it is understandable that these details are often overlooked. Some are just a random string-like state.

However, it is important to note these security measures add layers of protection to user authorization, mitigating risks such as CSRF attacks, Authorization Code interception, and unauthorized redirects.

These are just a small piece of the comprehensive security features offered by the OAuth protocol. OAuth provides a robust framework for secure authentication and authorization. It also offers flexible and open endpoints to meet various requirements in real-world product applications.

As developers and service providers, it is imperative to continuously prioritize the security of the user authorization flow. Staying vigilant, adhering to best practices, and keeping up with the latest developments in the OAuth ecosystem are essential to ensuring the integrity and protection of user identities and sensitive data. We’ll remain committed to upholding the highest standards of security in the implementation of OAuth and safeguarding the privacy and trust of our users.