I am working on a Spring Boot application that take the username and password of an existing user on the system and then generates a JWT token. I copied it from a tutorial and I changed it in order to work with my specific use cases. The logic is pretty clear to me but I have a big doubt about how the user is authenticated on the system. Following I will try to explain you as this is structured and what is my doubt.
The JWT generation token system is composed by two different micro services, that are:
The GET-USER-WS: this microservice simmply use Hibernate\JPA to retrieve the information of a specific user in the system. Basically it contains a controller class calling a service class that itself calla JPA repository in order to retrieve a specific user information:
@RestController
@RequestMapping("api/users")
@Log
public class UserController {
@Autowired
UserService userService;
@GetMapping(value = "/{email}", produces = "application/json")
public ResponseEntity<User> getUserByEmail(@PathVariable("email") String eMail) throws NotFoundException {
log.info(String.format("****** Get the user with eMail %s *******", eMail) );
User user = userService.getUserByEmail(eMail);
if (user == null)
{
String ErrMsg = String.format("The user with eMail %s was not found", eMail);
log.warning(ErrMsg);
throw new NotFoundException(ErrMsg);
}
return new ResponseEntity<User>(user, HttpStatus.OK);
}
}
As you can see this controller contains an API that use the e-mail parameter (that is the username on the system) and return a JSON containing the information of this user.
Then I have a second microservice (named AUTH-SERVER-JWT) that is the one that call the previous API in order to obtain the user information that will be used to generate the JWT token. To keep the description as simple as possible it contains this controller class:
@RestController
//@CrossOrigin(origins = "http://localhost:4200")
public class JwtAuthenticationRestController {
@Value("${sicurezza.header}")
private String tokenHeader;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
@Qualifier("customUserDetailsService")
//private UserDetailsService userDetailsService;
private CustomUserDetailsService userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationRestController.class);
@PostMapping(value = "${sicurezza.uri}")
public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtTokenRequest authenticationRequest)
throws AuthenticationException {
logger.info("Autenticazione e Generazione Token");
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
//final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final UserDetailsWrapper userDetailsWrapper = userDetailsService.loadCompleteUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetailsWrapper);
logger.warn(String.format("Token %s", token));
return ResponseEntity.ok(new JwtTokenResponse(token));
}
@RequestMapping(value = "${sicurezza.uri}", method = RequestMethod.GET)
public ResponseEntity<?> refreshAndGetAuthenticationToken(HttpServletRequest request)
throws Exception
{
String authToken = request.getHeader(tokenHeader);
if (authToken == null || authToken.length() < 7)
{
throw new Exception("Token assente o non valido!");
}
final String token = authToken.substring(7);
if (jwtTokenUtil.canTokenBeRefreshed(token))
{
String refreshedToken = jwtTokenUtil.refreshToken(token);
return ResponseEntity.ok(new JwtTokenResponse(refreshedToken));
}
else
{
return ResponseEntity.badRequest().body(null);
}
}
@ExceptionHandler({ AuthenticationException.class })
public ResponseEntity<String> handleAuthenticationException(AuthenticationException e)
{
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
}
private void authenticate(String username, String password)
{
Objects.requireNonNull(username);
Objects.requireNonNull(password);
try {
/// ???
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
catch (DisabledException e)
{
logger.warn("UTENTE DISABILITATO");
throw new AuthenticationException("UTENTE DISABILITATO", e);
}
catch (BadCredentialsException e)
{
logger.warn("CREDENZIALI NON VALIDE");
throw new AuthenticationException("CREDENZIALI NON VALIDE", e);
}
}
}
This class contains two method, the first one is used to generate a brand new JWT token and the second one it is used to refresh an existing JWT token. Consider now the first use case (generate a brand new JWT token) related to the createAuthenticationToken() method. This method take as input parmether the information related to the JWT token request: @RequestBody JwtTokenRequest authenticationRequest. Bascailly the JwtTokenRequest is a simple DTO object like this:
@Data
public class JwtTokenRequest implements Serializable
{
private static final long serialVersionUID = -3558537416135446309L;
private String username;
private String password;
}
So the payload in the body request will be something like this:
{
"username": "[email protected]",
"password": "password"
}
NOTE: in my DB I have a user having this username and password so the user will be retrieved and authenticated on the system.
As you can see the first effective operation that the createAuthenticationToken() method do is:
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
Basically it is calling the authenticate() method defined in the same class passing to it the previous credential ("username": "[email protected]" and "password": "password").
As you can see this is my authenticate() method
private void authenticate(String username, String password)
{
Objects.requireNonNull(username);
Objects.requireNonNull(password);
try {
/// ???
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
catch (DisabledException e)
{
logger.warn("UTENTE DISABILITATO");
throw new AuthenticationException("UTENTE DISABILITATO", e);
}
catch (BadCredentialsException e)
{
logger.warn("CREDENTIAL ERROR");
throw new AuthenticationException(""CREDENTIAL ERROR", e);
}
}
Basically it is passing these credential to the authenticate() method defined into the injected Spring Security AuthenticationManager instance, by this line:
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
This method seems to be able to authenticate or not these credentials. And it seems to works fine because if I put a wrong username or password it goes into the CREDENTIAL ERROR case and it throw the AuthenticationException exception.
And here my huge doubt: why it works?!?! How is it possible? If you come back to the createAuthenticationToken() controller method you can see that it does these two operation in the following order:
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
final UserDetailsWrapper userDetailsWrapper = userDetailsService.loadCompleteUserByUsername(authenticationRequest.getUsername());
It first perform the authenticate() method (that should check if theusername and password that was sent are correct), then call the service method that retrieve the user information.
Sho how the authenticate() method is able to check if the credential sent in the original payload are correct?
CodePudding user response:
Usually, the implementation of AuthenticationManager
is a ProviderManager
, which will loop through all the configured AuthenticationProvider
s and try to authenticate using the credentials provided.
One of the AuthenticationProvider
s, is DaoAuthenticationProvider
, which supports a UsernamePasswordAuthenticationToken
and uses the UserDetailsService
(you have a customUserDetailsService
) to retrieve the user and compare the password
using the configured PasswordEncoder
.
There is a more detailed explanation in the reference docs about the Authentication Architecture.