Setting Http only JWT cookie from backend: Spring Boot and React JS

42 Views Asked by At

I am working on a full-stack project using Spring Boot and React JS. For security, I am using JWT Access Token with HTTP only cookie.
The token is generated and set at the time of login-

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;

@Component
public class CookieUtil {

    public static void create(HttpServletResponse httpServletResponse, String name, String value, Boolean secure, Integer maxAge, String domain) {
        Cookie cookie = new Cookie(name, value);
        cookie.setSecure(secure);
        cookie.setHttpOnly(true);
        cookie.setMaxAge(maxAge);
        cookie.setDomain(domain);
        httpServletResponse.addCookie(cookie);
    }

    public static void clear(HttpServletResponse httpServletResponse, String name) {
        Cookie cookie = new Cookie(name, null);
        cookie.setMaxAge(1);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setDomain("localhost");
        httpServletResponse.addCookie(cookie);

    }
}

Now, when testing with Postman client, I am able to successfully set the cookies-

enter image description here

But, when using React JS frontend application, the cookie does not get set.

import { useState } from 'react';
import './Login.css'
import axios from "axios";
import { apiPrefixV1, baseUrl } from '../../constants/AppConstants';
import { login as authLogin } from '../../redux/slices/authSlice';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
const Login = () => {
    const navigate = useNavigate()
    const dispatch = useDispatch()
    const [username, setUsername] = useState('')
    const [password, setPassword] = useState('')
    const [error, setError]=useState('')

    const handleUsernameChange = (event) => {
        setUsername(event.target.value);
    };

    const handlePasswordChange = (event) => {
        setPassword(event.target.value);
    };

    const login = async (event) => {
        event.preventDefault();

        console.log('Email:', username);
        console.log('Password:', password);

       try{
        const response=await axios.post(
            `${baseUrl}/${apiPrefixV1}/consumer/login`,{
                "username": username,
                "password": password
            }
        )

      console.log(response)

        if(response.data['code']===2000){
            const userData=response.data['data'];
            if(userData) dispatch(authLogin({userData}));
            navigate('/')
        }
        else{
            setError(response.data['message'])
        }
       }
       catch(err){
            setError(err.message)
       }
    }

    return (
        <div className="login">
            <div className="login-page-heading">
                <h1>CACMP Admin Login</h1>
            </div>
            <p className="subheading">Login using your department credentials</p>
            <div className="login-form-container">
                <h1 className="login-heading">Login</h1>
                <form className="login-form" onSubmit={login}>
                    <input
                        type="text"
                        placeholder="Username"
                        value={username}
                        onChange={handleUsernameChange}
                    />
                    <input
                        type="password"
                        placeholder="Password"
                        value={password}
                        onChange={handlePasswordChange}
                    />
                    <button type='submit' >Login</button>
                </form>
            </div>
        </div>
    )

}

export default Login;

I am able to detect this, because I get the required cookie as null in my authentication filter-

package com.ayushsingh.cacmp_backend.config.security;

import com.ayushsingh.cacmp_backend.config.security.service.ConsumerDetailsService;
import com.ayushsingh.cacmp_backend.config.security.service.CustomUserDetailsService;
import com.ayushsingh.cacmp_backend.config.security.service.DepartmentDetailsService;
import com.ayushsingh.cacmp_backend.config.security.util.JwtUtil;
import com.ayushsingh.cacmp_backend.constants.AppConstants;
import com.ayushsingh.cacmp_backend.models.dtos.authDtos.LoginRequestDto;
import com.ayushsingh.cacmp_backend.util.exceptionUtil.ApiException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.util.WebUtils;

import java.io.IOException;
import java.util.Arrays;

import static com.ayushsingh.cacmp_backend.constants.AppConstants.AUTH_HEADER;
import static com.ayushsingh.cacmp_backend.constants.AppConstants.PUBLIC_URLS;

public class CustomAuthFilter extends OncePerRequestFilter {

    private final AuthenticationManager authenticationManager;
    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver exceptionResolver;
    @Autowired
    private CustomUserDetailsService customUserDetailsService;
    @Autowired
    private DepartmentDetailsService departmentDetailsService;
    @Autowired
    private ConsumerDetailsService consumerDetailsService;

    @Value("${jwt.accessTokenCookieName}")
    private String accessTokenCookieName;

    public CustomAuthFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String uri = request.getRequestURI(); //-get the uri
        //-if the uri is a login uri, then login
        if (uri.endsWith(AppConstants.SIGN_IN_URI_ENDING)) {
            //-obtain username and password
            LoginRequestDto jwtAuthRequest = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);
            String username = jwtAuthRequest.getUsername();
            String password = jwtAuthRequest.getPassword();
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
            Authentication authenticationResult = null;
            try {
                authenticationResult = this.authenticationManager.authenticate(authenticationToken);

            } catch (AuthenticationException e) {

                SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken.unauthenticated(username, password));
                exceptionResolver.resolveException(request, response, null, e);
            }
            if (authenticationResult != null) {
                SecurityContextHolder.getContext().setAuthentication(authenticationResult);
            }

            filterChain.doFilter(request, response);
        }
        //-if not a login uri, check for access token
        else {
            String headerToken = this.getTokenFromCookie(request); //-obtain token from cookie
            if (headerToken == null) {
                headerToken = request.getHeader(AUTH_HEADER); //-if no token, obtain token from header
            }
            System.out.println("Header token: " + headerToken);
            //-if still not found, return
            if (headerToken == null) {
                System.out.println("Token is not present");
                //-match uri with public urls
              try{
                  boolean isPublicUrl = Arrays.stream(PUBLIC_URLS).anyMatch(uri::endsWith);
                  if(isPublicUrl) {
                      filterChain.doFilter(request, response);
                      return;
                  }
                  else{
                      throw new ApiException("Access token is not present");
                  }
              }
              catch (ApiException e){
                  exceptionResolver.resolveException(request, response, null, e);
                  return;
              }
            }
            UserDetails userDetails = null;
            try {
                headerToken = StringUtils.delete(headerToken, AppConstants.BEARER_TOKEN_PREFIX).trim();
                String entityType = JwtUtil.extractEntityType(headerToken);
                String username = JwtUtil.extractUsername(headerToken);
                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    if (entityType.equals(AppConstants.ENTITY_TYPE_CONSUMER)) {
                        userDetails = this.consumerDetailsService.loadUserByUsername(username);
                    } else if (entityType.equals(AppConstants.ENTITY_TYPE_USER)) {
                        userDetails = this.customUserDetailsService.loadUserByUsername(username);
                    } else if (entityType.equals(AppConstants.ENTITY_TYPE_DEPARTMENT)) {
                        userDetails = this.departmentDetailsService.loadUserByUsername(username);
                    }
                    if (userDetails == null) {
                        throw new ApiException("User not found with username: " + username);
                    } else if (JwtUtil.validateToken(headerToken, userDetails)) {
                        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                        usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                        filterChain.doFilter(request, response);
                    } else {
                        throw new ApiException("Token validation returned false");
                    }
                } else {
                    throw new ApiException("Username not found in token");
                }
            } catch (ExpiredJwtException e) {
                SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken.unauthenticated(userDetails, null));
                exceptionResolver.resolveException(request, response, null, e);
            } catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException |
                     ApiException e) {
                SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken.unauthenticated(userDetails, null));
                exceptionResolver.resolveException(request, response, null, new ApiException(e.getMessage()));
            }
        }
    }

    private String getTokenFromCookie(HttpServletRequest httpServletRequest) {
        Cookie cookie = WebUtils.getCookie(httpServletRequest, accessTokenCookieName);
        return cookie != null ? cookie.getValue() : null;
    }
}

Here, the cookie in the getTokenFromCookie method, is found to be null!

Please help me resolve this issue! I am a bit new to frontend so am not sure, if everything is correct.

0

There are 0 best solutions below