Implementing double submit CSRF in microservices

Hi. This is the twenty-forth part of the diary about developing the “Programmers’ diary” blog. Previous part: https://hashnode.programmersdiary.com/implement-https-in-microservices-and-fe. The open source code of this project is on: https://github.com/TheProgrammersDiary. The first diary entry explains why this project was done: https://medium.com/@vievaldas/developing-a-website-with-microservices-part-1-the-idea-fe6e0a7a96b5.

Next entry:

—————

2024-01-26-27

Let’s continue developing web security by implementing CSRF. We will be using double submit token. It works like this: there are two tokens - one CSRF token is stored in cookies, another in e.g. memory. Both are sent when requests which require authentication are made. If tokens do not match, the request fails.

Since microservices are being used, creating a CSRF cookie would require additional measures to share it (e.g. new entry in Redis). CSRF tokens are related with authentication, so let’s place them inside JWT tokens. If a hacker is able to send a request to our website it will be able to send CSRF token inside JWT cookie, but fail to get CSRF token which is in memory and therefore a request should fail.

Let’s start the implementation by updating the Security lib:

In JwtToken class we generate a CSRF token:

public static String generateCsrfToken() {
        byte[] randomBytes = new byte[32];
        new SecureRandom().nextBytes(randomBytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
}

Then we simply add CSRF token as a JWT claim:

.claim("csrfToken", generateCsrfToken())

and add a getter for it:

public String csrfToken() {
        return Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload().get(
                "csrfToken", String.class
        );
}

We update JwtFilter by adding a CSRF validation method:

private boolean csrfDoubleSubmitTokenIsValid(HttpServletRequest request, JwtToken token) {
        return request.getHeader("X-CSRF-TOKEN").equals(token.csrfToken());
}

and placing it alongside token validation:

if (token.tokenIsValid() && csrfDoubleSubmitTokenIsValid(request, token)) {

In ITTests the CSRF token needs to be provided to the JwtFilter inside the request:

private void login(JwtToken token) {
        try {
            jwtFilter.doFilterInternal(
                        new FakeHttpServletRequest(
                        Map.of("X-CSRF-TOKEN", token.csrfToken()),
                        new Cookie[]{new Cookie("jwt", token.value())}
                        ),

After that, we increase the lib’s minor version.

You can check how this update fits all Security lib code: https://github.com/TheProgrammersDiary/Security/commit/7e4954.

We have created a CSRF token inside Security lib, now let’s make use of it.

In Post microservice:

We increase Security lib version in pom.xml to 2.3.0.

Then we modify CORS:

config.setAllowedMethods(List.of("GET", "POST", "OPTIONS"));
config.setAllowedHeaders(List.of("Origin", "Content-Type", "Accept", "Authorization", "X-CSRF-TOKEN"));

CORS must also accept OPTIONS as a request method and X-CSRF-TOKEN as a header.

Spring’s provided CSRF remains disabled since we have implemented a double-submit token CSRF.

You can check how the modifications fit in the whole code: https://github.com/TheProgrammersDiary/Post/commit/fe36db.

Now let’s move to the Blog microservice.

In blog/monolith service we increase Security lib version to 2.3.0

Then we modify CORS the same way as in Post microservice, leave Spring’s CSRF disabled, add CSRF in /login response body using the newly implemented csrfToken method from Security lib:

.put("csrf", token.csrfToken())

This will allow the JavaScript in the FE to read the CSRF token and store it in memory.

Lastly, we add:

"&csrf=" + URLEncoder.encode(token.csrfToken(), StandardCharsets.UTF_8)

to the query parameters of redirection response after successful OAuth login so JavaScript can read the query parameters and store CSRF token in memory.

Now we need to fix our GlobalTests repo. [Had some fun doing tests between microservices but now have some annoying trouble. :D]

In the repo we need to extract CSRF from login response and send it in X-CSRF-TOKEN header when creating a post.

In FE we will need to retrieve CSRF token from /login and store it to send in future requests. To do that we create CsrfProvider client component:

"use client"

import React, { createContext, useContext, useState } from 'react';

const AppContext = createContext(null);

export function CsrfProvider ({
  children
}: {
  children: React.ReactNode
}) {
  const [csrf, setCsrf] = useState(null);

  return (
    <AppContext.Provider value={{ csrf, setCsrf }}>
      {children}
    </AppContext.Provider>
  );
}

export const useAppContext = () => {
  return useContext(AppContext);
};

This will save csrf token to memory. To use it, wrap layout with CsrfProvider like this: https://github.com/TheProgrammersDiary/Frontend/commit/f5e23d0d2dfb9acb3e5789859e66d38efe2ea889#diff-eca96d2c09f31517696a26e1d0be4070e1fbab02831481bed006e275741d030bR25. With this wrap NextAuthProvider will be able to use CSRF if needed.

In login page, change the onSubmit function:

async function onSubmit(data, event) {
    event.preventDefault();
    const body = {"username": data.username, "password": data.password};
    await fetch(
      blogUrl + "/users/login",
      { 
        method: "POST",
        body: JSON.stringify(body),
        headers: { "Content-Type": "application/json" },
        credentials: "include"
      }
    )
    .then(response => response.json())
    .then(response => {
      setCsrf(response.csrf);
      signIn('credentials', { 
        username: response.username,
        redirect: false
      });
      router.push("/");
    });
  }

We extract CSRF from response body and save it to memory by calling setCsrf (don’t forget to import CsrfProvider). We also use:

redirect: false

in signIn method and instead use router since Next-auth redirection will reload the page, discarding the CSRF token.

We also change OAuth login:

export default function AuthLoginSuccess() {
  const searchParams = useSearchParams();
  const username = searchParams.get('username');
  const csrf = searchParams.get('csrf');
  const { setCsrf } = useAppContext();
  const router = useRouter();

  useEffect(() => {
    setCsrf(csrf);
    signIn('credentials', {
      username: username,
      redirect: false,
    }).then(() => {
      router.push('/');
    });
  }, []);

  return <p>Successfully logged in!</p>;
}

Once CSRF is saved, we can start using it - change /logout:

export default function Logout() {
    const { csrf } = useAppContext();
    const router = useRouter();
    useEffect(() => {
        const effect = async () => {
            await fetch(
                blogUrl + "/users/logout",
                {
                    method: "POST",
                    credentials: "include",
                    headers: {"X-CSRF-TOKEN": csrf}
                }
            ).then(_ => {
                signOut({redirect: false});
                router.push("/");
            });
        }
        effect();
    }, []);
}

Similarly update /post/create endpoint. Calling /users/logout and /post/create sends a CSRF token to backend.

Lastly, we update the creation of post unit test to mock CsrfProvider’s app context:

vi.mock('../../CsrfProvider', () => ({
      useAppContext() {
        return {
          csrf: "csrf"
        };
      },
}));

We have implemented double submit CSRF token.

—————

Thanks for reading.

The project logged in this diary is open source, so if you would like to code or suggest changes, please visit https://github.com/TheProgrammersDiary.

You can check out the website: https://www.programmersdiary.com/.

Next part coming up.

0
Subscribe to my newsletter

Read articles from Evaldas Visockas directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Evaldas Visockas
Evaldas Visockas