CAS single logout callback hits 403(forbidden) - java

I rephrased question and deleted old one. Hopefully I will get answers now.
CAS server attempt to send a callback request on SLO to protected app:
<Error Sending message to url endpoint [http://localhost:8080/j_spring_cas_security_check]. Error is [Server returned HTTP response code: 403 for URL: http://localhost:8080/j_spring_cas_security_check]>
On debug the org.jasig.cas.client.session.SingleSignOutFilter never gets hit.
#Override
public void configure(HttpSecurity http) throws Exception {
http.
addFilter(casFilter()).
addFilterBefore(requestSingleLogoutFilter(), LogoutFilter.class).
addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class)
.logout().permitAll().logoutSuccessUrl("http://localhost:8080/j_spring_cas_security_logout").invalidateHttpSession(true).and()
.authorizeRequests().antMatchers("/home2").hasAuthority("USER").and()
.exceptionHandling()
.authenticationEntryPoint(casEntryPoint)
.and()
.httpBasic();
}
Filter casFilter() {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setServiceProperties(getServiceProperties());
casAuthenticationFilter.setAuthenticationManager(getProviderManager());
return casAuthenticationFilter;
}
LogoutFilter requestSingleLogoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter("http://localhost:8089/cas/logout",
new SecurityContextLogoutHandler());
logoutFilter.setFilterProcessesUrl("/j_spring_cas_security_logout");
return logoutFilter;
}
SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
return singleSignOutFilter;
}
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.addListener(new org.jasig.cas.client.session.SingleSignOutHttpSessionListener());
}
I have everything apart from the logout. Currently it invalidates the session thanks to LogoutFilter, destroys the ticket(on redirect to CAS) BUT if the SLO request would be sent from other protected application, obviously it will have no impact on this application(as sessionid will still be here).
Any suggestions?

Had the same situation (403 status code) with CAS SLO, in my spring security log I found:
Invalid CSRF token found for http://localhost:8080/j_spring_cas_security_check
so I disabled csrf filter in my security config:
http.csrf().disable();
This might not be a good practice, just a quick solution that works for me now. Also I am not shure if I can get the right csrf token if SLO is initiated in other protected application.

Related

By extending WebSecurityConfigurerAdapter, How to construct configure with custom authentication logic

I am using okta to do authentication. Our company's okta disabled the 'default' authorization server. So right now I cannot use 'okta-spring-security-starter' to simple do this to verify token passed from url headers:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#Configuration
public class OktaOAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/health").permitAll()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt();
http.cors();
Okta.configureResourceServer401ResponseBody(http);
}
}
So I need to hit okta introspect endpoint (https://developer.okta.com/docs/reference/api/oidc/#introspect) to verify. So I am wondering can I integrate this procedure within the config of WebSecurityConfigurerAdapter. maybe something like this???:
import com.okta.spring.boot.oauth.Okta;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#Configuration
public class OktaOAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/health").permitAll()
.anyRequest().authenticated()
.and()
/*add something there*/
http.cors();
}
}
I saw something like override AuthenticationProvider(Custom Authentication provider with Spring Security and Java Config), and use httpbasic auth. Can I do similiar thing if I use .oauth2ResourceServer().jwt().
My idea is override the authentication provider and in the provider, hit the okta introspect endpoint, will this work???
Spring Security 5.2 ships with support for introspection endpoints. Please take a look at the Opaque Token sample in the GitHub repo.
To answer briefly here, though, you can do:
http
.authorizeRequests(authz -> authz
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaque -> opaque
.introspectionUri("the-endpoint")
.introspectionClientCredentials("client-id", "client-password")
)
);
If you are using Spring Boot, then it's a bit simpler. You can provide those properties in your application.yml:
spring:
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: ...
client-id: ...
client-secret: ...
And then your DSL can just specify opaqueToken:
http
.authorizeRequests(authz -> authz
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaque -> {})
);
I don't use Okta thus I don't know how exactly it works. But I have 2 assumptions:
Every request contains an accessToken in the Authorization header
You make a POST request to ${baseUrl}/v1/introspect and it will answer you with true or false to indicate that accessToken is valid or not
With these 2 assumptions in mind, if I have to manually implement custom security logic authentication, I would do following steps:
Register and implement a CustomAuthenticationProvider
Add a filter to extract access token from request
Registering custom authentication provider:
// In OktaOAuth2WebSecurityConfig.java
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider());
}
#Bean
CustomAuthenticationProvider customAuthenticationProvider(){
return new CustomAuthenticationProvider();
}
CustomAuthenticationProvider:
public class CustomAuthenticationProvider implements AuthenticationProvider {
private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationProvider.class);
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
logger.debug("Authenticating authenticationToken");
OktaTokenAuthenticationToken auth = (OktaTokenAuthenticationToken) authentication;
String accessToken = auth.getToken();
// You should make a POST request to ${oktaBaseUrl}/v1/introspect
// to determine if the access token is good or bad
// I just put a dummy if here
if ("ThanhLoyal".equals(accessToken)){
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("USER"));
logger.debug("Good access token");
return new UsernamePasswordAuthenticationToken(auth.getPrincipal(), "[ProtectedPassword]", authorities);
}
logger.debug("Bad access token");
return null;
}
#Override
public boolean supports(Class<?> clazz) {
return clazz == OktaTokenAuthenticationToken.class;
}
}
To register the filter to extract accessToken from request:
// Still in OktaOAuth2WebSecurityConfig.java
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAfter(accessTokenExtractorFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests().anyRequest().authenticated();
// And other configurations
}
#Bean
AccessTokenExtractorFilter accessTokenExtractorFilter(){
return new AccessTokenExtractorFilter();
}
And the filter it self:
public class AccessTokenExtractorFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(AccessTokenExtractorFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
logger.debug("Filtering request");
Authentication authentication = getAuthentication(request);
if (authentication == null){
logger.debug("Continuing filtering process without an authentication");
filterChain.doFilter(request, response);
} else {
logger.debug("Now set authentication on the request");
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
private Authentication getAuthentication(HttpServletRequest request) {
String accessToken = request.getHeader("Authorization");
if (accessToken != null){
logger.debug("An access token found in request header");
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("USER"));
return new OktaTokenAuthenticationToken(accessToken, authorities);
}
logger.debug("No access token found in request header");
return null;
}
}
I have uploaded a simple project here for your easy reference: https://github.com/MrLoyal/spring-security-custom-authentication
How it works:
The AccessTokenExtractorFilter is placed right after the UsernamePasswordAuthenticationFilter, which is a default filter by Spring Security
A request arrives, the above filter extracts accessToken from it and place it in the SecurityContext
Later, the AuthenticationManager calls the AuthenticationProvider(s) to authenticate request. This case, the CustomAuthenticationProvider is invoked
BTW, your question should contain spring-security tag.
Update 1: About AuthenticationEntryPoint
An AuthenticationEntryPoint declares what to do when an unauthenticated request arrives ( in our case, what to do when the request does not contain a valid "Authorization" header).
In my REST API, I simply response 401 HTTP status code to client.
// CustomAuthenticationEntryPoint
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.reset();
response.setStatus(401);
// A utility method to add CORS headers to the response
SecUtil.writeCorsHeaders(request, response);
}
Spring's LoginUrlAuthenticationEntryPoint redirects user to login page if one is configured.
So if you want to redirect unauthenticated requests to Okta's login page, you may use a AuthenticationEntryPoint.

How to mock authentication and authorization with Spring Security

I have a spring-boot REST API application. The REST endpoints are protected by spring-security.
This is the configuration of spring-security:
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.exceptionHandling().authenticationEntryPoint(new CustomForbiddenErrorHandler())
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/api/**").authenticated() // HTTP 403 if the request is not authenticated
.antMatchers("/**").permitAll();
}
It works fine. If I make a rest call without having an auth-token on the HTTP header I get back the proper HTTP error code.
Can I force spring somehow to add a self auth-token if it is not presented in order so I can make REST calls without having my own access management system? It will be installed later.
I am not asking how to write JUnit test. What I am asking is how to generate a mock auth-token on the fly and add it to the request if it does not exist.
You can override the existing authentication filter, or create a new custom filter, to check if a request contains a bearer token or not. Based on the result, you can either process the request as it is, or augment the request with your custom authentication object.
Check out OAuth2AuthenticationProcessingFilter, this extracts an OAuth2 token from the incoming request and uses it to populate the Spring Security context. You can either override its behavior or create a new filter that populates the security context with your mock authentication object.
Here is a sample code to get you started:
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
boolean debug = logger.isDebugEnabled();
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
try {
Authentication authentication = this.tokenExtractor.extract(request);
if (Objects.isNull(authentication)) {
final UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken("username", "password");
authenticationToken.setDetails(Collections.singletonMap("user_uuid", userUuid.toString()));
final OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(null, authenticationToken);
// You can either ask your authenticatoin manager to authenticate these credentials or directly publish auth success event with your mock auth object.
this.eventPublisher.publishAuthenticationSuccess(oAuth2Authentication);
SecurityContextHolder.getContext().setAuthentication(oAuth2Authentication);
} else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken)authentication;
needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
Authentication authResult = this.authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
this.eventPublisher.publishAuthenticationSuccess(authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
}

Could not verify the provided CSRF token because your session was not found in spring security

I am using spring security along with java config
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/*").hasRole("ADMIN")
.and()
.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class)
.exceptionHandling()
.authenticationEntryPoint(restAuthenticationEntryPoint)
.and()
.formLogin()
.successHandler(authenticationSuccessHandler)
.failureHandler(new SimpleUrlAuthenticationFailureHandler());
I am using PostMan for testing my REST services. I get 'csrf token' successfully and I am able to login by using X-CSRF-TOKEN in request header. But after login when i hit post request(I am including same token in request header that i used for login post request) I get the following error message:
HTTP Status 403 - Could not verify the provided CSRF token because your session was not found.
Can any one guide me what I am doing wrong.
According to spring.io:
When should you use CSRF protection? Our recommendation is to use CSRF
protection for any request that could be processed by a browser by
normal users. If you are only creating a service that is used by
non-browser clients, you will likely want to disable CSRF protection.
So to disable it:
#Configuration
public class RestSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
}
}
Note: CSRF protection is enabled by default with Java Configuration
try this: #Override protected boolean sameOriginDisabled() { return true;}
#Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
...
// Determines if a CSRF token is required for connecting. This protects against remote
// sites from connecting to the application and being able to read/write data over the
// connection. The default is false (the token is required).
#Override
protected boolean sameOriginDisabled() {
return true;
}
}
source: WebSocket Security: Disable CSRF within WebSockets
Disabling CSRF protection is a bad idea.
Spring will automatically generate a new CSRF token after each request, and you need to include it in all HTTP requests with side-effects (PUT, POST, PATCH, DELETE).
In Postman you can use a test in each request to store the CSRF token in a global, e.g. when using CookieCsrfTokenRepository
pm.globals.set("xsrf-token", postman.getResponseCookie("XSRF-TOKEN").value);
And then include it as a header with key X-XSRF-TOKEN and value {{xsrf-token}}.
Came to same error just with POST methods, was getting 403 Forbidden "Could not verify the provided CSRF token because your session was not found."
After exploring some time found solution by adding #EnableResourceServer annotation to config.
Config looks like that (spring-boot.version -> 1.4.1.RELEASE, spring-security.version -> 4.1.3.RELEASE, spring.version -> 4.3.4.RELEASE)
#Configuration
#EnableWebSecurity
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends ResourceServerConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(inMemoryUserDetailsManager()).passwordEncoder(passwordEncoder());
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.httpBasic();
http.sessionManagement().sessionCreationPolicy(STATELESS);
http.csrf().disable();
http.authorizeRequests().anyRequest()
.permitAll();
}
private InMemoryUserDetailsManager inMemoryUserDetailsManager() throws IOException {
// load custom properties
Properties properties = new Properties();
return new InMemoryUserDetailsManager(properties);
}
private PasswordEncoder passwordEncoder() {
return new TextEncryptorBasedPasswordEncoder(textEncryptor());
}
private TextEncryptor textEncryptor() {
return new OpenSslCompatibleTextEncryptor();
}
}
I get this error message (HTTP Status 403 - Could not verify the provided CSRF token because your session was not found.) when I do a JS fetch AJAX call without using the credentials: "same-origin" option.
Wrong way
fetch(url)
.then(function (response) { return response.json(); })
.then(function (data) { console.log(data); })
Correct way
fetch(url, {
credentials: "same-origin"
})
.then(function (response) { return response.json(); })
.then(function (data) { console.log(data); })
This is an old question but this might help someone. I had the similar issue and this is how I was able to resolve it.
In order for the CSRF to work with the REST API you need to obtain a CSRF token via API before every single call and use that token. Token is different every time and cannot be re-used.
Here is the controller to get the CSRF token:
#RequestMapping(value = "/csrf", method = RequestMethod.GET)
public ResponseEntity<CSRFDTO> getCsrfToken(HttpServletRequest request) {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
return ResponseEntity.ok(CSRFDTO.builder()
.headerName(csrf.getHeaderName())
.token(csrf.getToken())
.build());
}
Additionally, you might consider configuring your Spring app to disable the CSRF for the REST API endpoints. To quote an article I've read somewhere:
I'm very certain that CSRF tokens on a REST endpoint grant zero additional protection. As such, enabling CSRF protection on a REST endpoint just introduces some useless code to your application, and I think it should be skipped.
Hope this helps.
I have solved it by adding the last attribute in my login page,maybe it will do yo a favor.
<%# page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false"%>

How do I force Spring Security to update XSRF-TOKEN cookie?

A REST Spring Security /user service in a Spring Boot application is failing to immediately update the XSRF-TOKEN cookie when a user authenticates. This is causing the next request for /any-other-REST-service-url to return an Invalid CSRF certificate error, until the /user service is called again. How can this problem be resolved so that the REST /user service properly updates the XSRF-TOKEN cookie in the same request/response transaction in whichit first authenticates the user?
The backend REST /user service is called three times by a front end app, but the /user service only returns matched JSESSIONID/XSRF-TOKEN cookies on the first and third call, NOT on the second call.
In the first request to the server, no credentials (no username or password) are sent to the / url pattern, which I think calls the /user service, and the server responds with a JSESSIONID and XSRF-TOKEN that it associated with an anonymous user. The Network tab of the FireFox developer tools shows these cookies as:
Response cookies:
JSESSIONID:"D89FF3AD2ACA7007D927872C11007BCF"
path:"/"
httpOnly:true
XSRF-TOKEN:"67acdc7f-5127-4ea2-9a7b-831e95957789"
path:"/"
The user then makes various requests for publicly accessible resources without error, and the Network tab of the FireFox developer tools shows these same cookie values.
The second request to the /user service is done though a login form, which sends a valid username and password, which the/user service uses to authenticate the user. But the /user service only returns an updated jsessionid cookie, and does not update the xsrf-token cookie in this step. Here are the cookies shown in the Network tab of the FireFox developer tools at this point:
The 200 GET user included the following cookies in the Network tab of FireFox:
Response cookies:
JSESSIONID:"5D3B51A03B9AE218586591E67C53FB89"
path:"/"
httpOnly:true
AUTH1:"yes"
Request cookies:
JSESSIONID:"D89FF3AD2ACA7007D927872C11007BCF"
XSRF-TOKEN:"67acdc7f-5127-4ea2-9a7b-831e95957789"
Note that the response included a new JSESSIONID, but did not include a new XSRF-TOKEN. This results in a mismatch causing a 403 error (due to invalid csrf token) in the subsequent requests to other rest services, until this is resolved by a third call to the /user service. is there a way that we can force the preceding 200 get user to return the new XSRF-TOKEN also?
The third call to the backend REST /user service uses the very same username and password credentials that were used in the second request shown above, but this third call to /user results in the XSRF_TOKEN cookie being updated properly, while the same correct JSESSIONID is retained. Here is what the Network tab of the FireFox developer tools shows at this point:
The 200 GET user shows that the mismatched request forces an update of the XSRF-TOKEN in the response:
Response cookies:
XSRF-TOKEN:"ca6e869c-6be2-42df-b7f3-c1dcfbdb0ac7"
path:"/"
AUTH1:"yes"
Request cookies:
JSESSIONID:"5D3B51A03B9AE218586591E67C53FB89"
XSRF-TOKEN:"67acdc7f-5127-4ea2-9a7b-831e95957789"
The updated xsrf-token now matches the jsessionid, and thus subsequent requests to other backend rest services can now succeed.
What specific changes can be made to the code below to force an update of both the XSRF-TOKEN and the JSESSIONID cookies the first time the /user service is called with proper username and password by the login form? Do we make specific changes in the code for the backend /user method in Spring? Or is the change made in the Security Configuration classes? What can we try to fix this problem?
The code for the backend /user service and the Security Config are in the main application class of the Spring Boot backend app, which is in UiApplication.java as follows:
#SpringBootApplication
#Controller
#EnableJpaRepositories(basePackages = "demo", considerNestedRepositories = true)
public class UiApplication extends WebMvcConfigurerAdapter {
#Autowired
private Users users;
#RequestMapping(value = "/{[path:[^\\.]*}")
public String redirect() {
// Forward to home page so that route is preserved.
return "forward:/";
}
#RequestMapping("/user")
#ResponseBody
public Principal user(HttpServletResponse response, HttpSession session, Principal user) {
response.addCookie(new Cookie("AUTH1", "yes"));
return user;
}
public static void main(String[] args) {
SpringApplication.run(UiApplication.class, args);
}
#Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.US);
return slr;
}
#Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang");
return lci;
}
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
#Order(Ordered.HIGHEST_PRECEDENCE)
#Configuration
protected static class AuthenticationSecurity extends GlobalAuthenticationConfigurerAdapter {
#Autowired
private Users users;
#Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(users);
}
}
#SuppressWarnings("deprecation")
#Configuration
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
#EnableWebMvcSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests()
.antMatchers("/registration-form").permitAll()
.antMatchers("/confirm-email**").permitAll()
.antMatchers("/submit-phone").permitAll()
.antMatchers("/check-pin").permitAll()
.antMatchers("/send-pin").permitAll()
.antMatchers("/index.html", "/", "/login", "/message", "/home", "/public*", "/confirm*", "/register*")
.permitAll().anyRequest().authenticated().and().csrf()
.csrfTokenRepository(csrfTokenRepository()).and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
}
private Filter csrfHeaderFilter() {
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
String token = csrf.getToken();
if (cookie == null || token != null && !token.equals(cookie.getValue())) {
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
};
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
}
}
The relevant segment of the server logs showing the CSRF error is:
2016-01-20 02:02:06.811 DEBUG 3995 --- [nio-9000-exec-5] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher#70b8c8bb
2016-01-20 02:02:06.813 DEBUG 3995 --- [nio-9000-exec-5] o.s.security.web.FilterChainProxy : /send-pin at position 4 of 13 in additional filter chain; firing Filter: 'CsrfFilter'
2016-01-20 02:02:06.813 DEBUG 3995 --- [nio-9000-exec-5] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:9000/send-pin
What specific changes do I need to make to the code above to resolve this CSRF error?
How do I force an immediate update of the XSRF cookie upon whenever the backend /user service changes a user's status (login, logout, etc.)?
Note: I am guessing (based on my research) that the solution to this problem will involve changing the configuration of some combination of the following Spring Security classes, all of which are defined in the UiApplication.java shown below:
the WebSecurityConfigurerAdapter,
the OncePerRequestFilter,
the CsrfTokenRepository,
the GlobalAuthenticationConfigurerAdapter and/or
the Principal returned by the /user service.
But what specific changes need to be made to solve the problem?
Updated Answer
The reason you are getting a 401 is because a basic authentication header is found in the request when the user is registering. This means Spring Security tries to validate the credentials but the user is not yet present so it responds with a 401.
You should
Make the /register endpoint public and provide a controller that registers the user
Do not include the username/password for registration form in the Authorization header as this will cause Spring Security to try to validate the credentials. Instead include the parameters as JSON or form encoded parameters that your /register controller process
Original Answer
After authenticating, Spring Security uses CsrfAuthenticationStrategy to invalidate any CsrfToken's (to ensure that a session fixation attack is not possible). This is what triggers a new CsrfToken to be used.
However, the problem is that csrfTokenRepository is invoked before authentication is performed. This means that when csrfTokenRepository checks to see if the token has changed the result if false (it has not changed yet).
To resolve the issue, you can inject a custom AuthenticationSuccessHandler. For example:
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
String token = csrf.getToken();
if (cookie == null || token != null && !token.equals(cookie.getValue())) {
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
super.onAuthenticationSuccess(request,response,authentication);
}
}
Then you can configure it:
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.successHandler(new MyAuthenticationSuccessHandler())
.and()
.httpBasic().and()
.authorizeRequests()
.antMatchers("/registration-form").permitAll()
.antMatchers("/confirm-email**").permitAll()
.antMatchers("/submit-phone").permitAll()
.antMatchers("/check-pin").permitAll()
.antMatchers("/send-pin").permitAll()
.antMatchers("/index.html", "/", "/login", "/message", "/home", "/public*", "/confirm*", "/register*").permitAll()
.anyRequest().authenticated()
.and()
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
}

Spring Security for URL with permitAll() and expired Auth Token

I'm using Spring 4 with Spring Security, custom GenericFilterBean and AuthenticationProvider implementations. I have mostly secured URLs with the exception of a URL to create new session: /v2/session (e.g. login based on the username and password and returns Auth Token to be used in the subsequent requests that require authentication) configured as follows:
#Configuration
#ComponentScan(basePackages={"com.api.security"})
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ApiAuthenticationProvider apiAuthenticationProvider;
#Autowired
private AuthTokenHeaderAuthenticationFilter authTokenHeaderAuthenticationFilter;
#Autowired
private AuthenticationEntryPoint apiAuthenticationEntryPoint;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(apiAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(authTokenHeaderAuthenticationFilter, BasicAuthenticationFilter.class) // Main auth filter
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/v2/session").permitAll()
.anyRequest().authenticated();
http.exceptionHandling()
.authenticationEntryPoint(apiAuthenticationEntryPoint);
}
}
The authTokenHeaderAuthenticationFilter runs on every request and gets Token from the request header:
/**
* Main Auth Filter. Always sets Security Context if the Auth token Header is not empty
*/
#Component
public class AuthTokenHeaderAuthenticationFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
final String token = ((HttpServletRequest) request).getHeader(RequestHeaders.AUTH_TOKEN_HEADER);
if (StringUtils.isEmpty(token)) {
chain.doFilter(request, response);
return;
}
try {
AuthenticationToken authRequest = new AuthenticationToken(token);
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
} catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
return;
}
chain.doFilter(request, response); // continue down the chain
}
}
The custom apiAuthenticationProvider will try to authenticate all requests based on the token provided in the header and if authentication is unsuccessful - throws AccessException and client will receive HTTP 401 response:
#Component
public class ApiAuthenticationProvider implements AuthenticationProvider {
#Autowired
private remoteAuthService remoteAuthService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
AuthenticationToken authRequest = (AuthenticationToken) authentication;
String identity = null;
try {
identity = remoteAuthService.getUserIdentityFromToken(authRequest.getToken());
} catch (AccessException e) {
throw new InvalidAuthTokenException("Cannot get user identity from the token", e);
}
return new AuthenticationToken(identity, authRequest.getToken(), getGrantedAuthorites());
}
}
This works perfectly fine for the requests that require authentication. This works fine for the /v2/session request without the Authentication Header in it. However, for the /v2/session request that has an expired Auth Token in the header (or in the cookie - not shown in the code samples; this may happen sometimes if the client didn't clear the headers or continues sending cookies with requests) the security context will be initialized and apiAuthenticationProvider will throw an exception and respond with HTTP 401 to the client.
Since /v2/session has been configured as
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/v2/session").permitAll()
I would expect Spring Security to determine that before calling ApiAuthenticationProvider.authenticate(). What should be the way for the filter or auth provider to ignore/not throw the exception for the URLs configured as permitAll()?
Spring security filters get triggered before the request authorisation checks are performed. For the authorisation checks to work, it is assumed that the request has been through the filters and the Spring security context has been set (or not, depending on whether authentication credentials have been passed in).
In your filter you have check that continues with the filter chain processing if the token is not there. Unfortunately, if it is, then it will be passed to your provider for authentication, which throws an exception because the token has expired thus you're getting the 401.
Your best bet is to bypass filter execution for the URLs that you consider public. You can either do this in the filter itself or in your configuration class. Add the following method to your SecurityConfig class:
#Override
public void configure(WebSecurity webSecurity) {
webSecurity.ignoring().antMatchers(HttpMethod.POST, "/v2/session");
}
What this will do, is bypass your AuthTokenHeaderAuthenticationFilter completely for POST /v2/sessions URL.

Categories

Resources