2022 update
I maintain a set of tutorials and samples to configure resource-servers security for:
- both servlet and reactive applications
- decoding JWTs and introspecting access-tokens
- default or custom
Authentication
implementations
- any OIDC authorization-server(s), including Keycloak of course (most samples support multiple realms / identity-providers)
The repo also contains a set of libs published on maven-central to:
- mock OAuth2 identities during unit and integration tests (with authorities and any OpenID claim, including private ones)
- configure resource-servers from properties file (including source claims for roles, roles prefix and case processing, CORS configuration, session-management, public routes and more)
Sample for a servlet with JWT decoder
@EnableMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig {}
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.spring-addons-public.roles,resource_access.spring-addons-confidential.roles
com.c4-soft.springaddons.security.cors[0].path=/sample/**
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<version>6.0.3</version>
</dependency>
No, nothing more requried.
Unit-tests with mocked authentication
Secured @Component
without http request (@Service
, @Repository
, etc.)
@Import({ SecurityConfig.class, SecretRepo.class })
@AutoConfigureAddonsSecurity
class SecretRepoTest {
// auto-wire tested component
@Autowired
SecretRepo secretRepo;
@Test
void whenNotAuthenticatedThenThrows() {
// call tested components methods directly (do not use MockMvc nor WebTestClient)
assertThrows(Exception.class, () -> secretRepo.findSecretByUsername("ch4mpy"));
}
@Test
@WithMockJwtAuth(claims = @OpenIdClaims(preferredUsername = "Tonton Pirate"))
void whenAuthenticatedAsSomeoneElseThenThrows() {
assertThrows(Exception.class, () -> secretRepo.findSecretByUsername("ch4mpy"));
}
@Test
@WithMockJwtAuth(claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
void whenAuthenticatedWithSameUsernameThenReturns() {
assertEquals("Don't ever tell it", secretRepo.findSecretByUsername("ch4mpy"));
}
}
Secured @Controller
(sample for @WebMvcTest
but works for @WebfluxTest
too)
@WebMvcTest(GreetingController.class) // Use WebFluxTest or WebMvcTest
@AutoConfigureAddonsWebSecurity // If your web-security depends on it, setup spring-addons security
@Import({ SecurityConfig.class }) // Import your web-security configuration
class GreetingControllerAnnotatedTest {
// Mock controller injected dependencies
@MockBean
private MessageService messageService;
@Autowired
MockMvcSupport api;
@BeforeEach
public void setUp() {
when(messageService.greet(any())).thenAnswer(invocation -> {
final JwtAuthenticationToken auth = invocation.getArgument(0, JwtAuthenticationToken.class);
return String.format("Hello %s! You are granted with %s.", auth.getName(), auth.getAuthorities());
});
when(messageService.getSecret()).thenReturn("Secret message");
}
@Test
void greetWitoutAuthentication() throws Exception {
api.get("/greet").andExpect(status().isUnauthorized());
}
@Test
@WithMockAuthentication(authType = JwtAuthenticationToken.class, principalType = Jwt.class, authorities = "ROLE_AUTHORIZED_PERSONNEL")
void greetWithDefaultMockAuthentication() throws Exception {
api.get("/greet").andExpect(content().string("Hello user! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
}
}
Advanced use-cases
The most advanced tutorial demoes how to define a custom Authentication
implementation to parse (and expose to java code) any private claim into things that are security related but not roles (in the sample it's grant delegation between users).
It also shows how to extend spring-security SpEL to build a DSL like:
@GetMapping("greet/on-behalf-of/{username}")
@PreAuthorize("is(#username) or isNice() or onBehalfOf(#username).can('greet')")
public String getGreetingFor(@PathVariable("username") String username) {
return ...;
}