Spring Security Config
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/admin").hasRole("ADMIN")
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/test/**").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/favicon.ico").permitAll()
.antMatchers("/static/**").permitAll()
.antMatchers("/manifest.json").permitAll()
.antMatchers("/logo192.png").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
I also tried this but did not produce any result
.antMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
/api/auth/signup return
error: "Unauthorized"
message: "Full authentication is required to access this resource"
path: "/error"
status: 401
Request URL: https://mysuite.ru/api/auth/signup
How can I fix this problem?
UPDATE
#Configuration
public class MvcSecurityConfig implements WebMvcConfigurer {
#Value("${path.frontend}")
private String frontendPath;
#Value("${frontendStaticResourcesPathPatterns}")
private String[] frontendStaticResourcesPathPatterns;
private static final String BASE_API_PATH = "/";
public void addResourceHandlers(ResourceHandlerRegistry registry){
String pathToFrontend = "file:" + this.frontendPath;
String pathToIndexHTML = pathToFrontend + "/index.html";
registry
.addResourceHandler(frontendStaticResourcesPathPatterns)
.setCachePeriod(0)
.addResourceLocations(pathToFrontend);
registry.addResourceHandler("/", "/**")
.setCachePeriod(0)
.addResourceLocations(pathToIndexHTML)
.resourceChain(true)
.addResolver(new PathResourceResolver() {
#Override
protected Resource getResource(String resourcePath, Resource location) throws IOException {
if (resourcePath.startsWith(BASE_API_PATH) || resourcePath.startsWith(BASE_API_PATH.substring(1))) {
return null;
}
return location.exists() && location.isReadable() ? location : null;
}
});
}
}
This is my Spring MVC Config.
Could any of this cause the problem?
I also tried to do permitAll step by step along the path but it didn't work (api/, api/auth, api/autn/**)
By default, Spring Security comes with CSRF Protection enabled, so when you perform an unsafe request (POST, PUT, DELETE) you have to provide a CSRF Token.
In your configure method you can disable it to check if it will work.
http.csrf().disable()
I advise you that disabling CSRF protection can be harmful to your app and you should make sure if you need to use it or not.
Also, if you are using Spring Security's version 5.4 or higher, you can enable trace logs to help you debug it.
logging.level.org.springframework.security=TRACE
You can get more details in the reference docs.
In an Ant matcher, ** matches zero or more directories in a path. Given your request URL you just need to match zero or more characters. Having said that, try replacing your Ant matcher with the following:
.antMatchers(HttpMethod.POST, "/api/auth/*").permitAll()
By pass your filter because any API request throught Filter. Your API can not pass Filter so you get 401 response.
Try add this to your Spring Security Config:
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/auth/**");
}
Or add this to OncePerRequestFilter:
#Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
return new AntPathMatcher().match("/api/auth/**", request.getServletPath());
}
Related
somewhat related to this other stackoverflow topic which doesn't give a proper solution nor is applicable to Spring 6 (Spring Boot 3).
I came up with a basic spring-boot app to make my case.
There is a controller with two end-points, where one must be secured and the other accessible.
#RestController
public class TestController {
#GetMapping("/secured-api")
public String securedApi() {
return "secured";
}
#GetMapping("/public/open-api")
public String openApi() {
return "open";
}
}
Security context as follow, imagine that MyFilter is doing something fancy, e.g: validating a JWT token and firing an exception if the token is invalid / expired.
#Configuration
public class ComponentSecurityContext {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.addFilterAt(new MyFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests(customizer -> customizer
.requestMatchers(new AntPathRequestMatcher("/public/**"))
.permitAll()
.anyRequest()
.authenticated())
.build();
}
public static class MyFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
System.out.println("Filter is called for uri: " + request.getRequestURI());
// performs some authentication
filterChain.doFilter(request, response);
}
}
}
Executing the following two curls on the server
curl http://localhost:9003/public/open-api
curl http://localhost:9003/secured-api
is triggering MyFilter
Filter is called for uri: /public/open-api
Filter is called for uri: /secured-api
I would expect MyFilter to be called only for secured end-points, I don't care if an expired token is used to access an unprotected end-point.
Any advise on how to properly wire spring-security to achieve just that?
Working solution where the filter is scoped by the securityMatcher:
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher(new NegatedRequestMatcher(new AntPathRequestMatcher("/public/**")))
.addFilterAt(new MyFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated())
.build();
}
I have this configure method and i want to make user be able to register but i get 401 Unathorized. It is caused by the .apply(**) and i am not able to do it.
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/auth/register").permitAll()
.antMatchers("/auth/signin").permitAll()
.anyRequest().authenticated()
.and()
.apply(new JwtConfigurer(jwtTokenProvider, securityUtils));
}
JwtConfigurer.class
public class JwtConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final JwtTokenProvider jwtTokenProvider;
private final SecurityUtils securityUtils;
public JwtConfigurer(JwtTokenProvider jwtTokenProvider, SecurityUtils securityUtils) {
this.jwtTokenProvider = jwtTokenProvider;
this.securityUtils = securityUtils;
}
#Override
public void configure(HttpSecurity http) {
JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider, securityUtils);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
So when request is sent to /auth/register i dont want to add .apply(**). Do u have any suggestion please?
In your class that extends WebSecurityConfigurerAdapter where your http configure() method with .apply() is written, you can use the following to tell Spring Boot to bypass or ignore the filter if encountered with the uri for user registration.
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/auth/register");
}
Edit: Since you are facing the exception:
Cross-origin Request Blocked (Reason: CORS header ‘Access-Control-Allow-Origin’ missing and Reason: CORS request did not succeed)
it means that your OPTIONS request (preflight request) is failing. Are you using a different project as Front End for your application? If yes, you will need to specify in your spring boot configuration to allow origin and allow the specified methods from that particular origin.
Please refer to this official documentation to learn how to do that. You can enable Cors at Controller level or at global level. This StackOverflow thread should also be helpful in doing the implementation in case you are unable to proceed.
I have a specific application URL - https://baseurl/contextroot. It is an Angular5 app packaged within a spring boot application. I want to enable spring security for all urls except https://baseurl/contextroot/path1.
My application does not have user login.
I tried something like below, based on results from search forums. The below code does not even load my application https://baseurl/contextroot and it returns 403.
Error:
(type=Forbidden, status=403).
Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'.
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// Build the request matcher for CSFR protection
RequestMatcher csrfRequestMatcher = new RequestMatcher() {
// Disable CSFR protection on the following urls:
private AntPathRequestMatcher[] requestMatchers = { new AntPathRequestMatcher("/**/path1")
};
#Override
public boolean matches(HttpServletRequest request) {
// If the request match one url the CSFR protection will be disabled
for (AntPathRequestMatcher rm : requestMatchers) {
if (rm.matches(request)) {
return false;
}
}
return true;
} // method matches
}; // new RequestMatcher
http.headers().frameOptions().disable();
http.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
// Disable the csrf protection on some request matches
.csrf()
.requireCsrfProtectionMatcher(csrfRequestMatcher);
return;
} // method configure
}
Also my application will be a child application, loaded in a iframe and I wanted to allow only certain urls to be load my application and I found option to add header writer but I could not get something like below to work.
http.headers().frameOptions().disable();
http.headers().addHeaderWriter(new XFrameOptionsHeaderWriter(
new WhiteListedAllowFromStrategy(Arrays.asList("allowed-website-1","allowed-website-2"))));
You can try doing like this.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/contextroot/path1").permitAll();
}
The problem is as follows. I implemented the login through Keycloak Bearer Spring security like this
public class KeycloakSecurityConfiguration extends KeycloakWebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.sessionAuthenticationStrategy(sessionAuthenticationStrategy())
.and()
.csrf().disable()
.addFilterBefore(keycloakPreAuthActionsFilter(), LogoutFilter.class)
.addFilterBefore(keycloakAuthenticationProcessingFilter(), X509AuthenticationFilter.class)
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
.and()
.authorizeRequests().antMatchers(Constants.API_BASE_PATH + "/**").authenticated();
}
}
when I send the Authorization request header empty, keycloak throws error 401. Which I cannot catch through #ExceptionHandler like this :
#ExceptionHandler(RuntimeException.class)
protected ResponseEntity<Object> keycloakAuthenticationExceptionn(RuntimeException ex) {
return buildResponseEntity(new ErrorResponseWrapper(BAD_REQUEST,new
MessageResponse(ex.getLocalizedMessage()),ex,ErrorCode.NOT_AUTHORIZED));
}
KeyCloak has a KeycloakAuthenticationFailureHandler that handles authentication failures.
I was able to solve a similar problem by creating a Custom KeycloakAuthenticationFailureHandler then set my Custom class while overriding the
KeycloakAuthenticationProcessingFilter.
#Bean
#Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(this.authenticationManagerBean());
filter.setSessionAuthenticationStrategy(this.sessionAuthenticationStrategy());
filter.setAuthenticationFailureHandler(new CustomKeycloakAuthenticationFailureHandler());
return filter;
}
Inside my Custom Class...
public class CustomKeycloakAuthenticationFailureHandler implements AuthenticationFailureHandler {
public CustomKeycloakAuthenticationFailureHandler() {}
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if (!response.isCommitted()) {
if (KeycloakCookieBasedRedirect.getRedirectUrlFromCookie(request) != null) {
response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl((String)null));
}
//response.sendError(401, "Unable to authenticate using the Authorization header");
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().println("{ \"error\": \"" + exception.getMessage() + "\" }");
} else if (200 <= response.getStatus() && response.getStatus() < 300) {
throw new RuntimeException("Success response was committed while authentication failed!", exception);
}
}
}
I am able to use the response OutputStream to customize my response to the client.
I commented the default KeyCloak response.sendError line.
It looks like KeyCloak handles the exceptions internally.
This solution also solves the issue where the header: WWW-Authenticate
is not duplicated in the response.
Turning on DEBUG for KeyCloak troubleshooting HELPS: logging.level.org.keycloak=DEBUG
Hope this helps.
I'm not sure about the answer but i faced something like your question. ExceptionHandler working after http filters, so maybe Keycloak exception throws in his filter, before the request can to be handled using your ExceptionHandler. So you can trace the whole logs to see from where the exception thrown. I hope that will help you
**Thanks for answers!**
I think, this is the problem:
Earlier I used dependency keycloak version 4.0.0.Final
pom.xml dependency
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>4.0.0.Final</version>
</dependency>
and spring boot version 1.5.4.RELEASE. Everything worked great.
Now I'm using spring boot version 2.1.5.RELEASE and keycloak version 10.0.1
pom.xml dependency
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-2-adapter</artifactId>
<version>10.0.1</version>
</dependency>
I checked it again from Postman when sending Authorization token "bearer " + "token" in the request header. I get a response with two the same WWW-Authenticate values in the header.
Earlier, header came in a single copy.
Can you please tell me what the problem ?.
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.