If Tokens Must Reach the Browser

When a browser-only OAuth client is unavoidable, RFC 10017 does not offer a magic storage location that makes the public origin confidential. It offers a disciplined contract: use the right authorization flow, protect the redirect and token lifecycle, choose storage according to the asset and persistence requirement, and reduce the authority of every credential that remains exposed. This chapter follows that contract from authorization to expiry, then compares browser storage mechanisms by the attacker action each can and cannot address.

The browser-only lifecycle is a protocol contract

A browser-only client performs all OAuth responsibilities in the browser. The application obtains an authorization code from the authorization server, exchanges it for tokens, stores those tokens as securely as the available browser APIs allow, and sends the access token when calling the resource server. This arrangement can be straightforward to deploy, but its simplicity comes from leaving the browser in possession of the credentials. It is therefore a public client, not a confidential one.

The public-client status has a direct consequence: the application cannot safely contain a provisioned shared secret. Its source code is delivered to the end user, and one user can inspect the copy they received. A string included in that copy may identify the client, but it cannot prove to the authorization server that a request came from trusted application logic. An authorization server that still receives such a static secret must not treat it as proof of the client’s identity. Client authentication and client identification are different functions here.

The baseline flow is the Authorization Code grant with PKCE. The browser begins an authorization transaction and creates a code challenge tied to that transaction. The authorization server returns a code through the registered redirect URI. The browser then sends the code and the corresponding verifier to the token endpoint, which can check that the party redeeming the code has the transaction-specific proof. PKCE helps against authorization-code interception and injection: an attacker who merely obtains a code should not be able to redeem it without the verifier.

That protection has a clear stopping point. PKCE protects the authorization code transaction; it does not protect an access token or refresh token after issuance, and it does not stop malicious code already executing in the application’s origin from initiating a new transaction. A public client can be correctly using PKCE and still have a serious token-exposure problem. The protocol is necessary, but it is not a substitute for an authority boundary.

The redirect URI is part of that boundary. Clients must register one or more redirect URIs and use only exact registered values in authorization requests. Authorization servers must require an exact match. Wildcard domains, paths, or query components weaken the ability to associate a response with the intended client and can create opportunities to redirect an authorization code or access token to an unintended location. This is not a deployment detail to be relaxed because a frontend has many environments; each relaxation changes what the authorization server can know about the destination.

The redirect also needs a CSRF defense. RFC 10017 identifies several ways to provide it: the authorization server can require PKCE for the client; the client can create and verify a unique transaction-specific state value bound to the user agent; or an application using OpenID Connect can use and verify the corresponding nonce. The precise choice depends on the flow and the authorization-server capabilities. What matters is that a response arriving at the redirect URI is tied to a transaction the application actually initiated, rather than accepted because it merely contains a plausible code.

Browser-only applications sometimes run an OAuth flow in a popup or iframe to avoid reloading the main page or to bootstrap authentication silently. That changes the communication path but not the need for authentication and origin checks. When the application and authorization server operate in different frames, they use browser messaging such as postMessage instead of a top-level redirect. The initiator origin and receiver origin must both be verified. A message is not trustworthy merely because it arrived from a window that the application expected to exist; the sender and receiver relationship has to be checked using the origin information provided by the browser.

Cross-origin requests are another part of the lifecycle. A browser-only client typically calls a token endpoint and one or more resource servers on different origins.