Skip to content

Commit

Permalink
Fix AccessController
Browse files Browse the repository at this point in the history
  • Loading branch information
ch4mpy committed Feb 17, 2023
1 parent 8a89475 commit 015194d
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 87 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.acme.backend.springboot.users.config;

import com.acme.backend.springboot.users.support.access.AccessController;
import com.acme.backend.springboot.users.support.keycloak.KeycloakJwtAuthenticationConverter;
import lombok.RequiredArgsConstructor;
import java.util.List;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand All @@ -12,80 +11,82 @@
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;
import com.acme.backend.springboot.users.support.access.AccessController;
import com.acme.backend.springboot.users.support.keycloak.KeycloakJwtAuthenticationConverter;

import lombok.RequiredArgsConstructor;

/**
* Configuration applied on all web endpoints defined for this
* application. Any configuration on specific resources is applied
* in addition to these global rules.
* Configuration applied on all web endpoints defined for this application. Any configuration on specific resources is applied in addition to these global
* rules.
*/
@Configuration
@RequiredArgsConstructor
class WebSecurityConfig {

private final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter;

/**
* Configures basic security handler per HTTP session.
* <p>
* <ul>
* <li>Stateless session (no session kept server-side)</li>
* <li>CORS set up</li>
* <li>Require the role "ACCESS" for all api paths</li>
* <li>JWT converted into Spring token</li>
* </ul>
*
* @param http security configuration
* @throws Exception any error
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

http.sessionManagement(smc -> {
smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
});
http.cors(this::configureCors);
http.authorizeHttpRequests(ahrc -> {
// declarative route configuration
// .mvcMatchers("/api").hasAuthority("ROLE_ACCESS")
ahrc.requestMatchers("/api/**").access(AccessController::checkAccess);
// add additional routes
ahrc.anyRequest().fullyAuthenticated(); //
});
http.oauth2ResourceServer(arsc -> {
arsc.jwt().jwtAuthenticationConverter(keycloakJwtAuthenticationConverter);
});

return http.build();
}

@Bean
AccessController accessController() {
return new AccessController();
}

/**
* Configures CORS to allow requests from localhost:30000
*
* @param cors mutable cors configuration
*/
protected void configureCors(CorsConfigurer<HttpSecurity> cors) {

UrlBasedCorsConfigurationSource defaultUrlBasedCorsConfigSource = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
corsConfiguration.addAllowedOrigin("https://apps.acme.test:4443");
List.of("GET", "POST", "PUT", "DELETE").forEach(corsConfiguration::addAllowedMethod);
defaultUrlBasedCorsConfigSource.registerCorsConfiguration("/api/**", corsConfiguration);

cors.configurationSource(req -> {

CorsConfiguration config = new CorsConfiguration();

config = config.combine(defaultUrlBasedCorsConfigSource.getCorsConfiguration(req));

// check if request Header "origin" is in white-list -> dynamically generate cors config

return config;
});
}
public class WebSecurityConfig {

private final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter;

/**
* Configures basic security handler per HTTP session.
* <p>
* <ul>
* <li>Stateless session (no session kept server-side)</li>
* <li>CORS set up</li>
* <li>Require the role "ACCESS" for all api paths</li>
* <li>JWT converted into Spring token</li>
* </ul>
*
* @param http security configuration
* @throws Exception any error
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

http.sessionManagement(smc -> {
smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
});
http.cors(this::configureCors);
http.authorizeHttpRequests(ahrc -> {
// declarative route configuration
// .mvcMatchers("/api").hasAuthority("ROLE_ACCESS")
ahrc.requestMatchers("/api/**").access(AccessController::checkAccess);
// add additional routes
ahrc.anyRequest().fullyAuthenticated(); //
});
http.oauth2ResourceServer(arsc -> {
arsc.jwt().jwtAuthenticationConverter(keycloakJwtAuthenticationConverter);
});

return http.build();
}

@Bean
AccessController accessController() {
return new AccessController();
}

/**
* Configures CORS to allow requests from localhost:30000
*
* @param cors mutable cors configuration
*/
protected void configureCors(CorsConfigurer<HttpSecurity> cors) {

UrlBasedCorsConfigurationSource defaultUrlBasedCorsConfigSource = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
corsConfiguration.addAllowedOrigin("https://apps.acme.test:4443");
List.of("GET", "POST", "PUT", "DELETE").forEach(corsConfiguration::addAllowedMethod);
defaultUrlBasedCorsConfigSource.registerCorsConfiguration("/api/**", corsConfiguration);

cors.configurationSource(req -> {

CorsConfiguration config = new CorsConfiguration();

config = config.combine(defaultUrlBasedCorsConfigSource.getCorsConfiguration(req));

// check if request Header "origin" is in white-list -> dynamically generate cors config

return config;
});
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
package com.acme.backend.springboot.users.support.access;

import lombok.extern.slf4j.Slf4j;
import java.util.function.Supplier;

import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;

import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;

/**
* Example for generic custom access checks on request level.
*/
@Slf4j
public class AccessController {

private static final AuthorizationDecision GRANTED = new AuthorizationDecision(true);
private static final AuthorizationDecision DENIED = new AuthorizationDecision(false);

public static AuthorizationDecision checkAccess(Supplier<Authentication> authentication, RequestAuthorizationContext requestContext) {
private static final AuthorizationDecision GRANTED = new AuthorizationDecision(true);
private static final AuthorizationDecision DENIED = new AuthorizationDecision(false);

var auth = authentication.get();
log.info("Check access for username={} path={}", auth.getName(), requestContext.getRequest().getRequestURI());
public static AuthorizationDecision checkAccess(Supplier<Authentication> authentication, RequestAuthorizationContext requestContext) {

return GRANTED;
}
var auth = authentication.get();
if (auth == null) {
log.warn("Authentication provider returned null authentication");
return DENIED;
}
log.info("Check access for username={} path={}", auth.getName(), requestContext.getRequest().getRequestURI());
return auth.getAuthorities().stream().map(GrantedAuthority::getAuthority).filter("ROLE_ACCESS"::equals).count() > 0 ? GRANTED : DENIED;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,50 @@
package com.acme.backend.springboot.users;

import static org.hamcrest.CoreMatchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.test.web.servlet.MockMvc;

import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims;
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth;

@SpringBootTest
@AutoConfigureMockMvc
class BackendApiSpringboot3AppTests {

@Autowired
MockMvc api;

@Test
@WithAnonymousUser
void givenRequestIsAnonymous_whengetUsersMe_thenUnauthorized() throws Exception {
api.perform(get("/api/users/me")).andExpectAll(status().isUnauthorized());
}

@Test
@WithMockJwtAuth(claims = @OpenIdClaims(sub = "Tonton Pirate"))
void givenUserIsNotGrantedWithAccess_whengetUsersMe_thenForbidden() throws Exception {
// @formatter:off
api.perform(get("/api/users/me"))
.andExpect(status().isForbidden());
// @formatter:on
}

@Test
void contextLoads() {
@WithMockJwtAuth(authorities = { "ROLE_ACCESS" }, claims = @OpenIdClaims(sub = "Tonton Pirate"))
void givenUserIsGrantedWithAccess_whengetUsersMe_thenOk() throws Exception {
// @formatter:off
api.perform(get("/api/users/me"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message", is("Hello Tonton Pirate")));
// @formatter:on
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,24 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.test.web.servlet.MockMvc;

import com.acme.backend.springboot.users.config.WebSecurityConfig;
import com.acme.backend.springboot.users.support.keycloak.KeycloakJwtAuthenticationConverter;
import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims;
import com.c4_soft.springaddons.security.oauth2.test.annotations.WithMockJwtAuth;

@WebMvcTest(controllers = UsersController.class)
@Import({ WebSecurityConfig.class, KeycloakJwtAuthenticationConverter.class })
class UsersControllerTest {

@Autowired
MockMvc api;

@Test
@WithAnonymousUser
void givenRequestIsAnonymous_whengetUsersMe_thenUnauthorized() throws Exception {
api.perform(get("/api/users/me")).andExpectAll(status().isUnauthorized());
}
Expand All @@ -28,8 +35,7 @@ void givenRequestIsAnonymous_whengetUsersMe_thenUnauthorized() throws Exception
void givenUserIsNotGrantedWithAccess_whengetUsersMe_thenForbidden() throws Exception {
// @formatter:off
api.perform(get("/api/users/me"))
// should be forbidden as user does is not granted with "ROLE_ACCESS"
.andExpect(status().isOk());
.andExpect(status().isForbidden());
// @formatter:on
}

Expand Down

0 comments on commit 015194d

Please sign in to comment.