RFC 10017
OAuth 2.0 for Browser-Based Applications
Part 1 of 13 π
OAuth 2.0 for Browser-Based Applications π
Okay bestie, this is RFC 10017 (also BCP 212 β¨), and it's called OAuth 2.0 for Browser-Based Applications π. It's a Best Current Practice from the IETF, published in August 2026 πΈ. The authors are A. Parecki from Okta, P. De Ryck from Pragmatic Web Security, and D. Waite from Ping Identity π . The ISSN is 2070-1721 π¦. The header doesn't list anything it obsoletes or updates, so no drama there π.
Abstract
Okay so this spec spills all the tea on the threats, attack consequences, security considerations, and best practices you have to think about when you build browser-based apps that use OAuth 2.0 π. It's giving "read this before you ship" energy β¨.
Status of This Memo π
Lowkey, this memo is an Internet Best Current Practice, so it's basically the official, recommended way to do things π .
This document is a product of the Internet Engineering Task Force (IETF) π§. It represents the consensus of the IETF community π«Ά. It went through public review and was approved for publication by the Internet Engineering Steering Group (IESG) π. Further information on BCPs is available in Section 2 of RFC 7841.
Info about the current status of this document, any errata, and how to send feedback is at https://www.rfc-editor.org/info/rfc10017 π.
Copyright Notice π
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License π.
Table of Contents π§
1. Introduction
2. Notational Conventions
3. Terminology
4. History of OAuth 2.0 in Browser-Based Applications
5. The Threat of Malicious JavaScript
5.1. Attack Scenarios
5.1.1. Single-Execution Token Theft
5.1.2. Persistent Token Theft
5.1.3. Acquisition and Extraction of New Tokens
5.1.4. Proxying Requests via the User's Browser
5.2. Attack Consequences
5.2.1. Exploiting Stolen Refresh Tokens
5.2.2. Exploiting Stolen Access Tokens
5.2.3. Client Hijacking
6. Application Architecture Patterns
6.1. Backend for Frontend (BFF)
6.1.1. Application Architecture
6.1.2. Implementation Details
6.1.3. Security Considerations
6.1.4. Threat Analysis
6.2. Token-Mediating Backend
6.2.1. Application Architecture
6.2.2. Implementation Details
6.2.3. Security Considerations
6.2.4. Threat Analysis
6.3. Browser-Based OAuth 2.0 Client
6.3.1. Application Architecture
6.3.2. Implementation Details
6.3.3. Security Considerations
6.3.4. Threat Analysis
7. Discouraged and Deprecated Architecture Patterns
7.1. Single-Domain Browser-Based Applications (Not Using OAuth)
7.1.1. Threat Analysis
7.2. OAuth Implicit Grant
7.2.1. Historic Note
7.2.2. Threat Analysis
7.2.3. Further Attacks on the Implicit Grant
7.2.4. Disadvantages of the Implicit Grant
7.3. Resource Owner Password Credentials Grant
7.4. Handling the OAuth Flow in a Service Worker
7.4.1. Threat Analysis
8. Token Storage in the Browser
8.1. Cookies
8.2. Token Storage in a Service Worker
8.3. Token Storage in a Web Worker
8.4. In-Memory Token Storage
8.5. Persistent Token Storage
8.6. Filesystem Considerations for Browser Storage APIs
9. Security Considerations
9.1. Reducing the Authority of Tokens
9.2. Sender-Constrained Tokens
9.3. Authorization Server Mix-Up Mitigation
9.4. Isolating Applications Using Origins
10. IANA Considerations
11. References
11.1. Normative References
11.2. Informative References
Acknowledgements
Authors' Addresses
1. Introduction β¨
This spec describes different architectural patterns for building OAuth 2.0 clients in apps that run in a browser π¦. It also lays out the security challenges for browser-based apps and looks at how each pattern can help with some of them π .
This document focuses on JavaScript frontend apps acting as the OAuth client (defined in Section 1.1 of [RFC6749]) π§. These apps talk to the authorization server (Section 1.1 of [RFC6749]) to get access tokens and, optionally, refresh tokens π. The client uses the access token to reach protected resources on resource servers (Section 1.1 of [RFC6749]) πΈ. With OAuth, the client, the authorization server, and the resource servers are all treated as independent parties, even if the same entity owns or runs more than one of them π«Ά.
Heads up, though: lots of web apps have a frontend and an API living on the same domain, which means they can work without OAuth 2.0 at all π. That setup is covered in more detail in Section 7.1. Those apps can use OpenID Connect [OpenID] for federated user authentication, and then the app keeps track of the user's authentication state π. That scenario (which only uses OAuth 2.0 as the underlying spec of OpenID Connect) is not in scope for this specification π«.
For native app developers using OAuth 2.0 and OpenID Connect, there's an IETF BCP (Best Current Practice) that guides how to put these technologies together π. That document is formally known as [RFC8252] or BCP 212, but people often call it "AppAuth," after the OpenID Foundation-sponsored set of libraries that help developers adopt these practices π. [RFC8252] gives specific recommendations for how to implement OAuth clients securely in native apps, including adding extra OAuth extensions where needed π.
This specification, "OAuth 2.0 for Browser-Based Applications," shows how the security properties of browser-based apps are vastly different from those of native apps π. It also covers the similarities between building OAuth clients as native apps and as browser-based apps π¦. This document is mainly about OAuth, except where OpenID Connect adds extra considerations π.
Lots of these recommendations come from the Best Current Practice for OAuth 2.0 Security [RFC9700], because browser-based apps are expected to follow those recommendations too π. This document expands on and further restricts various recommendations given in [RFC9700] π₯.
2. Notational Conventions π
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here π. Only the all-caps versions count, so lowercase versions are just regular words β¨.
3. Terminology π¦
This spec uses the terms "access token", "authorization endpoint", "authorization grant", "authorization server", "client", "client identifier" (client ID), "protected resource", "refresh token", "resource server", and "token endpoint" as defined by OAuth 2.0 [RFC6749] π. It uses "bearer token" as defined by [RFC6750] πΈ.
Besides the terms defined in the referenced specs, this document also uses these terms π:
- OAuth: In this document, "OAuth" refers to "OAuth 2.0" [RFC6749] [RFC6750] β¨.
- Browser-based application: An application that gets downloaded and run dynamically in a web browser, usually written in JavaScript π. It's also sometimes called a "single-page application" or "SPA" π§.
This document is about the security of browser-based apps, which the browser runs in a runtime environment π«Ά. In most cases these apps are JavaScript apps running in a JavaScript execution environment π. Because that's so popular, this document uses the term "JavaScript" to mean all the mechanisms that let code execute in the app's runtime in the browser π₯. The recommendations and considerations here aren't only for the JavaScript language or its runtime, though. They also apply to other languages and runtime environments in the browser, such as WebAssembly (WASM) [W3C.wasm-core-2] π.
- PKCE: Proof Key for Code Exchange [RFC7636]. A mechanism that prevents various attacks on OAuth authorization codes π.
- DPoP: Demonstrating Proof of Possession [RFC9449]. An OAuth 2.0 mechanism that restricts access tokens so they can only be used by the client they were issued to π.
- CORS: Cross-Origin Resource Sharing [Fetch]. A mechanism that enables exceptions to the browser's same-origin policy π¦.
- CSP: Content Security Policy [W3C.CSP3]. A mechanism that restricts which resources a particular web page can fetch or execute π‘οΈ.
4. History of OAuth 2.0 in Browser-Based Applications β¨
okay so bestie, when OAuth 2.0 was first specced in [RFC6749] and [RFC6750], browser-based JavaScript apps had to follow the same-origin policy π ββοΈ like, strictly. Most OAuth 2.0 setups had the app living on a different domain from the authorization server, which meant the Authorization Code grant type (Section 4.1 of [RFC6749]) was basically off the table π since it needed a cross-origin POST request. That limitation was one of the reasons the Implicit grant (Section 4.2 of [RFC6749]) got made π¦. It returns the access token in the front channel through the fragment part of the URL, so no cross-origin POST was needed.
ngl though, the Implicit grant comes with a bunch of drawbacks π, mostly because the access token ends up exposed in the URL. See Section 7.2 for an analysis of these attacks and the drawbacks of using the Implicit grant in browsers. Additional attacks and security considerations can be found in [RFC9700].
in modern web dev, widespread adoption of Cross-Origin Resource Sharing (CORS) [Fetch] (which enables exceptions to the same-origin policy) lets browser apps use the OAuth 2.0 Authorization Code flow and make a POST request to swap the authorization code for an access token at the token endpoint π. Since the Authorization Code grant type enables refresh tokens, this behavior got adopted for browser clients too, even though they're still public clients (defined in Section 2.1 of [RFC6749]) with limited to no access to secure storage π«£. Adding Proof Key for Code Exchange (PKCE) [RFC7636] to the flow prevents authorization code injection, as well as ensures that even if an authorization code is intercepted, it is unusable by an attacker ππ.
so, for this reason and some other lessons learned, the current best practice for browser-based applications is to use the OAuth 2.0 Authorization Code grant type with PKCE π. There are various architectural patterns for deploying browser-based applications, both with and without a corresponding server-side component. Each of these architectures has specific trade-offs and considerations, which we'll dig into further in this document π§. Additional considerations apply for first-party common-domain applications.
5. The Threat of Malicious JavaScript π
Malicious JavaScript is a seriously big risk for browser-based apps π¨. Attack vectors like cross-site scripting (XSS) or the compromise of remote code files give an attacker the power to run arbitrary code in the app's execution context. And this malicious code isn't isolated from the main app's code in any way π. So it can not only take control of the running execution context, but also do stuff within the app's origin. Concretely, that means the malicious code can steal data from the current page, interact with other same-origin browsing contexts, send requests to a backend from within the app's origin, steal data from origin-based storage mechanisms (e.g., localStorage, IndexedDB), and so on π
.
first and foremost, it's crucial to take proactive measures so the attacker never gets a foothold in the first place π‘οΈ. That involves, but isn't limited to, the following:
- Strictly applying context-sensitive output encoding and sanitization when handling untrusted data
- Limiting or avoiding the loading of unchecked third-party resources
- Using Subresource Integrity [W3C.SRI] to restrict valid scripts that can be loaded
- Using a nonce-based or hash-based Content Security Policy (CSP) [W3C.CSP3] to prevent the execution of unauthorized script code
- Using origin isolation and HTML5 sandboxing to create boundaries between different parts of the application
more recommendations can be found in the Open Worldwide Application Security Project (OWASP) Cheat Sheet series [OWASPCheatSheet] π.
unfortunately, history shows that even when you apply these security guidelines, the attacker might still find a way to trigger the execution of malicious JavaScript π©. When analyzing the security of browser-based apps with malicious JavaScript in the picture, it's crucial to realize that the malicious JavaScript code has the same privileges as the legitimate application code. All JavaScript applications are exposed to this risk to some degree π₯².
apps might obtain OAuth tokens that confer the authorization they need to function. Combined, that effectively gives compromised code the ability to use that authorization for malicious ends π. The risk of attacker abuse of authorization is unavoidable, but there are ways to limit how far a compromised app can go with that authorization β¨. For instance, access might be limited to times when the app is in active use, by limiting the type of tokens that can be obtained, or by binding the tokens to the browser π.
when the legitimate application code can access variables or call functions, the malicious JavaScript code can do exactly the same π. Furthermore, the malicious JavaScript code can tamper with the regular execution flow of the app, as well as with any application-level defenses, since those are typically controlled from within the app itself. For example, the attacker can remove or override event listeners, modify the behavior of built-in functions (prototype pollution), and stop pages in frames from loading π.
the impact of malicious JavaScript on browser-based apps is a widely studied and well-understood topic π§ . However, the concrete impact of malicious JavaScript on browser-based apps acting as an OAuth client is quite unique, since the malicious JavaScript can now mess with the interactions during an OAuth flow π« . This section explores the threats malicious JavaScript poses to a browser-based app with the responsibilities of an OAuth client. Section 5.1 discusses a few scenarios attackers can use once they've found a way to run malicious JavaScript code. These scenarios paint a clear picture of the true power of the attacker, which goes way beyond simple token exfiltration π±. Section 5.2 analyzes the impact of these attack scenarios on the OAuth client.
the rest of this specification will refer back to these attack scenarios and consequences to analyze the security properties of the different architectural patterns π¦.
5.1. Attack Scenarios
this section presents several attack scenarios an attacker can execute once they've found a vulnerability that allows malicious JavaScript code to run π. The attack scenarios include trivial scenarios (Section 5.1.1) and elaborate scenarios (Section 5.1.3). Note that this enumeration is non-exhaustive, narrowly scoped to OAuth-specific features, and presented in no particular order π.
5.1.1. Single-Execution Token Theft
the single-execution token theft scenario is a simple token exfiltration attack, where the attacker grabs and exfiltrates the client's current tokens π₯·. This scenario goes like this:
- Execute malicious JavaScript code
- Obtain tokens from the application's preferred storage mechanism (Section 8)
- Send the tokens to a server controlled by the attacker
- Store or abuse the stolen tokens
the recommended defensive strategy to decrease the risk tied to compromised access tokens is to reduce the scope and lifetime of the token β³. For refresh tokens, using refresh token rotation (as defined in Section 4.14.2 of [RFC9700]) offers a detection and correction mechanism π. Sender-constrained tokens (Section 9.2) add an extra layer of protection against stolen access tokens π‘οΈ.
note that this attack scenario is trivial and gets used a lot to illustrate the dangers of malicious JavaScript π. When discussing the security of browser-based apps, it's crucial not to limit the attacker's capabilities to just the attack in this scenario π .
5.1.2. Persistent Token Theft
the persistent token theft attack scenario is a more advanced version of the single-execution token theft scenario (Section 5.1.1) π. Instead of stealing tokens right away when the malicious code runs, the attacker sets up the handlers needed to steal the app's tokens on a continuous basis. This scenario goes like this:
- Execute malicious JavaScript code
- Set up a continuous token theft mechanism (e.g., on a 10-second time interval)
- Obtain tokens from the application's preferred storage mechanism (Section 8)
- Send the tokens to a server controlled by the attacker
- Store the tokens
- Wait until the opportune moment to abuse the latest version of the stolen tokens
the crucial difference here is that the attacker always has access to the latest tokens the app uses π€. This slight variation already suffices to get around typical defenses against token theft, like short lifetimes or refresh token rotation π.
for access tokens, the attacker now obtains the latest access token for as long as the user's browser stays online π«’. Refresh token rotation isn't enough to prevent abuse of a refresh token. An attacker can easily make sure the app won't use the latest refresh token. For example, the attacker could clear the app's tokens after stealing them, wait until the user closes the app, or wait until the user's browser goes offline π ββοΈ. Since the app won't use the latest refresh token, there's no detectable refresh token reuse, which gives the attacker full control over the stolen refresh token π§ββοΈ.
5.1.3. Acquisition and Extraction of New Tokens π β¨
Okay so in the advanced acquisition and extraction of new tokens attack scenario π¦, the attacker completely ignores any tokens the application has already grabbed. Literally not interested. Instead, they use their ability to run malicious code under the application's origin π. With that, they can inject a hidden iframe and kick off a silent Authorization Code flow π. That silent flow reuses the user's existing session with the authorization server and ends with a brand new, independent access token (and optionally a refresh token) π. Here's how it goes down:
- Execute malicious JavaScript code π
- Set up a handler to obtain the authorization code from the iframe (e.g., by monitoring the frame's URL or via Web Messaging [WebMessaging])
- Insert a hidden iframe into the page and initialize it with an authorization request. The authorization request in the iframe will occur within the user's session and, if the session is still active, result in the issuing of an authorization code. Note that this step relies on the authorization server supporting silent frame-based flows, as discussed in the last paragraph of this scenario.
- Extract the authorization code from the iframe using the previously installed handler
- Send the authorization code to a server controlled by the attacker
- Exchange the authorization code for a new set of tokens
- Abuse the stolen tokens
The most important takeaway here is that this runs a whole new OAuth flow instead of stealing tokens that already exist π₯. Even if the app stores its tokens somewhere totally isolated from the attacker, the attacker can still just request a fresh set π. And since the attacker controls the app in the browser, their Authorization Code flow looks exactly like a legit one. Indistinguishable, bestie π.
This works because the security of public browser-based OAuth clients relies entirely on the redirect URI and the application's origin πΈ. Once malicious JavaScript is running in the application's origin, the attacker can inspect same-origin frames. So their code, running in the main execution context, can look at the redirect URI loaded in the same-origin frame and pull out the authorization code π.
Honest truth: there are no practical security mechanisms for frontend applications that counter this one π. Short access token lifetimes and refresh token rotation don't help, since the attacker has a fresh, independent set of tokens π . Advanced security mechanisms like DPoP [RFC9449] are equally useless here, since the attacker can use their own key pair to set up and use DPoP for the newly obtained tokens. Requiring user interaction with every Authorization Code flow would stop the automatic silent issuance of new tokens, sure, but it would seriously mess with totally normal patterns like bootstrapping an application on its first page load or single sign-on across multiple related applications. So it's not a practical measure π .
5.1.4. Proxying Requests via the User's Browser π§
This attack has the attacker sending requests to the OAuth resource server straight from inside the OAuth client app running in the user's browser π. They don't need to abuse the app to get tokens, because the browser already attaches its own cookies or tokens to the request. The attacker's requests look identical to the legit app's requests, since the attacker is running code in the same context as the real app π«Ά. Here's the flow:
- Execute malicious JavaScript code
- Send a request to a resource server and process the response
To get those requests authorized, the attacker just mimics the client app. So if the app programmatically attaches an access token to outgoing requests, the attacker does the same thing π. And if the app leans on an external component to add the right access token, that component will also add it to the attacker's request, no questions asked.
This attack pattern is well known and also happens with server-side web applications using HttpOnly session cookies π. It's commonly accepted that this scenario can't be stopped or prevented by application-level security measures. For example, DPoP [RFC9449] explicitly considers this attack scenario to be out of scope π«.
5.2. Attack Consequences β
A successful attack can end in the theft of access tokens and refresh tokens, or in the ability to hijack the client application running in the user's browser π€. Each of these matters for browser-based OAuth clients, and they're discussed below in decreasing order of severity.
5.2.1. Exploiting Stolen Refresh Tokens π₯
When the attacker gets a valid refresh token from a browser-based OAuth client, they can run a Refresh Token grant with the authorization server. The response contains an access token, which gives the attacker the ability to access protected resources (see Section 5.2.2). So abusing a stolen refresh token means long-term impersonation of the legit client application in requests to resource servers. Full villain arc π.
The attack is only stopped when the authorization server refuses a refresh token because it has expired or rotated, or when the refresh token is revoked. β οΈ
In a typical browser-based OAuth client, it's not uncommon for a refresh token to stay valid for multiple hours or even days π¬.
5.2.2. Exploiting Stolen Access Tokens π
If the attacker gets a valid access token, they can impersonate the legit client application in a request to a resource server. Possessing the token lets them send arbitrary requests to any resource server that accepts it. So abusing a stolen access token means short-term impersonation of the legit client to resource servers β³.
The attack ends when the access token expires or when a token is revoked with the authorization server. In a typical browser-based OAuth client, access token lifetimes can be pretty short, anywhere from minutes to hours β°.
Note that possession of the access token allows its unrestricted use by the attacker. They can send arbitrary requests to resource servers using any HTTP method, destination URL, header values, or body π₯.
The application can use DPoP to bind its access tokens to non-exportable keys held by the browser π. In that case, it becomes significantly harder for the attacker to abuse stolen access tokens. More specifically, with DPoP, the attacker can only abuse stolen application tokens by carrying out an online attack where the proofs are calculated in the user's browser. This attack is described in detail in Section 11.4 of [RFC9449]. However, when the attacker obtains a fresh access token (and optionally a refresh token), as described in Section 5.1.3, they can set up DPoP for these tokens using an attacker-controlled key pair. In that case, the attacker is again free to abuse this newly obtained access token without restrictions π€.
5.2.3. Client Hijacking π
When stealing tokens is not possible or desirable, the attacker can also hijack the OAuth client application running in the user's browser π§. This effectively lets them do anything the legitimate client application can do. Examples include inspecting data on the page, modifying the page, and sending requests to backend systems. Or they can use their access to the application to launch additional attacks, like tricking the client into acting on behalf of the attacker using session fixation [SessionFixation] π.
Note that client hijacking is less powerful than directly abusing stolen user tokens π«£. In a client-hijacking scenario, the attacker can't directly control the tokens and is restricted by the security policies enforced on the client application. For example, a resource server running on admin.example.org can be configured with a CORS policy that rejects requests coming from a client running on web.example.org. Even if the access token used by the client would be accepted by the resource server, the resource server's strict CORS configuration would not allow such a request π‘οΈ. A resource server without such a strict CORS policy can still be subject to adversarial requests coming from the compromised client application π.
6. Application Architecture Patterns ποΈ
There are three main architectural patterns available when building browser-based applications that rely on OAuth for accessing protected resources π:
- A browser-based application that relies on a backend component for handling OAuth responsibilities and forwards all requests through the backend component ("Backend for Frontend" or "BFF")
- A browser-based application that relies on a backend component for handling OAuth responsibilities but calls resource servers directly using the access token (token-mediating backend)
- A browser-based application acting as the client, handling all OAuth responsibilities in the browser (browser-based OAuth client)
Each of these patterns offers a different trade-off between security and simplicity βοΈ. The patterns in this section are presented in decreasing order of security π.
6.1. Backend for Frontend (BFF) π
okay so this section is all about the Backend for Frontend (BFF) π it's a browser-based app that leans on a backend piece to handle ALL the OAuth stuff and all the API calls. the BFF has three core responsibilities, and they're giving main character energy:
- the BFF talks to the authorization server as a confidential OAuth client (as defined in Section 2.1 of [RFC6749]) π
- the BFF manages OAuth access and refresh tokens inside a cookie-based session, so the tokens never get directly exposed to the browser-based app π
- the BFF forwards all requests to a resource server, slapping the correct access token on them before sending them along to the resource server π
the BFF runs on the server side, but it's still a part of the frontend app π«Ά so don't mix it up with API gateways or reverse proxies, bestie. the BFF becomes the OAuth client for the frontend app, and that's the whole vibe.
if an attacker manages to run malicious code inside the browser-based app, this architecture can withstand most of the attack scenarios we covered earlier π‘οΈ since tokens only live with the BFF, there's nothing for the attacker to pull out of the browser (see "Single-Execution Token Theft" (Section 5.1.1) and "Persistent Token Theft" (Section 5.1.2)). the BFF is a confidential client, which stops the attacker from kicking off a new flow inside the browser (see "Acquisition and Extraction of New Tokens" (Section 5.1.3)). the malicious code still runs in the app's origin though, so the attacker can still send requests to the BFF from inside the user's browser (see "Proxying Requests via the User's Browser" (Section 5.1.4)). note that using HttpOnly cookies stops the attacker from directly poking at the session state, which stops the escalation from client hijacking to session hijacking π
6.1.1. Application Architecture β¨
+---------------+ +----------+ +----------+
| | | | | |
| Authorization | | Token | | Resource |
| Endpoint | | Endpoint | | Server |
| | | | | |
+---------------+ +----------+ +----------+
^ ^ ^
| (F)| (K)|
| v v
|
| +------------------------------+
| | |
| | Backend for Frontend (BFF) |
(D)| | |
| +------------------------------+
|
| ^ ^ ^ + ^ +
| (B,I)| (C)| (E)| (G)| (J)| |(L)
v v v + v + v
+-----------------+ +-----------------------------------------+
| | (A,H) | |
| Static Web Host | +-----> | Browser |
| | | |
+-----------------+ +-----------------------------------------+
Figure 1: OAuth 2.0 BFF Pattern
so here's the flow π the browser code (usually JavaScript) gets loaded from a static web host into the browser (A), and then the app starts running in the browser. the app checks with the BFF to see if there's an active session by calling a "check session" API endpoint (B). if there's an active session, the app picks up its authenticated state and skips ahead to (J).
when there's no active session, the browser-based app triggers a navigation to the BFF (C) to start the Authorization Code flow with the PKCE extension (described in Section 6.1.3.1). the BFF responds by redirecting the browser to the authorization endpoint (D). when the user gets redirected back, the browser hands the authorization code to the BFF (E), and the BFF can then swap it for tokens at the token endpoint (F) using its client credentials and PKCE code verifier. π
the BFF links the tokens to the user's session (see Section 6.1.2.3) and sets a cookie in the response to keep track of that session (G). the redirect-based Authorization Code flow is now done, so the BFF hands control back to the frontend app. it does this by including a redirect in the response (G), which makes the browser fetch the frontend from the server (H). step (H) is identical to step (A), which likely means the resources can be loaded from the browser's cache π§ when the frontend loads, it checks with the BFF for an existing session (I), so the app can pick up its authenticated state again.
when the app wants to hit the resource server, it sends a request to the matching endpoint on the BFF (J). that request includes the cookie from (G), so the BFF can grab the right tokens for this user's session. the BFF removes the cookie from the request, attaches the user's access token, and forwards it to the actual resource server (K). then the BFF sends the response back to the browser-based app (L). β¨
6.1.2. Implementation Details π§
6.1.2.1. Session and OAuth Endpoints π
the BFF offers a set of endpoints that are crucial for the chat between the browser-based app and the BFF. this part breaks them down so you know exactly what each one is for π
the "check session" endpoint ((B) and (I) in Figure 1) is an API endpoint that the browser-based app calls. the request carries session info when it's available, so the BFF can check for an active session. the response should indicate to the browser-based app whether the session is active. the BFF can also toss in other info, like identity info about the authenticated user.
the endpoint that kicks off the Authorization Code flow (C) gets hit by the browser through a navigation. when the app detects an unauthenticated state after checking the session (B), it can navigate the browser to this endpoint. that lets the BFF respond with a redirect that sends the browser to the authorization server. this endpoint is typically included as the "login" endpoint by libraries that support OAuth 2.0 for confidential clients running on a web server π also, the BFF can start the Authorization Code flow in (B) when it detects there's no active session. in that case, the BFF would return the authorization URI in the response and expect the app to trigger a navigation event with that URI. but this scenario needs a custom implementation and makes it harder to use standard OAuth libraries π€
the endpoint that receives the authorization code (E) is called by a navigation event from inside the browser. at this point the app isn't loaded and isn't in a position to handle the redirect. just like the flow start, standard OAuth libraries offer the endpoint for handling the redirect too. the BFF can respond to this request with a redirect that triggers the browser to load the app.
finally, the BFF can also offer a "logout" endpoint to the app, which isn't depicted in Figure 1. what the logout endpoint actually does depends on the app's requirements. standard OAuth libraries usually come with an implementation of the "logout" endpoint too π«Ά
6.1.2.2. Refresh Tokens π
when using refresh tokens, as described in Section 4.14 of [RFC9700], the BFF obtains the refresh token (F) and associates it with the user's session.
if the BFF notices that the user's access token has expired and the BFF has a refresh token, it can use the refresh token to obtain a fresh access token. since the BFF OAuth client is a confidential client, it will use client authentication on the refresh token request. typically, the BFF performs these steps inline when handling an API call from the frontend. in that case, these steps (which aren't explicitly shown in Figure 1) would happen between (J) and (K). BFFs that keep all token information on the server side can also request fresh access tokens when they observe a token expiration event, which helps increase the performance of API requests β‘
when the refresh token expires, there's no way to get a valid access token without running an entirely new Authorization Code flow. so it makes sense to set the lifetime of the cookie-based session managed by the BFF equal to the maximum lifetime of the refresh token. also, when the BFF learns that a refresh token for an active session is no longer valid, it makes sense to invalidate the session π ββοΈ
6.1.2.3. Cookie-Based Session State πͺ
the BFF relies on browser cookies [RFC6265bis] to keep track of the user's session, which is what gets used to access the user's tokens. cookie-based sessions, both server-side and client-side, have some downsides π
server-side sessions expose only a session identifier and keep all the data on the server. this gives a great level of control over active sessions, along with the ability to revoke any session at will. the catch is scalability, which calls for solutions like "sticky sessions" or "session replication". given these downsides, using server-side sessions with a BFF is only recommended in small-scale scenarios πΈ
client-side sessions push all data to the browser in a signed, and optionally encrypted, object. this absolves the server of keeping track of any session data, but it severely limits control over active sessions and makes session revocation tricky. however, when client-side sessions are used in the context of a BFF, these properties change significantly π¦ since the cookie-based session is only used to obtain a user's tokens, all control and revocation properties follow from the use of access tokens and refresh tokens. revoking the user's access token and/or refresh token is enough to prevent ongoing access to protected resources, with no need to explicitly invalidate the cookie-based session π
best practices to secure the session cookie are discussed in Section 6.1.3.2. π
6.1.2.4. Combining OAuth and OpenID Connect β¨
okay so the OAuth flow this app architecture uses can totally be combined with OpenID Connect π«Ά all you have to do is put the OpenID Connect scopes in the authorization request (C). at the very least that means the scope openid, as defined in Section 3.1.2.1 of [OpenID]. once you do that, the BFF gets an ID Token in (F) π₯ and it can hook the info from that ID Token up to the user's session and pass it along to the application in (B) or (I). iconic, honestly.
and when it needs to, the BFF can use the access token tied to the user's session to make requests to the UserInfo endpoint π
6.1.2.5. Practical Deployment Strategies π¦
serving the static JavaScript code is its own job, separate from handling OAuth tokens and forwarding requests π in Figure 1, the BFF and the static web host are drawn as two separate entities. in real-world deployments though, these components can be deployed as a single service (i.e., the BFF serving the static JavaScript code), as two separate services (i.e., a Content Delivery Network (CDN) and a BFF), or as two components in a single service (i.e., static hosting and serverless functions on a cloud platform). so many options, bestie π
note that it's possible to customize this architecture even more for specific scenarios β¨ for example, an application relying on both internal and external resource servers can choose to host the internal resource server alongside the BFF. in that setup, requests to the internal resource server get handled directly at the BFF, so they never have to be forwarded over the network π§ authorization from the point of view of the resource server doesn't change at all, since the user's session is internally translated to the access token and its claims.
6.1.3. Security Considerations π
6.1.3.1. The Authorization Code Grant π
the main perk of using a BFF is that it gets to be a confidential client π so the BFF MUST act as a confidential client by establishing credentials with the authorization server. furthermore, the BFF MUST use the OAuth 2.0 Authorization Code grant as described in Section 2.1.1 of [RFC9700] to initiate a request for an access token. no skipping steps, periodt π
6.1.3.2. Cookie Security πͺ
the BFF uses cookies to create a user session, and that session is directly associated with the user's tokens through either server-side or client-side session state. these cookies are sensitive, so they must be properly protected π«£
the following cookie security guidelines are relevant for this particular BFF architecture:
- The BFF MUST enable the
Secureflag for its cookies. π - The BFF MUST enable the
HttpOnlyflag for its cookies. π ββοΈ - The BFF SHOULD enable the
SameSite=Strictflag for its cookies. - The BFF SHOULD set its cookie path to
/. - The BFF SHOULD NOT set the
Domainattribute for cookies. - The BFF SHOULD start the name of its cookie with a prefix indicating the cookie was set via HTTP, for example, by using the
__Host-Http-prefix defined in [COOKIES].
Note: in new deployments, all of the above requirements are likely to be straightforward to implement. the "SHOULD" items aren't deemed as "MUST" items, so that existing architectures can be compliant. the implications of these requirements are listed below. π§
these cookie security guidelines, combined with the use of HTTPS, help counter attacks that directly target a cookie-based session π‘οΈ session hijacking is not possible thanks to the Secure and HttpOnly cookie flags. the __Host-Http- prefix stops the cookie from being shared with subdomains, which counters subdomain-based session hijacking or session-fixation attacks π in a typical BFF deployment, there's no reason to use more relaxed cookie security settings than the requirements above. deviating from these settings requires proper motivation for the deployment scenario at hand, so don't just wing it π
additionally, when using client-side sessions that contain access tokens (as opposed to server-side sessions where the tokens only live on the server), the BFF SHOULD encrypt its cookie contents. the use of cookie encryption doesn't affect the security properties of the BFF pattern, but it does ensure that tokens stored in cookies are never written to the user's local persistent storage in plaintext format. this helps ensure the confidentiality of the tokens in case an attacker is able to read cookies from the hard drive, and that kind of attack can be launched through malware running on the victim's computer π note that while encrypting the cookie contents prevents direct access to embedded tokens, it still allows the attacker to use the encrypted cookie in a session hijacking attack. yikes.
for further guidance on cookie security best practices, refer to the OWASP Cheat Sheet series [OWASPCheatSheet] π
6.1.3.3. Cross-Site Request Forgery Protections π
the interactions between the browser-based application and the BFF rely on cookies for authentication and authorization. like other cookie-based interactions, the BFF is required to account for Cross-Site Request Forgery (CSRF) attacks. a successful CSRF attack could let the attacker's request to the BFF trigger outgoing calls to a protected resource π
the BFF MUST implement a proper CSRF defense. the exact mechanism, or combination of mechanisms, depends on the exact domain where the BFF is deployed, as discussed below π«Ά
6.1.3.3.1. SameSite Cookie Attribute π
configuring the cookies with the SameSite=Strict attribute (see Section 6.1.3.2) makes sure the BFF's cookies are only included on same-site requests, and not on potentially malicious cross-site requests β¨
this defense is adequate if the BFF is never considered to be same site with any other applications. however, it falls short when the BFF is hosted alongside other applications within the same site, defined as the eTLD+1 (see this definition in [Site] for more details). ngl that's the sneaky part π
for example, subdomains such as https://a.example.com and https://b.example.com are considered to be same site because they share the same site example.com. they're considered cross-origin though, because origins consist of the tuple <scheme, hostname, port>. so a subdomain-takeover attack against b.example.com can enable CSRF attacks against the BFF of a.example.com π these subdomain-based attacks follow the same pattern as CSRF attacks, just with a cross-origin nature instead of a cross-site nature.
6.1.3.3.2. Cross-Origin Resource Sharing π
the BFF can rely on CORS as a CSRF defense mechanism. CORS is a security mechanism implemented by browsers that restricts cross-origin requests, unless the server explicitly approves such a request by setting the proper CORS headers π
browsers typically restrict cross-origin HTTP requests initiated from scripts. CORS can lift that restriction if the target server approves the request, which gets checked through an initial "preflight" request. unless the preflight response explicitly approves the request, the browser will refuse to send the full request π ββοΈ
because of this property, the BFF can rely on CORS as a CSRF defense. when the attacker tries to launch a cross-origin request to the BFF from the user's browser, the BFF won't approve the request in the preflight response, so the browser blocks the actual request π₯ the attacker can always launch the request from their own machine, but then the request won't carry the user's cookies, so the attack fails. main character defense, no cap.
when relying on CORS as a CSRF defense, it's important to realize that certain requests are possible without a preflight. for these requests, called "CORS-safelisted requests", the browser will simply send the request and prevent access to the response if the server didn't send the proper CORS headers. this behavior is enforced for requests that can be triggered via other means than JavaScript, such as a GET request or a form-based POST request π
the consequence is that certain endpoints of the resource server could become vulnerable to CSRF, even with CORS enabled as a defense. for example, if the resource server is an API that exposes an endpoint to a bodiless POST request, there's no preflight request and no CSRF defense. π
to avoid these bypasses against the CORS policy, the BFF SHOULD require that the browser-based application includes a custom request header. cross-origin requests with a custom request header always require a preflight, which makes CORS an effective CSRF defense π§ when this mechanism is used, the BFF MUST ensure that every incoming request carries this static header. the exact naming of the header is up to the application and BFF π a sample configuration would be a request header with a static value, such as My-Static-Header: 1.
it's also possible to deploy the browser-based application on the same origin as the BFF. this way, legitimate interactions between the frontend and the BFF don't require any preflights, so there's no additional overhead π¦
6.1.3.3.3. Use Anti-Forgery/Double-Submit Cookies π§
some technology stacks and frameworks have built-in CSRF protection using anti-forgery cookies. this mechanism relies on a session-specific secret stored in a cookie, which can only be read by the legitimate frontend running in the domain associated with the cookie. the frontend is expected to read the cookie and insert its value into the request, usually by adding a custom request header. the backend verifies the value in the cookie against the value the frontend provided to identify legitimate requests. when this is implemented correctly for all state-changing requests, the mechanism effectively mitigates CSRF β¨
note that this mechanism isn't necessarily recommended over the CORS approach. however, if a framework offers built-in support for it, it can serve as a low-effort alternative to protect against CSRF π
6.1.3.4. Privacy Considerations in the BFF Architecture π΅οΈ
the BFF pattern requires that the browser-based application forwards all requests to a resource server through a backend BFF component. as a result, the BFF component is able to observe all requests and responses between the application and a resource server, which can have a considerable privacy impact ποΈ
when the browser-based application and BFF are built and deployed by the same party, the privacy impact is likely minimal πΈ but when this pattern is implemented using a BFF component that's provided or hosted by a third party, that privacy impact needs to be taken into account.
6.1.3.5. Operational Considerations π οΈ
since the BFF forwards all requests to the resource server on behalf of the frontend, care should be taken to make sure the resource server knows about this component and uses appropriate policies for rate limiting and other anti-abuse measures π«Ά for example, if the BFF is deployed as a single-instance service and the resource server rate limits requests based on IP address, the server might start blocking requests, because many users' browsers will appear to be coming from the single IP address of the BFF π that's a whole mess, so plan ahead, bestie π
6.1.3.6. Proxy Restrictions π₯
okay so the BFF is basically a proxy service πΈ it takes requests from the frontend and forwards them on to the resource server. the incoming request carries a cookie, and the BFF swaps that for an access token on the outgoing request. (ngl this makes it more of an application-layer reverse proxy than a plain HTTP proxy, which is a fun distinction π€) besides CSRF attacks, attackers might also try to trick the BFF into forwarding requests to hosts it was never meant to talk to. if they pull that off, they could redirect the BFF to some random server and leak the user's access token ππ
so to stop this, the BFF MUST enforce strict outbound request controls by checking destination hosts before it forwards anything. that means keeping an explicit allowlist of approved resource servers, so requests only get proxied to predefined backends. like, /bff/orders/create maps to https://order-api.example.com/create and nothing else π
if dynamic routing based on paths (like /bff/orders/{id}) is necessary, the BFF MUST apply strict validation so that only authorized destinations are reachable. also, restricting which HTTP methods are allowed on each endpoint can cut down on attack vectors even more π
when you have a dynamically configurable proxy, the BFF MUST ensure that it only allows requests to explicitly permitted hosts and paths. if it doesn't enforce these restrictions, you get unauthorized access and access token leakage. not a vibe π ββοΈ
6.1.3.7. Advanced Security π‘οΈ
in the BFF pattern, all OAuth responsibilities have been moved to the BFF, which is a server-side component acting as a confidential client. since server-side apps run in a more controlled environment than browser-based ones, it's way easier to adopt advanced OAuth security practices. examples include key-based client authentication and sender-constrained tokens π
6.1.4. Threat Analysis π§
this section revisits the attack scenarios and consequences from Section 5 and discusses potential additional defenses.
6.1.4.1. Attack Scenarios and Consequences π
if the attacker can run malicious code (like JavaScript or WASM) in the application's execution context, these attack scenarios become relevant:
- Proxying Requests via the User's Browser (Section 5.1.4)
note that this attack scenario results in the following consequences:
- Client Hijacking (Section 5.2.3)
client hijacking is an attack scenario that's built right into how browser-based apps work. so nothing can stop it except stopping the malicious code from running in the first place π things that help with that are following secure coding guidelines, code analysis, and deploying defense-in-depth mechanisms such as Content Security Policy [W3C.CSP3].
in this architecture, the BFF is a key component handling a bunch of security-specific jobs and proxy-based behavior. it's out of scope for this document to explain how to build a secure proxy-based app, but it's super important to remember that security vulnerabilities in the BFF can have a huge impact on the whole application π¨
finally, the BFF is uniquely placed to see all the traffic between the browser-based app and the resource servers π if a high-security app wants anomaly detection or rate limiting, the BFF would be the ideal spot for it. those restrictions can also help soften the consequences of client hijacking π¦
6.1.4.2. Mitigated Attack Scenarios π§
the other attack scenarios, listed below, are effectively mitigated by the BFF application architecture:
- Single-Execution Token Theft (Section 5.1.1)
- Persistent Token Theft (Section 5.1.2)
- Acquisition and Extraction of New Tokens (Section 5.1.3)
the BFF counters the first two scenarios by not exposing any tokens to the browser-based app. even if the attacker takes full control of the app, there are simply no tokens to steal β¨
the third scenario, where the attacker grabs a fresh access token (and optionally a refresh token) by running a silent flow, is mitigated by making the BFF a confidential client. even if the attacker manages to get an authorization code, they can't exchange it because they don't have the client credentials π plus, the use of PKCE guards against other attacks on the authorization code.
since refresh and access tokens are managed by the BFF and never exposed to the browser, these two consequences become irrelevant:
- Exploiting Stolen Refresh Tokens (Section 5.2.1)
- Exploiting Stolen Access Tokens (Section 5.2.2)
6.1.4.3. Summary π
the BFF architecture is a lot more complicated than a browser-only app. you have to deploy and operate a server-side BFF component, and this pattern requires all interactions between the app and the resource servers to be proxied through the BFF. depending on how you deploy it, that proxy behavior can put a real burden on the server-side components π© see Section 6.2.2.6 for extra notes if the BFF is acting as the resource server.
but because of the nature of the BFF pattern, it offers strong security guarantees π using a BFF also means the application's attack surface doesn't grow just because it uses OAuth. the only viable attack pattern left is hijacking the client app in the user's browser, which is a problem inherent to web applications.
this architecture is strongly recommended for business applications, sensitive applications, and applications that handle personal data π
6.2. Token-Mediating Backend π
this section describes the architecture of a browser-based app that relies on a backend component to handle OAuth responsibilities, getting tokens as a confidential client (as defined in Section 2.1 of [RFC6749]). the backend then gives the app the access token so it can talk to resource servers directly π¦
the token-mediating backend pattern is more lightweight than the BFF pattern (see Section 6.1), since it doesn't need to proxy every request and response between the app and the resource server. security-wise, the token-mediating backend is less secure than a BFF, but it still has big advantages over an OAuth client app running directly in the browser β¨
if an attacker can run malicious code inside the app, this architecture stops them from abusing refresh tokens (see "Single-Execution Token Theft" (Section 5.1.1) and "Persistent Token Theft" (Section 5.1.2)) or getting a fresh set of tokens (see "Acquisition and Extraction of New Tokens" (Section 5.1.3)). however, since the access token is directly exposed to the app, the attacker can steal it from client-side storage (see "Single-Execution Token Theft" (Section 5.1.1) and "Persistent Token Theft" (Section 5.1.2)) or ask the token-mediating backend for a fresh token (see "Proxying Requests via the User's Browser" (Section 5.1.4)). note that using HttpOnly cookies keeps the attacker from directly accessing the session state, which blocks the jump from access token theft to session hijacking π
6.2.1. Application Architecture πΊοΈ
+---------------+ +----------+ +----------+
| | | | | |
| Authorization | | Token | | Resource |
| Endpoint | | Endpoint | | Server |
| | | | | |
+---------------+ +----------+ +----------+
^ ^ ^
| (F)| |
| v |
| |
| +---------------------------+ |
| | | |
| | Token-Mediating Backend | |(J)
(D)| | | |
| +---------------------------+ |
| |
| ^ ^ ^ + |
| (B,I)| (C)| (E)| (G)| |
v v v + v v
+-----------------+ +-----------------------------------------+
| | (A,H) | |
| Static Web Host | +-----> | Browser |
| | | |
+-----------------+ +-----------------------------------------+
Figure 2: OAuth 2.0 Token-Mediating Backend Pattern
in this architecture, the browser-based code (like JavaScript or WASM) first gets loaded from a static web host into the browser (A), and then the app runs in the browser. the app checks with the token-mediating backend whether there's an active session (B). if there is one, the app gets the matching access token, picks up its authenticated state again, and skips ahead to (J).
if there's no active session, the app triggers a navigation to the token-mediating backend (C) to start the Authorization Code flow with the PKCE extension (described in Section 6.2.3.1). the backend answers by redirecting the browser to the authorization endpoint (D). when the user gets sent back, the browser hands the authorization code to the token-mediating backend (E), which can then exchange it for tokens at the token endpoint (F) using its client credentials and PKCE code verifier π§
the token-mediating backend links the tokens it got to the user's session (see Section 6.2.2.4) and sets a cookie in the response to keep track of that session (G). that response also makes the browser reload the app (H). when the app reloads, it checks with the token-mediating backend for an existing session (I), so it can pick up its authenticated state and grab the access token from the backend π
the app in the browser can then use the access token from (I) to make requests straight to the resource server (J) π
6.2.2. Implementation Details π
6.2.2.1. Session and OAuth Endpoints β¨
okay so most of the endpoints on the token-mediating backend are basically twins of the ones we already covered for a BFF π«Ά
- Check session endpoint π¦: this one is giving "is anyone home?" The browser-based application calls it ((B) and (I) in Figure 2). The request carries session information when it's available, so the backend can check for an active session. The response should tell the browser-based application whether the session is active. If there IS an active session, the backend includes the access token in the response. It can also add other stuff, like identity info about the logged-in user π
- Authorization Code flow starter (C) π: this endpoint is identical to the BFF one, so check Section 6.1.2.1 for the deets.
- Authorization code receiver (E) π§: also identical to the BFF version. See Section 6.1.2.1.
- Logout endpoint πΈ: same deal, identical to the BFF one. See Section 6.1.2.1 for more details.
6.2.2.2. Refresh Tokens π
When you're using refresh tokens (like in Section 4.14 of [RFC9700]), the token-mediating backend grabs the refresh token in (F) and ties it to the user's session π
If the resource server rejects the access token, the app can ping the token-mediating backend and ask for a new one. The backend relies on the cookies that came with that request to find the user's refresh token, then makes a token request using it. (These steps aren't drawn in Figure 2, fyi.) Note that this refresh token request comes from the backend, which is a confidential client, so it requires client authentication π
When the refresh token expires, there is literally no way to get a valid access token without starting a whole new Authorization Code grant π. So it makes sense to set the cookie-based session's lifetime to equal the refresh token's maximum lifetime, if you know that upfront. Also, when the backend learns that a refresh token for an active session is no longer valid, it makes sense to invalidate the session π
6.2.2.3. Access Token Scopes π
Depending on which resource servers are being accessed and how scopes are configured at the authorization server, the app might want access tokens with different scope configurations. That lets the app follow the best practice of using minimally scoped access tokens β¨
The app can tell the backend which scopes it wants when it checks for the active session ((A) and (I)). It's up to the backend to decide whether previously obtained access tokens fall within the desired scope criteria π§
Lowkey, this access token caching at the backend can cause scope-elevation risks if you apply it without thinking π¨. If the cached access token features a superset of the scopes the frontend asked for, the token-mediating backend SHOULD NOT return it to the frontend. Instead, it SHOULD use the refresh token to request an access token with the smaller set of scopes from the authorization server. Heads up: whether the authorization server supports this downscoping is totally at its discretion π€·
The backend can use a similar mechanism to the downscoping one when it relies on the extension described in [RFC8707] to get an access token for a specific resource server π§©
6.2.2.4. Cookie-Based Session State πͺ
Similar to the BFF, the token-mediating backend relies on browser cookies to keep track of the user's session. The implementation guidelines and security considerations for cookie-based sessions are the same as for a BFF, as discussed in Section 6.1.2.3 π
6.2.2.5. Combining OAuth and OpenID Connect π€
Just like a BFF, the token-mediating backend can choose to combine OAuth and OpenID Connect in a single flow. See Section 6.1.2.4 for more details β¨
6.2.2.6. Practical Deployment Scenarios π
Hosting the static JavaScript or WASM code is a separate job from handling the authorization server stuff. In Figure 2, the token-mediating backend and the static web host are shown as two separate entities. In real life, these can be deployed as a single service (the token-mediating backend serving the static code), as two separate services (a CDN plus a token-mediating backend), or as two components in a single service (static hosting plus serverless functions on a cloud platform) π
These deployment differences don't change the relationships described in this pattern, but they can impact other practicalities, like the need to properly configure CORS to enable cross-origin communication π
6.2.3. Security Considerations π‘οΈ
6.2.3.1. The Authorization Code Grant π
The main benefit of a token-mediating backend is that it can act as a confidential client, so the token-mediating backend MUST act as a confidential client. Furthermore, the token-mediating backend MUST use the OAuth 2.0 Authorization Code grant, as described in Section 2.1.1 of [RFC9700], to initiate a request for an access token.
6.2.3.2. Cookie Security πͺ
The token-mediating backend uses cookies to create a user session, which is directly associated with the user's tokens through either server-side or client-side session state. The same cookie security guidelines that apply to a BFF also apply here, as discussed in Section 6.1.3.2 π
6.2.3.3. Cross-Site Request Forgery Protections π«
Interactions between the browser-based application and the token-mediating backend rely on cookies for authentication and authorization. Just like a BFF, the token-mediating backend is required to account for CSRF attacks π€
Section 6.1.3.3 breaks down the nuances of the various mitigation strategies against CSRF attacks. For a token-mediating backend specifically, these CSRF defenses only apply to the endpoint or endpoints where the application can obtain its access tokens π―
6.2.3.4. Advanced OAuth Security π
The token-mediating backend is a confidential client running as a server-side component. It can adopt security best practices for confidential clients, such as key-based client authentication πͺ
6.2.4. Threat Analysis π
This section revisits the attack scenarios and consequences from Section 5 and discusses potential additional defenses π§
6.2.4.1. Attack Scenarios and Consequences π
If the attacker can execute malicious code in the application's execution context, these attack scenarios become relevant:
- Single-Execution Token Theft (Section 5.1.1) for access tokens
- Persistent Token Theft (Section 5.1.2) for access tokens
- Proxying Requests via the User's Browser (Section 5.1.4)
Note that these attack scenarios result in the following consequences:
- Exploiting Stolen Access Tokens (Section 5.2.2)
- Client Hijacking (Section 5.2.3)
Exposing the access token to the browser-based application is the core idea behind the architecture pattern of the token-mediating backend. As a result, the access token becomes vulnerable to token theft by malicious browser-based code π¨
6.2.4.2. Mitigated Attack Scenarios π‘οΈ
The other attack scenarios (listed below) are effectively mitigated by the token-mediating backend π
- Single-Execution Token Theft (Section 5.1.1) for refresh tokens
- Persistent Token Theft (Section 5.1.2) for refresh tokens
- Acquisition and Extraction of New Tokens (Section 5.1.3)
The token-mediating backend counters the first two by not exposing the refresh token to the browser-based application. Even when the attacker gains full control over the application, there are simply no refresh tokens to be stolen π ββοΈ
The third scenario, where the attacker obtains a fresh access token (and optionally a refresh token) by running a silent flow, is mitigated by making the token-mediating backend a confidential client. Even when the attacker manages to obtain an authorization code, they're prevented from exchanging it because they lack client credentials. Additionally, the use of PKCE prevents other attacks against the authorization code π
Because of the nature of the token-mediating backend, the following consequence of potential attacks becomes irrelevant:
- Exploiting Stolen Refresh Tokens (Section 5.2.1)
6.2.4.3. Additional Defenses β¨
While this architecture inherently exposes access tokens, there are some additional defenses that can help increase the security posture of the application π
6.2.4.3.1. Secure Token Storage ποΈ
Given the nature of the token-mediating backend pattern, there's no need for persistent token storage in the browser. When needed, the application can always use its cookie-based session to obtain an access token from the token-mediating backend. Section 8 gives more details on the security properties of various storage mechanisms in the browser π¦
Be aware that even when the access token is stored out of reach of malicious browser-based code, the malicious code can still mimic the legitimate application and send a request to the token-mediation backend to obtain the latest access token π»
6.2.4.3.2. Using Sender-Constrained Tokens π§©
Using sender-constrained access tokens isn't trivial in this architecture. The token-mediating backend is responsible for exchanging an authorization code or refresh token for an access token, but the application is the one that uses the access token. Using a mechanism such as DPoP [RFC9449] would require splitting responsibilities over two parties, which is not a scenario defined by the specification. Use of DPoP in such a scenario is out of the scope of this document π
6.2.4.4. Summary π¬
The architecture of a token-mediating backend is more complicated than a browser-only application, but less complicated than running a proxying BFF. Similarly, the security properties it offers lie somewhere between using a BFF and running a browser-only application βοΈ
A token-mediating backend addresses typical scenarios that grant the attacker long-term access on behalf of the user. However, because of the consequence of access token theft, the attacker still has the ability to gain direct access to resource servers π¬
When you're considering a token-mediating backend architecture, it is strongly recommended to evaluate whether adopting a full BFF, as discussed in Section 6.1, is a viable alternative. Only when the use cases or system requirements would prevent the use of a proxying BFF should the token-mediating backend be considered over a full BFF π«Ά
6.3. Browser-Based OAuth 2.0 Client π¦
okay bestie, this section is about a browser-based app that acts as the OAuth client and handles every single OAuth responsibility right there in the browser π . It grabs tokens straight from the authorization server with no backend involved β¨, no cap, that's a whole vibe.
If an attacker manages to run malicious code in the browser, this setup is open to every attack scenario we already covered (Section 5.1) π. The attacker can snag access tokens and refresh tokens from the authorization server π, which could mean long-term access to protected resources as the user π. yikes, so protect that browser π«Ά.
6.3.1. Application Architecture
+---------------+ +--------------+
| | | |
| Authorization | | Resource |
| Server | | Server |
| | | |
+---------------+ +--------------+
^ ^ ^ +
| | | |
|(B) |(C) |(D) |(E)
| | | |
| | | |
+ v + v
+-----------------+ +-------------------------------+
| | (A) | |
| Static Web Host | +-----> | Browser |
| | | |
+-----------------+ +-------------------------------+
Figure 3: Browser-Based OAuth 2.0 Client Pattern
okay so here's the flow π§. The code gets loaded from a static web host into the browser (A), and then the app runs in the browser. In this setup the browser-based app counts as a public client, which means it doesn't have client credentials to authenticate to the authorization server π.
The app gets an authorization code (B) by kicking off the Authorization Code flow with the PKCE extension (described in Section 6.3.2.1) π. Then it uses a browser API (e.g., [Fetch]) to send a POST request to the token endpoint (C) and swap that code for tokens π.
The app is then responsible for storing the access token and optional refresh token as securely as possible using appropriate browser APIs, as described in Section 8 π₯.
When the browser app wants to make a request to the resource server, it can talk to the resource server directly β¨. It includes the access token in the request (D) and gets the resource server's response back (E) π.
6.3.2. Implementation Details
Browser-based applications that are public clients (Section 2.1 of [RFC6749]) and use the Authorization Code grant type described in Section 4.1 of [RFC6749] MUST also follow the extra requirements in this section π . no skipping the fine print, bestie π.
6.3.2.1. The Authorization Code Grant
Browser-based applications that are public clients MUST implement the PKCE [RFC7636] extension when obtaining an access token, and authorization servers MUST support and enforce PKCE for such clients π.
PKCE is giving protection π‘οΈ. It prevents an attack where the authorization code gets intercepted and exchanged for an access token by a malicious client, because it gives the authorization server a way to verify that the client instance exchanging the code is the same one that started the flow π.
6.3.2.2. Cross-Site Request Forgery Protections
Browser-based applications MUST prevent CSRF attacks against their redirect URI π«. There are a few ways to make that happen, and any of these works:
- configuring the authorization server to require PKCE for this client
- using and verifying a unique value for the OAuth state parameter to carry a CSRF token
- using and verifying the OpenID Connect nonce parameter as described in [OpenID] if the application is also using OpenID Connect
See Section 2.1 of [RFC9700] for additional details on selecting a proper CSRF defense for the Authorization Code flow π.
6.3.2.3. Refresh Tokens
okay this one is spicy πΆοΈ. For browser-based clients, the refresh token is typically a bearer token unless the application explicitly uses DPoP [RFC9449]. So the risk of leaked refresh tokens is greater than the risk of leaked access tokens π, since an attacker may be able to keep using the stolen refresh token to get new access tokens, potentially without the authorization server noticing π.
Authorization servers may choose whether or not to issue refresh tokens to browser-based applications. However, with third-party cookie-blocking mechanisms in the mix, refresh tokens have become a lot more attractive π. [RFC9700] describes some additional requirements around refresh tokens on top of the recommendations of [RFC6749]. Applications and authorization servers conforming to this BCP MUST also follow the recommendations in [RFC9700] for refresh tokens if refresh tokens are issued to browser-based applications π«Ά.
In particular, authorization servers:
- MUST either rotate refresh tokens on each use OR use sender-constrained refresh tokens as described in Section 4.14.2 of [RFC9700]
- MUST either set a maximum lifetime on refresh tokens OR expire if the refresh token has not been used within some amount of time
- MUST NOT, upon issuing a rotated refresh token, extend the lifetime of the new refresh token beyond the lifetime of the initial refresh token if the refresh token has a preestablished expiration time
Limiting the overall refresh token lifetime to the lifetime of the initial refresh token ensures a stolen refresh token cannot be used indefinitely β³. that's the whole point, no drama π .
For example:
- A user authorizes an application, issuing an access token that lasts 10 minutes and a refresh token that lasts 8 hours.
- After 10 minutes, the initial access token expires, so the application uses the refresh token to get a new access token.
- The authorization server returns a new access token that lasts 10 minutes and a new refresh token that lasts 7 hours and 50 minutes.
- This continues until 8 hours pass from the initial authorization.
- At this point, when the application attempts to use the refresh token after 8 hours, the request will fail and the application will have to re-initiate an Authorization Code flow that relies on the user's authentication or previously established session π.
Authorization servers SHOULD link the lifetime of the refresh token to the user's authenticated session with the authorization server π. Doing so ensures that when a user logs out, previously issued refresh tokens to browser-based applications become invalid, which mimics a single-logout scenario πͺβ¨. Authorization servers MAY set different policies for refresh token issuance, lifetime, and expiration for browser-based applications compared to other public clients πΈ.
6.3.3. Security Considerations
6.3.3.1. Client Authentication
Since a browser-based application's source code is delivered to the end-user's browser, it is unfit to contain provisioned secrets π ββοΈ. As a consequence, browser-based applications are typically deployed as public clients, as defined by Section 2.1 of [RFC6749].
Secrets that are statically included as part of an app distributed to multiple users should not be treated as confidential secrets, because one user may inspect their copy and learn the shared secret π. For this reason, and those stated in Section 5.3.1 of [RFC6819], authorization servers MUST NOT require client authentication of browser-based applications using a shared secret, since that serves no value beyond client identification, which is already provided by the client_id parameter π.
Authorization servers that still require a statically included shared secret for SPA clients MUST treat the client as a public client and not accept the secret as proof of the client's identity π . Without additional measures, such clients are subject to client impersonation (see Section 6.3.3.2) π¬.
6.3.3.2. Client Impersonation
As stated in Section 10.2 of [RFC6749], the authorization server SHOULD NOT process authorization requests automatically without user consent or interaction, except when the authorization server can assure the identity of the client application πββοΈ.
If authorization servers restrict redirect URIs to a fixed set of absolute HTTPS URIs that prevent the use of wildcard domains, wildcard paths, or wildcard query string components, this exact match of registered absolute HTTPS URIs MAY be accepted by authorization servers as proof of identity of the client, for the purpose of deciding whether to automatically process an authorization request when a previous request for the client_id has already been approved π¦.
6.3.3.2.1. Authorization Code Redirect
Clients MUST register one or more redirect URIs with the authorization server and use only exact registered redirect URIs in the authorization request π.
Authorization servers MUST require an exact match of a registered redirect URI as described in Section 4.1.1 of [RFC9700]. This helps to prevent attacks targeting the authorization code π.
6.3.3.3. Security of In-Browser Communication Flows
In browser-based applications, it is common to execute the OAuth flow in a secondary window, such as a pop-up or iframe, instead of redirecting the primary window πͺ. In these flows, the browser-based app holds control of the primary window, for instance, to avoid page refreshes or to run frame-based flows silently π€«.
If the browser-based app and the authorization server are invoked in different frames, they have to use in-browser communication techniques like the postMessage API (a.k.a. [WebMessaging]) instead of top-level redirections π¨. To guarantee confidentiality and authenticity of messages, both the initiator origin and receiver origin of a postMessage MUST be verified using the mechanisms inherently provided by the postMessage API (Section 9.3.2 in [WebMessaging]) π.
Section 4.17 of [RFC9700] provides additional details about the security of in-browser communication flows and the countermeasures that browser-based applications and authorization servers MUST apply to defend against these attacks π‘οΈπ.
6.3.3.4. Cross-Origin Requests πΈ
Okay so bestie, in this setup the app uses a browser API to send requests to the authorization server AND the resource server π . Because of how OAuth 2.0 works, these are usually cross-origin requests, so the browser's cross-origin restrictions are gonna be all up in the business π. The authorization server and the resource server MUST send the necessary CORS headers (defined in [Fetch]) so the app can make those cross-origin requests β¨. Quick aside: in the extraordinary case where the browser-based OAuth client runs in the same origin as the authorization server or resource server, a CORS policy isn't needed for the interaction π.
For the authorization server, CORS matters most on the token endpoint, where the browser-based app swaps the authorization code for tokens ποΈ. If the authorization server also gives the app extra endpoints, like discovery metadata URLs, JSON Web Key Sets, dynamic client registration, revocation, introspection, or user info endpoints, the browser app might hit those too π§. So the authorization server is responsible for supporting CORS on these endpoints π.
This spec doesn't include guidelines for picking a concrete CORS policy π€·ββοΈ. It could be a wildcard origin or something more restrictive. Also, CORS has two modes with different security properties π€. The first mode applies to CORS-safelisted requests (formerly known as simple requests). The browser sends the request and uses the CORS response headers to decide if the response can be exposed to the client-side execution context. For non-CORS-safelisted requests, like one with a custom request header, the browser checks the CORS policy first with a preflight, and it only sends the actual request once the server gives its approval in the preflight response π¦.
Here's the tea: because of the authorization server's specific configuration, the CORS response to a preflight can be different from the CORS response to the actual request π³. During the preflight, the authorization server can only verify the provided origin. During the actual request, though, it has the full request data, like the client ID. So it can approve a known origin during the preflight and still reject the actual request after comparing the origin to this specific client's list of pre-registered origins π ββοΈ.
6.3.4. Threat Analysis π
This section revisits the attack scenarios and consequences from Section 5 and discusses potential additional defenses π‘οΈ.
6.3.4.1. Attack Scenarios and Consequences π
If the attacker can execute malicious code in the application's execution context, these attack scenarios become relevant:
- Single-Execution Token Theft (Section 5.1.1)
- Persistent Token Theft (Section 5.1.2)
- Acquisition and Extraction of New Tokens (Section 5.1.3)
- Proxying Requests via the User's Browser (Section 5.1.4)
The most dangerous attack scenario is the acquisition and extraction of new tokens π±. In this one, the attacker only interacts with the authorization server, which makes the actual implementation details of the OAuth functionality in the client irrelevant. Even if the legitimate client application finds a way to completely isolate the tokens from the attacker, the attacker will still be able to obtain tokens from the authorization server.
Note that these attack scenarios result in the following consequences:
- Exploiting Stolen Refresh Tokens (Section 5.2.1)
- Exploiting Stolen Access Tokens (Section 5.2.2)
- Client Hijacking (Section 5.2.3)
6.3.4.2. Additional Defenses π§±
While this architecture is inherently vulnerable to malicious browser-based code, there are some additional defenses that can help level up the application's security posture π. Just know that none of these defenses fix the underlying problem that lets the attacker run a new flow to obtain tokens π.
6.3.4.2.1. Secure Token Storage ποΈ
When handling tokens directly, the application can choose different storage mechanisms for access tokens and refresh tokens. Universally accessible storage areas, like Local Storage [WebStorage], are easier for malicious JavaScript to access than more isolated storage areas, like a Web Worker [WebWorker]. Section 8 discusses the different storage mechanisms and their trade-offs in more detail π.
A practical implementation pattern can use a Web Worker [WebWorker] to isolate the refresh token and provide the application with the access token it needs to make requests to resource servers π§. This prevents an attacker from using the application's refresh token to obtain new tokens β¨.
However, even a token storage mechanism that completely isolates the tokens from the attacker doesn't stop the attacker from running a new flow to get a fresh set of tokens (see Section 5.1.3) π.
6.3.4.2.2. Using Sender-Constrained Tokens π
Browser-based OAuth clients can implement DPoP [RFC9449] to transition from bearer access tokens and bearer refresh tokens to sender-constrained tokens π¦. In this kind of implementation, the private key used to sign DPoP proofs is handled by the browser (a non-extractable [CryptoKeyPair] is stored using [W3C.IndexedDB]). So DPoP effectively prevents scenarios where the XSS attacker exfiltrates the application's tokens (see Sections 5.1.1 and 5.1.2) π.
Note that DPoP doesn't stop the attacker from running a new flow to obtain a fresh access token (and optionally a refresh token); see Section 5.1.3. Even when DPoP is mandatory, the attacker can bind the fresh set of tokens to a key pair under their control. That lets them exfiltrate the sender-constrained tokens and use them by relying on the attacker-controlled key to calculate the necessary DPoP proofs π.
6.3.4.2.3. Restricting Access to the Authorization Server π§
The scenario where the attacker obtains a fresh access token and (optionally refresh token) Section 5.1.3 relies on the ability to directly interact with the authorization server from within the browser π΅οΈ. In theory, a defense that stops the attacker from silently interacting with the authorization server could solve the most dangerous attack scenario. In practice, though, those defenses are ineffective or impractical π©.
For completeness, this BCP lists a few options below. Note that none of these defenses is recommended, since they don't offer practically usable security benefits π ββοΈ.
The authorization server could block authorization requests that originate from within an iframe. While this would prevent the exact scenario from Section 5.1.3, it wouldn't work for slight variations of the attack. For example, the attacker can launch the silent flow in a pop-up window or a pop-under window. Plus, browser-only OAuth clients typically rely on a hidden iframe-based flow to bootstrap the user's authentication state, so this approach would seriously impact the user experience π.
The authorization server could also choose to make user consent mandatory in every Authorization Code flow (as described in Section 10.2 of [RFC6749]), thus requiring user interaction before issuing an authorization code. This would make it harder for an attacker to run a silent flow to obtain a fresh set of tokens. But it also seriously impacts the user experience by continuously requiring consent π΅. As a result, this approach leads to "consent fatigue", which makes it likely that the user will blindly approve the consent, even when it's tied to a flow that was initiated by the attacker π₯΄.
6.3.4.3. Summary π
To sum up, the architecture of a browser-based OAuth client application is straightforward, but it results in a significant increase in the application's attack surface π₯. The attacker is not only able to hijack the client but is also able to extract a full-featured set of tokens from the browser-based application π±.
This architecture is not recommended for business applications, sensitive applications, or applications that handle personal data π«.
7. Discouraged and Deprecated Architecture Patterns π
Client applications and backend applications have evolved a lot over the last two decades π°οΈ, along with threats, attacker models, and our general understanding of modern application security. As a result, previous recommendations that were generally accepted in the industry, as well as ones published by the OAuth Working Group, are often no longer recommended π¬. Proposed solutions often fall short of meeting the expected security requirements too.
This section discusses a few alternative architecture patterns that are not recommended for use in modern browser-based OAuth applications π«. It goes through each of the patterns along with a threat analysis that investigates the attack scenarios and consequences when relevant π.
7.1. Single-Domain Browser-Based Applications (Not Using OAuth) π
Too often, simple applications get made needlessly complex by using OAuth to replace the concept of session management π. A typical example is the modern incarnation of a server-side Model-View-Controller (MVC) application, which now consists of a browser-based frontend backed by a server-side API π§©.
In an application like that, using OpenID Connect to offload user authentication to a dedicated provider can significantly simplify the application's architecture and development π. However, using OAuth to govern access between the frontend and the backend is often not needed π€·ββοΈ. Instead of using access tokens, the application can rely on server-side cookie-based session state to keep track of the user's authentication status πͺ. The security guidelines to protect the session cookie are discussed in Section 6.1.3.2 π.
While the advice to not use OAuth seems out of place in this document, it's important to note that OAuth was originally created for third-party or federated access to APIs. So it may not be the best solution in a single common-domain deployment π. That said, there are still some advantages to using OAuth even in a common-domain architecture, such as:
- Allowing more flexibility in the future. For example, if you were to later add a new domain to the system, having OAuth already in place means that a new domain wouldn't require any additional rearchitecting.
- Being able to take advantage of existing library support rather than writing bespoke code for the integration.
- Centralizing login and multi-factor authentication support, account management, and recovery at the OAuth server rather than making it part of the application logic.
- Splitting responsibilities between authenticating a user and serving resources.
Using OAuth for browser-based applications in a first-party same-domain scenario provides these advantages, and it can be accomplished by any of the architectural patterns described above π¦.
7.1.1. Threat Analysis πΈοΈ
Due to the lack of using OAuth, this architecture pattern is only vulnerable to the following attack scenario: proxying requests via the user's browser (Section 5.1.4). As a result, this pattern can lead to the following consequence: client hijacking (Section 5.2.3) π.
7.2. OAuth Implicit Grant π¦
Okay so the OAuth 2.0 Implicit grant type (defined in Section 4.2 of [RFC6749]) works by having the authorization server hand out an access token right in the authorization response (front channel), with no authorization code exchange step π. The access token comes back in the fragment part of the redirect URI, which gives an attacker several chances to grab it and run π.
The security properties of the Implicit grant type make it no longer a recommended best practice π ββοΈ. To actually stop this flow, the authorization server MUST NOT issue access tokens in the authorization response, and MUST issue access tokens only from the token endpoint. Browser-based clients MUST use the Authorization Code grant type and MUST NOT use the Implicit grant type to get access tokens. No exceptions, bestie π .
7.2.1. Historic Note
Okay, history lesson π. Back in the day, the Implicit grant was actually kinda great for browser apps, because JavaScript could always read and change the fragment portion of the URL without triggering a page reload β¨. That was needed so the app could scrub the access token out of the URL after getting it. Plus, until CORS was widespread in browsers, the Implicit grant gave you a flow that didn't need CORS support in the browser or on the server. Iconic for its time π°οΈ.
Fast forward, and modern browsers have the Session History API (described in "Navigation and session history" of [HTML]) π. It lets you change the path and query string component of the URL without triggering a page reload. CORS is also super widespread now, and single-page apps use it for literally everything πΈ. So modern browser-based apps can use the OAuth 2.0 Authorization Code grant type with PKCE π¦. They can strip the authorization code out of the query string without a page reload thanks to the Session History API, and CORS support at the token endpoint means the app can still get tokens even when the authorization server lives on a different domain π.
7.2.2. Threat Analysis
The architecture pattern discussed in this section is vulnerable to the following attack scenarios π¨:
- Single-Execution Token Theft (Section 5.1.1)
- Persistent Token Theft (Section 5.1.2)
- Acquisition and Extraction of New Tokens (Section 5.1.3)
- Proxying Requests via the User's Browser (Section 5.1.4)
And as a result, this pattern can lead to the following consequences π©:
- Exploiting Stolen Refresh Tokens (Section 5.2.1)
- Exploiting Stolen Access Tokens (Section 5.2.2)
- Client Hijacking (Section 5.2.3)
7.2.3. Further Attacks on the Implicit Grant
Apart from the attack scenarios and consequences we already covered, there are a few more attacks that totally back up the deprecation of the Implicit grant type π. Lots of attacks on the Implicit grant described by [RFC6819] and Section 4.1.2 of [RFC9700] don't have sufficient mitigation strategies π. The sections below spell out the specific attacks that can't be mitigated while you keep using the Implicit grant type.
7.2.3.1. Manipulation of the Redirect URI
If an attacker can cause the authorization response to be sent to a URI under their control, they get direct access to the authorization response, including the access token π±. There are several ways to pull this off, all described in detail in [RFC9700].
7.2.3.2. Access Token Leak in Browser History
An attacker could get the access token from the browser's history π΅οΈββοΈ. The countermeasures recommended in [RFC6819] are limited to using short expiration times for tokens and noting that browsers should not cache the response. Neither of these fully prevents this attack; they only reduce the potential damage π₯².
It gets worse, though. Many browsers now sync browser history to cloud services and to multiple devices, which gives an even wider attack surface for pulling access tokens out of the URL π±.
This is discussed in more detail in Section 4.3.2 of [RFC9700] π.
7.2.3.3. Manipulation of Scripts
An attacker could modify the page or inject scripts into the browser in various ways, including when the browser's HTTPS connection is being intercepted by, for example, a corporate network π’. Attacks on the TLS layer are usually out of scope for basic security recommendations, but for browser-based apps they are much easier to pull off π¬. An injected script can get access to everything on the page π« .
The risk of a malicious script running on the page may be amplified when the application uses a known standard way of obtaining access tokens. Basically, the attacker can always look at the window.location variable to find an access token π. This threat profile is different from an attacker specifically targeting one application by knowing where or how an access token obtained via the Authorization Code flow may end up being stored π―.
7.2.3.4. Access Token Leak to Third-Party Scripts
It's super common to use third-party scripts in browser-based apps, like analytics tools, crash reporting, and even a social media "like" button π. The app's author may not be fully aware of all the code running in the app π€·ββοΈ. When an access token is returned in the fragment, it's visible to any third-party script on the page ποΈ.
7.2.4. Disadvantages of the Implicit Grant
There are several more reasons the Implicit grant type is a worse deal than the recommended Authorization Code grant type π.
- OAuth 2.0 provides no mechanism for a client to verify that a particular access token was intended for that client. This could lead to misuse and possible impersonation attacks if a malicious party hands off an access token it got some other way to the client π.
- Returning an access token in the front-channel redirect gives the authorization server no assurance that the token will actually end up at the application, since there are many ways this redirect can fail or be intercepted π.
- Supporting the Implicit grant type takes extra code, more upkeep, and an understanding of the related security considerations. Limiting the authorization server to just the Authorization Code grant type shrinks the attack surface of the implementation π§Ή.
- If the browser-based application gets wrapped into a native app, then [RFC8252] also requires the use of the Authorization Code grant type with PKCE anyway π±.
7.3. Resource Owner Password Credentials Grant
The Resource Owner Password Credentials Grant MUST NOT be used, as described in Section 2.4 of [RFC9700] π«. Instead, using the Authorization Code grant type and redirecting the user to the authorization server gives the authorization server a chance to prompt the user for secure, non-phishable authentication options π, take advantage of single sign-on sessions β¨, or use third-party identity providers πͺͺ. The Resource Owner Password Credentials Grant has none of that built in, so it would have to be extended with custom protocols π₯΄.
To conform to this best practice, browser-based applications using OAuth or OpenID Connect MUST use a redirect-based flow (e.g., the OAuth Authorization Code grant type) as described in this document π.
7.4. Handling the OAuth Flow in a Service Worker
Okay, so there's a pattern that tries to limit the attacker's ability to pull existing tokens or get a fresh set of tokens π§. It uses a Service Worker [W3C.service-workers] and has been suggested in the past π. Here's how it goes: the application's very first move on load is registering a Service Worker π οΈ. The Service Worker takes charge of running the Authorization Code flow to get tokens, and it attaches the proper access token to outgoing requests to the resource server π. On top of that, the Service Worker blocks the client application's code from making direct calls to the authorization server's endpoints π§. This restriction is aimed at the attack scenario "Acquisition and Extraction of New Tokens" (Section 5.1.3) π―.
The sequence diagram below shows how the client, the Service Worker, the authorization server, and the resource server interact π:
Service Resource Authorization
User Application Worker Server Server
| | | | |
| browse | | | |
|----------->| | | |
| |----------->| | /authorize |
| | |---------------------------------->|
| | | redirect w/ authorization code |
| | |< - - - - - - - - - - - - - - - - -|
| | | | |
| | | token request | |
| | | w/ auth code | /token |
| | |---------------------------------->|
| | |< - - - - - - - - - - - - - - - - -|
| | | | |
| | resource | | |
| | request | | |
| |----------->| | |
| | | resource request | |
| | | w/ access token | |
| | |------------------->| |
| | | | |
User Application Service Resource Authorization
Worker Server Server
Figure 4: OAuth 2.0 Service Worker Pattern
Note that this pattern never exposes the tokens to the application running in the browser π. The Service Worker runs in an isolated execution environment, so there's no shared memory and no way for the client application to influence how the Service Worker runs π.
7.4.1. Threat Analysis
The architecture pattern discussed in this section is vulnerable to the following attack scenarios π¨:
- Acquisition and Extraction of New Tokens (Section 5.1.3)
- Proxying Requests via the User's Browser (Section 5.1.4)
And as a result, this pattern can lead to the following consequences π©:
- Exploiting Stolen Refresh Tokens (Section 5.2.1)
- Exploiting Stolen Access Tokens (Section 5.2.2)
- Client Hijacking (Section 5.2.3)
7.4.1.1. Attacking the Service Worker β¨
Okay so Service Workers look SO promising on paper π₯Ή, and they do come with real security perks, but we need to talk about their limits π . To fully protect the app against the attack scenarios in Section 5.1 π‘οΈ, the Service Worker needs to meet two security requirements:
- Prevent an attacker from exfiltrating tokens.
- Prevent an attacker from acquiring a new set of tokens.
Once it's registered, the Service Worker runs an Authorization Code flow and gets the tokens π. Since it keeps track of tokens in its own isolated execution environment, they're out of reach for any application code, including potentially malicious code π. So the Service Worker meets the first requirement and stops token exfiltration. That basically neutralizes the first two attack scenarios from Section 5.1. Iconic π
For the second requirement, the Service Worker must be able to guarantee that an attacker controlling the legitimate application cannot run a new Authorization Code grant. That attack is covered in Section 5.1.3. Because of how Service Workers work, the registered Service Worker can block all outgoing requests that start such a new flow, even when they happen in a frame or a new window π¦.
BUT, the malicious code running inside the application can unregister this Service Worker π. Unregistering has a big functional impact on the app, so the browser doesn't treat it lightly π«Ά. Instead, an unregistered Service Worker gets marked as such, while all of its currently running instances stay active until their browsing context is terminated (like closing the tab or window). So even if an attacker unregisters it, the Service Worker is still running and can keep the attacker from reaching the authorization server π.
Here's where it gets messy. Once you unregister a Service Worker, it won't be present when a new browsing context opens π. So if the attacker unregisters it first and then starts a new flow in a frame, there's no Service Worker attached to that frame's browsing context. The attacker can then run their own new Authorization Code grant, pull the authorization code out of the frame's URL, and exchange it for tokens. Not me being shocked π±. In essence, the Service Worker fails the second requirement and is vulnerable to the scenario where the attacker acquires a new set of tokens (Section 5.1.3).
Because of these shortcomings, plus the significant complexity of registering and maintaining a Service Worker, this pattern is not recommended π«.
Finally, the use of a Service Worker by itself does not increase the attack surface of the app πΈ. In practice, people often use Service Workers to retrofit a legacy app so it can attach OAuth access tokens to outgoing requests. In those cases the Service Worker doesn't change the app's security properties. It just makes development and maintenance easier π§.
8. Token Storage in the Browser β¨
When a browser-based app handles OAuth access tokens or refresh tokens directly, it becomes responsible for storing them, either temporarily or persistently πΎ. That means the app has to decide how to manage the tokens (for example, in-memory versus persistent storage) and what steps to take to keep the tokens isolated from the main application code π. This section walks through a few storage mechanisms and their properties π. The recommendations here account for the unique properties of OAuth tokens, and some of them may overlap with general browser security advice π.
When we talk about the security properties of browser-based token storage, it's important to understand what an attacker can do once they've compromised a browser-based app π΅οΈββοΈ. Like in earlier discussions, there are two main attack scenarios to keep in mind:
- The attacker obtaining tokens from storage.
- The attacker obtaining tokens from the provider (for example, the authorization server or the token-mediating backend).
Here's the thing: the attacker's code becomes indistinguishable from the legitimate app's code. So the attacker can always request tokens from the provider in exactly the same way the legitimate app does π€. That means not even a completely isolated token storage solution can stop the second threat, where the attacker requests tokens from the provider.
That said, the different security properties of browser-based storage solutions do affect how easily an attacker can grab existing tokens from storage π.
8.1. Cookies
Browser cookies are both a storage mechanism and a transport mechanism πͺ. The browser handles both automatically through the corresponding request and response headers. That means cookies get stored in the browser and get included on outgoing requests automatically, as long as they match the cookie's domain, path, or other properties π.
Next to header-based control over cookies, browsers also offer a JavaScript Cookie API for getting and setting cookies. People often mistake this Cookie API for an easy way to store data in the browser π. In that setup, the JavaScript code stores a token in a cookie so it can grab the token later and put it in the Authorization header of an API call. The problem is that the cookie is tied to the domain of the browser-based app, so the browser will also send that cookie, token and all, whenever it makes a request to the server on that domain. A classic example is the browser loading the app again after a previous visit ((A) in Figure 3) π.
Because of these unintentional side effects of using cookies for JavaScript-based storage, this practice is NOT RECOMMENDED β.
Note that this is different from using cookies in a BFF (discussed in Section 6.1.3.2). There, the cookie is inaccessible to JavaScript and is meant to be sent to the backend π«Ά.
8.2. Token Storage in a Service Worker
A Service Worker [W3C.service-workers] gives you a fully isolated environment to keep track of tokens π°. The tokens are inaccessible to the client application, so they're effectively protected against exfiltration π‘οΈ. To keep those tokens secure, the Service Worker can't share them with the application. So whenever the app wants to do something with a token, it has to ask the Service Worker to perform that operation and hand back the result π.
When trying to isolate tokens from the application's execution context, the Service Worker MUST NOT store tokens in any persistent storage API that is shared with the main window π«. For example, right now IndexedDB is shared between the browsing context and the Service Worker, so it's not a good place for the Service Worker to persist data that should stay hidden from the main window π ββοΈ. Which means the Service Worker currently doesn't have access to an isolated persistent storage area π©.
As we covered before, using a Service Worker doesn't stop an attacker from obtaining a new set of tokens π« . Similarly, if the application is the one getting tokens from the authorization server and then handing them to a Service Worker for management, the attacker can perform the same operation as the legit app to get those tokens too π.
8.3. Token Storage in a Web Worker
The app can use a Web Worker [WebWorker], which creates an almost identical scenario to the Service Worker one we just talked about π―. The difference between them is their level of access and their runtime behavior. Service Workers can intercept and modify outgoing requests, while Web Workers are really just a way to run background tasks π§ββοΈ. Web Workers are ephemeral and disappear when the browsing context closes, while Service Workers are persistent services registered in the browser π¦.
The security properties of a Web Worker are identical to those of a Service Worker. Once tokens are exposed to the application, they're vulnerable. When tokens need to be used, the operation relying on them has to be carried out by the Web Worker π.
One common way to isolate the refresh token is to use Web Workers π. In that setup, the app starts an Authorization Code flow from a Web Worker. The authorization code from the redirect gets forwarded to the Web Worker, which exchanges it for tokens. The Web Worker keeps the refresh token in memory and sends the access token to the main app, which uses it however it likes π. When the app needs to run a refresh token flow, it asks the Web Worker to do it, and then the app gets a fresh access token π.
In this scenario, the app's own refresh token is effectively protected against exfiltration, but the access token is not π¬. And nothing stops an attacker from getting their own tokens by running a new Authorization Code flow (Section 5.1.3) π.
8.4. In-Memory Token Storage
Another option is keeping tokens in memory without any persistent storage π§ . This limits how exposed the tokens are, since they only live in the current execution context π·. The downside is that tokens can't persist between page loads π.
In a JavaScript execution environment, you can make in-memory token storage even safer by using a closure variable to shield the token from direct access π. With closures, the token is only reachable through the predefined functions inside the closure, like a function that makes a request to the resource server π.
Closures work great in simple, isolated environments, but they're tricky to secure in something as complex as the browser's execution environment πͺοΈ. For example, a closure relies on a bunch of outside functions to do its work, like toString or networking APIs. With prototype poisoning, an attacker can swap those functions for malicious versions, so the closure's future operations end up using them π. Inside the malicious function, the attacker can get access to the function arguments, which may expose the tokens from inside the closure to the attacker π.
8.5. Persistent Token Storage πΎ
okay so the persistent storage APIs that browsers actually have as of this writing are localStorage [WebStorage], sessionStorage [WebStorage], and IndexedDB [W3C.IndexedDB] ποΈ let's break them down bestie π
localStorage πΈ sticks around between page reloads and is shared across all tabs. It's accessible to the entire origin and it lives for the long term π but here's the tea: it does nothing to protect you from malicious JavaScript. An attacker running code inside the same origin can just read whatever is in localStorage π It's also a synchronous API, which means it blocks every other piece of JavaScript until the operation finishes. not iconic π
sessionStorage π¦ is basically the same vibe as localStorage, except its lifetime is tied to the browser tab. It's also not shared between multiple tabs open to pages on the same origin, which slightly reduces how exposed the tokens are π§
IndexedDB π is a persistent storage mechanism like localStorage, but it's shared between multiple tabs as well as between the browsing context and Service Workers. It's also an asynchronous API, which is preferred over the synchronous localStorage API. slay π
Note that the main difference between these patterns is how exposed the data is, but none of these options can fully mitigate token exfiltration when the attacker can execute malicious code in the application's execution environment. So the real takeaway: different vibes, same risk if someone's running malicious code in your app π«Ά
8.6. Filesystem Considerations for Browser Storage APIs ποΈ
In all cases, as of this writing, there is no guarantee that browser storage is encrypted at rest. ngl this one is scary π± That behavior could potentially expose tokens to attackers who can read files on disk. Those attacks need capabilities way beyond browser-based apps, sure, but this topic highlights an important attack vector against modern apps β¨ More and more malware is built specifically to crawl users' machines and hunt for browser profiles to grab high-value tokens and session cookies, which leads to account-takeover attacks π
While a browser-based app can't stop these attacks itself, it can mitigate the consequences by keeping data confidential with encryption π The [W3C.WebCryptoAPI] gives JavaScript a way to generate a secret key, plus an option for that key to be non-exportable. A JavaScript app could then use this API to encrypt and decrypt tokens before storing them. Here's the catch, though: the [W3C.WebCryptoAPI] specification only ensures that the key is not exportable to the browser code. It doesn't place any requirements on how the key itself is stored with the operating system π So a non-exportable key can't be relied on as a way to protect against exfiltration from the underlying filesystem.
To protect tokens from filesystem exfiltration, the encryption keys would need to live somewhere other than the filesystem, like on a remote server. That adds new complexity for a purely browser-based app, so it's out of scope for this document π§
9. Security Considerations π‘οΈ
9.1. Reducing the Authority of Tokens π
A general security best practice in the OAuth world is to minimize the authority associated with access tokens. This applies to all the architectures discussed in this specification. Concretely, the following considerations can help minimize the authority of access tokens:
- Reduce the lifetime of access tokens and rely on refresh tokens for access token renewal
- Reduce the scopes or permissions associated with the access tokens
- Restrict access tokens to a single resource using the extension described in [RFC8707]
When OpenID Connect is used, it is important to avoid sensitive information disclosure through the claims in the ID Token. The authorization server SHOULD NOT include any ID Token claims that aren't used by the client π ββοΈ
9.2. Sender-Constrained Tokens π
As discussed throughout this document, the use of sender-constrained tokens does not solve the security limitations of browser-only OAuth clients. However, when the level of security offered by a token-mediating backend (Section 6.2) or a browser-only OAuth client (Section 6.3) suffices for the use case at hand, sender-constrained tokens can be used to enhance the security of both access tokens and refresh tokens. One method of implementing sender-constrained tokens in a way that is usable from browser-based applications is DPoP [RFC9449] β¨
When using sender-constrained tokens, the OAuth client has to prove possession of a private key in order to use the token, so the token isn't usable by itself π If a sender-constrained token is stolen, the attacker wouldn't be able to use the token directly, and they would also need to steal the private key. In essence, using sender-constrained tokens shifts the challenge of securely storing the token to securely storing the private key. Ideally, the app should use a non-exportable private key, such as one generated with the [W3C.WebCryptoAPI]. With an unencrypted token in the browser storage protected by a non-exportable private key, an XSS attack would not be able to extract the key, so the token would not be usable by the attacker π‘οΈπ
If the application is unable to use an API that generates a non-exportable key, the app should take measures to isolate the private key from its own execution context. The techniques for doing so are similar to using a secure token storage mechanism, as discussed in Section 8 π§©
While a non-exportable key is protected from exfiltration from within the JavaScript context, exfiltration of the underlying private key from the filesystem is still a potential attack vector. At the time of writing, there is no guarantee made in [W3C.WebCryptoAPI] that a non-exportable key is actually protected by a Trusted Platform Module (TPM) or stored in an encrypted form on disk. Exfiltration of the non-exportable key from the underlying filesystem may still be possible if the attacker can get access to the filesystem of the user's machine, for example via malware. This effectively makes the potential attack vector equivalent to a session hijacking attack ππ¨
9.3. Authorization Server Mix-Up Mitigation β οΈ
Authorization server mix-up attacks are a severe threat to every client that supports at least two authorization servers. Section 4.4 of [RFC9700] provides additional details about mix-up attacks and the countermeasures mentioned above π
9.4. Isolating Applications Using Origins ποΈ
Many of the web's security mechanisms rely on origins, which are defined as the triple <scheme, hostname, port>. For example, browsers automatically isolate browsing contexts with different origins, limit resources to certain origins, and apply CORS restrictions to outgoing cross-origin requests π
Therefore, it is considered a best practice to avoid deploying more than one application in a single origin. An architecture that only deploys a single application in an origin can leverage these browser restrictions to increase the security of the application. Plus, having a single origin per application makes it easier to configure and deploy security measures such as CORS, CSP, etc. π
10. IANA Considerations π
This document has no IANA actions π
11. References π
11.1. Normative References π
- [Fetch] WHATWG, "Fetch", WHATWG Living Standard,
<https://fetch.spec.whatwg.org/>. Commit snapshot:<https://fetch.spec.whatwg.org/commit-snapshots/d9c7b6d31067a7eb13110950ef67db54c6362cfa/>. - [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997,
<https://www.rfc-editor.org/info/rfc2119>. - [RFC6749] Hardt, D., Ed., "The OAuth 2.0 Authorization Framework", RFC 6749, DOI 10.17487/RFC6749, October 2012,
<https://www.rfc-editor.org/info/rfc6749>. - [RFC6750] Jones, M. and D. Hardt, "The OAuth 2.0 Authorization Framework: Bearer Token Usage", RFC 6750, DOI 10.17487/RFC6750, October 2012,
<https://www.rfc-editor.org/info/rfc6750>. - [RFC7636] Sakimura, N., Ed., Bradley, J., and N. Agarwal, "Proof Key for Code Exchange by OAuth Public Clients", RFC 7636, DOI 10.17487/RFC7636, September 2015,
<https://www.rfc-editor.org/info/rfc7636>. - [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017,
<https://www.rfc-editor.org/info/rfc8174>. - [RFC8252] Denniss, W. and J. Bradley, "OAuth 2.0 for Native Apps", BCP 212, RFC 8252, DOI 10.17487/RFC8252, October 2017,
<https://www.rfc-editor.org/info/rfc8252>. - [RFC8707] Campbell, B., Bradley, J., and H. Tschofenig, "Resource Indicators for OAuth 2.0", RFC 8707, DOI 10.17487/RFC8707, February 2020,
<https://www.rfc-editor.org/info/rfc8707>. - [RFC9449] Fett, D., Campbell, B., Bradley, J., Lodderstedt, T., Jones, M., and D. Waite, "OAuth 2.0 Demonstrating Proof of Possession (DPoP)", RFC 9449, DOI 10.17487/RFC9449, September 2023,
<https://www.rfc-editor.org/info/rfc9449>. - [RFC9700] Lodderstedt, T., Bradley, J., Labunets, A., and D. Fett, "Best Current Practice for OAuth 2.0 Security", BCP 240, RFC 9700, DOI 10.17487/RFC9700, January 2025,
<https://www.rfc-editor.org/info/rfc9700>. - [W3C.service-workers] Chintala, M., Ed. and Y. Yanagisawa, Ed., "Service Workers Nightly", W3C Recommendation, 26 January 2026,
<https://www.w3.org/TR/2026/CRD-service-workers-20260126/>. Latest version available at<https://www.w3.org/TR/service-workers/>. - [WebMessaging] WHATWG, "HTML - Cross-document messaging", WHATWG Living Standard,
<https://html.spec.whatwg.org/#web-messaging>. Commit snapshot:<https://html.spec.whatwg.org/commit-snapshots/b4b94b1db80aa55a143e1f74f6ad9d4a185f2333/#web-messaging>.
11.2. Informative References πβ¨
okay bestie, here's the extra reading list π these are the informative refs, so grab them when you want more context π§
[COOKIES] van Kesteren, A. and J. Hofmann, "Cookies: HTTP State Management Mechanism", Work in Progress, Internet-Draft, draft-ietf-httpbis-layered-cookies-02, 21 May 2026, https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-layered-cookies-02.
[CryptoKeyPair] Mozilla Developer Network, "CryptoKeyPair", https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair.
[HTML] WHATWG, "HTML", WHATWG Living Standard, https://html.spec.whatwg.org/. Commit snapshot: https://html.spec.whatwg.org/commit-snapshots/b4b94b1db80aa55a143e1f74f6ad9d4a185f2333/.
[OpenID] Sakimura, N., Bradley, J., Jones, M., de Medeiros, B., and C. Mortimore, "OpenID Connect Core 1.0 incorporating errata set 2", 15 December 2023, https://openid.net/specs/openid-connect-core-1_0-errata2.html.
[OWASPCheatSheet] "OWASP Cheat Sheet", https://cheatsheetseries.owasp.org/.
[RFC6265bis] Bingler, S., Ed., West, M., Ed., and J. Wilander, Ed., "Cookies: HTTP State Management Mechanism", Work in Progress, Internet-Draft, draft-ietf-httpbis-rfc6265bis-22, 1 December 2025, https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-22.
[RFC6819] Lodderstedt, T., Ed., McGloin, M., and P. Hunt, "OAuth 2.0 Threat Model and Security Considerations", RFC 6819, DOI 10.17487/RFC6819, January 2013, https://www.rfc-editor.org/info/rfc6819.
[SessionFixation] "Session Fixation", https://owasp.org/www-community/attacks/Session_fixation.
[Site] Mozilla Developer Network, "Site", https://developer.mozilla.org/en-US/docs/Glossary/Site.
[W3C.CSP3] West, M., Ed. and A. Sartori, Ed., "Content Security Policy Level 3", W3C Working Draft, 11 February 2026, https://www.w3.org/TR/2026/WD-CSP3-20260505/. Latest version available at https://www.w3.org/TR/CSP3/.
[W3C.IndexedDB] Becker, S., Ed., "Indexed Database API 3.0", W3C Working Draft, 13 August 2025, https://www.w3.org/TR/2025/WD-IndexedDB-3-20250813/. Latest version available at https://www.w3.org/TR/IndexedDB/.
[W3C.SRI] Braun, F., Ed., "Subresource Integrity", W3C Working Draft, 10 July 2025, https://www.w3.org/TR/2025/WD-sri-2-20250710/. Latest version available at https://www.w3.org/TR/sri-2/.
[W3C.wasm-core-2] Rossberg, A., Ed., "WebAssembly Core Specification", W3C Candidate Recommendation Draft, 10 February 2026, https://www.w3.org/TR/2026/CRD-wasm-core-2-20260210/. Latest version available at https://www.w3.org/TR/wasm-core-2/.
[W3C.WebCryptoAPI] Huigens, D., Ed., "Web Cryptography Level 2", W3C First Public Working Draft, 22 April 2025, https://www.w3.org/TR/2025/WD-webcrypto-2-20250422/. Latest version available at https://www.w3.org/TR/webcrypto-2/.
[WebStorage] WHATWG, "HTML - Web storage", WHATWG Living Standard, https://html.spec.whatwg.org/#webstorage. https://html.spec.whatwg.org/commit-snapshots/b4b94b1db80aa55a143e1f74f6ad9d4a185f2333/#webstorage.
[WebWorker] WHATWG, "HTML - Web workers", WHATWG Living Standard, https://html.spec.whatwg.org/#workers. Commit snapshot: https://html.spec.whatwg.org/commit-snapshots/b4b94b1db80aa55a143e1f74f6ad9d4a185f2333/#workers.
Acknowledgements π«Ά
bestie, the authors gave a huge shoutout to William Denniss and John Bradley π, whose recommendations for native applications basically shaped a ton of the best practices for browser-based applications π₯.
The authors also wanted to thank Hannes Tschofenig and Torsten Lodderstedt, the attendees of the Internet Identity Workshop 27 session where this BCP was first proposed πΈ, and the following individuals who contributed ideas, feedback, and wording that shaped and formed the final specification π:
Andy Barlow, Andy Newton, Annabelle Backman, Brian Campbell, Brock Allen, Christian Mainka, Damien Bowden, Daniel Fett, Deb Cooley, Elar Lang, Emmanuel Gautier, Erik Kline, Γric Vyncke, Eva Sarafianou, Filip Skokan, George Fletcher, Hannes Tschofenig, Janak Amarasena, John Bradley, Joseph Heenan, Justin Richer, Karl McGuinness, Karsten Meyer zu Selhausen, Leo Tohill, Louis Jannett, Marc Blanchet, Martin Thomson, Matthew Bocci, Mike Bishop, Mike Jones, Mohamed Boucadair, Orie Steele, Qin Wu, Rifaat Shekh-Yusef, Roman Danyliw, Sean Kelleher, Thomas Broyer, Thomas Fossati, Tomek Stojecki, Torsten Lodderstedt, Vittorio Bertocci, Watson Ladd, William Duncan, and Yannick Majoros. iconic cast πβ¨
Authors' Addresses π
Aaron Parecki Okta Email: aaron@parecki.com URI: https://aaronparecki.com
Philippe De Ryck Pragmatic Web Security Email: philippe@pragmaticwebsecurity.com
David Waite Ping Identity Email: dwaite@pingidentity.com
π¦ where this page comes from
- ietf: rfc10017 2026-08
- π girlified by claude-haiku-5-5 on : a language model's rewrite of the original, under its terms