Spring Security unexpected behavior for REST endpoints authentication? - spring

The scenario we are looking for is as follows:
client connects with REST to a REST login url
Spring microservice (using Spring Security) should return 200 OK and a login token
the client keeps the token
the client calls other REST endpoints using the same token.
However, I see that the client is getting 302 and a Location header, together with the token. So it does authenticate, but with un-desired HTTP response status code and header.
The Spring Security configuration looks like this:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable() // Refactor login form
// See https://jira.springsource.org/browse/SPR-11496
.headers()
.addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
.and()
.formLogin()
.loginPage("/signin")
.permitAll()
.and()
.logout()
.logoutUrl("/signout")
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated();
...
}
I tried adding interceptors and filters but can't see where 302 and Location being set and added in Spring side.
However, the Location header does show in the response headers received at the client side (together with the rest of the Spring Security headers LINK):
Server=Apache-Coyote/1.1
X-Content-Type-Options=nosniff
X-XSS-Protection=1; mode=block
Cache-Control=no-cache, no-store, max-age=0, must-revalidate
Pragma=no-cache
Expires=0
X-Frame-Options=DENY, SAMEORIGIN
Set-Cookie=JSESSIONID=D1C1F1CE1FF4E1B3DDF6FA302D48A905; Path=/; HttpOnly
Location=http://ec2-35-166-130-246.us-west-2.compute.amazonaws.com:8108/ <---- ouch
Content-Length=0
Date=Thu, 22 Dec 2016 20:15:20 GMT
Any suggestion how to make it work as expected ("200 OK", no Location header and the token)?
NOTE: using Spring Boot, Spring Security, no UI, just client code calling REST endpoints.

If you need a rest api, you must not use http.formLogin(). It generates form based login as described here.
Instead you can have this configuration
httpSecurity
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.disable()
.addFilterBefore(authTokenFilter, UsernamePasswordAuthenticationFilter.class);
Create a class, AuthTokenFilter which extends Spring UsernamePasswordAuthenticationFilter and override doFilter method, which checks for an authentication token in every request and sets SecurityContextHolder accordingly.
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse resp = (HttpServletResponse) response;
resp.setHeader("Access-Control-Allow-Origin", "*");
resp.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
resp.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, " + tokenHeader);
HttpServletRequest httpRequest = (HttpServletRequest) request;
String authToken = httpRequest.getHeader(tokenHeader);
String username = this.tokenUtils.getUsernameFromToken(authToken); // Create some token utility class to manage tokens
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(-------------);
// Create an authnetication as above and set SecurityContextHolder
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
Then create an AuthenticationController, mapped with /login url, which checks credentials, and returns token.
/*
* Perform the authentication. This will call Spring UserDetailsService's loadUserByUsername implicitly
* BadCredentialsException is thrown if username and password mismatch
*/
Authentication authentication = this.authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
authenticationRequest.getUsername(),
authenticationRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetailsImp userDetails = (UserDetailsImp) authentication.getPrincipal();
// Generate token using some Token Utils class methods, using this principal
To understand loadUserByUsername , UserDetailsService and UserDetails, please refer Spring security docs
}
For better understanding, please thoroughly read above link and subsequent chapters.

It's a 302 response telling the browser to redirect to your login page. What do you expect to happen? 302 response must have a Location header.

http.formLogin()
is designed for form-based login. So the 302 status and Location header in the response is expected if you attempt to access a protected resource without being authenticated.
Based on your requirement/scenario,
client connects with REST to a REST login url
have you considered using HTTP Basic for authentication?
http.httpBasic()
Using HTTP Basic, you can populate the Authorization header with the username/password and the BasicAuthenticationFilter will take care of authenticating the credentials and populating the SecurityContext accordingly.
I have a working example of this using Angular on the client-side and Spring Boot-Spring Security on back-end.
If you look at security-service.js, you will see a factory named securityService which provides a login() function. This function calls the /principal endpoint with the Authorization header populated with the username/password as per HTTP Basic format, for example:
Authorization : Basic base64Encoded(username:passsword)
The BasicAuthenticationFilter will process this request by extracting the credentials and ultimately authenticating the user and populating the SecurityContext with the authenticated principal. After authentication is successful, the request will proceed to the destined endpoint /principal which is mapped to SecurityController.currentPrincipal which simply returns a json representation of the authenticated principal.
For your remaining requirements:
Spring microservice (using Spring Security) should return 200 OK and a login token
the client keeps the token
the client calls other REST endpoints using the same token.
You can generate a security/login token and return that instead of the user info. However, I would highly recommend looking at Spring Security OAuth if you have a number of REST endpoints deployed across different Microservices that need to be protected via a security token. Building out your own STS (Security Token Service) can become very involved and complicated so not recommended.

You can implement your custom AuthenticationSuccessHandler and override method "onAuthenticationSuccess" to change the response status as per your need.
Example:
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
ObjectMapper mapper = new ObjectMapper();
Map<String, String> tokenMap = new HashMap<String, String>();
tokenMap.put("token", accessToken.getToken());
tokenMap.put("refreshToken", refreshToken.getToken());
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
mapper.writeValue(response.getWriter(), tokenMap);
}

You need to override the default logout success handler to avoid redirect. In spring boot2 you can do as below:
....logout().logoutSuccessHandler((httpServletRequest,httpServletResponse,authentication)->{
//do nothing not to redirect
})
For more details: Please check this.

You can use headers().defaultsDisabled() and then chain that method to add the specific headers you want.

Related

spring security permit all considering basic auth passed and validating it

I am doing POC on spring security recently and saw some strange behavior. API endpoint configured as permit all is authenticating the request if client calls this endpoint using basic auth.
Sample code:
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/health").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic().authenticationEntryPoint(entryPoint)
.and()
.csrf().disable()
.cors().and()
.formLogin().disable();
return http.build();
Calling using curl as below is giving error:
curl -X GET -u "user:password" -H Content-Type:application/json http://localhost:20000/health
Error:
{"responseCode":401,"responseStatus":"Unauthorized","errorMessage":["Not authorized to access"],"responseMsg":null}
And if called without and user and password in above curl command then it works and return the desired response.
Can anyone please suggest if this is correct behavior and how to override it.
Auth entrypoint implementation:
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint{
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
ResponseData responseData = new ResponseData();
responseData.setResponseStatus(HttpStatus.UNAUTHORIZED.getReasonPhrase());
responseData.setErrorMessage(new ArrayList<>(Arrays.asList("Not authorized to access")));
responseData.setResponseCode(HttpServletResponse.SC_UNAUTHORIZED);
ObjectMapper mapper = new ObjectMapper();
mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
String jsonResponse = mapper.writeValueAsString(responseData);
PrintWriter printWriter = response.getWriter();
printWriter.append(jsonResponse);
printWriter.flush();
printWriter.close();
}
}
permitAll does not mean that there is no authentication, it just states that there will be no authorization checks for that endpoint. Therefore, if you have httpBasic enabled and send the credentials in the request, the credentials will be checked by the BasicAuthenticationFilter against your UserDetailsService.
I assume that your credentials are wrong and that's why you are receiving a 401.

how to skip jwt authorization for configured apis in springboot rest

I am using springboot to build REST microservices. I have configured certain APIs to not require authorization through SecurityConfig as shown below:
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll() // white list the auth apis
.anyRequest()
.authenticated();
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
I also have the JWT filter as shown below:
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// get JWT from HTTP request
String jwtToken = getJWTFromRequest(request);
// validate the token
if ( StringUtils.hasText(jwtToken) && tokenProvider.validateToken(jwtToken, request) ){
// retrieve the user from token
String userName = tokenProvider.getUsernameFromJWT(jwtToken);
// load user from the database associated with the token
UserDetails userDetails = customUserDetailService.loadUserByUsername(userName);
// set spring security
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
This all works fine if I do not send a JWT token in the request header. However, if I do send a JWT token (for example an expired token) for an API call that has been configured to NOT REQUIRE any authorization, the filter code still tries to validate the JWT token even though it is not necessary.
What I would like to do is, in the JWT token filter, to skip the token processing complete if the end point being invoked does not require authorization. How can I access the end points configured as permitAll in the JWT token filter?
Thanks for your help.

Log Out does not work correctly in Spring Security OAuth2

There are a Zuul gateway as Spring-Security-OAuth2 client and Authorization Server. Those are located here
The part of Zuul config:
http
.csrf()
.disable()
.headers().cacheControl().disable()
.and()
.headers()
.cacheControl()
.disable()
.frameOptions()
.sameOrigin()
.and()
.httpBasic().disable()
.authorizeRequests()
.requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.mvcMatchers("/uaa/**", "/login**", "/favicon.ico", "/error**").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl("/app/Index.jsp")
.logoutRequestMatcher(new AntPathRequestMatcher("/reza"))
.addLogoutHandler(ssoLogoutHandler);
And the SsoLogoutHandler class of Zuul application as Spring-Security-OAuth2 client:
#Component
public class SSOLogoutHandler implements LogoutHandler {
#Override
public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
Object details = authentication.getDetails();
String token = ((OAuth2AuthenticationDetails) details).getTokenValue();
RestTemplate restTemplate = new RestTemplate();
String url = "http://192.168.10.97:9191/uaa/token/revoke?token=" + token;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Object> requestEntity = new HttpEntity<Object>(headers);
ResponseEntity<Boolean> result = restTemplate.exchange(url, HttpMethod.GET, requestEntity, new ParameterizedTypeReference<Boolean>() {
});
}
}
and the RevokeTokenController config class of Authorization Server:
#RestController
public class RevokeTokenController {
#Autowired
private TokenStore tokenStore;
#RequestMapping(method = RequestMethod.GET, value = "/token/revoke")
#ResponseBody
public Boolean revoke(String token) throws Exception {
OAuth2AccessToken tokenObj = tokenStore.readAccessToken(token);
tokenStore.removeAccessToken(tokenObj);
tokenStore.removeRefreshToken(tokenObj.getRefreshToken());
return true;
}
}
In order to above configuration that you see, SsoLogoutHandler of client calls as restTemplate to RevokeTokenController of Authorzation Server to log out, Token and Refresh Token are removed but the client again requests as /uaa/authorize... to get new access token and log out is not happen.
Where is wrong? I want to log out after removing both Token and Refresh Token instead of getting again the Access Token. Another hand I want to redirect to login page after removing the token.
Update:
I was deep into the client request after removing the token, the client requests like .../uaa/authorize?client_id=..., so the location attribute of its response is .../gateway/login?code=[code], because of the code, the client is not redirected to login page.
I have solved the problem by two logout endpoints in both Gateway and UAA, in that way first, by the /logout endpoint, the request is redirected to Gateway for logging out, so the its own logoutSuccessUrl is /uaa/logout endpoint of UAA, in order to these endpoints, both the logout of Gateway and UAA are happen.
like this:
In the Gateway
.and()
.logout()
.logoutSuccessUrl("/uaa/logout")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
and the part of UAA config is:
.and()
.logout()
.logoutSuccessUrl("/login")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
I would suggest to move your token revocation logic in SSOLogoutHandler and then redirect to login page from there instead of calling separate API call.
Because if something fails to execute token revocation logic due API call then that token will be there and you have to separately handle those tokens later on which will be more complex.
In this particular case, if autowire for TokenStore does not work then register/create a bean of SSOLogoutHandler in one of the connfiguration file and provide TokeStore dependancy from there for SSOLogoutHandler.

Spring Security with filters permitAll not working

I've got this security config:
#Override
public void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(
new JwtLoginFilter("/login", authenticationManager()),
UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(
new JwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
http.csrf().disable()
.authorizeRequests().antMatchers("/", "/register").permitAll()
.and()
.authorizeRequests().anyRequest().authenticated();
}
The two filters are doing authentication work: loginFilter checks credentials in the post body and then add cookie to the response. The authenticationFilter checks the auth cookie.
However, permitAll does not let the root route and "/register" route pass (aka. still going through the authenticationFilter, which I thought permitAll would let these routes pass the filters)
What's wrong?
permitAll() does not ignore filters. It simply grants access regardless of whether or not an Authentication is present in a request's security context after all filters have been processed.
You should check your filters and any AuthenticationProvider implementations that they use to to ensure that they are not breaking the execution flow of Spring Security by throwing unchecked/uncaught exceptions or expressly sending a response on a failed authentication.

Spring Security Disable Login Page / Redirect

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:
Therefore there is no dedicated login page. The login information needs to be submitted via Ajax. If an error occurs it should return JSON specifying the error and use the proper HTTP Status code. If authentication checks out it should return a 200 and then javascript can handle it from there.
I hope that makes sense unless there is any easier way to accomplish this with Spring Security. I don't have much experience with Spring Security. I assume this has to be a common practice, but I didn't find much.
Current spring security configuration
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/public/**").permitAll()
.antMatchers("/about").permitAll()
.anyRequest().fullyAuthenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.usernameParameter("email")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.deleteCookies("remember-me")
.logoutSuccessUrl("/")
.permitAll()
.and()
.rememberMe();
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
Update:
I tried using HttpBasic() but then it asks for login creds not matter what and its the ugly browser popup which is not acceptable to the end user. It looks like I may have to extend AuthenticationEntryPoint.
At the end of the day I need Spring security to send back JSON saying the authentication succeeded or failed.
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
}
});
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)
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");
}
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.

Resources