Learn Spring Security
Contents
Generate Token
Tokens are always better than username/password as they are uniquely generated on each login. And they do not hang around for a long time as they are invalidated either when the user logout or when it reaches its expiry time.
Token-based auth requires the user to provide username/password only once during login. It then generates a unique token (generally a random string) for the user on successful authentication. This unique token can be sent on subsequent requests to identify the user until the token expires or invalidated.
But using tokens requires a bit of learning curve on how to generate, verify and invalidate them which is not the case with Basic Auth. We will implement token-based authentication mechanism in this section using our own token of random string i.e., UUID.
Note
JSON Web Token (JWT) is an open, industry standard token specification widely popular for many reasons. Rather than focusing on the type of the token first, let's understand how token-based authentication mechanism works. And then we will replace our token with JWT using battle-tested production grade JWT library.
AuthenticationService
First we will create an AuthenticationService component to implement generateToken() method which accepts username and password and returns UUID on successful authentication.
@Service
public class AuthenticationService {
public String generateToken(String username, String password) {
// Authenticate using Spring Security's AuthenticationManager with username/password
// Generate and return token if authentication success
}
}
AuthenticationManager
We are not going to perform authentication by ourselves by checking the existence of username and password in the database. We will use AuthentionManager, we talked about in Current Authenticated User chapter, where it accepts Authentication object as authentication request and returns the same as authenticated principal on successful authentication.
We will use UsernamePasswordAuthenticationToken to create an Authentication object as authentication request using username & password provided by the user. AuthenticationManager delegates the authentication process to a list of AuthenticationProvider. DaoAuthenticationProvider is one of such implementations we discussed in DB Backed UserDetailsService chapter, which uses our DbUserDetailsService to load the user details by username. It will then perform the authentication and return the authenticated principal. We will then return the UUID token after verifying the authenticated principal.
@Service
public class AuthenticationService {
@Autowired
private AuthenticationManager authenticationManager;
public String generateToken(String username, String password) {
Authentication authentication = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
authentication = authenticationManager.authenticate(authentication);
String token = null;
if (authentication.isAuthenticated()) {
token = UUID.randomUUID().toString();
}
return token;
}
}
By default AuthenticationManager is not exposed as a bean, we have to retrieve it from AuthenticationConfiguration and expose it as a bean from SecurityConfig as below:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends GlobalMethodSecurityConfiguration {
// Other details omitted for brevity
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
AuthenticationController
Now let's create a REST API to get the username and password from the user and return the generated token back from AuthenticationService.
@RestController
@RequestMapping("auth")
public class AuthenticationController {
@Autowired
private AuthenticationService authenticationService;
@PostMapping(path = "token", consumes = {APPLICATION_FORM_URLENCODED_VALUE})
public ResponseEntity<Map<String,String>> generateToken(@RequestParam("username") String username,
@RequestParam("password") String password) {
String token = authenticationService.generateToken(username, password);
Map<String, String> tokenResponse = Collections.singletonMap("token", token);
return ResponseEntity.ok(tokenResponse);
}
}
As GenerateToken API must be accessible to everyone, we have to update HttpSecurity to permit this API. So we will add the API url in SecurityConstants as well.
@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement(
httpSecuritySessionManagementConfigurer ->
httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeRequests(auth -> auth
.antMatchers(POST, API_AUTH_TOKEN).permitAll()
.antMatchers(GET, PUBLIC_API_LIST).permitAll()
.anyRequest().authenticated()
);
return http.build();
}
Token Generation
Submit a POST request to GenerateToken API with Admin user credentials in x-www-form-urlencoded body type like below, we will get the UUID token returned after successful authentication.
Please note that we have removed httpBasic() in HttpSecurity configuration as we are replacing it with token-based authentication. This will cause all the protected APIs to be inaccessible even for the authorized users. And this is because Spring Security can not identify the user at this stage, as we are yet to verify the token and tell Spring Security who the user is and what are they allowed to do.
But before verifying the token we have to associate the token with the user who requested it by storing it in a persistant datastore to verify it on each requests.