Spring Security, is a flexible and powerful authentication and access control framework to secure Spring-based Java web applications. In this blog i would like to cover the internal architecture of the core modules of spring security.
- Authentication
- Authorization
- Exception Handling
In part 1 of this blog mainly I cover the Authentication module and will cover the rest of the modules in follow up blogs.
Authentication:-
Spring security supports multiple types of logins. Here I am going to cover Form based login. The below diagram describes the flow of the form based login.
UsernamePasswordAuthenticationFilter handles the authentication request which extends from AbstractAuthenticationProcessingFilter.
For form based login, your login form must present two parameters to this filter: “username” and “password“. And this filter by default responds to the URL “/login“. But if you would like to have different parameters and different URL, then you have to have your custom filter which extends from UsernamePasswordAuthenticationFilter and have to override the attemptAuthentication() method.
1 2 3 4 5 6 7 8 |
public class CustomUsernamePasswordAuthFilter extends UsernamePasswordAuthenticationFilter { public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // Your logic here } } |
AbstractAuthenticationProcessingFilter : –
This filter has following abstract method which is implemented by UsernamePasswordAuthenticationFilter.
1 |
public abstract Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) |
- This filter does the following operations.
- First it checks for whether authentication is required or not based on our HttpSecurity configuration. If authentication is not required, it simply invoke the next filter in the chain.
- If authentication requires, then it calls the attemptAuthentication(request, response) which is implemented by UsernamePasswordAuthenticationFilter and this method returns the Authentication object.
UsernamePasswordAuthenticationFilter:-
- In attemptAuthentication method first it obtains the username & password from the request.
- Then it constructs the UsernamePasswordAuthenticationToken using the below code. Which is nothing but “Authentication” object.
1 |
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); |
This Authentication object will be as mentioned below.
1 2 3 4 |
principal = username credentials = password authorities = null isAuthenticated = false |
- So once it build the UsernamePasswordAuthenticationToken/Authentication object, then it invokes the authenticate() method of “Authentication Manager”. Means this filter delegates the job to the “AuthenticationManager”.
this.getAuthenticationManager().authenticate(authRequest);
AuthenticationManager:-
- ProviderManager is the implementation of AuthenticationManager and which has the following method.
public Authentication authenticate(Authentication authentication);
ProviderManager:-
- ProviderManager iterates through all the provided/configured Authentication providers and delegate the actual Authentication job to Authentication providers.
1 2 3 |
for(AuthenticationProvider provider : getProviders()) { Authentication result = provider.authenticate(authentication); } |
AuthenticationProvider:-
- There are many implementations for AuthenticationProvider. One of the implementation is DAOAuthenticationProvider. Which extends from the AbstractUserDetailsAuthenticationProvider.
- As mentioned above AuthenticationManager delegates the job to AuthenticationProvider to authenticate the user. To this AuthenticationProvider we can pass/inject the following information.
- UserDetailsService
- Salt
- PasswordEncoder
UserDetailsService:-
- Which is responsible to load the actual user details which means UserDetails object. We will have our custom implementation of UserDetailsService to load or retrieve the UserDetails object either from internal memory or from Database.
1 2 3 4 5 6 7 |
public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String userinput) throws UsernameNotFoundException { // Write a logic to retrieve the UserDetails from DB. } } |
PasswordEncoder:-
- We have multiple implementations of Password Encoder like
- MD4PasswordEncoder
- MD5PasswordEncoder
- ShaPasswordEncoder
- PlaintextPasswordEncoder
- We can have our own implementation of password encoder. Here i’m providing custom implementation of password encoder.
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 |
public class PBKDF2PasswordEncoder implements PasswordEncoder { private static final int ITERATIONS = 10000; private static final int KEY_LENGTH = 256; private final static Logger LOG = LoggerFactory.getLogger(PBKDF2PasswordEncoder.class); @Override public String encodePassword(String rawPass, Object salt) { byte[] hashedPassword = null; if (rawPass != null && salt != null) { char[] passwordChars = rawPass.toCharArray(); byte[] saltBytes = (byte[]) salt; PBEKeySpec spec = new PBEKeySpec(passwordChars, saltBytes, ITERATIONS, KEY_LENGTH); try { SecretKeyFactory key = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); hashedPassword = key.generateSecret(spec).getEncoded(); } catch (InvalidKeySpecException e) { LOG.error("Invalid key spec exception", e); throw new RuntimeException(e); } catch (NoSuchAlgorithmException e) { LOG.error("Not a valid algorithm", e); throw new RuntimeException(e); } return toBase64(hashedPassword); } return null; } @Override public boolean isPasswordValid(String encPass, String rawPass, Object salt) { byte[] saltBytes = fromBase64((String) salt); String encPasswd = encodePassword(rawPass,saltBytes); if(encPasswd != null && encPass != null) { return encPasswd.equals(encPass); } return false; } public byte[] fromBase64(String hex) throws IllegalArgumentException { return DatatypeConverter.parseBase64Binary(hex); } public String toBase64(byte[] array) { return DatatypeConverter.printBase64Binary(array); } } |
Salt:-
Salt is a random Byte [] array. We can generate the salt as mentioned below.
1 2 3 4 5 6 |
public byte[] generateSalt() { byte[] salt = new byte[32]; Random random = new SecureRandom(); random.nextBytes(salt); return salt; } |
Now finally AuthenticationProvider authenticate the user and build the “Authentication” object and return to the AuthenticationManager. Here i have mentioned some of the code blocks which helps you to understand the flow in detail.
1 2 3 4 5 6 7 |
public Authentication authenticate(Authentication authentication) { UserDetails user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); return createSuccessAuthentication(principalToReturn, authentication, user); } |
This method retrieves the UserDetails object from DB using custom implementation of UserDetailsService.
1 2 3 4 5 |
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); return loadedUser; } |
This method checks whether the provided password is valid or not.
1 2 3 4 5 6 7 8 |
protected void additionalAuthenticationChecks(UserDetails user , UsernamePasswordAuthenticatonToken authentication) { String presentedPassword = authentication.getCredentials().toString(); if(!passwordEncoder.isPasswordValid(userDetails.getPassword(),presentedPassword,salt)) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } |
Once password validated successfully, this method creates the Authentication object with Authorities and sets the isAuthenticated flag as true.
1 2 3 4 5 6 7 |
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); return result; } |
- createSuccessAuthentication method creates the following Authentication object.
1 2 3 4 |
principal = username credentials = password authorities = REGULAR_USER isAuthenticated = true |
- AuthenticationProvider returns the Authentication object to AuthenticationManager.
- In the above mentioned Authentication process if Authentication fails, then filter clears the Security context and invokes the failure-handler.
1 2 3 4 5 6 7 8 |
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); failureHandler.onAuthenticationFailure(request, response, failed); } |
- If it successfully gets the Authentication object then it does following things.
- It stores the Authentication object in the SecurityContextHolder.
- And it invokes the success-handler.
1 2 3 4 5 6 7 8 9 |
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityContextHolder.getContext().setAuthentication(authResult); successHandler.onAuthenticationSuccess(request, response, authResult); } |
I’ll cover Authorization and Exception handling modules in part 2. Which i’m going to publish soon.
Happy Blogging…:)
References:-
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/