4
votes

I am trying to create an application that will primarily access a REST API using Spring, and am trying to configure the security side. Trying to present the actual structure of the application using this picture: enter image description here

  1. Request can come from any platform to "abc.com/rest_api/"
  2. Request will be send to point 3 or point 5. If user has already authenticated by username and password then request will be verified against Token else will be redirected to database.
  3. If username and password will have to be authenticated by database then a token will be generated and send back in response.
  4. After that only token based authentication will work.

I have tried to create a basic structure and I think must be making a minor mistake due to which it's not working as expected.

@Configuration
@EnableWebSecurity
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(securedEnabled=true, prePostEnabled=true)
public class UserDetailsSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private NSecurityContextHolder securityContextHolder;

    @Autowired
    private NHttpServletRequestBinder<Authentication> authenticationBinder;

    public static final String DEF_USERS_BY_USERNAME_QUERY
            = "SELECT user ";


public static final String GROUPS_BY_USERNAME_QUERY =
        "SELECT groups by user";
public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY =
        "SELECT  authorities";


@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

        auth.jdbcAuthentication().dataSource(getDataSourceFromJndi())
                .usersByUsernameQuery(DEF_USERS_BY_USERNAME_QUERY).
                authoritiesByUsernameQuery(DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY).
                groupAuthoritiesByUsername(GROUPS_BY_USERNAME_QUERY);


    }
      private DataSource getDataSourceFromJndi() {
        try {


             DataSource dataSource = (DataSource) new InitialContext().lookup("DS");
            return dataSource;

        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

      @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
          auth.jdbcAuthentication().dataSource(getDataSourceFromJndi())
                .usersByUsernameQuery(DEF_USERS_BY_USERNAME_QUERY).
                authoritiesByUsernameQuery(DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY).
                groupAuthoritiesByUsername(GROUPS_BY_USERNAME_QUERY);

    }


      @Override
    protected void configure(HttpSecurity http) throws Exception {


                // The http.formLogin().defaultSuccessUrl("/path/") method is required when using stateless Spring Security
        // because the session cannot be used to redirect to the page that was requested while signed out. Unfortunately
        // using this configuration method will cause our custom success handler (below) to be overridden with the
        // default success handler. So to replicate the defaultSuccessUrl("/path/") configuration we will instead
        // correctly configure and delegate to the default success handler.
        final SimpleUrlAuthenticationSuccessHandler delegate = new SimpleUrlAuthenticationSuccessHandler();
        delegate.setDefaultTargetUrl("/api/");
          // Make Spring Security stateless. This means no session will be created by Spring Security, nor will it use any
        // previously existing session.
        http.sessionManagement().sessionCreationPolicy(STATELESS);
        // Disable the CSRF prevention because it requires the session, which of course is not available in a
        // stateless application. It also greatly complicates the requirements for the sign in POST request.
        http.csrf().disable();
        // Viewing any page requires authentication.
        http.authorizeRequests().anyRequest().authenticated();
        http
          .formLogin().loginPage("http://localhost/web/ui/#access/signin")
          .permitAll()
            // Override the sign in success handler with our stateless implementation. This will update the response
            // with any headers and cookies that are required for subsequent authenticated requests.
            .successHandler(new NStatelessAuthenticationSuccessHandler(authenticationBinder, delegate));
        http.logout().logoutUrl("http://localhost/web/ui/#access/signin").logoutSuccessUrl("http://localhost/web/ui/#access/signin");
        // Add our stateless authentication filter before the default sign in filter. The default sign in filter is
        // still used for the initial sign in, but if a user is authenticated we need to acknowledge this before it is
        // reached.
        http.addFilterBefore(
            new StatelessAuthenticationFilter(authenticationBinder, securityContextHolder),
            UsernamePasswordAuthenticationFilter.class
        );

} 

}

And I have two types of authenticationBinder i.e. TokenBased and UserNameBased.

TokenBased:

@Component
public class NXAuthTokenHttpServletRequestBinder implements NHttpServletRequestBinder<String> {

    private static final String X_AUTH_TOKEN = "X-AUTH-TOKEN";
    private final NTokenFactory tokenFactory;

    @Autowired
    public NXAuthTokenHttpServletRequestBinder(NTokenFactory tokenFactory) {
        this.tokenFactory = tokenFactory;
    }

    @Override
    public void add(HttpServletResponse response, String username) {

        final String token = tokenFactory.create(username);

        response.addHeader(X_AUTH_TOKEN, token);
        response.addCookie(new Cookie(X_AUTH_TOKEN, token));
    }

    @Override
    public String retrieve(HttpServletRequest request) {

        final String cookieToken = findToken(request);

        if (cookieToken != null) {
            return tokenFactory.parseUsername(cookieToken);
        }

        return null;
    }

    private static String findToken(HttpServletRequest request) {
        Enumeration<String> it = request.getHeaderNames();
        while(it.hasMoreElements()){
            System.out.println(it.nextElement());
        }
        final String headerToken = request.getHeader(X_AUTH_TOKEN);

        if (headerToken != null) {
            return headerToken;
        }

        final Cookie[] cookies = request.getCookies();

        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (X_AUTH_TOKEN.equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }

        return null;
    }
}

UserBased:

@Component
@Primary
public class NUserAuthenticationFactory implements NHttpServletRequestBinder<Authentication> {

    private final NHttpServletRequestBinder<String> httpServletRequestBinder;

    @Autowired

    public NUserAuthenticationFactory(NHttpServletRequestBinder<String> httpServletRequestBinder) {
        this.httpServletRequestBinder = httpServletRequestBinder;
    }

    @Override
    public void add(HttpServletResponse response, Authentication authentication) {
        httpServletRequestBinder.add(response, authentication.getName());
    }

    @Override
    public UserAuthentication retrieve(HttpServletRequest request) {

        final String username = httpServletRequestBinder.retrieve(request);

        if (username != null) {

            return new UserAuthentication(new CustomJDBCDaoImpl().loadUserByUsername(username));
        }

        return null;
    }
}

Problem Whenever I load my application it comes into UserBased Authentication and then try to fetch the username from token instead of validating it from database. But, by that time no token was there as it's the first post request I make from the UI. And it redirects me back to the same Login Page.

Logs:

Fine: / at position 1 of 12 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter' Fine: / at position 2 of 12 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter' Fine: / at position 3 of 12 in additional filter chain; firing Filter: 'HeaderWriterFilter' Fine:
Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@a837508 Fine: / at position 4 of 12 in additional filter chain; firing Filter: 'LogoutFilter' Fine: Checking match of request : '/'; against 'http://localhost/web/ui/#access/signin' Fine: / at position 5 of 12 in additional filter chain; firing Filter: 'StatelessAuthenticationFilter' Fine: / at position 6 of 12 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter' Fine: Request 'GET /' doesn't match 'POST http://localhost/web/ui/#access/signin Fine: / at position 7 of 12 in additional filter chain; firing Filter: 'RequestCacheAwareFilter' Fine: / at position 8 of 12 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter' Fine: / at position 9 of 12 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter' Fine: Populated SecurityContextHolder with anonymous token: 'org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS' Fine: / at position 10 of 12 in additional filter chain; firing Filter: 'SessionManagementFilter' Fine: Requested session ID 3e2c15a2a427bf47e51496d2a186 is invalid. Fine: / at position 11 of 12 in additional filter chain; firing Filter: 'ExceptionTranslationFilter' Fine: / at position 12 of 12 in additional filter chain; firing Filter: 'FilterSecurityInterceptor' Fine: Secure object: FilterInvocation: URL: /; Attributes: [authenticated] Fine: Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS Fine: Voter: org.springframework.security.web.access.expression.WebExpressionVoter@2ac71565, returned: -1 Fine: Access is denied (user is anonymous); redirecting to authentication entry point org.springframework.security.access.AccessDeniedException: Access is denied

1

1 Answers

2
votes

Step wise solution for your problem can be..

  1. Use spring security UsernamePasswordAuthenticationFilter to authenticate the user through Username and password and generate a unique token.
  2. Write another custom security filter and AuthenticationProvider implementation to authenticate the user for successive request.
  3. Place custom security filter befor UsernamePasswordAuthenticationFilter as given below

    http.addFilterBefore(CustomTokenBasedAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

  4. register your AuthenticationProvider implementation with AuthenticationManager

  5. And Thats it!!!

Note:- A better way to secure your rest APIs is to use some standard protocols like oauth1a, oauth2.0 etc. Spring provides a novel implementation of oauth1a and oauth2.0 protocol.