Programmersdiary security upgrades

Hi. This is the twenty-eigth part of the diary about developing the “Programmers’ diary” blog. Previous part: https://hashnode.programmersdiary.com/programmersdiary-blog-a-short-day. 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-02-02

In previous part a flag was added to hide the HTTP body and headers in logs to prevent credentials being stolen if log database was compromised.

However, path is not hidden and there is no point in hiding it. But there is one issue: in current CSRF token design when we redirect from oauth2 login the CSRF token is shown in the path. So if an attacker peeks into the logs we will be able to see CSRF token and then do CSRF attacks on users.

In this part let’s secure the CSRF token by providing it in cookies (Yummy).

So we have this:

    response.sendRedirect(
            frontendUrl + "/auth_login_success" +
                    "?username=" + URLEncoder.encode(username) +
                    "&csrf=" + URLEncoder.encode(token.csrfToken())
    );

Let’s add a cookie:

    ResponseCookie csrfCookie = ResponseCookie.from("csrf", token.csrfToken())
            .httpOnly(false)
            .secure(true)
            .maxAge(Duration.ofMinutes(2))
            .path("/")
            .build();

We can’t use HTTP only (which would dissalow JS interaction with the cookie) since we need JS to provide it when HTTP request is made. This is not security issue because of double submit token design (another CSRF token is in JWT cookie, attacker would need to send both CSRF tokens. The JWT cookie is HTTP only, attacker can’t read the JWT token because of it and can’t forge it cuz its encrypted.) Here is how the design was implemented in this software: https://hashnode.programmersdiary.com/implementing-double-submit-csrf-in-microservices.

Now let’s change that redirection response to this:

response.sendRedirect(frontendUrl + "/auth_login_success");

Exposed CSRF cookie in path has been relocated. Username is also gone from path, being instead provided in a similar non HTTP-only cookie.

The cookies need to be added to a header:

response.addHeader(HttpHeaders.SET_COOKIE, csrfCookie.toString());
// Also add username cookie.

For FE to handle this change we install js-cookie library and extract data from cookies instead of query parameters:

"use client"

import "../../globals.css";
import { signIn } from 'next-auth/react';
import Cookies from 'js-cookie';
import { useRouter } from 'next/navigation';
import { useAppContext } from '../MemoryStorage';

export default function AuthLoginSuccess() {
  const username = Cookies.get("username").replace(/\\+/g, ' ');
  const csrf = Cookies.get("csrf");
  const { setCsrf, setLoginType } = useAppContext();
  const router = useRouter();

  setCsrf(csrf);
  setLoginType("oauth");
  signIn('credentials', {
    username: username,
    redirect: false,
  }).then(() => {
    Cookies.remove("username");
    Cookies.remove("csrf");
    router.push('/');
  });

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

The line:

const username = Cookies.get("username").replace(/\\+/g, ' ');

replaces any occurrences of the + sign with a space (' '). Spaces are encoded as +, so this converts spaces back.

OK, we have secured the CSRF token and updated the FE to handle the new approach.

One other thing. I was strugling on how to delete HttpOnly cookie on logout - since JS cannot access and delete the cookie. I have found a way how to do it on server side: in blog microservice on logout we can send a server response telling browser to overwrite the cookie with a new one which has maxAge set to 0:

@PostMapping("/logout")
    void logout(HttpServletRequest request, HttpServletResponse response) {
        JwtToken
                .existing(request, key.value(), blacklistedJwtTokenRepository)
                .ifPresentOrElse(
                        blacklistedJwtTokenRepository::blacklistToken,
                        () -> {
                            throw new RuntimeException("Possible security issue. Logout is missing jwt token.");
                        }
                );
        ResponseCookie deleteCookie = ResponseCookie.from("jwt", "")
                .httpOnly(true)
                .secure(true)
                .maxAge(0)
                .path("/")
                .build();
        response.addHeader(HttpHeaders.SET_COOKIE, deleteCookie.toString());
    }

Make sure the names of the cookies match to overwrite.

Here is the full code of all today’s changes: https://github.com/TheProgrammersDiary/Blog/commit/dcd801174b1e2d729903b5f8618992310013cfc4.

By the way, the upper part of logout method containing blacklistToken method was described here: https://hashnode.programmersdiary.com/login-session-in-nextauth-with-jwt.

Although we blacklist the tokens in the future I am planning to remove the blacklist to improve efficiency and reduce code complexity. Since the tokens are short-lived the security tradeoff should be minimal.

—————

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.

Next part: https://hashnode.programmersdiary.com/storing-data-in-minio-making-posts-editable.

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