30

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.

Alexander Arutinyants
  • 1,619
  • 2
  • 23
  • 49
adeelmahmood
  • 2,371
  • 8
  • 36
  • 59

4 Answers4

68

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();
     }
    

    }

starball
  • 20,030
  • 7
  • 43
  • 238
Matt MacLean
  • 19,410
  • 7
  • 50
  • 53
  • How does it work? What is "Authorization" header key? What are auth urls? Thanks – Tayfun Yaşar May 10 '17 at 07:07
  • I have tried this solution and I see that the `OTPAuthenticationProvider` is never called after successfully accessing `/otp_login`. I get a 404 Not Found. – dondragon2 May 28 '19 at 21:14
16

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);
        }
      }
    }
Brice Roncace
  • 10,110
  • 9
  • 60
  • 69
2

I want to just add to mclema's answer. You may need to add override for successful authentication and continue the filter chain or else user gets redirected to default url ("/") instead of the original one (eg: /myrest/server/somemethod)

@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
        Authentication authResult) throws IOException, ServletException {
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication(authResult);
    SecurityContextHolder.setContext(context);
    chain.doFilter(request, response);
}
Mir3
  • 93
  • 2
  • 7
1

The accepted answer has the issue that the current request is not granted ie. only for the following requests the session is established! Therefore I needed to configure in point 2

public class MyAuthorizationFilter extends AbstractAuthenticationProcessingFilter {

    public MyAuthorizationFilter() {
        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
        }

        // required to use the token 
        myNewToken = new MyAuthorizationToken( request.getHeader( "Authorization" ) );
        // and set in the current context ==> the current request is as well authorized
        SecurityContextHolder.getContext().setAuthentication(myNewToken);
        // return a new authentication token to be processed by the authentication provider
        return myNewToken;
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        // try to authenticate the current request
        attemptAuthentication((HttpServletRequest) req, (HttpServletResponse) res);
        super.doFilter(req, res, chain);
    }
}

otherwise the current request is not yet authenticated although a session is already created!!! (And the Providers I do not need, i.e. adding filter is sufficient.)

LeO
  • 4,238
  • 4
  • 48
  • 88