I have the simplest oauth2 client:
#EnableAutoConfiguration
#Configuration
#EnableOAuth2Sso
#RestController
public class ClientApplication {
#RequestMapping("/")
public String home(Principal user, HttpServletRequest request, HttpServletResponse response) throws ServletException {
return "Hello " + user.getName();
}
public static void main(String[] args) {
new SpringApplicationBuilder(ClientApplication.class)
.properties("spring.config.name=application").run(args);
}
}
I also have the following application.yml:
server:
port: 9999
servlet:
context-path: /client
security:
oauth2:
client:
client-id: acme
client-secret: acmesecret
access-token-uri: http://localhost:8080/oauth/token
user-authorization-uri: http://localhost:8080/oauth/authorize
resource:
user-info-uri: http://localhost:8080/me
logging:
level:
org.springframework.security: DEBUG
org.springframework.web: DEBUG
It is the full code. I don't have any additional source code. It works properly.
But now I want to add a logout feature. I've added an endpoint but it doesn't work. I tried to do the following:
#RequestMapping("/logout")
public void logout(HttpServletRequest request, HttpServletResponse response) throws ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
authentication.setAuthenticated(false);
new SecurityContextLogoutHandler().logout(request,response,authentication);
SecurityContextHolder.clearContext();
request.logout();
request.getSession().invalidate();
}
But I am still logged in and can access / url and it responds to me with the username.
Can you help me fix this issue?
Update
I tried the approach described here https://spring.io/guides/tutorials/spring-boot-oauth2/#_social_login_logout :
#EnableAutoConfiguration
#Configuration
#EnableOAuth2Sso
#Controller
public class ClientApplication extends WebSecurityConfigurerAdapter {
private Logger logger = LoggerFactory.getLogger(ClientApplication.class);
#RequestMapping("/hello")
public String home(Principal user, HttpServletRequest request, HttpServletResponse response, Model model) throws ServletException {
model.addAttribute("name", user.getName());
return "hello";
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.antMatcher("/**")
.authorizeRequests()
.antMatchers( "/login**", "/webjars/**", "/error**").permitAll()
.anyRequest()
.authenticated()
.and().logout().logoutSuccessUrl("/").permitAll()
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// #formatter:on
}
public static void main(String[] args) {
new SpringApplicationBuilder(ClientApplication.class)
.properties("spring.config.name=application").run(args);
}
}
and on FE I wrote:
<script type="text/javascript">
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (settings.type == 'POST' || settings.type == 'PUT'
|| settings.type == 'DELETE') {
if (!(/^http:.*/.test(settings.url) || /^https:.*/
.test(settings.url))) {
// Only send the token to relative URLs i.e. locally.
xhr.setRequestHeader("X-XSRF-TOKEN",
Cookies.get('XSRF-TOKEN'));
}
}
}
});
var logout = function () {
$.post("/client/logout", function () {
$("#user").html('');
$(".unauthenticated").show();
$(".authenticated").hide();
});
return true;
};
$(function() {
$("#logoutButton").on("click", function () {
logout();
});
});
</script>
and
<input type="button" id="logoutButton" value="Logout"/>
But it still doesn't work. It results in the following behavior:
Post http://localhost:9999/client/logout redirects to the http://localhost:9999/client but this page doesn't exist
source code on gitub:
client - https://github.com/gredwhite/logour_social-auth-client (use localhost:9999/client/hello url)
server - https://github.com/gredwhite/logout_social-auth-server
You can delete the refresh token as well as access token from database to save space.
#PostMapping("/oauth/logout")
public ResponseEntity<String> revoke(HttpServletRequest request) {
try {
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.contains("Bearer")) {
String tokenValue = authorization.replace("Bearer", "").trim();
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
tokenStore.removeAccessToken(accessToken);
//OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(tokenValue);
OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
tokenStore.removeRefreshToken(refreshToken);
}
} catch (Exception e) {
return ResponseEntity.badRequest().body("Invalid access token");
}
return ResponseEntity.ok().body("Access token invalidated successfully");
}
The URL to logout will be : http://localhost:9999/oauth/logout
Also, pass the access token in the Authorization header, as
Authorization: Bearer 0cb72897-c4f7-4f01-aed9-2f3f79a75484
where, 0cb72897-c4f7-4f01-aed9-2f3f79a75484 is the access token.
Since, its Spring security, don't forget to bypass /oauth/logout url from authorize access, as
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/hello", "/oauth/logout");
}
Hope, it will solve your logout problem in Springboot2+Oauth2. Its working for me.
Add following code snippet to your ClientApplication class. This will also clear your session details.
Replace below code with the configure method of your web security adapter class.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers( "/login**", "/webjars/**", "/error**").permitAll()
.anyRequest()
.authenticated()
.and().logout().invalidateHttpSession(true)
.clearAuthentication(true).logoutSuccessUrl("/login?logout").deleteCookies("JSESSIONID").permitAll().and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
You probably want to use the Spring Security built-in support for the /logout endpoint which will do the right thing (clear the session and invalidate the cookie). To configure the endpoint extend the existing configure() method in our WebSecurityConfigurer:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.and().logout().logoutSuccessUrl("/").permitAll();
}
You can change Post to
Get http://localhost:9999/client/logout
it works for me
Try to add logout url to your security configuration.
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.permitAll();
Related
I need to redirect automatically to login page after session timeout or at least show alert that session is expired, I tried to configure Spring Security, but it is not working ,debugger don't catch code in LogoutSuccessHandlerService after timeout. Maybe I missed something or this approach with Spring Security is wrong from the start?
If it is so, can somebody provide full working example of such task? I am using Spring Boot 2.5.6, Spring Security, front-end is html, javascript, JQuery and dataTable.
Here is my code:
SecurityConfig.java
private final AppProperties appProperties;
#Autowired
private LogoutSuccessHandlerService logoutSuccessHandlerService;
#Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers("/static/**")
.antMatchers("/webjars/**")
.antMatchers("/css/**")
.antMatchers("/fonts/**")
.antMatchers("/img/**")
.antMatchers("/js/**")
.antMatchers("/scripts/**")
;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login")
.permitAll()
.defaultSuccessUrl("/", true)
.failureUrl("/login?error=true")
.loginProcessingUrl("/j_spring_security_check")
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.logout()
.invalidateHttpSession(true)
.logoutSuccessHandler(logoutSuccessHandlerService)
.logoutSuccessUrl("/login")
.permitAll()
.and()
.csrf().disable();
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
AppProperties.Security security = appProperties.getSecurity();
auth.inMemoryAuthentication()
.withUser(security.getUser())
.password(passwordEncoder().encode(security.getPassword()))
.roles(security.getRole());
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
LogoutSuccessHandlerService.java extends SimpleUrlLogoutSuccessHandler
#Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
if (authentication != null) {
}
log.info("logout success");
setDefaultTargetUrl("/login");
super.onLogoutSuccess(request, response, authentication);
}
application-local.yml
server:
port: 8086
servlet:
session:
timeout: 2m
Found a solution. Spring security is unable to solve it, I used JavaScript. This solution sends request every minute and if response data is not null, redirect occurs. It works only with one logged in user in browser.
Header html page
<script>
setInterval(function() {
$.ajax({
url: "/check-session",
method: "GET",
contentType: 'application/json; charset=utf-8',
success: function(data){
if (data && data.length > 0) {
window.location.replace("/login");
}
},
error: function (data) {
console.log("error");
console.log(data);
}
})
}, 60000);
</script>
LoginController
#GetMapping("/check-session")
public ResponseEntity<String> checkSession() {
return new ResponseEntity<>(HttpStatus.OK);
}
I'm making an Angular application as the frontend with a Springboot backend. I have set up SpringSecurity to handle logins and if I try it using Postman everything works, but when I try it using Angulars login, the "request.getParameter" calls always return me "null". I have tried changing it in several ways but the result is always the same.
The successful login return information about the user and a token.
Here is the Java part:
#Configuration
#EnableWebSecurity
#ComponentScan
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Lazy
#Autowired
CurrentUserDetailsServiceImpl userDetailsService;
#Autowired
TokenAuthenticationService tokenAuthService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers().cacheControl().disable();
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.headers().xssProtection();
http
.exceptionHandling().and()
.anonymous().and()
.servletApi().and()
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/auth/**").permitAll()
.anyRequest().authenticated().and()
.addFilterBefore(
new LoginFilter("/auth/login", authenticationManager(), tokenAuthService),
UsernamePasswordAuthenticationFilter.class)
// Custom Token based authentication based on the header previously given to the
// client
.addFilterBefore(new JWTFilter(tokenAuthService), UsernamePasswordAuthenticationFilter.class);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
#Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
// Allow anonymous resource requests
.antMatchers("/favicon.ico");
}
}
The LoginFilter does quite some things, but this is the minimum:
public class LoginFilter extends AbstractAuthenticationProcessingFilter {
private TokenAuthenticationService tokenAuthenticationService;
public LoginFilter(String urlMapping, AuthenticationManager authenticationManager, TokenAuthenticationService tokenAuthenticationService) {
super(new AntPathRequestMatcher(urlMapping));
setAuthenticationManager(authenticationManager);
this.tokenAuthenticationService = tokenAuthenticationService;
}
#Override
#Transactional
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println("USERNAME: " + username + " - PASSWORD: " + password);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authentication) throws IOException, ServletException {
CurrentUser loggedUser = (CurrentUser) authentication.getPrincipal();
...
}
}
Finally, the Angular form does some validations and calls to a service:
#Injectable({
providedIn: 'root'
})
export class LoginService {
apiUrl: string = environment.API_URL;
constructor(private http: HttpClient) {}
login(data: LoginInterface): Observable<LoginResult> {
const formData: string =
'username=' + data.username + '&password=' + data.password;
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
})
};
return this.http.post<LoginResult>(
this.apiUrl + '/auth/login',
formData,
httpOptions
);
}
}
What am I doing wrong? Why is it working with Postman?
Thanks!
Edit: As requested, here are the console details for the call in Chrome
Headers:
Body:
Postman headers:
Postman data:
And here is Eclipse showing the null value that arrived:
Thanks!
You can probably do it by using HttpParams as the request body:
login(data: LoginInterface): Observable<LoginResult> {
const params = new HttpParams({
fromObject: {
username: data.username,
password: data.password,
},
});
return this.http.post<LoginResult>(this.apiUrl + '/auth/login', params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
}
I have SpringSecurityWebAppConfig class that uses my own filter class JwtAuthenticationFilter.
The question now is how do i bypass my JwtAuthenticationFilter for api calls that does not have request header and/or token. Do I set it as a configurable and read it in the filter?
My JwtAuthenticationFilter is an imported class from another library. The purpose is to reuse the file for the other microservices.
Example:
/scanFile does not need request token. When I add into SpringSecurityWebAppConfig. My filter is throw 401 as it does not have request token.
SpringSecurityWebAppConfig class:
public class SpringSecurityWebAppConfig extends WebSecurityConfigurerAdapter {
#Autowired
public JwtAuthenticationFilter jwtAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/homePage").access("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
.antMatchers("/userPage").access("hasRole('ROLE_USER')")
.antMatchers("/adminPage").access("hasRole('ROLE_ADMIN')")
.antMatchers(HttpMethod.DELETE, "/data").access("hasRole('ROLE_ADMIN')")
.antMatchers("/login").permitAll()
.antMatchers("/logout").authenticated()
.and()
.anonymous().disable()
.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.and()
.headers()
.httpStrictTransportSecurity()
.includeSubDomains(true).maxAgeInSeconds(31536000);
// Add a filter to validate the tokens with every request
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
JwtAuthenticationFilter Class:
private void getJwtFromRequest(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String bearerToken = request.getHeader("Authorization");
// Step 1: Check if bearer token exist in authorization header and if bearer token start with "Bearer "
if (!StringUtils.hasText(bearerToken) || !bearerToken.startsWith("Bearer ")) {
String errorMsg = "No access token found in request headers.";
Error err = new Error(JWT_AUTHENTICATION_FILTER, "AccessTokenMissingException", errorMsg);
// Java object to JSON string
String jsonString = mapper.writeValueAsString(err);
log.error(jsonString);
throw new AccessTokenMissingException(errorMsg);
}
//rest of the processing here ...
}
so you want to whitelist some apis even without spring security authentication?
you can use web.ignoring().antMatchers() in your SpringSecurityWebAppConfig, to exclude the urls from spring security.
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/scanFile/**”);
}
I am wondering if there is a way to provide two separate types of authentication?
User should log, register, get user data for endpoints /login, /register, /user using basic auth. And when I call /api it should only be authenticated with JWT token provided in headers.
But when I call /api I get all data without any authentication. When user is logged and call /user, API gives JWT to access /api.
My code:
Configuration for basic auth:
#Configuration
#EnableWebSecurity
#Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable();
http
.authorizeRequests()
.antMatchers("/user").authenticated()
.antMatchers("/register").permitAll()
.and()
.formLogin().permitAll()
.defaultSuccessUrl("/user");
}
Configuration for JWT auth:
#Configuration
#Order(2)
public class JWTSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.antMatcher("/api/**")
.addFilterAfter(new JWTAuthorizationFilter(),UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic().disable();
}
I had the same problem, I wanted basic Authentication for some endpoints and for some other I wanted other authentication methods. like yours. you wanna basic authentication for some of the endpoints (/login,/register, /user ) and JWT authentication for some other(/api/**).
I used some tutorials about multiple entry points in spring security but it didn't work.
So here is my solution (It worked)
Separate basic authentication from JWT authentication by creating a custom filter.
Add a prefix path for the endpoints that should be authenticated using basic authentication. like :
(/basic/login, /basic/register,/basic/user)
Create a new custom filter for /basic prefix (for /basic requests) and check basic authentication
#Component
public class BasicAuthenticationFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//Check for the requests that starts with /basic
if (httpServletRequest.getRequestURI().startsWith("/basic/")) {
try {
//Fetch Credential from authorization header
String authorization = httpServletRequest.getHeader("Authorization");
String base64Credentials = authorization.substring("Basic".length()).trim();
byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
String credentials = new String(credDecoded, StandardCharsets.UTF_8);
final String username = credentials.split(":", 2)[0];
final String password = credentials.split(":", 2)[1];
//Check the username and password
if (username.equals("admin") && password.equals("admin")) {
//continue
chain.doFilter(request, response);
} else
throw new AuthenticationCredentialsNotFoundException("");
} catch (Exception e) {
throw new AuthenticationCredentialsNotFoundException("");
}
} else chain.doFilter(request, response);
}
}
Write main security configuration just for JWT and permit /basic URL
#Configuration
#EnableWebSecurity
public class JWTSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/basic/**").permitAll().and()
.csrf().disable()
.antMatcher("/api/**")
.addFilterAfter(new JWTAuthorizationFilter(),UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic().disable();
}
I'm using Spring 4.3.12.RELEASE Version, AngularJS 1.4.8. I'm trying to prevent the CSRF Attack on the application.
#Configuration
#Order(2)
public static class SecurityConfig extends WebSecurityConfigurerAdapter {
String[] pathsToRemoveAuthorizaton = {
"/mobile/**",
"/logout",
"/j_spring_security_logout",
"/login",
};
private final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
#Override
public void configure(HttpSecurity http) throws Exception {
logger.info("http configure");
http.antMatcher("/**").authorizeRequests().antMatchers(pathsToRemoveAuthorizaton).permitAll()
.antMatchers("/**").authenticated()
.and().formLogin().loginPage("/login")
.usernameParameter("employeeId").passwordParameter("password")
.successForwardUrl("/dashboard").defaultSuccessUrl("/dashboard", true)
.successHandler(customAuthenticationSuccessHandler()).loginProcessingUrl("/j_spring_security_check")
.and().logout().logoutSuccessUrl("/logout").logoutUrl("/j_spring_security_logout")
.logoutSuccessHandler(customLogoutSuccessHandler()).permitAll().invalidateHttpSession(true)
.deleteCookies("JSESSIONID").and().sessionManagement().sessionFixation().newSession()
.maximumSessions(1).maxSessionsPreventsLogin(true).and()
.sessionCreationPolicy(SessionCreationPolicy.NEVER).invalidSessionUrl("/logout").and()
.exceptionHandling().accessDeniedPage("/logout");
// http.csrf().csrfTokenRepository(csrfTokenRepository()).and()
// .addFilterAfter(new StatelessCSRFFilter(), CsrfFilter.class);
http.csrf().csrfTokenRepository(csrfTokenRepository());
// http.csrf().disable();
// http.csrf().ignoringAntMatchers("/mobile/**");
http.authorizeRequests().anyRequest().authenticated();
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
#Bean
public AuthenticationSuccessHandler customAuthenticationSuccessHandler() {
return new CustomAuthenticationSuccessHandler();
}
#Bean
public LogoutSuccessHandler customLogoutSuccessHandler() {
return new CustomLogoutSuccessHandler();
}
}
Below is my angular service code
govtPMS.service('Interceptor', function($q, $location, $rootScope, pinesNotifications, Auth) {
return {
request: function(config) {
config.headers.Authorization = 'Bearer '+$rootScope.authToken;
// document.cookie = 'CSRF-TOKEN=' + $rootScope.generateKey;
return config;
},
requestError: function (rejection) {
return $q.reject(rejection);
},
response: function(res) {
if(res.status === 200 || res.status === 201){
if(res.data.response !== undefined){
if(res.data.status === 1 || res.data.status === 3 || res.data.status === 2) {
pinesNotifications.notify({
'title': 'Success',
'text': res.data.message,
'type': 'success',
'delay': 5000
});
}
else if(res.data.status === 5) {
pinesNotifications.notify({
'title': 'Warning',
'text': res.data.message,
'type': 'warning',
'delay': 5000
});
}
}
}
return res || $q.when(res);
},
responseError: function(error) {
return $q.reject(error);
}
};
}).config(['$httpProvider', function($httpProvider) {
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-TOKEN';
$httpProvider.defaults.xsrfCookieName = 'CSRF-TOKEN';
$httpProvider.interceptors.push('Interceptor');
}])
I'm still not able to see the CSRF Token header along with requests.
In this application, we are using 3 jsp pages - login.jsp, logout.jsp and dashboard.jsp
angular scope is defined in the dashboard.jsp, hence login and logout are out of scope of AngularJS.
I've also tried it the stateless way from this and this examples, where angular is generating a UUID and appending with cookie and request header, the below filter was doing the job fine.
Until the logout attack. In this attack, the attacker is trying to succesfully logout the user since to logout from the application, we are simply using a href.
<li></i><span>Logout</span></li>
Now since its logout is out of angular, the angularjs interceptor is cannot attach the UUID there.
I've been struggling on this since past week, Any help will be appreciated.
StatelessCSRFFilter.java
package com.leadwinner.sms.config.filters;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.web.filter.OncePerRequestFilter;
public class StatelessCSRFFilter extends OncePerRequestFilter {
private static final String CSRF_TOKEN = "CSRF-TOKEN";
private static final String X_CSRF_TOKEN = "X-CSRF-TOKEN";
private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
List<String> excludedUrls = new ArrayList<>();
excludedUrls.add("/resources");
excludedUrls.add("/j_spring_security_check");
excludedUrls.add("/j_spring_security_logout");
excludedUrls.add("/login");
excludedUrls.add("/logout");
excludedUrls.add("/mobile");
excludedUrls.add("/migrate");
excludedUrls.add("/dashboard");
String path = request.getServletPath();
System.out.println(path);
AtomicBoolean ignoreUrl = new AtomicBoolean(false);
excludedUrls.forEach(url -> {
if (request.getServletPath().startsWith(url.toLowerCase()) || request.getServletPath().equals("/")) {
ignoreUrl.set(true);
}
});
if (!ignoreUrl.get()) {
final String csrfTokenValue = request.getHeader(X_CSRF_TOKEN);
final Cookie[] cookies = request.getCookies();
System.out.println("**************************************************");
System.out.println("--------------------------------------------------");
String csrfCookieValue = null;
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(CSRF_TOKEN)) {
csrfCookieValue = cookie.getValue();
}
}
}
System.out.println("csrfTokenValue = "+csrfTokenValue);
System.out.println("csrfCookieValue = "+csrfCookieValue);
System.out.println("--------------------------------------------------");
System.out.println("**************************************************");
if (csrfTokenValue == null || !csrfTokenValue.equals(csrfCookieValue)) {
accessDeniedHandler.handle(request, response, new AccessDeniedException(
"Missing or non-matching CSRF-token"));
return;
}
}
filterChain.doFilter(request, response);
}
}
If the request can be made by the browser and credentials be handed up automatically (session cookie, basic auth creds), then CSRF protection is necessary, even with a mobile API.
Given that you have a mobile API as part of the application, the question is can those APIs be successfully addressed by the browser?
What I'd recommend is that you create two separate filter chains, like so, one for the web app and one for the mobile API:
#Configuration
#Order(100)
public class WebAppConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) {
http
.requestMatchers()
.antMatchers("/app/**")
.and()
.authorizeRequests()
// ...
.anyRequest().authenticated()
.and()
.formLogin();
}
}
#Configuration
#Order(101)
public class MobileApiConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) {
http
.requestMatchers()
.antMatchers("/api/**")
.and()
.authorizeRequests()
// ...
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt();
}
}
What this achieves is it separates the two configurations. The endpoints relative to the web application use one setup, and the endpoints relative to the mobile API use another setup.
Now, a couple of comments about the mobile API. I'm assuming that you are authenticating using OAuth 2.0 Bearer tokens, which is why the configuration above uses oauth2ResourceServer() from Spring Security 5.1+. What this does is selectively disables CSRF for requests that contain an Authorization: Bearer xyz header.
But, since you are using Spring Security 4.3, then you may need to do something more like the following (unless you can upgrade):
#Order(101)
public class MobileApiConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) {
http
.requestMatchers()
.antMatchers("/api/**")
.and()
.authorizeRequests()
// ...
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(NEVER)
.and()
.addFilterBefore(new MyMobileAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf().disable();
}
}
What you'd need to make sure of, though, is that your custom authentication filter doesn't use an authentication mechanism that is automatically sent by the browser from any origin (session cookies, Authorization: Basic).
`Hi Shiva,
Your code in the configure method of SecurityConfig should look like the below code:
http
.authorizeRequests()
.antMatchers(patterns)
.permitAll()
.antMatchers("/hello/**")
.hasRole("USER")
.and()
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.requireCsrfProtectionMatcher(csrfProtectionMatcher(patterns))
.and()
.httpBasic()
.and()
.addFilterAfter(csrfFilter(patterns), FilterSecurityInterceptor.class)
.addFilterAfter(new StatelessCSRFFilter(), CsrfFilter.class);
And in the StatelessCSRFFilter, use the following code:
#Override
public void init(FilterConfig filterConfig) throws ServletException {}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
CsrfToken csrf = (CsrfToken) servletRequest.getAttribute(CsrfToken.class.getName());
String token = csrf.getToken();
if (token != null && isAuthenticating(servletRequest)) {
HttpServletResponse response = (HttpServletResponse) servletResponse;
Cookie cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
filterChain.doFilter(servletRequest, servletResponse);
}
private boolean isAuthenticating(ServletRequest servletRequest) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
return request.getRequestURI().equals("/login");
}`
<pre>
Add this code for the csrfTokenRepository method
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
Add this for the csrfFilter method
private Filter csrfFilter(String[] patterns) {
CsrfFilter csrfFilter = new CsrfFilter(csrfTokenRepository());
csrfFilter.setRequireCsrfProtectionMatcher(csrfProtectionMatcher(patterns));
return csrfFilter;
}
Add this for the csrfProtectionMatcher method
private NoAntPathRequestMatcher csrfProtectionMatcher(String[] patterns) {
return new NoAntPathRequestMatcher(patterns);
}
Also remove these lines in configure method
.csrfTokenRepository(csrfTokenRepository())
.requireCsrfProtectionMatcher(csrfProtectionMatcher(patterns))
Move these lines below .csrf() in configure method:
.and()
.addFilterAfter(csrfFilter(patterns), FilterSecurityInterceptor.class)
</pre>