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:
- Request can come from any platform to "abc.com/rest_api/"
- 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.
- If username and password will have to be authenticated by database then a token will be generated and send back in response.
- 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