Jordi Bassaganas
Jordi Bassaganas

Jordi Bassaganas

How to Store JWT and OAuth Access Tokens as per OWASP Guidelines

Photo by Hack Capital on Unsplash

How to Store JWT and OAuth Access Tokens as per OWASP Guidelines

A cookie-based implementation is better than a local storage one

Jordi Bassaganas's photo
Jordi Bassaganas
·Feb 26, 2021·

4 min read

Play this article

There seems to be a debate when it comes to using an HTTP cookie or HTML local storage in single page apps (SPA) to store access tokens such as JWT or OAuth. It appears to be agreement that a cookie-based implementation for session identifiers is more secure than its local storage counterpart.

The other day I decided that I'd do some research about mitigating XSS attacks and CSRF attacks. I soon found myself searching the Web for terms like OWASP, SPA, JWT, OAuth and security with little success at the beginning so I thought that I'd write this post to help out in your search too.

OWASP's HTML5 Security Cheat Sheet is clear in that respect:

Do not store session identifiers in local storage as the data is always accessible by JavaScript. Cookies can mitigate this risk using the httpOnly flag.

Let me take a stance on the cookie vs localStorage vs sessionStorage debate to suggest go for the cookie implementation as per OWASP guidelines. I gathered the following resources on how to authorize users with cookies in SPA web apps, please sit back and feel comfortable to read:

With a cookie the access token is sent back and forth between the client and the server automatically because this is how the HTTP protocol works, just make sure the auth server will set the following cookie flags as described next.

FlagValueDescription
HttpOnlytrueHelps mitigate the risk of client side script accessing the cookie. Read more.
SecuretruePrevents from being observed by unauthorized parties due to the transmission of the cookie in clear text. Read more.
SameSitestrictPrevents the cookie from being sent by the browser to the target site in all cross-site browsing context. Read more.

As you may know I've built a few side-projects — Frankenstein apps — for the sake of experimentation and testing. Warthog is one of such experiments available on GitHub using the following tech stack: React, Redux, PHP, MySQL, Laravel, ACLs (access control lists) with JWT and Docker. Warthog allows foodies to review restaurants.

It aimed to be an all-in-one web app I suppose but hopefully it'll be useful to illustrate these concepts in practice.

Now, let's look at an example.

This is how the access token cookie is set by the auth server after Alice, an editor with moderation permissions, is successfully logged in to the app.

figure-01.png Figure 1. Alice before logging into the SPA

figure-02.png Figure 2. Application panel in Chrome DevTools showing cookies after successfully logging in

As you can see in Figure 2, in this particular example two cookies are sent to the browser: access_token and session.

The former is an HttpOnly JWT token which cannot be accessed through client side script whereas the latter is the so-called GUI session sent as a non-HttpOnly cookie — this one contains non-sensitive information such as the current user's role and we do want it to be read by our JavaScript code, for example to render React components based on that.

This is how the secure, httponly, and samesite cookie flags are set:

Set-Cookie: access_token=eyJ...tNQ; expires=Sat, 27-Feb-2021 21:51:21 GMT; Max-Age=28800; path=/; secure; httponly; samesite=strict

The Set-Cookie HTTP response header is used to send a cookie from the server to the user agent, so the user agent can send it back to the server later.

This way:

  • The access token won't be read by client side script (XSS mitigation)
  • The browser won't send the token over an unencrypted HTTP request
  • The token won't be sent along with requests initiated by third party websites (CSRF mitigation)

More specifically, here is a basic example on how an auth server may look like implemented with PHP — this is a Laravel controller using the Symfony\Component\HttpFoundation\Cookie class.

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Cookie;

class AuthController extends Controller
{
    const COOKIE_ACCESS_TOKEN = 'access_token';
    const COOKIE_SESSION = 'session';

    public function login()
    {
        $credentials = request(['email', 'password']);

        if (!$token = auth()->attempt($credentials)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        $session = [
            'role' => auth()->user()->getAttributes()['role'],
        ];

        return response(null, 204)
            ->cookie(
                self::COOKIE_ACCESS_TOKEN,  // name
                $token,                     // value
                480,                        // minutes
                null,                       // path
                null,                       // domain
                true,                       // secure
                true,                       // HttpOnly
                false,                      // raw
                Cookie::SAMESITE_STRICT     // SameSite
            )->cookie(
                self::COOKIE_SESSION,
                json_encode($session),
                480,
                null,
                null,
                true,
                false,
                false,
                Cookie::SAMESITE_STRICT
            );
    }

    public function logout()
    {
        $accessTokenCookie = \Cookie::forget(self::COOKIE_ACCESS_TOKEN);
        $sessionCookie = \Cookie::forget(self::COOKIE_SESSION);

        return response(null, 204)
                    ->withCookie($accessTokenCookie)
                    ->withCookie($sessionCookie);
    }
}

Conclusion

OWASP is clear about how to store session identifiers in single-page applications; remember, localStorage is vulnerable to XSS attacks because it can be read using JavaScript. HTTP cookies can mitigate that risk if set properly, make sure to set HttpOnly to true, Secure to true and SameSite to strict.

This is all for now, thank you so much for reading!

Did you find this article valuable?

Support Jordi Bassaganas by becoming a sponsor. Any amount is appreciated!

Learn more about Hashnode Sponsors
 
Share this