Is there a way to disable the redirect for Spring Security and the login page. My requirements specify the login should be part of the navigation menu.
Example:
The redirect behavior comes from SavedRequestAwareAuthenticationSuccessHandler which is the default success handler. Thus an easy solution to remove the redirect is to write your own success handler. E.g.
http.formLogin().successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//do nothing
}
});
When a browser gets a 401 with "WWW-Authetication: Basic ... ", it pops up a Dialog. Spring Security sends that header unless it sees "X-Requested-With" in the request.
You should send "X-Requested-With: XMLHttpRequest" header for all requests, this is an old fashioned way of saying - I am an AJAX request.
On my project I implemented it for the requirements:
1) For rest-request 401 status if user is not authorized
2) For simple page 302 redirect to login page if user is not authorized
public class AccessDeniedFilter extends GenericFilterBean {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
try {
filterChain.doFilter(request, response);
} catch (Exception e) {
if (e instanceof NestedServletException &&
((NestedServletException) e).getRootCause() instanceof AccessDeniedException) {
HttpServletRequest rq = (HttpServletRequest) request;
HttpServletResponse rs = (HttpServletResponse) response;
if (isAjax(rq)) {
rs.sendError(HttpStatus.FORBIDDEN.value());
} else {
rs.sendRedirect("/#sign-in");
}
}
}
}
private Boolean isAjax(HttpServletRequest request) {
return request.getContentType() != null &&
request.getContentType().contains("application/json") &&
request.getRequestURI() != null &&
(request.getRequestURI().contains("api") || request.getRequestURI().contains("rest"));
}
}
And enable the filter:
@Override
protected void configure(HttpSecurity http) throws Exception {
...
http
.addFilterBefore(new AccessDeniedFilter(),
FilterSecurityInterceptor.class);
...
}
You can change handle AccessDeniedException for you requirements in the condition:
if (isAjax(rq)) {
rs.sendError(HttpStatus.FORBIDDEN.value());
} else {
rs.sendRedirect("/#sign-in");
}
For annotation based settings use the following:
.defaultSuccessUrl("/dashboard",true)
You need to disable redirection in a couple of different places. Here's a sample based on https://github.com/Apress/beg-spring-boot-2/blob/master/chapter-13/springboot-rest-api-security-demo/src/main/java/com/apress/demo/config/WebSecurityConfig.java
In my case, I don't return json body but only HTTP status to indicate success/failure. But you can further customize the handlers to build the body. I also kept CSRF protection on.
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
public void initialize(AuthenticationManagerBuilder auth, DataSource dataSource) throws Exception {
// here you can customize queries when you already have credentials stored somewhere
var usersQuery = "select username, password, 'true' from users where username = ?";
var rolesQuery = "select username, role from users where username = ?";
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(usersQuery)
.authoritiesByUsernameQuery(rolesQuery)
;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// all URLs are protected, except 'POST /login' so anonymous user can authenticate
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.anyRequest().authenticated()
// 401-UNAUTHORIZED when anonymous user tries to access protected URLs
.and()
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
// standard login form that sends 204-NO_CONTENT when login is OK and 401-UNAUTHORIZED when login fails
.and()
.formLogin()
.successHandler((req, res, auth) -> res.setStatus(HttpStatus.NO_CONTENT.value()))
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
// standard logout that sends 204-NO_CONTENT when logout is OK
.and()
.logout()
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.NO_CONTENT))
// add CSRF protection to all URLs
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
;
}
}
Here's a deep explanation of the whole process, including CSRF and why you need a session: https://spring.io/guides/tutorials/spring-security-and-angular-js/
Scenarios that I tested:
happy path
GET /users/current (or any of your protected URLs)
request --> no cookie
<- response 401 + cookie XSRF-TOKEN
POST /login
-> header X-XSRF-TOKEN + cookie XSRF-TOKEN + body form with valid username/password
<- 204 + cookie JSESSIONID
GET /users/current
-> cookie JSESSIONID
<- 200 + body with user details
POST /logout
-> header X-XSRF-TOKEN + cookie XSRF-TOKEN + cookie JSESSIONID
<- 204
=== exceptional #1: bad credentials
POST /login
-> header X-XSRF-TOKEN + cookie XSRF-TOKEN + body form with bad username/password
<- 401
=== exceptional #2: no CSRF at /login (like a malicious request)
POST /login
-> cookie XSRF-TOKEN + body form with valid username/password
<- 401 (I would expect 403, but this should be fine)
=== exceptional #3: no CSRF at /logout (like a malicious request)
(user is authenticated)
POST /logout
-> cookie XSRF-TOKEN + cookie JSESSIONID + empty body
<- 403
(user is still authenticated)