Apply dynamic ACL with spring security and JWT - spring-boot

I have implemented authentication using SpringSecurity, JWT and OAuth2.
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.POST,"/v1/register").permitAll()
.antMatchers(HttpMethod.GET,"/v1/public").permitAll();
List<String> permisos = roleService.findPermisos();
for(String name: permisos) {
String[] data = name.split(",");
http.authorizeRequests().antMatchers(data[0], data[1]).hasRole(data[2]);
}
http.authorizeRequests()
.anyRequest().authenticated()
.and().cors().configurationSource(corsConfigurationSource());
}
Currently the configure(HttpSecurity http) method works on the OAuth2 side. In my database I have all the #RestController to which they are allowed or denied access for each Role, the problem is, it is applied only once the app is compiled and I need is for it to be applied when I modify the allowed or denied access without the need to recompile a dynamic acl shown in List<String> permisos = roleService.findPermisos().
Searching I read that I can use Filters with HttpSecurity, I have not been able to find examples, your help please.

Doing some research I modified my solution as follows:
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.PUT,"/v1/menu").permitAll()
.anyRequest().authenticated().and()
.addFilterAfter(new CustomFilter(), AbstractPreAuthenticatedProcessingFilter.class)
.cors().configurationSource(corsConfigurationSource());
}
I add a Filter addFilterAfter(new CustomFilter(), AbstractPreAuthenticatedProcessingFilter.class) and in CustomFilter() is:
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ServletContext servletContext = request.getServletContext();
if(usuarioService == null){
WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
usuarioService = webApplicationContext.getBean(IUsuarioService.class);
}
String[] path = request.getRequestURI().split("/");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Usuario usuario = usuarioService.findByEmail(authentication.getName());
if(usuarioService.existsPermission(request.getMethod(), path[2], authentication.getName())) {
filterChain.doFilter(request, response);
} else {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
So I changed my solution from a loop to a query through a filter, the problem is that all the Rest must be registered and validated, this would be a problem to allow access to resources that I do not want to be verified.
Please, if you have a better solution, I would really appreciate your help.

Related

How to configure Spring-Security (Spring 6) for not having Filters executed on unsecured routes?

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();
}

Spring security permitall return 401

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());
}

AntMatcher seems not to match requested path (Spring Security)

We are trying to use Spring Security to secure our webservice. We are using a customer filter (a class that extends GenericFilterBean) to read a JSON Web Token from the HTTP header. If a token exists, it is stored as PreAuthenticatedAuthenticationToken in the Security Context. Protected resources should use our customer Authentication Provider to verify if the token is valid and to load user info (which includes roles).
The problem is that I don't get it to configure an AntMatcher for a specific resource.
If I use antMatchers("/**").hasRole("USER") the resource is protected, but we don't want all resources to match, so I tried an AntMachter for one resource like this:
.antMatchers(HttpMethod.GET,"/rest/security/v1/currentuser").hasRole("USER")
But this matcher seems not to match the requested resource and so the Authentication Provider is not called. But I don't know why. I tried several combinations of ant pattern but nothing worked yet.
I set a breakpoint in the custom filter to check the current path and when I call servletRequest.getPathInfo() I get excatly what I thought it should be our ant pattern: /rest/security/v1/currentuser
Spring Security configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.anonymous().disable()
.csrf().disable()
.authorizeRequests()
//.antMatchers("/**").hasRole("USER")
.antMatchers(HttpMethod.GET, "/rest/security/v1/currentuser").hasRole("USER")
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint())
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.addFilterBefore(new JwtAuthenticationFilter(), BasicAuthenticationFilter.class);
}
Custom filter:
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
Optional<String> token = Optional.ofNullable(httpRequest.getHeader(HTTP_AUTHENTICATION_HEADER));
if (token.isPresent()) {
PreAuthenticatedAuthenticationToken requestAuthentication = new PreAuthenticatedAuthenticationToken(token, null);
SecurityContextHolder.getContext().setAuthentication(requestAuthentication);
}
filterChain.doFilter(servletRequest, servletResponse);
}
Authentication provider:
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken authenticationToken = null;
try {
// Token Validation, Loading User Info
} catch (Exception e) {
LOG.error("Failed to authenticate", e);
}
return authenticationToken;
}

Keycloak spring adapter - check that the authToken is active with every http request

Problem I want to solve:
For every call made to the service I want to check that the token is active, if it isn't active I want to redirect the user to the login page.
Current setup: Grails 3.2.9 , Keycloak 3.4.3
Ideas so far:
This article looked promising: https://www.linkedin.com/pulse/json-web-token-jwt-spring-security-real-world-example-boris-trivic
In my security config I added a token filter
#Bean
public TokenAuthenticationFilter authenticationTokenFilter() throws Exception {
return new TokenAuthenticationFilter();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure http
http
.addFilterBefore(authenticationTokenFilter(), BasicAuthenticationFilter.class)
.logout()
.logoutSuccessUrl("/sso/login") // Override Keycloak's default '/'
.and()
.authorizeRequests()
.antMatchers("/assets/*").permitAll()
.anyRequest().hasAnyAuthority("ROLE_ADMIN")
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
My TokenAuthenticationFilter just prints out the request headers at the moment :
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private String getToken( HttpServletRequest request ) {
Enumeration headerEnumeration = request.getHeaderNames();
while (headerEnumeration.hasMoreElements()) {
println "${ headerEnumeration.nextElement()}"
}
return null;
}
#Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String authToken = getToken( request );
}
}
Which returns:
host
user-agent
accept
accept-language
accept-encoding
cookie
connection
upgrade-insecure-requests
cache-control
The code/logic I want to implement in the filter is something like:
KeycloakAuthenticationToken token = SecurityContextHolder.context?.authentication
RefreshableKeycloakSecurityContext context = token.getCredentials()
if(!context.isActive()){
// send the user to the login page
}
However I'm lost as to how to get there.
Any help greatly appreciated
As far as I understand, your question is about "how to check the token is active?" and not "how to redirect the user to login page?".
As I see you added the tag "spring-boot" and "keycloak" maybe you could use "Keycloak Spring Boot Adapter". Assuming you use the version 3.4 of Keycloak (v4.0 still in beta version), you can found some documentation here.
If you can't (or don't want to) use Spring Boot Adapter, here is the part of the KeycloakSecurityContextRequestFilter source code that could be interesting for your case:
KeycloakSecurityContext keycloakSecurityContext = getKeycloakPrincipal();
if (keycloakSecurityContext instanceof RefreshableKeycloakSecurityContext) {
RefreshableKeycloakSecurityContext refreshableSecurityContext = (RefreshableKeycloakSecurityContext) keycloakSecurityContext;
if (refreshableSecurityContext.isActive()) {
...
} else {
...
}
}
and here is the (Java) source code of the getKeycloakPrincipal method:
private KeycloakSecurityContext getKeycloakPrincipal() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
Object principal = authentication.getPrincipal();
if (principal instanceof KeycloakPrincipal) {
return KeycloakPrincipal.class.cast(principal).getKeycloakSecurityContext();
}
}
return null;
}
And if you want to understand how the Authentication is set in the SecurityContextHolder, please read this piece of (Java) code from KeycloakAuthenticationProcessingFilter:
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (authResult instanceof KeycloakAuthenticationToken && ((KeycloakAuthenticationToken) authResult).isInteractive()) {
super.successfulAuthentication(request, response, chain, authResult);
return;
}
...
SecurityContextHolder.getContext().setAuthentication(authResult);
...
try {
chain.doFilter(request, response);
} finally {
SecurityContextHolder.clearContext();
}
}
As an alternative you could also check this github repository of dynamind:
https://github.com/dynamind/grails3-spring-security-keycloak-minimal
Hoping that can help.
Best regards,
Jocker.

Spring Security Filter is not being triggered by configuration

I am configuring the security of my REST and I don't know how I can secure my methods, however allowing a filter to trigger to set my Authority
http
.authorizeRequests()
.antMatchers(PERSISTENCE_SERVICE_URL)
.hasAuthority(AUTHORITY_PERSISTENCE_SERVICE)
.and()
.csrf()
.disable();
And in my Filter which extends OncePerRequestFilter does something like this
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
this.authenticationImpl.init();
String jwt = request.getHeader("jwt");
String refresh = request.getHeader("refresh");
if(jwt != null) {
this.jwtPropertyExtractor.commitJwt(jwt, refresh);
String jwtId = this.jwtPropertyExtractor.getIdentityId();
String securityRole = this.jwtPropertyExtractor.getSecurityRole();
this.authenticationImpl.setIdentityId(jwtId);
this.authenticationImpl.updateSecurityRole(securityRole);
SecurityContextHolder.getContext().setAuthentication(this.authenticationImpl);
}
filterChain.doFilter(request, response); }
So when I place .hasAuthority(AUTHORITY_PERSISTENCE_SERVICE) in my configuration, my filter is not even being triggered but i need him to set my authentication.
Ok i added
.addFilterBefore(jwtAuthorisationFilter(),
BasicAuthenticationFilter.class);
}
#Bean
public JwtAuthorisationFilter jwtAuthorisationFilter() {
return new JwtAuthorisationFilter(jwtExtractor(),
authenticationImpl());
}
Now its working fine thx for that hint :>

Resources