I am facing an issue with Null CSRF token from the request.
//For getting CSRF token
Object obj=request.getSession().getAttribute(WebSessionServerCsrfTokenRepository.class.getName() .concat(".CSRF_TOKEN"));
We have implemented below code for CSRF generating.
//For generating CSRF token
#Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
http.csrf()
.csrfTokenRepository(new WebSessionServerCsrfTokenRepository())
.requireCsrfProtectionMatcher(getURLsForDisabledCSRF()).and()
.authorizeExchange()
.pathMatchers(ALLOWED_PATHS).permitAll()
.pathMatchers(ALLOWED_METHODS).permitAll()
.anyExchange()
.authenticated().and()
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance()).formLogin().disable()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
.and().csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()));
return http.build();
//.httpBasic().disable().formLogin().disable().build();
}
Please help here I am stuck on this issue last 2 weeks.
Thanks in advance
We have find the solution for the reading the token from the cookie.
CsrfToken obj=new CookieCsrfTokenRepository().loadToken(request);
We have also implement the WebFilter for the XSRF-TOKEN setting in the cookie.
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String key=CsrfToken.class.getName();
Mono<CsrfToken> csrfToken =null!=exchange.getAttribute(key) ?
exchange.getAttribute(key):Mono.empty();
return csrfToken.doOnSuccess(token->{
ResponseCookie cookie=ResponseCookie.from("XSRF-TOKEN", token.getToken()).maxAge(Duration.ofHours(1))
.httpOnly(false).path("/").build();
//System.out.println("Cookie {} : "+cookie);
exchange.getResponse().getCookies().add("XSRF-TOKEN", cookie);
}).then(chain.filter(exchange));
}
Related
I have created application for generating JWT tokens for users that either logging in or registering.
I have another demo endpoint for getting some data from API.
But because the project is Spring Security it checks if token that has been passed in Bearer Header is valid or not. And It detects the token is expired giving this ExpiredJwtException. I want to catch that exception and return custom Response to Client.
Exception when jwt token is expired:
io.jsonwebtoken.ExpiredJwtException: JWT expired at
2023-02-16T10:54:29Z. Current time: 2023-02-16T12:44:41Z, a difference
of 6612302 milliseconds. Allowed clock skew: 0 milliseconds. at
io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:427)
~[jjwt-impl-0.11.5.jar:0.11.5] at
io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:529)
~[jjwt-impl-0.11.5.jar:0.11.5] at
io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:589)
~[jjwt-impl-0.11.5.jar:0.11.5] at
io.jsonwebtoken.impl.ImmutableJwtParser.parseClaimsJws(ImmutableJwtParser.java:173)
~[jjwt-impl-0.11.5.jar:0.11.5]
But It returns 403Forbidden with empty body.
How can I change this response?
This is my SecurityConfiguration class:
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class SecurityConfiguration{
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers("/api/v1/auth/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
This is my ErrorHandler:
#RestControllerAdvice
#Log4j2
public class ErrorHandler extends ResponseEntityExceptionHandler {
private static final String LOG_ERROR = "Error. {} \n{}";
//There are other methods here that handle other exceptions
#ResponseStatus(HttpStatus.UNAUTHORIZED)
#ExceptionHandler(ExpiredJwtException.class)
public ResponseEntity<ErrorResponse> handleJwtExpiredTokenException(ExpiredJwtException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse(ErrorCode.TOKEN_EXPIRED, ex.getMessage()));
}
}
I explained issue to ChatGpt. It recommended some code changes like adding new AuthenticationEntryPoint and others. But this didn't solve my problem
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.
Below is code which authorise JWT token (Keyclock) but in case of exception , server never returns 401
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain securityWebFilterChain(final ServerHttpSecurity http) {
// the matcher for all paths that need to be secured (require a logged-in user)
http.authorizeExchange(exchanges -> exchanges.pathMatchers("/actuator/**").permitAll()
.pathMatchers("/abcde/auth").permitAll()
.pathMatchers("/abcde/auth/refresh").permitAll()
.anyExchange().authenticated())
.csrf().disable()
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.jwt(withDefaults())
).exceptionHandling(exception-> exception.authenticationEntryPoint((swe, e) -> Mono.fromRunnable(() ->
{
swe.getResponse()
.setStatusCode(HttpStatus.UNAUTHORIZED);
}
)
)
);
return http.build();
}
Another question :
Will this piece of code only validate expiry of JWT token or also other validation? What exactly happens is what i am interested to know.
In nutshell is this code sufficient enough for keyclock JWT validation through issuer URL?
I'm developing a REST API based on Spring Boot (spring-boot-starter-web) where I use Spring Security (spring-security-core e spring-security-config) to protect the different endpoints.
The authentication is done by using a local database that contains users with two different sets of roles: ADMIN andUSER. USER should be able toGET all API endpoints and POST to endpoints based onrouteA. ADMIN should be able to do the same asUSER plus POST andDELETE to endpoints based on `routeB
However the behavior I'm getting is that I can do GET requests to any endpoint but POST requests always return HTTP 403 Forbidden for either type of user - ADMIN and USER - which is not expected what I'm expecting based on my SecurityConfiguration.
Any ideas of what am I missing?
SecurityConfiguration.java
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
#Autowired
private RESTAuthenticationEntryPoint authenticationEntryPoint;
#Autowired
private DataSource dataSource;
#Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
logger.info("Using database as the authentication provider.");
builder.jdbcAuthentication().dataSource(dataSource).passwordEncoder(new BCryptPasswordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().
authorizeRequests().antMatchers(HttpMethod.GET, "/**").hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.POST, "/routeA/*").hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.POST, "/routeB/*").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/routeB/*").hasRole("ADMIN").and().
requestCache().requestCache(new NullRequestCache()).and().
httpBasic().authenticationEntryPoint(authenticationEntryPoint).and().
cors();
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
RouteBController .java
#RestController
public class RouteBController {
static final Logger logger = LoggerFactory.getLogger(RouteBController.class);
public RouteBController() { }
#RequestMapping(value = "routeB", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.GET)
public String getStuff() {
return "Got a hello world!";
}
#RequestMapping(value = "routeB", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.POST)
public String postStuff() {
return "Posted a hello world!";
}
}
RESTAuthenticationEntryPoint.java
#Component
public class RESTAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
#Override
public void afterPropertiesSet() throws Exception {
setRealmName("AppNameHere");
super.afterPropertiesSet();
}
}
BEFORE disabling the CSFR as a way of fixing this issue, please check the resources on Mohd Waseem's answer to better understand why it is important and to have an idea of how it can be properly set up. As RCaetano has said, CSFR is here to help us from attacks and it should not be disabled blindly.
Since this answer still explained the 2 issues on my original questions, I'll leave it as the marked answer to create awareness about possible issues with the CSFT and security routes but don't take it literally.
There were 2 issues in SecurityConfiguration.java that made it misbehave.
Although the 403 Forbidden error message didn't contain any message indication of why it was failing (see example below) it turns out it was due to having CSRF enabled. Disabling it allowed for POST and DELETE requests to be processed.
{
"timestamp": "2018-06-26T09:17:19.672+0000",
"status": 403,
"error": "Forbidden",
"message": "Forbidden",
"path": "/routeB"
}
Also the expression used in antMatched(HttpMethod, String) for RouteB was incorrect because /routeB/* expects it to have something after /. The correct configurtion is /routeB/** since more paths can be present (or not).
The corrected SecurityConfiguration.java is
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().
authorizeRequests().antMatchers(HttpMethod.GET, "/**").hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.POST, "/routeA/**").hasAnyRole("ADMIN", "USER")
.antMatchers(HttpMethod.POST, "/routeB/**").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/routeB/**").hasRole("ADMIN").and().
requestCache().requestCache(new NullRequestCache()).and().
httpBasic().authenticationEntryPoint(authenticationEntryPoint).and().
cors().and().
csrf().disable();
}
Source: StackOverflow em Português
Cross-site request forgery is a web security vulnerability that allows an attacker to induce users to perform actions that they do not
intend to perform.
In your case disabling CSRF protection exposes user to this vulnerability.
Note: If it was pure Rest API with O-Auth protection then CSRF was not
needed. Should I use CSRF protection on Rest API endpoints?
But In your case when user logs in a session is created and cookie is returned in response and without CSRF token Attacker can exploit it and perform CSRF.
It wouldn't be a good idea to disable CSRF instead you can configure your app to return CSRF token in response headers and then use it in all your subsequent state changing calls.
Add this line of code in your SecurityConfiguration.java
// CSRF tokens handling
http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class);
CsrfTokenResponseHeaderBindingFilter.java
public class CsrfTokenResponseHeaderBindingFilter extends OncePerRequestFilter {
protected static final String REQUEST_ATTRIBUTE_NAME = "_csrf";
protected static final String RESPONSE_HEADER_NAME = "X-CSRF-HEADER";
protected static final String RESPONSE_PARAM_NAME = "X-CSRF-PARAM";
protected static final String RESPONSE_TOKEN_NAME = "X-CSRF-TOKEN";
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, javax.servlet.FilterChain filterChain) throws ServletException, IOException {
CsrfToken token = (CsrfToken) request.getAttribute(REQUEST_ATTRIBUTE_NAME);
if (token != null) {
response.setHeader(RESPONSE_HEADER_NAME, token.getHeaderName());
response.setHeader(RESPONSE_PARAM_NAME, token.getParameterName());
response.setHeader(RESPONSE_TOKEN_NAME, token.getToken());
}
filterChain.doFilter(request, response);
}
}
Header Response form Server:
Note that we now have CSRF token in the header. This will not change untill the session expires.
Also read: Spring Security’s CSRF protection for REST services: the client side and the server side for better understanding.
It's simple CSRF enabled issue that doesn't allow POST requests. I faced the same problem here's the solution: (Explained)
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.POST,"/form").hasRole("ADMIN") // Specific api method request based on role.
.antMatchers("/home","/basic").permitAll() // permited urls to guest users(without login).
.anyRequest().authenticated()
.and()
.formLogin() // not specified form page to use default login page of spring security
.permitAll()
.and()
.logout().deleteCookies("JSESSIONID") // delete memory of browser after logout
.and()
.rememberMe().key("uniqueAndSecret"); // remember me check box enabled.
http.csrf().disable(); // ADD THIS CODE TO DISABLE CSRF IN PROJECT.**
}
Above code:
http.csrf().disable();
will solve the problem.
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.