Spring Security - Authentication Process

Authentication Process

Authentication Process - The Big Picture

Authentication Filter

Authentication filter usually extends AbstractAuthenticationProcessingFilter class.

Example Authentication Filters are

Authentication Filter’s basic responsibility

  1. Authentication Filter creates Authentication from the user Request. For example, UsernamePasswordAuthenticationFilter creates UsernamePasswordAuthenticationToken
  2. Authentication Filter delegates the work to AuthenticationManager
  3. If authentication is success, then set the Authetnication on the SecurityContextHolder

AuthenticationManager

  • AuthenticationManager is one of the most important class in Spring Security.
  • Responsible for processing Authentication request and returns a fully populated Authentication object.
  • AuthenticationManager is an interface, the most common implementation is ProviderManager. ProviderManager delegates to a List of AuthenticationProviders.
  • You don’t usually work with AuthenticationManager directly, you use AuthenticationManagerBuilder to create it.
  • Spring Security Reference on AuthenticationManager
  • java doc
  • source code

AuthenticationManager Interface source code

1
2
3
4
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}

authenticate method Attempts to authenticate the passed Authentication argument

  • returning a fully populated Authentication object (including granted authorities, authenticated=true) if successful.
  • if input request is not valid, throw AuthenticationException
  • reutrns null if can’t decide

ProviderManager

ProviderManager

Iterates an Authentication request through a list of AuthenticationProviders.
AuthenticationProviders are usually tried in order until one provides a non-null response. A non-null response indicates the provider had authority to decide on the authentication request and no further providers are tried.

Like AuthenticationManager, you don’t work directly with ProviderManager.

AuthenticationProvider

  • It is the actual class that does the authentication.
  • AuthenticationProvider has a supports method that tells AuthenticationManager what Authentication object it supports.
  • An app can have multiple AuthenticationProviders(Ldap, Dao, OAuth2 etc)
  • java doc
  • source code
1
2
3
4
5
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}

There are manay providers implements AuthenticationProvider interface. Some of the most important ones are

  • DaoAuthenticationProvider - this is the most common AuthenticationProvider you will see
  • LdapAuthenticationProvider - An AuthenticationProvider implementation that authenticates against an LDAP server.
  • JwtAuthenticationProvider - Jwt-encoded Bearer Tokens for protecting OAuth 2.0 Resource Servers.

UserDetailsService

Core interface which loads user-specific data.

It is used throughout the framework as a user DAO and is the strategy used by the DaoAuthenticationProvider.

The interface requires only one read-only method, which simplifies support for new data-access strategies.

UserDetailsService source code

1
2
3
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

Known Implementing Classes:

  • CachingUserDetailsService
  • InMemoryUserDetailsManager
  • JdbcDaoImpl
  • JdbcUserDetailsManager
  • LdapUserDetailsManager
  • LdapUserDetailsService

Authentication Process Walkthrough

We can walkthrough a In-memory authentication process to understand how Spring Security authentication works.

UsernamePasswordAuthenticationFilter

  • UsernamePasswordAuthenticationFilter Extends AbstractAuthenticationProcessingFilter abstract class.
  • is the filter used for username password authentication.
  • Processses authentication submission using request parameter “username” and “password”.
  • Source code

AbstractAuthenticationProcessingFilter.doFilter method - This is the entry point for UsernamePasswordAuthenticationFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);

return;
}

if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}

Authentication authResult;

try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);

return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);

return;
}

// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}

successfulAuthentication(request, response, chain, authResult);
}

The most important method call is authResult = attemptAuthentication(request, response);. If method call is successful it calls successfulAuthentication method, otherwise call unsuccessfulAuthentication method.

attemptAuthentication method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

String username = obtainUsername(request);
String password = obtainPassword(request);

if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);

// Allow subclasses to set the "details" property
setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
}

attemptAuthentication method gets request parameter “username” and “password”, trim it and then create a UsernamePasswordAuthenticationToken object. UsernamePasswordAuthenticationToken is an implementation of Authentication. UsernamePasswordAuthenticationFilter delegates the authentication to AuthenticationManager.

successfulAuthentication method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {

if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}

SecurityContextHolder.getContext().setAuthentication(authResult);

rememberMeServices.loginSuccess(request, response, authResult);

// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}

successHandler.onAuthenticationSuccess(request, response, authResult);
}

If authentication is successful, UsernamePasswordAuthenticationFilter calls SecurityContextHolder.getContext().setAuthentication(authResult); to set the SecurityContext.

Then call successHandler.onAuthenticationSuccess to let the successHandler handle the redirect

unsuccessfulAuthentication method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();

if (logger.isDebugEnabled()) {
logger.debug("Authentication request failed: " + failed.toString(), failed);
logger.debug("Updated SecurityContextHolder to contain null Authentication");
logger.debug("Delegating to authentication failure handler " + failureHandler);
}

rememberMeServices.loginFail(request, response);

failureHandler.onAuthenticationFailure(request, response, failed);
}

If login attempt is unsuccessful, clear the SecurityContextHolder.

Then call failureHandler.onAuthenticationFailure to let the failureHandler handle the redirect

ProviderManager

  • ProviderManager is the most used AuthenticationManager. authenticate method is the most important method of ProviderManager
  • source code

authenticate method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
	public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();

for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}

try {
result = provider.authenticate(authentication);

if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}

// skip code not related to the main authentication process

if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}

// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}

// skip the remaining code for authenticate method
throw lastException;
}

ProviderManager contains a list of AuthenticationProviders. AuthenticationProviders will tried until an AuthenticationProvider can support indicated Authentication, the AuthenticationProvider will do the authentication. if the process is successful, then remove credentials and other secret data from authentication and return it. If none of the AuthenticationProviders can successfully authenticate the request, an AuthentiationException is thrown.

DaoAuthenticationProvider

  • DaoAuthenticationProvider is the most used AuthenticationProvider
  • It retrieves userdetails from UserDetailsService. Then compare the password with the saved password.
  • DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider.
  • Source code

support method - DaoAuthenticationProvider supports Authentication of type UsernamePasswordAuthenticationToken

1
2
3
4
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}

authenticate method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));

// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();

boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);

if (user == null) {
cacheWasUsed = false;

try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");

if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}

Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}

try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}

postAuthenticationChecks.check(user);

if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}

Object principalToReturn = user;

if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}

return createSuccessAuthentication(principalToReturn, authentication, user);
}

authenticate method retrieve user by calling retrieveUser method. if user is not found, then throw BadCredentialsException. If authentication checks fail, throw BadCredentialsException too. If check is successful, call createSuccessAuthentication method.

retrieveUser method retrieve user using its UserDetailsService.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}

additionalAuthenticationChecks method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");

throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}

String presentedPassword = authentication.getCredentials().toString();

if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");

throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}

additionalAuthenticationChecks method - gets credentials from Authentication and them compare it with password from userdetails. Throws BadCredentialsException if password not found or password does not match.

InMemoryUserDetailsManager

  • InMemoryUserDetailsManager implements UserDetailsManager and UserDetailsPasswordService interface.
  • The most important method is loadUserByUsername. This method loads UserDetails given the username
  • Javadoc
  • Source code

loadUserByUsername method

1
2
3
4
5
6
7
8
9
10
11
12
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserDetails user = users.get(username.toLowerCase());

if (user == null) {
throw new UsernameNotFoundException(username);
}

return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), user.getAuthorities());
}

AuthenticationException

AuthenticationException - Abstract superclass for all exceptions related to an Authentication object being invalid for whatever reason.

BadCredentialsException and UsernameNotFoundException are both subclasses of AuthenticationException. There are other subclasses of AuthenticationException that may be thrown during authentication process.

UsernamePasswordAuthenticationFilter will call unsuccessfulAuthentication method to handle the exception. see unsuccessfulAuthentication method from above.