Authentication with Spring Boot and Spring Security — JWT and Postgres
Take a look at how you can create authentication with REST API calls using JSON Web Tokens.
Join the DZone community and get the full member experience.
Join For FreeI am going to talk about what has become a very common requirement for web/cloud-applications these days: Authentication in the context of REST API calls using JSON Web Token, or commonly known as JAWT these days.
The idea behind JSON WebToken is to provide authentication in a stateless API world. The tokens are generated with a Key on the basis of Subject (could be a unique field or combination for a user e.g id, username etc.) using an encryption algorithm such as SHA-256 etc.
Technologies:
1. Spring Boot 2
2. Spring Security
3. Postgres
4. Spring Data
I am using Postgres as a backend database. But, with minor property changes the same code and approach can be used for other databases, such as MySQL.
Motivation
- The motivation for the article has been the observation that teams spend lot of time in the implementation of authentication in their application and generally start from scratch (even when JAWT has been around for quite sometime). Some even delay it for later stages — creating more confusion in their own team and dependent API streams such as mobile, web, etc.
- Spring Security provides lot of standard classes and hooks for authentication, but somehow they all fit together and interact is not easily understandable.
- In references, I have given the video link where we got the crux of the implementation, but still we have to make some changes to integrate with web interface in Angular. Backend implementation was not originally provided in the reference video.
Enable Web Security
To begin include the dependency in your pom
x
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Enable web security in our application we created a file Securitycontext.java and used the annotation @EnableWebSecurity. There are couple of methods that we have overridden the noticeable ones being -
-
configure()
— We explicitly disable csrf and cors, specify the urls where we want Spring Security to be by-passed, create stateless sessions and specify the sequence of filters
xxxxxxxxxx
httpSecurity.cors().disable().csrf().disable()
.authorizeRequests().antMatchers("/v1/login").permitAll().
anyRequest().authenticated()
.and().
exceptionHandling().
and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
2. AuthenticationManager — We would need an instance of Authentication Manager to generate authentication token after checking valid username and password.
xxxxxxxxxx
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
Authentication
We created a class AuthenticationController
with a post endpoint /v1/login.
xxxxxxxxxx
value = "/login") (
public ResponseEntity<?> createAuthenticationToken( AuthenticationRequest authenticationRequest) throws Exception
Here, we call authenticate()
method of AuthenticationManager
class passing username and password received in an enclosing UserNamePasswordAuthenticationToken
.
xxxxxxxxxx
Authentication authenticate = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
);
Also, we created a class JPAUserDetailService
that implements UserDetailsService
that is hooked when we call authenticate method (above).
In this class, we have overridden the method that is used to check whether the user exists or not.
xxxxxxxxxx
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
Optional<User> user = userRepository.findByUsername(userName);
user.orElseThrow(() -> new UsernameNotFoundException("Not found: " + userName));
return user.map(MyUserDetails::new).get();
}
We are also using userRepository pointing to userDetails table in postgres database. The dependency for which is included in our pom file
xxxxxxxxxx
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
There is a hierarchy of classes and implements that provides for this hook in the background. If this is too verbose, you may skip this part. The AuthenticationManager
interface is implemented by the ProviderManager
class that uses various AuthenticationProvider
. E.g An authentication provider implementation with name DAOAuthenticationProvider
. This authentication provider will use UserDetailService
and will call the implementation method loadUserByUsername()
for which concrete implementation is provided in our class JPAUserDetailService
.
xxxxxxxxxx
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
JWT Generation
Include the dependency below in your pom for jwt features
xxxxxxxxxx
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
In JWTUtil.java we have various functions dealing with jwt such as generatetoken, extract and validate user details from token, set expiration of token etc. The default time we have set is 10 hours converted in milliseconds below.
xxxxxxxxxx
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
}
If the details are correct then the token will be issued. A sample response of the token, when generated successfully, is shown below.
Filters
There is a filter class that works like a gatekeeper for authentication is JWTFilter
and it uses JWTUtil
class methods to extract and validate user details from token that is to be passed in header in postman. Below is a sample of how to pass Authorization
in the header using syntax: Bearer <space> <<token>>
Below are some flows on how the filters and Spring Security function with respect to obtaining token, and subsequent calls requiring authentication.
- /v1/login - is an endpoint where we generate the token. In
SecurityContext
we configured to ignore this URL requiring authentication as this is the first call to generate token so Spring Security is ignored for this call.
2. /v1/hello - an endpoint to demonstrate our authentication flow. Here, JWTFilter looks for the authentication header, checks validity of token and whether it belongs to a valid user or not. After these checks, we set the authentication information in the security context. It is important step, as this allows us to preserve the user information that may be required in downstream steps.
xxxxxxxxxx
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
That's pretty much it. You should be able to see JAWT in action with the steps I discussed. The code is available for you at this link below. I will discuss CORS specific settings in my next article. Please share your feedback and comments.
Opinions expressed by DZone contributors are their own.
Comments