Blog Posts
Where to Store OAuth Access Tokens
First blog entry in a while because I've been busy with other things, not the least being Crossauth, my cross-platform web application authentication framework.
It is in fact Crossauth that has prompted this post: where should you store OAuth2 access tokens?
For those less familiar with OAuth, it is a authorization protocol that results in bearer tokens being issued. I've written much more on OAuth in my book, Secure Web Application Development, but the important point for this discussion is the tokens it issues. A bearer token is used in the Authorization
HTTP header, for example:
Authoritation: Bearer my-super-secret-access-token
This differs from cookie-based session authorization. In the latter, the server sends the client a cookie. The client doesn't have to do anything special with a cookie and, if the HttpOnly
cookie flag is set, Javascript can't see it anyway. The browser automatically sends it with each request and the server checks it is valid. Browsers however do not automatically sent Authorization
headers. This is up to the client to add, therefore the client needs to store the token.
If you have a server-side-rendered application, this is easy. Since it is server code that is making requests to the OAuth resource, it is the server that stores the token. This is (hopefully) easy to keep secret as it is not transmitted to the browser. If you have a JavaScript client though, it needs to be able to attach the access token in its requests to the OAuth resource.
The obvious solution is to simply store the access token in a Javasript variable. Then your code that does a fetch
to make a request to the OAuth resource fetches the token from this variable and adds it in an Authorization
header.
There are two problems with this approach:
- If you refresh the page, the variable is cleared. Then you have to log in again. This is also true if you close and reopen the tab.
- Malicious code, eg XSS code that is uploaded through a vulnerability in your site or malicious third-party Javascript libraries you may have installed, may be able to read the variable
An approach that addresses problem 1 is to put the tokens in local or session storage. However that, if anything, makes problem 2 worse.
So where can store them securely. There are a few ways. I will describe 3:
- Javascript closures
- Cookies
- The Token Handler Pattern (or Backend-for-Frontend)
Javascript closures
Some object oriented-languages have the concept of private member variables. These can only be accessed from methods defined in that class. This makes them useful for storing tokens.
Typescript kind of has the concept of private variables, but it only isssues a compile-time warning. It is not enforced and, anyway, it compiles down to Javascript which does not have private variables. We can, however, simulate them with closures.
Closures are a way of encapsulating a varibale inside an anonymous function so that nothing else can access them. Consider the following code:
const oauthresource = (function () {
let accessToken = undefined;
return {
function setAccessToken(val) {
accessToken = val;
},
function get(onSuccess, onFailure) {
accessToken = val;
fetch(url, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + access_token,
},
})
.then(onSuccess)
.catch(onFailure);
},
}
})();
The variable oauthresource
is set to a function that, when called, returns an object contains methods that are safe to expose. Notice the ()
at the end of the oauthresource
definition. The variable is set to what the function returns when it is called, not to the function itself. This is called an Immediately Invoked Function Expression or IIFE.
When we receive an access token, we put it into oauthresource
with something like
oauthresource.setAccessToken(oauthCallThatFetchesAccessToken(...));
When we want to fetch the OAuth resource, we do
oauthresource.get(mySuccessFunction, myFailureFunction)
Obviously, if you are are using asynchronous Javascript, you can implement something similar with async ... await
instead of then
and catch
.
Using this approach, it should not be possible for any code external to oauthresource
to access the access token. This solves problem 2 above, namely tokens being available to malicious code, but not problem 1: persisting the tokens across page refreshes. Still, for some applications, this approach is sufficient.
Cookies
If you have control over the OAuth server, you could store tokens in cookies and let the browser send them automatically rather than add them to the Authorization
header manually.
This addresses the problem of token persistence after browser refresh, and even after closing and reopening the page. However, it goes against the OAuth specification. According to that specification, the token should be returned as part of the Json response. the client should then send it in the Authorization
header when requesting a resource. However, if you have full control over the authorization server, resource server and client, this is a viable option.
The authorization server should set any tokens it issues in a cookie instead of or in addition to sending it in the Json response. The resource server should check the cookie for the token rather than the Authorization
header. The client doesn't have to do anything as the browser is responsible for sending the cookie.
There are a few gotchas. Firstly, you should always set HttpOnly
when creating the cookie to ensure it cannot be read in Javascript. Otherwise you do not solve the problem of malicious code being able to access your tokens. Secondly, you should only send the cookie over HTTPS and preferably also set the Secure
flag.
If making requests to the resource server using fetch
, you will need to set the credentials
option to include
or same-origin
(the former will instruct the browser to always attach cookies to requests for the site it is created for, the latter only from pages loaded from the site that created the cookie, ie the authorization server).
Extra steps are also needed if your client page is not loaded from the same origin as the authorization server, or the authorization and resource servers are on different origins. Recall that origin means the same protocol, hostname and port combination. Consider the following general case. Your client is at https://myclient.com
. You request an access token from https://authorization-server.com
and you use this token to access resources on https://resource-server.com
. You may have the authorization server and resource server at the same origin, or all three may be at the same origin, but let's look at the general case.
As it is the authorization server that sends cookies, the browser will only send cookies to that domain. If the resource server is different, it will not get the cookies. If your resource server is different from the authorization server, you will have to create an endpoint on the resource server to receive the access token in the Json body of a POST
request. When this endpoint is called with a valid access token in the body, it sends it back using Set-Cookie
. This means you will temporarily have to store the cookie received from the authorization server in a variable. So long as that variable is scoped inside a block, eg a function, this is secure enough. If that is not the case, you should use the closure method in the previous section.
Whether or not the authorization and resource servers are at the same origin, if the client is at a different origin then the browser will not attach the cookie to requests unless the cookie has SameSite
set to none
or lax
. If set to none
, the requests to the resource server will contain the cookie, irrespective how how it is called (top-level page, fetch, etc). For fetch
of course, this is only true if you set credentials
to include
.
If your requests to the authorization server are always top-level page loads, you can set SameSite
to lax
instead and gain some security. For API calls, however, this is generally not the case.
Depending on the developers' current mood, browsers sometimes refuse to send cookies with SameSite
set to none
unless Secure
is also set. Chrome has flip-flopped on this policy recently so it is something to watch for. In production of course this is rarely an issue but for testing and development, it means you will need to create a self-signed certificate to run your development server over HTTPS.
Finally, even with the credentials
option set, the browser will still not send the cookie without enabling CORS on the resource server. Again, I go into this in detail in my book but, by default, browsers will not send cookies in cross-origin requests unless the server explicitly allows it (the so-called Same-Origin Policy or SOP). CORS, which stands for Cross-Origin Resource Sharing, is a header specification that overrides this. For the browser to send a cookie cross-origin, the server must have set the Access-Control-Allow-Credentials
header.
There is another gotcha. Since browsers automatically send cookies, it leaves your resource server open to CSRF attacks. For this reason, you should also require a CSRF token.
Personally, I do not like access tokens in cookies. Firstly, they go against the OAuth specification. Secondly, they require custom code if the authorization and resource servers are at different origins. Thirdly, it makes them pretty much the same as cookie based session management. There is a middle road solution though which I quite like: using cookies for only refresh tokens.
Refresh Tokens
Refresh tokens are an optional feature of OAuth. In addition to an access token, an authorization server can issue a refresh token. Like access tokens, this also has an expiry time but is typically longer than that of the access token (though not necessarily so, which I will come back to). When the access token expires or is nearing expiry, the client initiates the OAuth refresh token flow (grant type refresh_token
), sending the authorization server (not the resource server) the refresh token that was previously issued to it. If it is valid, the authorization server responds with a new access token.
Optionally it can also send a new refresh token. This is called refresh token rotation and it means refresh tokens can have a shorter expiry time.
One compromise approach is to store your access tokens in a Javascript closure and store the refresh token in a cookie. As the refresh token is both set and received by the authorization server, not the resource server, it avoids the issues resulting from the authorization and resource server being on different origins. It doesn't avoid the issue of the client and authorization server being on different origins but that problem can be solved by cookie settings and CORS on the authorization server.
If the browser tab is refreshed, or closed and reopened, the access token will be lost but you can write your Javascript client to automatically request a new one so long as the refresh token has not expired. This can be done without interaction with the user.
The Token Handler Pattern
If you are writing a pure client-side application, in my view a refresh token stored in a cookie is the best compromise, with CSRF tokens implemented for the refresh token flow. The security is not unacceptably compromised, it breaks the OAuth specification a little on the authorization server but not on the resource server.
However, if you do have server-side rendering, the Token Handler Pattern in my view is the best one. You can also implement this pattern if you have a single-page application (SPA) if you are using a framework such as Next.js, Nuxt or Sveltekit which also have server-side code.
Another term for the Token Handler Pattern is Backend-for-Frontend or BFF. This, in my view, is a more descriptive title. Rather than your client initiating requests for an access token directly, it accesses a URL server-side (on the same server the client was served from). This backend endpoint initiates the request to the authorization server (for the authorization code flow this will have to be a 302 Found
redirect but for the client credentials or password flow, the server could do a POST
fetch
directly to the authorization server). In the case of the authorization code flow, the backend server has a URL to receive tokens and this is what will have previously been set as the Redirect Uri (see the OAuth specification for details on this).
When the authorization server calls your backend client's Redirect Uri, the code implementing that endpoint will store the tokens it receives, associating them with the user's session. Of course, this means that your client must have session management configured but, depending on how you create session IDs, this does not necessarily means you need user authentication between your client frontend and backend. Most web frameworks offer session data storage out of the box or as optional extensions (Django, Fastify, Next.js, Nuxt.js and Sveltekit all do, for example).
Now when you want to access a resource on the resource server, you need an intermediate endpoint on your client backend. Your frontend makes a request to this endpoint. Your backend code looks up the access token associated with the session and adds it in an Authorization
header before relaying the request to the resource server, and relaying the response back to the frontend.
The following diagranm illustrates the approach.

It should be noted that this approach does use cookies between the client front- and backends in order to store the tokens associated with the frontend session. Therefore, your calls from the client frontend to the client backend should require CSRF tokens. These do not need to be passed to the authorization and resource servers.
Conclusion
So which should you choose? Personally, unless I want to write a client which has no backend other than to servera Javascript application, eg Apache or Nginx with no web development framework, I would choose the Token Handler Pattern/BFF. The server-side code is easy to implement, the OAuth specification can be implemented as-is (in fact, the authorization and resource servers do not have to be aware you choose this approach), the security is good and you don't have to worry about CORS. You do have to implement sessions and CSRF tokens between the client frontend and backend, but your framework probably provides that (and my Crossauth implementation does too). The cost, apart from the session management, is an extra hop to access resources.
If you do have a pure JavaScript client and, for one reason or another, can't or don't want to implement server-side functionality, my choice would be to use a cookie for the refresh token and keep the access token (and ID token if you have that too) in a closure on the front end.
If you have no control over the authorization server and it doesn't support cookies for refresh tokens, your only choice is closures. If you really do want persistence and neither BFF nor cookies are an option for you, use session storage if presistence between sessions is not needed or local storage if it is, but be aware of the risk of other code accessing the tokens (in the case of local storeage, code in other tabs can as well).