7

I have about Spring Security from various resources and I know how filters and authentication managers work separately but I am not sure of the exact sequence in which a request works with them. If I am not wrong, in short the request first goes through the filters and the filters call their respective authentication managers.

I want to allow two kinds of authentication - one with JWT tokens and another with username and password. Below is the extract from security.xml

Security.xml

<http pattern="/api/**" create-session="stateless" realm="protected-apis" authentication-manager-ref="myAuthenticationManager" >
        <csrf disabled="true"/>
        <http-basic entry-point-ref="apiEntryPoint" />
        <intercept-url pattern="/api/my_api/**" requires-channel="any" access="isAuthenticated()" />  <!-- make https only. -->
        <custom-filter ref="authenticationTokenProcessingFilter" position = "FORM_LOGIN_FILTER"/>
</http>

<beans:bean id="authenticationTokenProcessingFilter"
                class="security.authentication.TokenAuthenticationFilter">
    <beans:constructor-arg value="/api/my_api/**" type="java.lang.String"/>
</beans:bean>

<authentication-manager id="myAuthenticationManager">
    <authentication-provider ref="myAuthenticationProvider" />
</authentication-manager>   

<beans:bean id="myAuthenticationProvider"
                class="security.authentication.myAuthenticationProvider" />

MyAuthenticationProvider.java

public class MyAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication)
                                      throws AuthenticationException {
        // Code
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
    }
}

TokenAuthenticationFilter.java

public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter{
    protected TokenAuthenticationFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl); //defaultFilterProcessesUrl - specified in applicationContext.xml.
        super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(defaultFilterProcessesUrl)); //Authentication will only be initiated for the request url matching this pattern
        setAuthenticationManager(new NoOpAuthenticationManager());
        setAuthenticationSuccessHandler(new TokenSimpleUrlAuthenticationSuccessHandler());
        setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
    }

    /**
     * Attempt to authenticate request
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException,
                                 IOException,
                                 ServletException {
        String tid = request.getHeader("authorization");
        logger.info("token found:"+tid);
        AbstractAuthenticationToken userAuthenticationToken = authUserByToken(tid,request);
        if(userAuthenticationToken == null) throw new AuthenticationServiceException("Invalid Token");
        return userAuthenticationToken;
    }

    /**
     * authenticate the user based on token
     * @return
     */
    private AbstractAuthenticationToken authUserByToken(String token,HttpServletRequest request) throws
                                                                                               JsonProcessingException {
        if(token==null) return null;

        AbstractAuthenticationToken authToken =null;

        boolean isValidToken = validate(token);
        if(isValidToken){
            List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
            authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
            authToken = new UsernamePasswordAuthenticationToken("", token, authorities);
        }
        else{
            BaseError error = new BaseError(401, "UNAUNTHORIZED");
            throw new AuthenticationServiceException(error.getStatusMessage());

        }
        return authToken;
    }

    private boolean validate(String token) {
        if(token.startsWith("TOKEN ")) return true;
        return false;
    }


    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain) throws IOException, ServletException {
        super.doFilter(req, res, chain);
        }
    }

Through myAuthenticationProvider I want username-password based authentication & through the custom filter I want to check for JWT tokens. Can someone let me know if I am going in the right direction?

manish
  • 19,695
  • 5
  • 67
  • 91
I_dont_know
  • 341
  • 1
  • 4
  • 17

2 Answers2

20

Solution overview


Broadly speaking, the requirement to have multiple AuthenticationProviders fall into two categories:

  1. Authenticate requests for different types of URLs with different authentication modes, for example:
    1. Authenticate all requests for /web/** using form-based username-password authentication;
    2. Authenticate all requests for /api/** using token-based authentication.
  2. Authenticate all requests with one of multiple supported authentication modes.

The solutions are slightly different for each, but they are based on a common foundation.

Spring Security has out-of-the-box support for form-based username-password authentication, so regardless of the two categories above, this can be implemented quite easily.

Token-based authentication however, is not supported out-of-the-box, so custom code is required to add the necessary support. The following components are required to add this support:

  1. A POJO extending AbstractAuthenticationToken that will hold the token to use for authentication.
  2. A filter extending AbstractAuthenticationProcessingFilter that will extract the token value from the request and populate the POJO created on step 1 above.
  3. An AuthenticationProvider implementation that will authenticate requests using the token.
  4. Spring Security configuration for option 1 or 2 above, depending on the requirement.

AbstractAuthenticationToken


A POJO is required to hold the JWT token that should be used for authenticating the request, therefore, the simplest AbstractAuthenticationToken implementation could look like:

public JWTAuthenticationToken extends AbstractAuthenticationToken {
  private final String token;

  JWTAuthenticationToken(final String token, final Object details) {
    super(new ArrayList<>());

    this.token = token;

    setAuthenticated(false);
    setDetails(details);
  }

  @Override
  public Object getCredentials() { return null; }

  @Override
  public String getPrincipal() { return token; }
}

AbstractAuthenticationProcessingFilter


A filter is required to extract the token from the request.

public class JWTTokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
  public JWTTokenAuthenticationFilter (String defaultFilterProcessesUrl) {
    super(defaultFilterProcessesUrl);
  }

  @Override
  public Authentication attemptAuthentication(final HttpServletRequest request
  , final HttpServletResponse response)
  throws AuthenticationException {
    final JWTAuthenticationToken token = new JWTAuthenticationToken(/* Get token from request */
    , authenticationDetailsSource.buildDetails(request));

    return getAuthenticationManager().authenticate(token);
  }
}

Notice that the filter does not attempt to perform the authentication; rather, it delegates the actual authentication to an AuthenticationManager, which ensures that any pre and post authentication steps are also performed correctly.

AuthenticationProvider


The AuthenticationProvider is the actual component responsible for performing the authentication. It is called by the AuthenticationManager automatically, if configured properly. A simple implementation would look like:

public class JWTAuthenticationProvider implements AuthenticationProvider {
  @Override
  public boolean supports(final Class<?> authentication) {
    return (JWTAuthenticationToken.class.isAssignableFrom(authentication));
  }

  @Override
  public Authentication authenticate(final Authentication authentication)
         throws AuthenticationException {
    final JWTAuthenticationToken token = (JWTAuthenticationToken) authentication;
    ...
  }
}

Spring Security configuration for different authentication modes for different URLs


Use different http elements for each of the URL families, such as:

<bean class="com.domain.path.to.provider.FormAuthenticationProvider" "formAuthenticationProvider" />
<bean class="com.domain.path.to.provider.JWTAuthenticationProvider" "jwtAuthenticationProvider" />

<authentication-manager id="apiAuthenticationManager">
  <authentication-provider ref="jwtAuthenticationProvider" />
</authentication-manager>

<authentication-manager id="formAuthenticationManager">
  <authentication-provider ref="formAuthenticationProvider" />
</authentication-manager>

<bean class="com.domain.path.to.filter.JWTAuthenticationFilter" id="jwtAuthenticationFilter">
  <property name="authenticationManager" ref="apiAuthenticationManager" />
</bean>

<http pattern="/api/**" authentication-manager-red="apiAuthenticationManager">
  <security:custom-filter position="FORM_LOGIN_FILTER" ref="jwtAuthenticationFilter"/>

  ...
</http>

<http pattern="/web/**" authentication-manager-red="formAuthenticationManager">
  ...
</http>

Since different authentication modes are required for different URL families, we need two different AuthenticationManagers and two different http configurations, one for each of the URL families. For each, we choose which mode of authentication is supported.

Spring Security configuration for multiple authentication modes for same URLs


Use a single http element, as follows:

<bean class="com.domain.path.to.provider.FormAuthenticationProvider" "formAuthenticationProvider" />
<bean class="com.domain.path.to.provider.JWTAuthenticationProvider" "jwtAuthenticationProvider" />

<authentication-manager id="authenticationManager">
  <authentication-provider ref="formAuthenticationProvider" />
  <authentication-provider ref="jwtAuthenticationProvider" />
</authentication-manager>

<bean class="com.domain.path.to.filter.JWTAuthenticationFilter" id="jwtAuthenticationFilter">
  <property name="authenticationManager" ref="authenticationManager" />
</bean>

<http pattern="/**">
  <security:custom-filter after="FORM_LOGIN_FILTER" ref="jwtAuthenticationFilter"/>

  ...
</http>

Note the following:

  1. AuthenticationManager need not be explicitly specified for the http element as there is only one in the configuration and its identifier is authenticationManager, which is the default value.
  2. The token filter is inserted after the form login filter, instead of replacing it. This ensures that both form login and token login will work.
  3. The AuthenticationManager is configured to use multiple AuthenticationProviders. This ensures that multiple authentication mechanisms are tried, until one is found that is supported for a request.
manish
  • 19,695
  • 5
  • 67
  • 91
  • Is `authentication-manager-red` a misspelling of `authentication-manager-ref`? Is `id=` or something missing in the `` nodes near the beginning of the code samples? – Pang Jun 15 '23 at 07:41
5

The way I did it was using 2 security configurers. I have an example with Java config but if you understand it, you can port it to xml. Please note that this is just one of the ways and not the only way.

@Configuration
    @Order(1)                                                        
    public static class LoginSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {

        @Override       
        public void configure(AuthenticationManagerBuilder auth) 
          throws Exception {            
            auth.inMemoryAuthentication().withUser("user").password("user").roles("USER");
            auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN");
        }

        protected void configure(HttpSecurity http) throws Exception {
            http
                .antMatcher("/api/login/**")                               
                .authorizeRequests()
                .antMatchers("/api/login/**").authenticated()
                    .and()
                .httpBasic();
        }
    }

    @Configuration
    @Order(2)
    public static class JWTSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

        @Override       
        public void configure(AuthenticationManagerBuilder auth) 
          throws Exception {

            auth.inMemoryAuthentication().withUser("user1").password("user").roles("USER");
            auth.inMemoryAuthentication().withUser("admin1").password("admin").roles("ADMIN");
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .antMatcher("/api/**")
                .authorizeRequests()
                .antMatchers("/api/**").authenticated()
                    .and()          
            .addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        }
    }

EXPLANATION:

In the LoginSecurityConfigurerAdapter, I am intercepting only api/login urls. So first time, the login request will be caught here and on successful authentication you can issue a JWT. Now in JWTSecurityConfigurerAdapter, I am catching all the other requests. Using tokenauthenticationfilter it will validate the JWT and only in case of valid JWT it will aloow access to an API.

tryingToLearn
  • 10,691
  • 12
  • 80
  • 114
  • Can't I use the same JWT-Authentication Filter for 2 WebSecurityConfigurerAdapter ? – Arun Apr 10 '19 at 07:23