A hacker implementing OWASP guidelines

There seems to be a debate as to whether an HTTP cookie or HTML local storage should be used in single page apps in order to store access tokens such as JWT or OAuth tokens. It appears to be agreement that a cookie-based implementation for session identifiers would be 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 just gathered the following resources that will help you authorize users with cookies in your 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, you just make sure the auth server will set the following cookie flags as it is described next.

Flag Value Description
HttpOnly true Helps mitigate the risk of client side script accessing the cookie. Read more.
Secure true Prevents from being observed by unauthorized parties due to the transmission of the cookie in clear text. Read more.
SameSite strict Prevents 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.

Figura 1

Figure 1. Alice before logging into the SPA

Figura 2

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 for reading! If you liked this post please share it with your friends and fellow developers.

You may also be interested in...

Previous Post