OAuth 2.0 Security Best Practices for Developers

In January 2025, the IETF (Internet Engineering Task Force) standards body published the Best Current Practice for OAuth 2.0 Security. It’s been over a decade since the OAuth 2.0 Authorization Framework was established in 2012. Since then, technology has advanced on all fronts: applications and access management have become more complex, security has evolved, and security attacks have become more sophisticated.

OAuth 2.0 is broadly used for delegated authorization, and OpenID Connect (OIDC: an authentication protocol extending OAuth 2.0) is relied upon for countless logins. Best practices for OAuth 2.0 and OIDC have evolved continually over the years, and RFC 9700: Best Current Practice for OAuth 2.0 Security is a culmination of that work thus far.

Introduction

In this article, I have summarized the Best Current Practice for OAuth 2.0 Security, condensing the contents into a (hopefully) more approachable format. 

Target audience

The target audience for this overview is developers on the client side of OAuth implementation. Some sections are not fully summarized. Best practices for authorization server providers are not covered in-depth because this article is to help developers who are adding OAuth 2.0 (and/or OIDC) capabilities to their applications. If you are an authorization server provider, please see the full OAuth 2.0 Best Practices specification.

Assumed knowledge

This overview assumes basic familiarity with OAuth 2.0 and, to a lesser extent, OpenID Connect. It assumes you understand the parties involved and their roles (e.g., client, authorization server, resource server, etc.), authorization flows, and related entities (e.g., access token, ID token, authorization code, etc.).

Throughout the overview, I have included supporting links so readers can quickly access additional resources. If there are places where more resources or explanation would be helpful, please let me know.

Terminology

I have done my best to simplify industry speak, to a logical extent. Of course, internet specifications are steeped in jargon, so this article assumes awareness of commonly used Identity and Access Management terms.

Some terms may be ambiguous to readers who are familiar with how OAuth 2.0 works but less familiar with the specification itself. Some of those terms are defined below. Please let me know if there are more definitions that would be helpful.

Confidential client: an application running in a centralized location capable of securely storing secrets (e.g., a traditional backend web app)

Public client: an application running on a user’s device, not capable of securely storing secrets (e.g., Single Page Apps, mobile apps)

Browser-based client: an application running in the user’s browser (e.g., Single Page Apps, static sites)

Resource owner: the user granting access to some of their resources (e.g., data, actions, etc.)

Resource server: an API that receives and verifies requests and access tokens to handle appropriate access to data (e.g., your own API, Google Maps, Firebase, etc.)

Why do we need new best practices for OAuth 2.0?

As mentioned above, technology has evolved since OAuth 2.0 was established, and its usage has expanded tremendously. What are some of the newer concerns that the original OAuth 2.0 Authorization Framework specification did not address?

Whenever security standards are put in place, attackers will exploit any weaknesses they can find. OAuth has been around for over ten years, which is more than enough time for attackers to discover and take advantage of implementation weaknesses and anti-patterns. OAuth is also being used in environments with high security requirements, like open banking, healthcare, government, and electronic signatures. OAuth 2.0 doesn’t address the necessary precautions needed for this level of security.

OAuth is also being used in more complex and dynamic setups. The standard was established when the relationships between applications, authorization servers, and APIs were more static. Apps knew server URLs at deployment time. However, now clients can access multiple service providers, and multitenant environments are common. As these complexities evolved, extensions of OAuth 2.0 were developed to support dynamic scenarios, such as OAuth 2.0 Dynamic Client Registration Protocol and OAuth 2.0 Authorization Server Metadata.

In addition, internet technology has changed. For example, browsers no longer handle redirection fragments the same way. These kinds of technology changes impact the security model that OAuth 2.0 relies on.

The best current practices give security recommendations, new requirements, and deprecations to address the current state of authorization and authentication with OAuth 2.0 and OIDC. Best practices will be incorporated into OAuth 2.1.

1. Protect redirect flows

In some OAuth 2.0 and OIDC flows, the user is redirected in the browser to complete steps for authorization, authentication, and consent. You (as the implementer) configure redirect URIs that the authorization server uses to transmit information between itself and your app (like an authorization code), and to redirect users once they’ve completed an action (like logging in).

For example, in the authorization code flow, the authorization server sends a code to the app in the browser in a query parameter. The app uses this code to request access tokens and/or ID tokens. After the user logs in successfully with the authorization server, they are redirected to a page in your application that you specify.

Redirects are targets of attack because they can be hijacked if not configured properly. An attacker could redirect the authorization code to their own domain (instead of yours) and then impersonate a public app and exchange the code with the authorization server for tokens.

Do not redirect to URIs obtained from a query parameter

Clients and authorization servers must not redirect to URIs obtained from a query parameter, as these can be tampered with. For example, consider the following URI:

https://your-site.com/login?redirect_url=https://yuor-site.com/account

The redirect will actually send the user to yuor-site.com instead of your-site.com. Because the URLs are similar, the user may not even notice the difference. This is called an open redirect: the user has control over the redirect since it’s clearly visible (and can be modified) in the URL.

Use exact string matching when configuring allowed redirect URIs

Authorization servers should require exact string matching when accepting user configured allowed redirect URIs, as opposed to pattern-matching. Unsafe patterns or naive implementations on the authorization server can result in unwanted outcomes. For example, consider the following pattern:

https://*.your-site.com/*

The intent here is to allow any subdomain of your-site and any subpage of your-site as a redirect, but if the authorization server interprets the wildcard * as “any character” instead of “any character valid for a domain name,” something like https://attacker.com/.your-site.com could be valid based on this pattern. Alternately, an attacker could establish a subdomain that would be allowed by this pattern even if it is implemented properly.

Authorization servers should ensure exact string matching is required, but if your authorization server does allow pattern-matching, don’t use it. Instead, only add exact URLs to the authorization server’s list of allowed redirect URIs. The only exception to this is for ports on localhost for native apps.

Protect against Cross Site Request Forgery (CSRF) attacks

CSRF attacks are malicious requests that come from somewhere other than the authorization server. OAuth 2.0 and OIDC provide methods to mitigate CSRF attacks. Best practices state that CSRF protection must be implemented.

OAuth 2.0 Proof Key for Code Exchange (PKCE) or OIDC nonce are both sufficient measures to protect against CSRF attacks.

Validate token issuer when using more than one authorization server

Clients using more than one authorization server should validate the issuer (the authorization server that issued the token) iss parameter in any tokens they receive. This ensures interaction with the correct authorization server.

Authorization Code Grant

Public clients must use Proof Key for Code Exchange

Public clients (like native apps or browser-based apps) using OAuth 2.0 must use Proof Key for Code Exchange with the authorization code grant. PKCE (pronounced “pixie”) adds an additional layer of security to the authorization code exchange between your app and the authorization server.

With PKCE, the app creates a random string (called the code_verifier) for each authorization request. It then hashes the code verifier using a hashing function called a code_challenge_method. Hashing is irreversible: once a string has been hashed, it’s impossible to retrieve the original input. The app sends the code_challenge and the code_challenge_method to the authorization server along with the authorization request.

Now the authorization server knows the code_challenge (the hashed code_verifier) and the code_challenge_method (the hashing method used to create the code_challenge). The authorization server then creates a code and associates the code_challenge and code_challenge_method with the code. It sends the code to the app in a query parameter.

The app takes the code and the code_verifier (the original, unmodified string) and sends them to the authorization server with an access token request. Although the authorization server can’t “un-hash” the code_challenge, it now has everything it needs to repeat the same hashing steps the app took before the app sent the authorization request.

The authorization server uses the code_challenge_method to hash the code_verifier and then compares the hashed output to the code_challenge it received from the app earlier. If the output matches the code_challenge, the request is legitimate, and the authorization server issues tokens.

Confidential clients (such as traditional web apps) should also use PKCE. PKCE protects against CSRF and authorization code injection, an attack where an attacker tries to exchange a stolen authorization code for tokens. In OAuth 2.1, all apps (public and confidential) using authorization code grant will be required to use PKCE.

Do not use implicit grant

In implicit grant, tokens are delivered to a public app in the browser in a query parameter. All public apps must use authorization code grant with PKCE. Implicit grant must not be used. It’s not secure to expose tokens in a browser URL where they can easily be accessed and stolen.

2. Prevent token replay

Token replay is when an attacker attempts to re-use another user’s token to gain access to protected resources.

Use sender constrained access tokens

In order to prevent token replay, access tokens should be sender constrained. Sender constraint scopes a token to a specific sender (e.g., the authorization server that issued the token). The token recipient (e.g., a protected API) will only accept a token if the sender can prove they know the necessary secret. Methods of sender constraint include mutual TLS or Demonstrating Proof of Possession.

Public client refresh tokens must be sender constrained or rotated

Public clients, such as browser-based or native applications, must also implement sender constraint for refresh tokens. Refresh tokens are long-lived. They are used to keep a user’s access valid without forcing them to manually reauthenticate every time the short-lived access token expires. Because of their longevity and use case, they are prime targets for exploitation. Sender constraining refresh tokens protects them from being used in CSRF attacks.

Alternatively, refresh tokens should be rotated by the authorization server. Refresh token rotation means every time the authorization server receives a request to refresh the access token for a user, a new refresh token is also issued, and the previous token is invalidated.

3. Access tokens should grant the minimum privileges needed

Access tokens provide authorization to access protected resources. Overloading access tokens with more permissions than needed increases risk. Access tokens should grant the bare minimum permissions.

Restrict access token use to a specific API (or at the most, a small set of APIs). An access token uses an aud (audience) parameter to store the ID of the API (or resource server) it’s intended for. Any resource server not listed in the aud must reject the token and deny access.

Once an API verifies it’s the intended audience of the access token, it still needs to restrict access to specific resources or actions. The token may contain a scope parameter or authorization_details that tell the API what to allow. For every request, the API must verify that the token does indeed grant the requested access.

4. Don’t use user credentials to obtain access tokens

Resource Owner Password Credentials (ROPC) is an OAuth 2.0 grant type that uses user credentials (such as username and password) for authorization to obtain an access token. This must not be used. Considering the other best practices above, it may already be clear why this is now prohibited. ROPC insecurely exposes the user’s credentials to the client. It has an increased attack surface and isn’t designed to work with multi-factor authentication. Since ROPC is bound to a specific web origin, WebCrypto and WebAuthn may be impossible to use with it.

5. Authorization servers should enforce client authentication

This best practice is more relevant to authorization server providers than app developers, but it provides important context. Client authentication is when an authorization provider verifies the app is who it claims to be. Authorization servers should issue and register confidential client credentials to do this.

The authorization server should use asymmetric cryptography such as mTLS for OAuth 2.0 or Signed JSON Web Token (Private Key JWT) to verify the identity of an app requesting authorization. JWT asymmetric cryptography means a private key is used to encrypt data and a different, public key is needed to decrypt it. This way, authorization servers don’t need to store sensitive symmetric keys (which can both encrypt and decrypt data).

6. Other recommendations

  • The authorization server must not allow Cross-Origin Resource Sharing (CORS) at its authorization endpoint since the client redirects the browser to this endpoint rather than accessing it directly.
  • OAuth Authorization Server Metadata can help improve security. Authorization servers should publish metadata and clients should use this metadata for configuration.
    • This ensures that security features and new OAuth features can be enabled automatically by software libraries.
    • Metadata reduces misconfiguration.
    • Metadata can help cryptographic key rotation.
  • Authorization servers should not allow clients to influence their own client_id.
  • End-to-end TLS (Transport Layer Security) between the client and API is recommended.
  • Unencrypted network connections (HTTP) should not be allowed when transmitting authorization responses (except on native clients that use loopback interface redirection).
  • If the authorization response is sent with in-browser communication like postMessage instead of HTTP redirects, both the sender and receiver must be verified.
  • Endpoints directly accessed by clients may support CORS (i.e., without browser redirection), such as the token endpoint, metadata endpoint, etc.

Attacker model, attacks, and mitigations

RFC 9700 Best Current Practice for OAuth 2.0 Security also includes an updated OAuth 2.0 attacker model and descriptions of security attacks and guidance for their mitigation. Though this article doesn’t cover these parts of the specification in detail, some attacks and mitigations are briefly covered with the best practices above. To learn more about the attacker model, attacks, and mitigations, please see the specification.

Conclusion

I hope this developer’s overview of OAuth 2.0 Best Current Practice was useful! If you have any questions or just want to chat, please reach out to me on LinkedIn, Bluesky, or X.

Leave a Reply