Spring Boot Social Login and Google Calendar API - spring-boot

Problem
Reuse End-User Google Authentication via Spring Security OAuth2 to access Google Calendar API in Web Application
Description
I was able to create a small Spring Boot Web application with Login through Spring Security
application.yaml
spring:
security:
oauth2:
client:
registration:
google:
client-id: <id>
client-secret: <secret>
scope:
- email
- profile
- https://www.googleapis.com/auth/calendar.readonly
When application starts I can access http://localhost:8080/user and user is asked for google login. After successful login profile json is shown in a browser as the response from:
SecurityController
#RestController
class SecurityController {
#RequestMapping("/user")
fun user(principal: Principal): Principal {
return principal
}
}
SecurityConfiguration.kt
#Configuration
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
}
}
Question
I want to reuse this authentication to retrieve all user's Calendar Events. The following code is taken from google's tutorial on accessing calendar API but it creates a completely independent authorization flow and asks user to log in.
#Throws(IOException::class)
private fun getCredentials(httpTransport: NetHttpTransport): Credential {
val clientSecrets = loadClientSecrets()
return triggerUserAuthorization(httpTransport, clientSecrets)
}
private fun loadClientSecrets(): GoogleClientSecrets {
val `in` = CalendarQuickstart::class.java.getResourceAsStream(CREDENTIALS_FILE_PATH)
?: throw FileNotFoundException("Resource not found: $CREDENTIALS_FILE_PATH")
return GoogleClientSecrets.load(JSON_FACTORY, InputStreamReader(`in`))
}
private fun triggerUserAuthorization(httpTransport: NetHttpTransport, clientSecrets: GoogleClientSecrets): Credential {
val flow = GoogleAuthorizationCodeFlow.Builder(
httpTransport, JSON_FACTORY, clientSecrets, SCOPES)
.setDataStoreFactory(FileDataStoreFactory(File(TOKENS_DIRECTORY_PATH)))
.setAccessType("offline")
.build()
val receiver = LocalServerReceiver.Builder().setPort(8880).build()
return AuthorizationCodeInstalledApp(flow, receiver).authorize("user")
}
How can I reuse already done authentication to access end user's calendar events on Google account?

If I understand correctly, what you mean be reusing the authentication is that you want to use the access and refresh tokens Spring retrieved for you in order to use them for requests against Google API.
The user authentication details can be injected into an endpoint method like this:
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
#RestController
class FooController(val historyService: HistoryService) {
#GetMapping("/foo")
fun foo(#RegisteredOAuth2AuthorizedClient("google") user: OAuth2AuthorizedClient) {
user.accessToken
}
}
With the details in OAuth2AuthorizedClient you should be able to do anything you need with the google API.
If you need to access the API without a user making a request to your service, you can inject OAuth2AuthorizedClientService into a managed component, and use it like this:
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService
import org.springframework.stereotype.Service
#Service
class FooService(val clientService: OAuth2AuthorizedClientService) {
fun foo() {
val user = clientService.loadAuthorizedClient<OAuth2AuthorizedClient>("google", "principal-name")
user.accessToken
}
}

Related

Multi-tenant Spring Webflux microservice with Keycloak OAuth/OIDC

Use Case:
I am building couple of reactive microservices using spring webflux.
I am using Keycloak as authentication and authorization server.
Keycloak realms are used as tenants where tenant/realm specific clients and users are configured.
The client for my reactive microservice is configured in each realm of Keycloak with the same client id & name.
The microservice REST APIs would be accessed by users of different realms of Keycloak.
The APIs would be accessed by user using UX (developed in React) publicly as well as by other webflux microservices as different client internally.
The initial part of the REST API would contain tenant information e.g. http://Service-URI:Service-Port/accounts/Keycloak-Realm/Rest-of-API-URI
Requirements:
When the API is called from UX, I need to invoke authorization code grant flow to authenticate the user using the realm information present in the request URI. The user (if not already logged in) should be redirected to the login page of correct realm (present in the request URI)
When the API is called from another webflux microservice, it should invoke client credential grant flow to authenticate and authorize the caller service.
Issue Faced:
I tried to override ReactiveAuthenticationManagerResolver as below:
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders;
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
#Component
public class TenantAuthenticationManagerResolver implements ReactiveAuthenticationManagerResolver<ServerWebExchange> {
private static final String ACCOUNT_URI_PREFIX = "/accounts/";
private static final String ACCOUNTS = "/accounts";
private static final String EMPTY_STRING = "";
private final Map<String, String> tenants = new HashMap<>();
private final Map<String, JwtReactiveAuthenticationManager> authenticationManagers = new HashMap<>();
public TenantAuthenticationManagerResolver() {
this.tenants.put("neo4j", "http://localhost:8080/realms/realm1");
this.tenants.put("testac", "http://localhost:8080/realms/realm2");
}
#Override
public Mono<ReactiveAuthenticationManager> resolve(ServerWebExchange exchange) {
return Mono.just(this.authenticationManagers.computeIfAbsent(toTenant(exchange), this::fromTenant));
}
private String toTenant(ServerWebExchange exchange) {
try {
String tenant = "system";
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if (path.startsWith(ACCOUNT_URI_PREFIX)) {
tenant = extractAccountFromPath(path);
}
return tenant;
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
private JwtReactiveAuthenticationManager fromTenant(String tenant) {
return Optional.ofNullable(this.tenants.get(tenant))
.map(ReactiveJwtDecoders::fromIssuerLocation)
.map(JwtReactiveAuthenticationManager::new)
.orElseThrow(() -> new IllegalArgumentException("Unknown tenant"));
}
private String extractAccountFromPath(String path) {
String removeAccountTag = path.replace(ACCOUNTS, EMPTY_STRING);
int indexOfSlash = removeAccountTag.indexOf("/");
return removeAccountTag.substring(indexOfSlash + 1, removeAccountTag.indexOf("/", indexOfSlash + 1));
}
}
Then I used the overridden TenantAuthenticationManagerResolver class in to SecurityWebFilterChain configuration as below:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfig {
#Autowired
TenantAuthenticationManagerResolver authenticationManagerResolver;
#Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/health").hasAnyAuthority("ROLE_USER")
.anyExchange().authenticated()
.and()
.oauth2Client()
.and()
.oauth2Login()
.and()
.oauth2ResourceServer()
.authenticationManagerResolver(authenticationManagerResolver);
return http.build();
}
}
Blow is the configuration in application.properties:
server.port=8090
logging.level.org.springframework.security=DEBUG
spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.registration.keycloak.client-id=test-client
spring.security.oauth2.client.registration.keycloak.client-secret=ZV4kAKjeNW2KEnYejojOCsi0vqt9vMiS
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.scope=openid
spring.security.oauth2.client.registration.keycloak.redirect-uri={baseUrl}/login/oauth2/code/keycloak
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/realms/master
When I call the API using master realm e.g. http://localhost:8090/accounts/master/health it redirects the user to Keycloak login page of master realm and once I put the user id and password of a user of master realm, the API call is successful.
When I call the API using any other realm e.g. http://localhost:8090/accounts/realm1/health it still redirects the user to Keycloak login page of master realm and if I put the user id and password of a user of realm1 realm, the login is not successful.
So it seems that multi-tenancy is not working as expected and it is only working for the tenant configured in application.properties.
What is missing in my implementation w.r.t. multi-tenancy?
How to pass the client credentials for different realms?
I tried to use JWKS in place of client credentials but somehow it is not working. Below is the configuration used for JWKS in application.properties.
spring.security.oauth2.client.registration.keycloak.keystore=C:\\Work\\test-client.jks
spring.security.oauth2.client.registration.keycloak.keystore-type=JKS
spring.security.oauth2.client.registration.keycloak.keystore-password=changeit
spring.security.oauth2.client.registration.keycloak.key-password=changeit
spring.security.oauth2.client.registration.keycloak.key-alias=proactive-outreach-admin
spring.security.oauth2.client.registration.keycloak.truststore=C:\\Work\\test-client.jks
spring.security.oauth2.client.registration.keycloak.truststore-password=changeit
Note: JWKS is not event working for master realm configured in application.properties.
Need help here as I am stuck for many days without any breakthrough. Let me know if any more information is required.
Client V.S. resource-server configuration
Authentication is not the responsibility of the resource-servers: request arrive authorized (or not if some endpoints accept anonymous requests) with a Bearer access-token. Do not implement redirection to login there, just return 401 if authentication is missing.
It is the responsibility of OAuth2 clients to acquire (and maintain) access-tokens, with sometimes other flows than authorization-code, even to authenticate users: won't you use refresh-token flow for instance?
This OAuth2 client could be either a "public" client in a browser (your react app) or a BFF in between the browser and the resource-servers (spring-cloud-gateway for instance). The later is considered more secure because tokens are kept on your servers and is a rather strong recommendation lately.
If you want both client (for oauth2Login) and resource-server (for securing a REST API) configurations in your app, define two separated security filter-chains as exposed in this other answer: Use Keycloak Spring Adapter with Spring Boot 3
Multi-tenancy in Webflux with JWT decoder
The recommended way is to override the default authentication-manager resolver with one capable of providing the right authentication-manager depending on the access-token issuer (or header or whatever from the request): http.oauth2ResourceServer().authenticationManagerResolver(authenticationManagerResolver)
To easily configure multi-tenant reactive resource-servers (with JWT decoder and an authentication-manager switching the tenant based on token issuer), you should have a look at this Spring Boot starter I maintain: com.c4-soft.springaddons:spring-addons-webflux-jwt-resource-server. sample usage here and tutorials there. As it is open-source, you can also browse the source to see in details how this is done and what this reactive multi-tenant authentication-manager looks like.

Spring RSocket Accessing setup information

I am using Spring RSocket support and Spring Security RSocket. I setup the security configuration so that on each request/response interaction, the requester send bearer token which my code can access after successful authentication by using ReactiveSecurityContextHolder.
My current configuration looks like this :
#EnableRSocketSecurity
#SpringBootApplication
public class MyConfiguration{
#Bean
fun rSocketStrategies() : RSocketStrategies = RSocketStrategies.builder()
.encoder(BearerTokenAuthenticationEncoder())
.encoder(KotlinSerializationJsonEncoder())
.decoder(KotlinSerializationJsonDecoder())
.build()
#Bean
fun rSocketInterceptor(rSocketSecurity: RSocketSecurity,authenticationManager: ReactiveAuthenticationManager) : PayloadSocketAcceptorInterceptor =
rSocketSecurity
.authorizePayload { authorize -> authorize.anyExchange().authenticated() }
.jwt { jwtSpec -> jwtSpec.authenticationManager(authenticationManager) }
.build()
#Bean
fun authenticationManager(...) = ReactiveAuthenticationManager{
...
}
}
I want to ask if it's possible to do the authentication once during setup exchange and then access the token in RSocket controllers? And if yes, what kind of configuration changes should I make?
My controllers' code currently looks like this(which works only because the token is included in every request/response interaction):
#MessageMapping("${v1}.message")
#Controller
class MessageRSocketController(private val messageService: MessageService) {
#MessageMapping("stream")
suspend fun messages() : Flow<EncryptedMessageWithID> {
val jwt = ReactiveSecurityContextHolder.getContext().awaitSingle().authentication.name
return messageService.messages(jwt)
}
}

How to make the path public in RSocketSecurity(Spring)

I have config class for RSocketSecurity
Something like that
#Configuration
#EnableRSocketSecurity
#EnableReactiveMethodSecurity
class RSocketAuthConfiguration {
and authorization for it (allows only authenticated users to subscribe )
security.addPayloadInterceptor(interceptor).authorizePayload {
it.setup().authenticated().anyRequest().permitAll()
}
I want to set some routes with public access, but most of them should be with authorization. What is the best way to achieve that?
Spring Security Rsocket configures the setup and route respectively.
The following is an example of the configuration part.
#Bean
public PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
return rsocket
.authorizePayload(
authorize -> {
authorize
// must have ROLE_SETUP to make connection
.setup().hasRole("SETUP")
// must have ROLE_ADMIN for routes starting with "greet."
.route("greet*").hasRole("ADMIN")
// any other request must be authenticated for
.anyRequest().authenticated();
}
)
.basicAuthentication(Customizer.withDefaults())
.build();
}
Get the complete example from my Github.
Something along the following lines should work:
#Configuration
#EnableRSocketSecurity
#EnableReactiveMethodSecurity
class RSocketSecurityConfiguration(val authenticationService: AuthenticationService) {
#Bean
fun authorization(security: RSocketSecurity): PayloadSocketAcceptorInterceptor {
return security
.authorizePayload {
it.route("route-A").hasRole("role-A")
.route("route-B").permitAll()
}
.simpleAuthentication(Customizer.withDefaults())
.authenticationManager(authenticationService)
.build()
}
}
route-A is authenticated and requires role-A while route-B is publicly available.

How to get all attributes from java.security.Principal?

I learning Spring security and write simple web app with Spring Security 5 and OAuth2 Login. I want to get information from Principal (email, username e.t.c) but I can't find any method for it. Write some JSON-parser not a best idea because I pretty sure there is some method for obtaining user account details.
Config
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and().oauth2Login();
}
}
Controller
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.security.Principal;
#Controller
public class HomeController {
#GetMapping({"", "/"})
#ResponseBody
public Principal getHome(Principal principal) {
return principal;
}
}
Application.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: xyz1
client-secret: secret1
facebook:
client-id: xyz2
client-secret: secret2
github:
client-id: xyz3
client-secret: secret3
Through trial and error, and with Spring Boot 2.3.2 I found out that in THAT particular scenario (OAuth2 with Spring Boot) you have to cast the Authentication object you get on onAuthenticationSuccess method (and yes, that is the difference of my scenario, because I observed this object in my authentication success handler - but would this scenario be much different?) to OAuth2AuthenticationToken. If you actually pause execution and add Authentication object to the Watches, you will see that it's really that type. Now this object's getPrincipal() returns properly cast OAuth2User object, which has a method getAttributes(). I really wish it was easier to figure that out.
In fact, in MY scenario, OAuth2User object could be passed to my Controller method directly (and I am not sure if that annotation #AuthenticationPrincipal is necessary or proper..):
#Controller
#RequestMapping("/authorized")
public class AuthorizedController {
#RequestMapping("/default")
public String handle(#AuthenticationPrincipal OAuth2User principal,
Model m) {
// ...
}
}
Create a new object representing the subset of data you want to be returned from source,
i.e, source of those details have multiple values therefore Use some List or set or map to store that data and represent it,
As You mentioned you are working with OAuth2 :
#RequestMapping("/user")
public Authentication user(OAuth2Authentication authentication) {
LinkedHashMap<String, Object> details = (LinkedHashMap<String, Object>) authentication.getUserAuthentication().getDetails();
return details.get("email");
}

Spring Security: mapping OAuth2 claims with roles to secure Resource Server endpoints

I'm setting up a Resource Server with Spring Boot and to secure the endpoints I'm using OAuth2 provided by Spring Security. So I'm using the Spring Boot 2.1.8.RELEASE which for instance uses Spring Security 5.1.6.RELEASE.
As Authorization Server I'm using Keycloak. All processes between authentication, issuing access tokens and validation of the tokens in the Resource Server are working correctly. Here is an example of an issued and decoded token (with some parts are cut):
{
"jti": "5df54cac-8b06-4d36-b642-186bbd647fbf",
"exp": 1570048999,
"aud": [
"myservice",
"account"
],
"azp": "myservice",
"realm_access": {
"roles": [
"offline_access",
"uma_authorization"
]
},
"resource_access": {
"myservice": {
"roles": [
"ROLE_user",
"ROLE_admin"
]
},
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "openid email offline_access microprofile-jwt profile address phone",
}
How can I configure Spring Security to use the information in the access token to provide conditional authorization for different endpoints?
Ultimately I want to write a controller like this:
#RestController
public class Controller {
#Secured("ROLE_user")
#GetMapping("userinfo")
public String userinfo() {
return "not too sensitive action";
}
#Secured("ROLE_admin")
#GetMapping("administration")
public String administration() {
return "TOOOO sensitive action";
}
}
After messing around a bit more, I was able to find a solution implementing a custom jwtAuthenticationConverter, which is able to append resource-specific roles to the authorities collection.
http.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new JwtAuthenticationConverter()
{
#Override
protected Collection<GrantedAuthority> extractAuthorities(final Jwt jwt)
{
Collection<GrantedAuthority> authorities = super.extractAuthorities(jwt);
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
Map<String, Object> resource = null;
Collection<String> resourceRoles = null;
if (resourceAccess != null &&
(resource = (Map<String, Object>) resourceAccess.get("my-resource-id")) !=
null && (resourceRoles = (Collection<String>) resource.get("roles")) != null)
authorities.addAll(resourceRoles.stream()
.map(x -> new SimpleGrantedAuthority("ROLE_" + x))
.collect(Collectors.toSet()));
return authorities;
}
});
Where my-resource-id is both the resource identifier as it appears in the resource_access claim and the value associated to the API in the ResourceServerSecurityConfigurer.
Notice that extractAuthorities is actually deprecated, so a more future-proof solution should be implementing a full-fledged converter
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken>
{
private static Collection<? extends GrantedAuthority> extractResourceRoles(final Jwt jwt, final String resourceId)
{
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
Map<String, Object> resource;
Collection<String> resourceRoles;
if (resourceAccess != null && (resource = (Map<String, Object>) resourceAccess.get(resourceId)) != null &&
(resourceRoles = (Collection<String>) resource.get("roles")) != null)
return resourceRoles.stream()
.map(x -> new SimpleGrantedAuthority("ROLE_" + x))
.collect(Collectors.toSet());
return Collections.emptySet();
}
private final JwtGrantedAuthoritiesConverter defaultGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
private final String resourceId;
public CustomJwtAuthenticationConverter(String resourceId)
{
this.resourceId = resourceId;
}
#Override
public AbstractAuthenticationToken convert(final Jwt source)
{
Collection<GrantedAuthority> authorities = Stream.concat(defaultGrantedAuthoritiesConverter.convert(source)
.stream(),
extractResourceRoles(source, resourceId).stream())
.collect(Collectors.toSet());
return new JwtAuthenticationToken(source, authorities);
}
}
I have tested both solutions using Spring Boot 2.1.9.RELEASE, Spring Security 5.2.0.RELEASE and an official Keycloak 7.0.0 Docker image.
Generally speaking, I suppose that whatever the actual Authorization Server (i.e. IdentityServer4, Keycloak...) this seems to be the proper place to convert claims into Spring Security grants.
Here is another solution
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
}
The difficulty you are experiencing is partly due to your roles being positioned in the JWT under resource_server->client_id. This then requires a custom token converter to extract them.
You can configure keycloak to use a client mapper that will present the roles under a top-level claim name such as "roles". This makes the Spring Security configuration simpler as you only need JwtGrantedAuthoritiesConverter with the authoritiesClaimName set as shown in the approach taken by #hillel_guy.
The keycloak client mapper would be configured like this:
As already mentioned by #hillel_guy's answer, using an AbstractHttpConfigurer should be the way to go. This worked seamlessly for me with spring-boot 2.3.4 and spring-security 5.3.4.
See the spring-security API documentation for reference: OAuth2ResourceServerConfigurer
UPDATE
Full example, as asked in the comments:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String JWT_ROLE_NAME = "roles";
private static final String ROLE_PREFIX = "ROLES_";
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.and().csrf().disable()
.cors()
.and().oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
// create a custom JWT converter to map the roles from the token as granted authorities
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(JWT_ROLE_NAME); // default is: scope, scp
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(ROLE_PREFIX ); // default is: SCOPE_
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
In my case, I wanted to map roles from the JWT instead of scope.
Hope this helps.
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 ...;
}
If you are using Azure AD Oath there is a much easier way now:
http
.cors()
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new AADJwtBearerTokenAuthenticationConverter("roles", "ROLE_"));
The ADDJwtBearerTokenAuthenticationConverter allows you to add your claim name as the first argument and what you want your role prefixed with as the second argument.
My import so you can find the library:
import com.azure.spring.aad.webapi.AADJwtBearerTokenAuthenticationConverter;

Resources