In this post I'd like to share how to write own custom Filter in Spring Security framework to authenticate. If you have your web application built using this security framework this post might be useful for you.
Well it is very often needed allow user login into the application on the fly without any input forms. For example by link from the email or SMS, by some header in the REST API etc. I take the letter as an example wehere there this the link with parameter:
Hi, dear user!
There is a new comment on your page.
See all the activity in your profile.
And the activity link href is:
http://example.com/activity.htm?token=25390bd438e1f17bee7b0d320a62707f
And your system must be able to authenticate the user just having this token: 25390bd438e1f17bee7b0d320a62707f
First of all you need to know how to restore the user info by this token value and vice versa: you know how to generate this token matching the user info.
The simplest solution is to store them in database providing the expiration info and other checks if needed.
Now we need to write the Filter to find this token in request:
Filter:
Now let's configure the framework: register the filter and code to authenticate.
In Spring Security there is the possibility to provide additional fallback code to provide the user authentication when primary one is failing using user details service. Sample config where we register our filter and authentication manager might look like:
Now lets write this fallback authentication manager:
That's it!
This filter will be invoked only once when user is not logged out.
If you need to logout already authenticated user in the system - you need to create new Filter checking this token and insert it after the standard logout filter same as I did for TokenFilter and logout existing. See all available in org.springframework.security.config.annotation.web.builders.FilterComparator#FilterComparator.
For the case when you have REST API with a header you can modify token filter to extract the token, f.e. get it from the header with Authentication name
Well it is very often needed allow user login into the application on the fly without any input forms. For example by link from the email or SMS, by some header in the REST API etc. I take the letter as an example wehere there this the link with parameter:
Hi, dear user!
There is a new comment on your page.
See all the activity in your profile.
And the activity link href is:
http://example.com/activity.htm?token=25390bd438e1f17bee7b0d320a62707f
And your system must be able to authenticate the user just having this token: 25390bd438e1f17bee7b0d320a62707f
First of all you need to know how to restore the user info by this token value and vice versa: you know how to generate this token matching the user info.
The simplest solution is to store them in database providing the expiration info and other checks if needed.
Now we need to write the Filter to find this token in request:
Filter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; import javax.servlet.http.HttpServletRequest; public class TokenFilter extends AbstractPreAuthenticatedProcessingFilter { static final String TOKEN_PARAMETER = "token"; @Override protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { return request.getHeader(TOKEN_PARAMETER); } @Override protected Object getPreAuthenticatedCredentials(HttpServletRequest request) { return request.getHeader(TOKEN_PARAMETER); } } |
Now let's configure the framework: register the filter and code to authenticate.
In Spring Security there is the possibility to provide additional fallback code to provide the user authentication when primary one is failing using user details service. Sample config where we register our filter and authentication manager might look like:
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 | import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; import javax.annotation.Resource; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource private UserDetailsService userDetailsService; /* Fallback authentication mananger */ @Resource private TokenAuthenticationManager tokenAuthenticationManager; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.parentAuthenticationManager(tokenAuthenticationManager); auth.userDetailsService(userDetailsService); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/assets/**"); } @Override protected void configure(HttpSecurity http) throws Exception { /* This our FILTER */ TokenFilter filter = new TokenFilter(); filter.setAuthenticationManager(authenticationManager()); http.addFilterBefore(filter, AbstractPreAuthenticatedProcessingFilter.class); http.logout().logoutUrl("/auth/doLogout").logoutSuccessUrl("/auth/login.htm"); http.rememberMe(); http.authorizeRequests() .antMatchers("/reports/**/*").hasAnyAuthority(new String[]{"ADMIN", "USER"}) .anyRequest().permitAll() .and() .formLogin() .usernameParameter("username") .passwordParameter("password") .loginPage("/pages/login.htm") .failureUrl("/pages/failure.htm") .defaultSuccessUrl("/index.htm") .loginProcessingUrl("/auth/doLogin") .permitAll(); } } |
Now lets write this fallback authentication manager:
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 | import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.Objects; @Component public class TokenAuthenticationManager implements AuthenticationManager { @Resource private TokenService tokenService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { if (!(authentication instanceof PreAuthenticatedAuthenticationToken)) { throw new BadCredentialsException("Not supported auth"); } final Object principal = authentication.getPrincipal(); final Object creds = authentication.getCredentials(); checkFilterProcessed(principal, creds); try { Token token = tokenService.getByValue(principal.toString()); if (/* check token is valid: exists, not expired, etc */ false) { throw new BadCredentialsException("Bad token"); } User user = tokenService.restoreUserByToken(token); // User implements org.springframework.security.core.userdetails.UserDetails UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(user, creds, user.getAuthorities()); result.setDetails(user); return result; } catch (TokenNotFoundException e) { throw new BadCredentialsException(e.getMessage()); } } private void checkFilterProcessed(Object principal, Object creds) { if (!Objects.equals(principal, creds)) { throw new BadCredentialsException("Not supported auth"); } } } |
That's it!
This filter will be invoked only once when user is not logged out.
If you need to logout already authenticated user in the system - you need to create new Filter checking this token and insert it after the standard logout filter same as I did for TokenFilter and logout existing. See all available in org.springframework.security.config.annotation.web.builders.FilterComparator#FilterComparator.
For the case when you have REST API with a header you can modify token filter to extract the token, f.e. get it from the header with Authentication name
No comments:
Post a Comment