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:
- User credentials
- Header for authorization according to basic authorization spec on MDN web docks Authorization header
- 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'
},
})