3

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.

2 Answers2

5

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:

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'
      },
    })
AzJa
  • 111
  • 8
  • I want to move ahead this basic auth and rather sending the user and pass again just want to validate the session. – Ankit Aggarwal Apr 26 '20 at 14:57
  • @AnkitAggarwal I am going to improve my answer with JWT. I also struggled with searching latest JWT implementation in Spring Boot but somehow I have working example, where JWT is stored in HttpOnly cookie, at my gh repo. [Bank-Api](https://github.com/jaca1119/Bank-api). I think everything important for you is in configuration folder with JWTFilter. You can also check my question where I pointed few important things [Spring security - 403 status response on OPTIONS call](https://stackoverflow.com/questions/61352167/spring-security-403-status-response-on-options-call) – AzJa Apr 26 '20 at 15:08
  • @AnkitAggarwal I edited my answer. You can check JWT example implementation – AzJa Apr 28 '20 at 20:39
0

For my first spring security fullstack project,

I used React for front end, spring boot spring security for backend.

Some easy steps to kick start.

  1. Create a user manually in database

  2. From react login page call post login api:

    const config = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } };

    axios.post('http://localhost:9090/login',querystring.stringify( { username: username.value, password: password.value }),config).then(response => {
      setLoading(false);
      setUserSession(null, username.value);
    
      props.history.push('/landingpage');
    })
    
    1. Create a spring boot project where - create a post rest api to handle login - Add spring security dependency in pom - Create a class which implement UserDetailsService (UserDetailsService is provided by spring secutity to fetch user details from db and provide to security interceptors) - Now configure UserDetailsService class in configuration file using @Bean so spring security should be able to identify it - Also , in your configuration file extends WebSecurityConfigurerAdapter class and declare configure method according to your use (you can find multiple examples of this on web)

Thats it, you are ready to go after that.

There are multiple ways and this is just a simple way to learn to validate form login

GK7
  • 116
  • 3