Implementing JWT token authorization in FE

Hi. This is the fifteenth part of the diary about developing the “Programmers’ diary” blog. The open source code of this project is on https://github.com/TheProgrammersDiary. The fourteenth part: https://hashnode.programmersdiary.com/user-sign-in-with-jwt-in-spring.

The first diary entry contains explanation on why this project was done: https://medium.com/@vievaldas/developing-a-website-with-microservices-part-1-the-idea-fe6e0a7a96b5.

Next entry:

—————

2023-11-11:

As of now we can send a login request in Postman and receive a JWT token.

However, to make use of the JWT token login we need to be able to receive the token to authorize our requests in FE.

I have researched a bit that one of the safe ways to store the JWT token to prevent misuse is to include it in HttpOnly cookies. These cookies can’t be read by JavaScript and therefore we are safe from a wide range of malicious attacks.

In UserController class we modify login endpoint:

@PostMapping("/login")
public ResponseEntity<Date> login(@RequestBody User user, HttpServletResponse response) {
    Authentication authentication = authManager.authenticate(
            new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())
    );
    SecurityContextHolder.getContext().setAuthentication(authentication);
    String jwt = jwtUtils.generateJwtToken(authentication);
    ResponseCookie cookie = ResponseCookie.from("jwt", jwt)
            **.httpOnly(true)**
            //.secure(true) //TODO: uncomment then HTTPS is enabled.
.maxAge(Duration.ofMinutes(10))
            .path("/")
            .build();
    response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
    return ResponseEntity.ok(jwtUtils.expirationDate(jwt));
}

The main point is set a response cookie of type http only. It will expire after 10 minutes for security measures. The path is “/” which means the token cookie can be included in any further request from browser’s JavaScript if necessary. We have not yet implemented HTTPS so secure option is disabled (secure option would not allow sending the token if connection between client and the server is HTTP instead of HTTPS).

Instead of sending jwt as String we send the expiration date (since we won’t be able to read the expiration date of HttpOnly cookie from JavaScript). JwtUtils class code:

public Date expirationDate(String token) {
    return Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload().getExpiration();
}

If we need to debug the JWT token with Postman we will be able to do so: Postman has Cookies tab:

For FE we will need to change the cross-origin annotation:

@CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true")

. If we don’t include allowCredentials the CORS will fail, failing the FE request.

In FE, let’s add a form handler when login button is pressed:

async function onSubmit(data, event) {
    event.preventDefault();
    const body = {"username": data.username, "password": data.password};
    await fetch(
      "<http://localhost:8080/users/login>",
      { 
        method: "POST",
        body: JSON.stringify(body),
        headers: { "Content-Type": "application/json" },
        credentials: "include",
      }
    ).then(_ => {
      router.push("/");
});

Don’t forget to link the function in the form tag:

<form className="mt-6" onSubmit={handleSubmit(onSubmit)}>

OK, so this will request the BE to send the JWT token cookie. After receiving it, the browser will save it as a HttpOnly cookie protecting it from JavaScript reads.

Now we can test if JWT token works (if we have authorization) by making comment requests.

In CommentController of blog microservice we need to allowCredentials so FE requests can succeed:

@CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true")

Then in FE, list-comments endpoint needs to include credentials. This orders the browser to send the HttpOnly cookie (JavaScript can’t read it but it can order the browser to send it):

await fetch(
    "<http://localhost:8080/comments/list-comments/>" + postId,
    { method: "GET", **credentials: "include",** }
)

Also, then creating new comments, jwt token also needs to be included:

await fetch(
    "<http://localhost:8080/comments/create>",
    {
        method: "POST",
        body: JSON.stringify(body),
        headers: { "Content-Type": "application/json" },
        credentials: "include",
    }
);

Since JWT token is included in cookies instead of being sent as a String, we will need to update JWT extraction method:

private Optional<String> parseJwt(HttpServletRequest request) {
    return Optional
            .ofNullable(request.getHeader("Authorization"))
            .map(this::parseJwtFromHeader)
            .orElse(parseJwtFromCookies(request.getCookies()));
}

private Optional<String> parseJwtFromHeader(String authHeader) {
    if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
        return Optional.of(authHeader.substring(7));
    }
    return Optional.empty();
}

private Optional<String> parseJwtFromCookies(Cookie[] cookies) {
    if(cookies == null) {
        return Optional.empty();
    }
    return Arrays
            .stream(cookies)
            .filter(cookie -> cookie.getName().equals("jwt"))
            .findFirst()
            .map(Cookie::getValue);
}

The checking for Authorization headers is not removed: let’s not prevent the option of including JWT token in Authorization headers.

Lastly, security config needs to be created:

.requestMatchers(HttpMethod.OPTIONS)
.permitAll()

Then Content-type of Http request is json, the FE will send a preflight request (Http Method OPTIONS), to check if the endpoint deals with the appropriate format. However, since the preflight request does not have the JWT cookies (with which we authorize the request) and all endpoints (except sign-up, login and Prometheus) are secured, the request will fail. So we need to permit all OPTIONS requests without authorization. This should be generally safe, since we will not respond with our domain details in OPTIONS requests.

With this being done JWT token authentication/authorization is implemented in FE.

—————

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/.

There will be more content.