Currently I have a single authentication mechanism in my application which is to use LDAP for authentication and authorization. My security configuration looks like this
@Configuration @EnableWebMvcSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .anyRequest().fullyAuthenticated() .and() .httpBasic(); } @Configuration protected static class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter { @Value("${ldap-${env}.manager.dn}") private String managerDn; @Value("${ldap-${env}.manager.pass}") private String managerPass; @Value("${ldap-${env}.server.url}") private String url; @Value("${ldap.password.attribute:userPassword}") private String passwordAttr; @Override public void init(AuthenticationManagerBuilder auth) throws Exception { auth.ldapAuthentication().userDnPatterns("uid={0},ou=people").groupSearchBase("ou=groups") .groupSearchFilter("(member={0})").userSearchBase("ou=people").userSearchFilter("(uid={0})") .userDetailsContextMapper(new CustomLdapPersonContextMapper()) // .passwordCompare() // .passwordAttribute(passwordAttr) // .passwordEncoder(new PlaintextPasswordEncoder()) // .and() .contextSource().managerDn(managerDn).managerPassword(managerPass).url(url); } } }
There are situations though where users might come in with a session token which can authentication from a session key server and valid token returns a username which can then be used to load authrization information from LDAP for that user. So my second authentication mechanism should happen first where if a session token is present in http headers it should perform the token authentication and then ldap lookup and if no session token is present it should just fall to current authentication mechanism. How can I add this second layer of authentication.
When using Java configuration, the way to define multiple security realms is to have multiple @Configuration classes that extend the WebSecurityConfigurerAdapter base class – each with its own security configuration. These classes can be static and placed inside the main config.
Authentication Provider calls User Details service loads the User Details and returns the Authenticated Principal. Authentication Manager returns the Authenticated Object to Authentication Filter and Authentication Filter sets the Authentication object in Security Context .
The UsernamePasswordAuthenticationToken is an implementation of interface Authentication which extends the interface Principal . Principal is defined in the JSE java. security . UsernamePasswordAuthenticationToken is a concept in Spring Security which implements the Principal interface.
I spent quite some time wrapping my head around spring-security when using pure java configuration. There are a few steps involved in getting this to work. It should be something along these lines. The basic process is as follows:
Create custom filters to check requests for specific authorization information
Each filter returns null (if no authorization of that type is found), or a custom AbstractAuthenticationToken
If a filter returns a token, each AuthenticationProvider's supports(class) method will be invoked with that token returning true|false if it should try authentication
attemptAuthentication will then be called on the AuthenticationProvider which supports the token. Here you do any service calls to authenticate the user. You can then throw LoginException's or call authentication.setAuthenticated(true) and return the token for a successful authentication.
I have been using this setup for a while supporting various authentication methods (signed request, username/password, oauth etc) and it works quite well.
You can also pass AuthenticationSuccessHandler's and AuthenticationFailuersHandler's to the custom security filters to provide custom redirection strategies and failure handling.
Also be sure to setup the ant matchers in the filter's constructors to control what url patterns the filters apply too. For example, an ldap request filter would probably need to be check with any request "/*" whereas a username/password filter can just be checked on POST's to /login or something similar.
Example Code:
1) Create custom AuthenticationToken's for each type of authentication you want to support
public class LDAPAuthorizationToken extends AbstractAuthenticationToken { private String token; public LDAPAuthorizationToken( String token ) { super( null ); this.token = token; } public Object getCredentials() { return token; } public Object getPrincipal() { return null; } } public class OTPAuthorizationToken extends UsernamePasswordAuthenticationToken { private String otp; public OTPAuthorizationToken( String username, String password, String otp ) { super( username, password ); this.otp = otp; } public String getOTP() { return otp; } }
2) Create custom security filters for each type
public class LDAPAuthorizationFilter extends AbstractAuthenticationProcessingFilter { @Autowired private UserDetailsService userDetailsService; public LDAPAuthorizationFilter() { super( "/*" ); // allow any request to contain an authorization header } public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response ) throws AuthenticationException { if ( request.getHeader( "Authorization" ) == null ) { return null; // no header found, continue on to other security filters } // return a new authentication token to be processed by the authentication provider return new LDAPAuthorizationToken( request.getHeader( "Authorization" ) ); } } public class OTPAuthorizationFilter extends AbstractAuthenticationProcessingFilter { @Autowired private UserDetailsService userDetailsService; public OTPAuthorizationFilter() { super( "/otp_login" ); } public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response ) throws AuthenticationException { if ( request.getParameter( "username" ) == null || request.getParameter( "password" ) == null || request.getParameter( "otp" ) == null ) { return null; } // return a new authentication token to be processed by the authentication provider return new OTPAuthorizationToken( request.getParameter( "username" ), request.getParameter( "password" ), request.getParameter( "otp" ) ); } }
3) Create custom AuthenticationProviders
public class LDAPAuthenticationProvider implements AuthenticationProvider { @Autowired private MyAuthenticationService sampleService; @Override public Authentication authenticate( Authentication authentication ) throws AuthenticationException { LDAPAuthorizationToken auth = (LDAPAuthorizationToken)authentication; String username = sampleService.verifyToken( auth.getCredentials() ); if ( username == null ) { throw new LoginException( "Invalid Token" ); } auth.setAuthenticated( true ); return auth; } @Override public boolean supports( Class<?> authentication ) { if ( authentication.isAssignableFrom( LDAPAuthorizationToken.class ) ) { return true; } return false; } } public class OTPAuthenticationProvider implements AuthenticationProvider { @Autowired private MyAuthenticationService sampleService; @Override public Authentication authenticate( Authentication authentication ) throws AuthenticationException { OTPAuthorizationToken auth = (OTPAuthorizationToken)authentication; String error = sampleService.loginWithOTP( auth.getPrincipal(), auth.getCredentials(), auth.getOTP() ); if ( error != null ) { throw new LoginException( error ); } auth.setAuthenticated( true ); return auth; } @Override public boolean supports( Class<?> authentication ) { if ( authentication.isAssignableFrom( OTPAuthorizationToken.class ) ) { return true; } return false; } }
4) Configure spring security
public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure( HttpSecurity http ) throws Exception { // configure filters http.addFilterBefore( new LDAPAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class ); http.addFilterBefore( new OTPAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class ); // configure authentication providers http.authenticationProvider( new LDAPAuthenticationProvider() ); http.authenticationProvider( new OTPAuthenticationProvider() ); // disable csrf http.csrf().disable(); // setup security http.authorizeRequests() .anyRequest() .fullyAuthenticated() .and().httpBasic(); } }
Hope that helps!
Another, option to add a second authentication provider: Simply specify another one on the AuthenticationManagerBuilder
. Because the @EnableWebSecurity
annotation is itself annotated with EnableGlobalAuthentication
you can configure the global instance of AuthenticationManagerBuilder
. (See the javadocs for more details.)
For example, here we have an LDAP authentication provider as well as an in memory (hard-coded) authentication provider (this is something we do in development to have local users to test with):
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Value("${user.role}") private String userRole; // i.e. ROLE_APP_USER @Value("${include.test.users}") private boolean includeTestUsers; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/**/js/**").permitAll() .antMatchers("/**/images/**").permitAll() .antMatchers("/**/favicon.ico").permitAll() .antMatchers("/**/css/**").permitAll() .antMatchers("/**/fonts/**").permitAll() .antMatchers("/**").hasAnyRole(userRole) .and().formLogin().loginPage("/login").permitAll().and().logout().permitAll(); http.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth, LdapContextSource contextSource) throws Exception { auth.ldapAuthentication() .userSearchBase("OU=Users OU") .userSearchFilter("sAMAccountName={0}") .groupSearchBase("OU=Groups OU") .groupSearchFilter("member={0}") .contextSource(contextSource); if (includeTestUsers) { auth.inMemoryAuthentication().withUser("user").password("u").authorities(userRole); } } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With