Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Security with Rest API with React

I am trying to implement Spring Security with Rest API and React as Front end, as this is my first Full Stack Development Project, I am clueless on how to achieve proper authentication mechanism.

I have searched a lot and found article on Spring Security with Basic Auth but I am not able to figure out how to convert that authentication to rest api and then same managed through session/cookies. Even whatever github references that I have got are very old or they have not completely migrated to spring security 5.

So not able to figure out the correct approach on securing a rest api. (Would it be just spring security, spring security + jwt, spring security + jwt + spring session + cookie)

Edit

User Name Validation from DB

@Component
CustomUserDetailsService -> loadUserByUsername -> Mongo Db 

Pass Encryption

@Bean
public PasswordEncoder passwordEncoder() { ... }

Cross Origin

@Bean
public WebMvcConfigurer corsConfigurer() { ... }

Registration Controller

@RestController
public class RegistrationController {
@PostMapping("/registration")
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
public ResponseEntity registerUserAccount(... ) { ... }
]

Mongo Session

build.gradle
implementation 'org.springframework.session:spring-session-data-mongodb'
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'

@Configuration
@EnableMongoHttpSession

So above is what I have already implemented. After that I am stuck on how to keep the user in session and keep verifying the user from that.

like image 873
Ankit Aggarwal Avatar asked Oct 14 '25 15:10

Ankit Aggarwal


1 Answers

Basic authorization:

(I assume that you know how to create endpoints, and you have basic knowlage about creating both simple Spring Boot application, and react app, so I'll stick only to authorization topic.)

With basic authorization yours frontend application have to send user credentials on every call to API. And we have to take into account that your backend is probably open on localhost:8080 and frontend localhost:3000 so we have to deal with CORS. (more about CORS Cross-Origin Resource Sharing (CORS) and CORS in Spring Security Spring Security CORS)

Let's start with security configuration where we see endpoints.

 @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
            // by default uses a Bean by the name of corsConfigurationSource
                .cors(withDefaults())
                .csrf().disable()
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, "/login").authenticated()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .antMatchers(HttpMethod.GET, "/cars").authenticated()
                .anyRequest().authenticated()
                .and()
                .httpBasic();
    }
//and cors configuration
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();

        configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST", "OPTIONS"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }

We have /login and /cars endpoints which requires authentication. If you run backend app and open browser on localhost:8080/login (or /cars doesn't matter) then window with basic authorization will pop up at middle of screen. Default username in Spring Security is user and password is generated in your console. Copy paste password it will pass.

Now go to front end app. Assume that we have some simple app with two fields: username and password and button: login. Now we have to implement logic.

...
basicAuthorize = () => {
             let username = this.state.username;
             let password = this.state.password;

            fetch("http://localhost:8080/login", {
                headers: {
                    "Authorization": 'Basic ' + window.btoa(username + ":" + password)
                }
            }).then(resp => {
                console.log(resp);
                if (resp.ok) {
                    this.setState({
                        isLoginSucces: true});
                } else {
                    this.setState({isLoginSucces: false});
                }

                return resp.text();
            });
    }
...

Going from top we have:

  1. User credentials
  2. Header for authorization according to basic authorization spec on MDN web docks Authorization header
  3. If response is ok we can store somewhere user credentials and on next calls to API we have to include again authorization header. (but we shouldn't store user sensitive data in place like LocalStorage or SessionStorage for production but for development is ok Storing Credentials in Local Storage)

JWT:

What is JWT you can read on this site Jwt.io. You can also debug tokens what is helpful at begging.

Make authentication endpoint and logic.
JWT is quite hard to implement so it is helpful to create some classes which help implementing this.

Like there the most important is:

  • JwtTokenRequest tokenRequest - which is POJO with username and password, just to get it from login from front end and send it further.
  • JwtTokenResponse, also POJO, is just only token string which is send in cookie
  • I also get TimeZone to set token expiration.
@PostMapping("/authenticate")
    public ResponseEntity<String> createJwtAuthenticationToken(@RequestBody JwtTokenRequest tokenRequest, HttpServletRequest request, HttpServletResponse response, TimeZone timeZone)
    {
        try
        {
            JwtTokenResponse accessToken = authenticationService.authenticate(tokenRequest, String.valueOf(request.getRequestURL()), timeZone);

            HttpCookie accessTokenCookie = createCookieWithToken("accessToken", accessToken.getToken(), 10 * 60);


            return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()).body("Authenticated");
        }
        catch (AuthenticationException e)
        {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
        }
    }

//creating cookie
private HttpCookie createCookieWithToken(String name, String token, int maxAge)
    {
        return ResponseCookie.from(name, token)
                .httpOnly(true)
                .maxAge(maxAge)
                .path("/")
                .build();
    }

Service responsible for authentication and token creation

@Service
public class JwtAuthenticationService
{
    private AuthenticationManager authenticationManager;

    private final String SECRET_KEY = "SecretKey";

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

    public JwtTokenResponse authenticate(JwtTokenRequest tokenRequest, String url, TimeZone timeZone) throws AuthenticationException
    {
        UserDetails userDetails = managerAuthentication(tokenRequest.getUsername(), tokenRequest.getPassword());

        String token = generateToken(userDetails.getUsername(), url, timeZone);

        return new JwtTokenResponse(token);
    }

Managing authentication. You don't need check if password belongs to username manualy because if you have loadByUsername implemented, Spring will use this method to load user and check password. Manually Authenticate User with Spring Security

private UserDetails managerAuthentication(String username, String password) throws AuthenticationException
    {
        Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));

        return (UserDetails) authenticate.getPrincipal();
    }

If no exception is thrown, that means user credentials are correct then we can generate JWT token.

In this example I am using Java JWT library, which you can add in pom.xml file.

This method generates token according to timezone from request and also stores information request url.

private String generateToken(String username, String url, TimeZone timeZone)
    {
        try
        {
            Instant now = Instant.now();

            ZonedDateTime zonedDateTimeNow = ZonedDateTime.ofInstant(now, ZoneId.of(timeZone.getID()));

            Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
            String token = JWT.create()
                    .withIssuer(url)
                    .withSubject(username)
                    .withIssuedAt(Date.from(zonedDateTimeNow.toInstant()))
                    .withExpiresAt(Date.from(zonedDateTimeNow.plusMinutes(10).toInstant()))
                    .sign(algorithm);

            return token;
        }
        catch (JWTCreationException e)
        {
            e.printStackTrace();
            throw new JWTCreationException("Exception creating token", e);
        }
    }

If everything were ok, then token is stored in http-only cookie.

When we have token then if request is done to authenticated endpoint we have to filter that request before. We need to add our custom filter:

  • Firstly extend filter (you can read here why this What is OncePerRequestFilter?)
  • Add key
public class JwtFilter extends OncePerRequestFilter
{
    private final String SECRET_KEY = "SecretKey";
}

//or load from other source
public class JwtFilter extends OncePerRequestFilter
{
    private final String SECRET_KEY = ApplicationConstants.SECRET_KEY;
}
  • Implement method from parent class
  • Depends on from where you are getting tokens we just have to load it. In this example I am using HttpOnly cookie
  • If the cookie is present then do authorization
@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException
    {
        Cookie tokenCookie = null;
        if (request.getCookies() != null)
        {
            for (Cookie cookie : request.getCookies())
            {
                if (cookie.getName().equals("accessToken"))
                {
                    tokenCookie = cookie;
                    break;
                }
            }
        }

        if (tokenCookie != null)
        {
            cookieAuthentication(tokenCookie);
        }

        chain.doFilter(request, response);
    }
  • If the all validation is passed then set in SecurityContextHolder that this user is authenticated What is SecurityContextHolder you can read here 10.1. SecurityContextHolder
private void cookieAuthentication(Cookie cookie)
    {
        UsernamePasswordAuthenticationToken auth = getTokenAuthentication(cookie.getValue());

        SecurityContextHolder.getContext().setAuthentication(auth);
    }

private UsernamePasswordAuthenticationToken getTokenAuthentication(String token)
    {
        DecodedJWT decodedJWT = decodeAndVerifyJwt(token);

        String subject = decodedJWT.getSubject();

        Set<SimpleGrantedAuthority> simpleGrantedAuthority = Collections.singleton(new SimpleGrantedAuthority("USER"));

        return new UsernamePasswordAuthenticationToken(subject, null, simpleGrantedAuthority);
    }

    private DecodedJWT decodeAndVerifyJwt(String token)
    {
        DecodedJWT decodedJWT = null;
        try
        {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET_KEY))
                    .build();

            decodedJWT = verifier.verify(token);

        } catch (JWTVerificationException e)
        {
            //Invalid signature/token expired
        }

        return decodedJWT;
    }

And now, request is filtered with token in cookie. We have to add custom filter in Spring Security:

@Override
    protected void configure(HttpSecurity http) throws Exception
    {
...
//now 'session' is managed by JWT        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class);
    }

In front end you don't have much work.
In your request you just have to add withCredentials: 'include', then cookies will be send with request. You have to use 'include' because it's cross-origin request. Request.credentials

Example request:

fetch('http://localhost:8080/only-already-authenticated-users', {
      method: "GET",
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json'
      },
    })
like image 99
AzJa Avatar answered Oct 17 '25 03:10

AzJa



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!