SBN

OAuth security guide: Flows, vulnerabilities and best practices

OAuth security guide: Flows, vulnerabilities and best practices

Phil Condon

Security Consultant

OAuth is a commonly used authorisation framework, that allows websites and web applications to request limited access to a user’s account on another application. Users can grant this limited access to their account, without ever needing to expose their password with the requesting website or application.  This is commonly seen with sites that allow you to log in with popular accounts such as a social media login, Microsoft or Google account. Whilst useful for users, its implementation is not without its complexities, and as such it is often misconfigured leaving opportunities for would be attackers to gain unauthorised access to sensitive data or at times bypass authentication entirely.

Overview and background of the OAuth protocol

OAuth was originally intended as a way of sharing access to specific data between applications. At a high level this is means that OAuth was designed strictly for authorisation i.e. granting access to resources and not authentication or verifying of identity. It’s often however confused with authentication protocols like OpenID Connect which builds on OAuth to add identity verification. With this in mind, let’s take a quick look at the history of OAuth.

In the mid-2000s, developers sought a way to allow third party apps to access user data from services such as Google and Twitter without requiring users to share their credentials with a multitude of 3rd party sites. Released in December 2007, OAuth 1.0 was officially created to solve this issue. This version was groundbreaking for its time but not without its issues. It was designed primarily for server-side applications at a time when mobile app usage was rising. Whilst this version included token exchanges, they were often difficult to implement and lacked a standardised structure. The protocol itself was rigid in design and lacked the ability to adapt to emerging use cases like device authorisation or federated identity.

These limitations led to the development of OAuth 2.0 which was finalised in 2012, by the IETF OAuth Working Group (RFC 6749). This version made some key improvements.

Token handling was simplified with the introduction of bearer tokens, which meant each request no longer needed to be cryptographically signed. It introduced multiple grant types that supported various authorisation flows (e.g. authorization code, implicit, client credentials and resource owner password credentials). The protocol was designed to be modular allowing for extensions like device authorisation and token introspection. However, OAuth 2.0 still delegated much of the responsibility for securing validating clients to developers, which led to inconsistent implementations and potential vulnerabilities.

Then the unthinkable happened. In order to address the need for identity verification, something OAuth was never intended to handle, OpenID Connect (OIDC) was introduced in 2014. This identity layer built on top of OAuth 2.0 allowed clients to verify the identity of the user and obtain basic profile information. It wasn’t long before this became to go-to standard for single sign-on (SSO) and federated identity across the web, and now we can see why so much confusion arises between the actual use of OAuth when compared to what it was designed to accomplish.

OAuth grant types explained

Before looking at OAuth v2.0 there are some key terms to understand.

  • Resource Owner – The user who owns the data and grants access to it
  • Client – The application requesting access to the user’s data (e.g. a mobile app or website)
  • Authorization Server – The server that authenticates the user and issues tokens (e.g. Google, Facebook)
  • Resource Server – The server that holds the user data and accepts access tokens (e.g. an API)

These terms are important to grasp as we explore OAuth authorization flows (also called grant types). These flows are a way for an app to obtain an access token that lets it interact with protected resources like user data securely and with defined permissions (also known as scope).

OAuth 2.0 defines a number of flows and each flow is designed to suit different types of applications and use cases. Some flows are designed for user involvement and may support features such as Multi Factor Authentication, whilst others are designed for servers interacting with each other without any user interaction. As a result, it is important to ensure that the correct flow type is used. Importantly some of the common flow types are now deprecated as they do not incorporate security best practices and their use should be discouraged.  The key types of flow types are

  • Authorization Code Flow (with PKCE)
  • Client Credentials Flow
  • Device Authorization Flow
  • Resource Owner Password Credentials Flow (Deprecated)
  • Implicit Flow (Deprecated)

Let’s take a look at how these flows work and their applicable use cases.

The authorization code flow is the most robust and secure OAuth 2.0 grant type, designed for applications that can securely store a client secret – typically server-side web apps or mobile apps using PKCE (Proof Key for Code Exchange). This PKCE (pronounced pixie) is designed to prevent interception. Typically, the flow involves a user being redirected to the authorization server to log in.

GET /authorize?
  response_type=code
  &client_id=CLIENT_ID
  &redirect_uri=REDIRECT_URI
  &scope=read write
  &state=xyz
  &code_challenge=abc123
  &code_challenge_method=S256

The user logs in and consents to the requested scopes. These could be read-only access to information such a user profile. The server then redirects back to the client with a short-lived code:

HTTP 302 Found
Location: https://client.com/callback?code=AUTH_CODE&state=xyz

The client then sends this code to the token endpoint:

POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=REDIRECT_URI
&client_id=CLIENT_ID
&code_verifier=abc123

PKCE is a security extension to the authorization code flow and mitigates interception attacks by requiring a code verifier and code challenge pair. It is particularly useful in mobile apps and SPAs as it replaces the need for a client secret which public clients can’t safely store.

Example of the OAuth authorisation code flow

A common use case that you’ve probably encountered is websites allowing you to login with Google.  Imagine using a new app (which will serve as our client). This app presents a login with Google option which when clicked contacts the Authorization Server (Google) and you are asked to authenticate. The user is presented with a consent screen asking if you would like to grant the app limited access to your account. This limited access is known as scope and commonly includes permissions like read-only or write access. Once you consent to this access, Google redirects you back to the app with an authorization code. The app then sends this authorization code to Google’s server and requests an Access Token as part of a Token Exchange process.  The app uses this Access Token to call Google’s Resource Server and retrieves your Profile information. The app can now personalise your experience and you have not needed to provide a password to the application.

This is example of an authorization code flow, which is one of the most common and secure OAuth2.0 flows for apps that securely store secrets like web servers as the token exchange occurs server side.

OAuth client credentials flow

Next, we will take a look at the client credentials flow.  This flow is one of the simplest flows and is typically used for machine-to-machine (M2M) communication where no user interaction is involved. A common use case for this is when the client (e.g. an application) needs to access resources or APIs on behalf of itself and not on behalf of a user. This is a scenario commonly seen in backend services such as scheduled tasks or within microservices.

The client (e.g. a backend service) has a Client ID and Client Secret which have been issued by the authorisation server. These credentials are used to identify the client application. The client then sends a HTTP POST request to the Authorisation Servers token endpoint supplying the client ID and client secret, and specifying grant type is client_credentials. It may also define the scope, which can be used to limit what the client can access.

POST /token
Grant_type=client_credentials&client_id=12345&client_secret=supersecuresecret

The authorisation server on receiving this request validates the credentials supplied. If these are found to be valid it returns an access token normally in the form a JWT. The client can then use this token in the Authorization header to call on the resource server (e.g. an API).

The next flow to look at is one you may have encountered on a smart TV, games console or any device that doesn’t have an easy way to input text. The device authorisation flow (or the device code flow) is specifically designed for such devices, and you may have encountered this on devices where you are asked to scan a QR code on a mobile device in order to authorise the device. In this flow the device e.g. a Smart TV sends a request to the authorization server for a device code and user code. This request includes its client_id and a defined scope.

Upon receipt of this request, the authorization server responds and returns the following:

  • device_code (used by the device to poll for tokens and is secret)
  • user_code (a short code for the user to enter)
  • verification_uri (URL where the user code is to be entered)
  • expires_in and interval (polling information)

The device then displays instructions to visit the verification_uri and enter the user_code, or may include these within a QR code. The user then visits this URL on a different device to approve the request. The device then repeatedly calls the token endpoint with the device_code to determine if the request has been approved.

If the user as not yet approved the request the server will return an authorization_pending response. However, if the user has approved the request the server will return an access token. Once the user has approved the request and the device has received the token it can access resources on behalf of the user.

OAuth resource owner password credentials flow (deprecated)

The Resource Owner Password Credentials (ROPC) flow is one of the simplest OAuth 2.0 grant types, but it’s considered legacy and not recommended due to security concerns. As this flow requires sending the username and password directly to the application in order to receive a token, in this manner its very similar to a traditional login request and places a great deal of trust in the application to manage and handle user credentials safely.

In the ROPC flow, the user enters their username and password directly into the client application (not on a page from the authorization server). The client application then makes a POST request to the OAuth token endpoint with the following parameters:

  • grant_type=password
  • username
  • password
  • client_id and client_secret (if applicable)
  • scope (optional)

The authorization server on receipt of this request checks if the credentials are correct and if the client is allowed to use ROPC. If the credentials are valid the server returns an access token and may optionally return a refresh token, which we will discuss later.

OAuth implicit grant flow (deprecated)

The implicit grant flow is one of the original OAuth 2.0 flows, designed for a time before modern browser security practices. It was designed to be used by Single-Page Applications (SPAs) running entirely in the browser where storing a client secret wasn’t possible. It is now considered deprecated and its use discouraged, having been replaced and superseded by Authorization Code + PKCE. Despite this, it is still used so understanding it is useful.

The concept in theory is simple; SPAs couldn’t safely store a client secret and redirecting through a backend server wasn’t always possible. In order to get an access token that was directly accessible in the browser, it was decided that the authorization server would skip the code exchange step and just return an access token immediately in an URL fragment that could be easily accessed in the browser.

To achieve this, the SPA redirects the user’s browser to the authorization servers’ “/authorize” endpoint with a GET request that looks like the following.

GET https://auth.example.com/oauth2/authorize?
  response_type=token
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/callback
  &scope=openid%20profile%20email
  &state=xyz123

Some key takeaways here are that the response_type is set to “token”, which denotes that this is the implicit flow, and that all this information is being submitted in a GET request as URL parameters.

Upon receiving this request, the authorization server loads the login page, and the user authenticates and approves the requested scopes. Once approved the authorization server redirects the browser back to the redirect_uri contained in the request. However, instead of then sending an authorization code, it sends the access token directly within the URL fragment.

HTTP/1.1 302 Found
Location: https://yourapp.com/callback#
  access_token=eyJhbGciOi...
  &token_type=Bearer
  &expires_in=3600
  &state=xyz123 

Some key points here are that the token is within a URL fragment (#) and not a query string (?). This fragment is not sent to the server and is designed that only the browser should see it, where it could easily be accessed in JavaScript typically using “window.location.hash”.

Unfortunately, this is where the security issues begin. As its loaded within a URL, the token appears in browser history. It may also leak via referrer headers and appear on 3rd party sites if referrer headers are not correctly configured (see Application Security 101 – HTTP Headers). On top of this, as it was designed to be easily accessible to JavaScript, it can be accessed just as easily by malicious JavaScript such as those seen in Cross Site Scripting (XSS) attacks. Another big weakness of this flow is that there is no way of extending the life of the access token e.g. there is no refresh token, which means that once the access token expires the app must repeat the entire flow.  It is for these reasons that the Implicit Flow is now deprecated, and its use should be replaced with the Authorization Code + PKCE flow.

OAuth refresh token flow

The OAuth flow types we’ve seen typically return an access token used to gain access to the resource server. This token has an expiry, after which time it is no longer valid and should not be accepted. This expiry time is commonly configured to be short lived and only valid for a few minutes to a few hours. Some flows, most commonly the authorization code flow, authorization code flow + PKCE and the device code flow return access tokens alongside a refresh token. The refresh token is designed to have a longer life than the access token. Importantly the refresh token should not grant access to resources but instead must be exchanged for an access token. This is where the refresh token flow comes in. The refresh token flow is one of the most useful parts of OAuth 2.0, because it lets an application exchange a refresh token for a new access token and therefore lets an app keep a user signed in without asking them to log in again. If done correctly, this is seamless and a silent re-authentication method that ensures that the user is not logged out and retains access.

Typically, the user has obtained an access token and refresh token using one of the methods previously described e.g. the authorization code flow.

When the access token eventually expires and the app tries to call an API, it will receive a response like below indicating the session token has expired.

HTTP/1.1 401 Unauthorized

Upon receiving this response, the app sends the refresh token to the authorization server in a POST request.

grant_type=refresh_token
refresh_token=YOUR_REFRESH_TOKEN
client_id=YOUR_CLIENT_ID
client_secret=YOUR_SECRET 

The authorization server then validates the refresh token, to ensure it is still valid, it hasn’t been revoked and matches the client_id provided.  If its valid, it will then return a new short lived access token that can now be used to access resources. Commonly, it will also return a new refresh token that can be used in the future.

As mentioned, done correctly this process is seamless and completed in the background with the user unaware. Whilst refresh tokens don’t grant access to resources themselves, they can easily be exchanged for access tokens, which makes them very powerful and a likely target for malicious actors. As such they should be closely managed and controlled, and there are some best practice recommendations for how they should be protected.

In short, they should not be stored within browsers local storage or exposed to JavaScript in Single Page Applications (SPAs). Instead, it is preferable to keep them within HTTP cookies that have appropriate security attributes (e.g. HttpOnly) configured. Mobile and Desktop applications should use the secure storage available for their device e.g. MacOS and IOS Keychain, Android Keystore or Windows Credential Guard.

OAuth JWT bearer token flow

The JSON Web Token (JWT) Bearer flow is an elegant OAuth 2.0 extension that allows a client to prove its identity by presenting a signed JWT instead of a password, client secret or user credentials. It is widely used for secure server to server communications. In short this a grant type where the client sends a JWT assertion to the authorization service. If the server trusts the signature and the claims, it issues an access token.

As a quick recap, a JWT is compact digitally signed token used to represent claims between two parties. JWTs consist of 3 parts a header, payload and signature, and each part is Base64URL encoded and separated by a full stop character (.). The header gives some information on the type of token and the algorithm used, and the payload contains actual data or claims.

The signature is the part of the token that makes it tamperproof, and is created by signing the header and payload with a secret (HMAC) or a private key (RSA/EC). If the header or payload are altered, the signature will be invalid, meaning servers can validate the integrity of the token.

The JWT bearer token flow leverages the strengths of JWTs, where the server trusts the signature and the claims being made. Whilst the traditional client authentication relies on either sending the client_id and the client_secret, or user credentials (in the deprecated flows), these secrets can leak. JWTs are considered more secure as they can be validated cryptographically, can be short lived and are easily rotated. This means that this flow is ideal for enterprise integrations and microservices.

In the simplest example of this flow, the client creates a JWT containing important claims within the payload such as the issuer (iss), the subscriber (sub), the audience (aud), expiration time (exp) and issued at time (iat). The client signs the JWT using a private key or a shared secret. This token is then sent in a POST request to the token endpoint, which specifies that the grant type is jwt-bearer. The actual JWT is sent within the assertion.

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=eyJhbGc...

As seen, above the request contains no password and no client secrets, just a JWT that makes an assertion that you are an authenticated user.  Upon receipt, if the server validates the JWT successfully and confirms it hasn’t been tampered with it returns an access token (and optionally a refresh_token).

OAuth security weaknesses and common attacks

As we’ve seen, OAuth is extremely powerful, incredibly flexible and widely deployed, but it’s also easy to get wrong, and the consequences of doing so can be dire. Whilst there are some security weaknesses within the deprecated flows, most vulnerabilities come not from the core standard but instead from implementation mistakes. Perhaps this is somewhat understandable as there are a number of flows to choose from with some best suited to specific applications, which adds to the complexity and confusion surrounding OAuth. So, we will try break down these vulnerabilities into some major categories.

Redirect URI manipulation

Malicious actors will commonly attempt to exploit weak redirect URI validation to steal tokens or authorization codes. Instead of redirecting users to a trusted URI, instead the attacker can alter this URL and sent the user to a malicious domain. Should an attacker be successful, this typically leads to unauthorised access or in some cases full account takeover.

This can be prevented by not allowing wildcard redirect URIs or arbitrary subdomains and ensuring that only HTTPS is allowed and not HTTP. Additionally, the redirect_uri parameter should be strictly validated to ensure exact string matching against a list of trusted URIs.

Missing state parameter

The state parameter is designed to prevent Cross Site Request Forgery (CSRF) and session fixation attacks. Attackers can leverage poor configurations or a lack of the state parameter to launch these attacks with the aim of tricking users into authorising the attacker’s account or stealing their session. To prevent these attacks, the state parameter should be configured and importantly it should be validated correctly. When configuring the values for the state parameter, it is important that this is done in an unpredictable manner that attacks cannot attempt to reverse engineer or brute force.

Should an attacker gain access to an authorisation code as used within the authorisation code flow, they can exchange this for an access token. This will effectively grant them access and result in a compromise of the assets the token provides authorisation for.

There are a number of ways an attacker could come to be in possession of an authorisation code. This could be the result of using the authorisation code flow without PKCE, or the use of insecure redirect URIs. It’s also possible that an attacker with physical access may be able to recover these from browser history or even logs. It is for this reason that when using the Authorisation Code flow that care is taken to implement it with PKCE.

Implicit flow token leakage

We’ve already seen that the implicit flow is inherently unsafe as the tokens are returned directly within URL fragments. This means that they will appear in the browser’s history and if HTTP headers are not correctly configured could be sent to 3rd parties via the referrer header. As this flow was also designed to make accessing the token via JavaScript easy, a traditional Cross Site Scripting vulnerability could also be leveraged to steal and exfiltrate the token. It is for these reason that this flow is deprecated and should be replaced with the Authorisation Code flow with PKCE.

Refresh token mismanagement

Access tokens are generally short lived, if correctly configured, which limits the time an attacker has access to sensitive information. Refresh tokens however are long lived and if captured could grant an attacker long term persistent access to resources. For this reason, they should be treated as extremely sensitive and managed accordingly.

Unfortunately, it is not uncommon to see these tokens stored and used incorrectly. Some developers may mistakenly store refresh tokens within the browser’s local storage or within cookies which lack appropriate protections, such as the HTTPOnly flag. As a result, they may be exposed and stolen by malicious JavaScript.

It’s also common that refresh tokens are not adequately rotated and invalidated whenever used. By issuing new refresh tokens and rotating them every time a token is used, the tokens can be made one time use and attempts to use the wrong token can be flagged and recorded, making this a very powerful protection against silent account takeover.

Another issue is not binding refresh tokens to clients. Refresh tokens are classed as bearer tokens, meaning whoever has a token can use it, so if stolen there is no restriction on who can use it. Token binding can solve this problem by binding the token to a specific client key so that only the legitimate user can use it. There are two key methods to accomplish this:

Demonstration of Proof of Possession (DPoP), which adds a signed proof in the form of a JWT with each request. This means that even if an attacker steals a refresh token, they would need the private key used to sign the JWT in order to conduct a successful attack.

Mutual TLS (mTLS), where the client authenticates with a client certificate during the TLS handshake. When the client presents their certificate, the server binds the token to that certificate, which means that only the device with the certificate can use the token. This results in extremely strong security and control around the use of the refresh token, which is why this approach is often favoured in banking and high security APIs.

Scope misconfiguration

Scope defines what a token can do and if misconfigured can easily lead to privilege escalation, resulting in unauthorised access to data. Common misconfigurations here are overly broad and permissive scopes or reusing the same scope for multiple applications, which allows for lateral movement across applications. Whilst this may seem obvious, it’s very common to see scopes which are not validated on the resource server meaning that attackers can alter the scope from the intended in order to broaden their permissions.

Weak token validation

Similarly to not validating the scope, it’s common to see tokens which are not validated correctly. In fact, this is not unique to OAuth implementations and is commonplace across applications that routinely use JWTs. It is important that the expiration time of tokens is honoured to ensure that tokens cannot be used after this time. The issuer and audience should also be validated to ensure that they are valid and allowed access. Finally, it is crucial that unsigned or weakly signed JWTs are not accepted and any JWT whose signature is not valid should be rejected, otherwise an attacker could place any details they like into a JWT as a claim, and it would be accepted.

There are a number of misconfigurations and vulnerabilities, beyond the scope of this post that apply to JWTs including the use of weak symmetric algorithms such as HS256, weak easily guessable signing keys, the use of “none” or a derivative spelling as the signing algorithm. Suffice to say it is important that JWTs are adequately protected.

Open redirects

An open redirect occurs when an attacker can control a parameter that the application will later redirect to the user to. OAuth leverages redirect_uris extensively, so it is important that these are adequately validated and restricted to known and trusted URLs. Should an attacker be able to redirect users to an arbitrary URL under their control, this can be used to steal tokens or as part of a phishing attack, ultimately leading to account takeover.

Misconfigured client secrets

Client secrets are incredibly important during OAuth flows and should be treated as sensitive information. Unfortunately, these are often misconfigured ranging from the use of weak and easily guessable secrets to storing these within frontend code. Another significant issue, and one which was found to be responsible for a data breach at Uber in 2016, is the disclosure of client secrets and client IDs in a public code repository. Secrets scanning tooling should be included in development pipelines in order to prevent such information from being accidentally disclosed.

Summarising our review of OAuth security

We have looked at OAuth, its confusing history, and how it has evolved into the authorisation framework widely used today. From there, we explored the different grant types and flows, including the authorisation code, device authorisation, and client credentials flows, as well as the now-deprecated Resource Owner Password Credentials (ROPC) and implicit flows. We also examined how refresh tokens can be exchanged for new access tokens, and how JWTs can be used within the JWT Bearer flow to obtain access tokens in more advanced scenarios.

Along the way, we highlighted common weaknesses seen in OAuth implementations. While many vulnerabilities stem from configuration and implementation errors, not all responsibility lies with engineers, some issues originate from the design of deprecated flows created for a very different application landscape.

What becomes clear is that OAuth is both extremely powerful and but also inherently complex. Much of that complexity comes from its flexibility and extensibility, allowing it to support a wide range of use cases and emerging technologies. The key takeaway is that understanding the characteristics of each flow, selecting the correct one for your specific use case, and implementing it securely are essential to using OAuth effectively.

Sentrium can help identify vulnerabilities and reduce your application’s exposure to vulnerabilities with our web application penetration testing services.

*** This is a Security Bloggers Network syndicated blog from Labs Archive - Sentrium Security authored by Phil Condon. Read the original post at: https://www.sentrium.co.uk/labs/oauth-security-guide-flows-vulnerabilities-and-best-practices