I am working on an API service that is meant to do the following:
Allow users to sign in via Google.
Create the user in the database based on the information retrieved.
Provide the user with a JWT token to be used for authentication so that requests are uniquely identified with said user.
Allow the user to be able to use the obtained token to perform API requests against my service.
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
I am unsure how can I go about this and what exactly do I need. So far I have the following
Main Application class:
#SpringBootApplication
#EnableWebSecurity
#Configuration
class ApiServiceApplication {
#Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http.authorizeHttpRequests {
it.antMatchers("/", "/login", "/error", "/webjars/**").permitAll().anyRequest().authenticated()
}
.logout {
it.logoutSuccessUrl("/").permitAll()
}
.exceptionHandling {
it.authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
}
.oauth2Login { oauth2Login ->
oauth2Login.loginPage("/login")
oauth2Login.defaultSuccessUrl("/user", true)
}
.oauth2Client { oauth2Client -> }
.csrf {
it.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
}
return http.build()
}
}
fun main(args: Array<String>) {
runApplication<ApiServiceApplication>(*args)
}
User Service class for saving the user to the DB
#RestController
class UserService : OidcUserService() {
#Autowired
lateinit var userRepository: UserRepository
#Autowired
lateinit var loginRepository: LoginRepository
private val oauth2UserService = DefaultOAuth2UserService()
#GetMapping("/login")
fun authenticate(): RedirectView {
return RedirectView("/oauth2/authorization/google")
}
override fun loadUser(userRequest: OidcUserRequest?): OidcUser {
val loadedUser = oauth2UserService.loadUser(userRequest)
val username = loadedUser.attributes["email"] as String
var user = userRepository.findByUsername(username)
if (user == null) {
user = OauthUser()
user.username = username
}
loadedUser.attributes.forEach { loadedAttribute ->
val userAttribute = user.oauthAttributes.find { loadedAttribute.key == it.attributeKey && it.active }
val newAttribute = OauthAttribute(loadedAttribute.key, loadedAttribute.value?.toString())
if(userAttribute == null){
user.oauthAttributes.add(newAttribute)
}
else if(userAttribute.attributeValue != loadedAttribute.value?.toString()){
userAttribute.active = false
user.oauthAttributes.add(newAttribute)
}
}
user.oauthAuthorities = loadedUser.authorities.map { OauthAuthority(it.authority) }.toMutableList()
user.oauthToken = OauthToken(
userRequest?.accessToken?.tokenValue!!,
Date.from(userRequest.accessToken.issuedAt),
Date.from(userRequest.accessToken.expiresAt)
)
userRepository.save(user)
val login = Login(user)
loginRepository.save(login)
return user
}
}
I am not providing the data classes and corresponding repositories because what's above works fine - upon accessing the /login endpoint, the user is redirected to Google where after authentication the user is saved in the database along with the corresponding information.
My main issue is that I am not really sure how to go about authenticating each request. I've tried to provide an authentication Bearer in Postman that is the same as the one obtained from Google in the loadUser method, but I'm getting back 401 unauthorized codes. When I access the server through the browser and I authenticate I can access all the endpoints just fine, but I'm guessing that it's just my session that is authenticated.
You are trying to configure a resource-server (REST API serving resources) as a UI client (application consuming resources). That won't work.
You should not implement oauth2 login and logout on resource-server, this are UI client concerns and should be removed from your Java conf. An exception is if your application also serves UI with Thymeleaf, JSF or other server-side rendered UI, in which case you should create a second "client" security filter-chain bean and move login & logout there as described there: Use Keycloak Spring Adapter with Spring Boot 3).
Unless you are in the "exception" above (UI client) or use a REST client auto-configured by spring-boot (WebClient, #FeignClient, RestTemplate) to consume resources from other resource-servers, remove all spring.security.oauth2.client properties from your yaml file and spring-boot-starter-oauth2-client from your dependencies.
Details for configuring resource-servers in the answer linked above (applies to any OIDC authorization-server, not just Keycloak) or the tutorials of this repo of mine.
I have managed to achieve what I wanted by doing the following:
Adding a resource server definition to my spring.security.oauth2 configuration:
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
resourceserver:
jwt:
issuer-uri: https://accounts.google.com
jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs
Adding the OAuth2ResourceServerConfigurer and specifying the default JwtConfigurer via .oauth2ResourceServer().jwt(), and specifying the authorization matches for the path I want to be secured by JWT. I've also split the filter chains, thanks to the comment from ch4mp, so that only /api endpoint is secured via JWT:
#Bean
#Order(HIGHEST_PRECEDENCE)
fun apiFilterChain(http: HttpSecurity): SecurityFilterChain {
http.antMatcher("/api/**").authorizeRequests { authorize ->
authorize.antMatchers("/api/**").authenticated()
}.exceptionHandling {
it.authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
}
.csrf().disable()
.oauth2ResourceServer().jwt()
return http.build()
}
#Bean
fun uiFilterChain(http: HttpSecurity): SecurityFilterChain {
http.authorizeRequests { authorize ->
authorize.antMatchers("/", "/login", "/error", "/webjars/**").permitAll().anyRequest()
.authenticated()
}.logout {
it.logoutSuccessUrl("/").permitAll()
}.exceptionHandling {
it.authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
}.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.oauth2Login { oauth2Login ->
oauth2Login.loginPage("/login")
oauth2Login.defaultSuccessUrl("/", true)
}.oauth2Client()
return http.build()
}
Now, in the method mapped to the path I can do some more specific authentication logic:
#GetMapping("/api/securedByJWT")
fun getResponse(#AuthenticationPrincipal jwt: Jwt): ResponseEntity<String> {
val email = jwt.claims["email"] as String
val oauthUser = userRepository.findByUsername(email)
if(oauthUser == null){
return ResponseEntity("User not registered.", UNAUTHORIZED)
}
return ResponseEntity("Hello world!", HttpStatus.OK)
}
Related
I updated to Spring Boot 3 in a project that uses the Keycloak Spring Adapter. Unfortunately, it doesn't start because the KeycloakWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter which was first deprecated in Spring Security and then removed. Is there currently another way to implement security with Keycloak? Or to put it in other words: How can I use Spring Boot 3 in combination with the Keycloak adapter?
I searched the Internet, but couldn't find any other version of the adapter.
You can't use Keycloak adapters with spring-boot 3 for the reason you found, plus a few others related to transitive dependencies. As most Keycloak adapters were deprecated in early 2022, it is very likely that no update will be published to fix that.
Directly use spring-security OAuth2 instead. Don't panic, it's an easy task with spring-boot.
spring-addons starters for resource server (app exposes a REST API)
I maintain 4 thin wrappers around "official" boot resource-server starter because, in my opinion, auto-configuration can be pushed one step further to:
make OAuth2 configuration more portable: with a configurable authorities converter, switching from an OIDC provider to another is just a matter of editing properties (Keycloak, Auth0, Cognito, Azure AD, etc.)
ease app deployment on different environments: CORS configuration is controlled from properties file
reduce drastically the amount of Java code (things get even more complicated if you are in multi-tenancy scenario)
reduce chances of misconfiguration (easy to de-synchronise CSRF protection and sessions configuration for instance)
It is very thin (each is composed of three files only) and greatly simplifies resource-servers configuration:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<!-- replace "webmvc" with "weblux" if your app is reactive -->
<!-- replace "jwt" with "introspecting" to use token introspection instead of JWT decoding -->
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<!-- this version is to be used with spring-boot 3.0.1, use 5.4.x for spring-boot 2.6.x or before -->
<version>6.0.13</version>
</dependency>
#Configuration
#EnableMethodSecurity
public static class WebSecurityConfig { }
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/realm1
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,ressource_access.some-client.roles,ressource_access.other-client.roles
com.c4-soft.springaddons.security.cors[0].path=/some-api
com.c4-soft.springaddons.security.permit=all=/actuator/health/readiness,/actuator/health/liveness,/v3/api-docs/**
Nothing more is needed to configure a multi-tenant resource-server with fine tuned CORS policy and authorities mapping. Bootiful, isn't it?
By "multi-tenant", I mean that, as you can guess from this issuers property being an array, you can trust as many OIDC authorization-server instances as you need (multiple Keycloak realms & instances, or even mix with other OIDC providers like Auth0, Cognito, etc.), each with it's own authorities mapping configuration.
Client configuration (UI with oauth2Login())
If your Spring application exposes secured UI elements you want to be accessible with a browser (with OAuth2 login), you'll have to provide a FilterChain with "client" configuration.
If this app exposes both a REST API and a UI to manipulate it (with oauth2Login()), then you'll have to setup two security filter-chains: one with client config and the other with resource-server config.
Add this to pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Here we demo a SecurityFilterChain applying only to a list of routes defined with a securityMatcher.
This assumes that an additional resource-server SecurityFilterChain is defined, with lower order and no securityMatcher so that all routes are intercepted after all filter chains are evaluated in order. This other filter chain could be defined either implicitly (by spring-addons as described above) or explicitly (with Spring Boot official starter as described below).
Remove the securityMatcher section if your app is solely a client:
// Give higher precedence to security filter-chains with "securityMatcher"
#Order(Ordered.HIGHEST_PRECEDENCE)
#Bean
SecurityFilterChain uiFilterChain(
HttpSecurity http,
ServerProperties serverProperties,
GrantedAuthoritiesMapper authoritiesMapper) throws Exception {
http.securityMatcher(new OrRequestMatcher(
// add path to your UI elements instead
new AntPathRequestMatcher("/ui/**"),
// those two are required to access Spring generated login page
// and OAuth2 client callback endpoints
new AntPathRequestMatcher("/login/**"),
new AntPathRequestMatcher("/oauth2/**")));
http.oauth2Login().userInfoEndpoint().userAuthoritiesMapper(authoritiesMapper);
http.authorizeHttpRequests()
.requestMatchers("/ui/index.html").permitAll()
.requestMatchers("/login/**").permitAll()
.requestMatchers("/oauth2/**").permitAll()
.anyRequest().authenticated();
// If SSL enabled, disable http (https only)
if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
http.requiresChannel().anyRequest().requiresSecure();
}
// Many defaults are kept compared to API filter-chain:
// - sessions (and CSRF protection) are enabled
// - unauthorized requests to secured resources will be redirected to login (302 to login is Spring's default response when authorisation is missing or invalid)
return http.build();
}
#Bean
GrantedAuthoritiesMapper userAuthoritiesMapper(Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter) {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (authority instanceof OidcUserAuthority oidcAuth) {
mappedAuthorities.addAll(authoritiesConverter.convert(oidcAuth.getIdToken().getClaims()));
} else if (authority instanceof OAuth2UserAuthority oauth2Auth) {
mappedAuthorities.addAll(authoritiesConverter.convert(oauth2Auth.getAttributes()));
}
});
return mappedAuthorities;
};
}
The code above assumes that a Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> bean is exposed. One is auto-configured by spring-addons starters for resource-server and the "official" starter section below defines one. Take the later as sample if your app is solely a client.
Last, client properties:
spring.security.oauth2.client.provider.keycloak.issuer-uri=https://localhost:8443/realms/master
spring.security.oauth2.client.registration.spring-addons-public.provider=keycloak
spring.security.oauth2.client.registration.spring-addons-public.client-name=spring-addons-public
spring.security.oauth2.client.registration.spring-addons-public.client-id=spring-addons-public
spring.security.oauth2.client.registration.spring-addons-public.scope=openid,offline_access,profile
spring.security.oauth2.client.registration.spring-addons-public.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.spring-addons-public.redirect-uri=http://bravo-ch4mp:8080/login/oauth2/code/spring-addons-public
"Official" Spring Boot resource-server starter
As spring-addons-{webmvc|webflux}-{jwt|introspecting}-resource-server are thin wrappers around spring-boot-starter-oauth2-resource-server, you can of course do the same with just the later.
Here is what it takes to configure a resource-server with a unique Keycloak realm as authorization-server:
#Configuration
#EnableWebSecurity
#EnableMethodSecurity
public class WebSecurityConfig {
public interface Jwt2AuthoritiesConverter extends Converter<Jwt, Collection<? extends GrantedAuthority>> {
}
#SuppressWarnings("unchecked")
#Bean
public Jwt2AuthoritiesConverter authoritiesConverter() {
// This is a converter for roles as embedded in the JWT by a Keycloak server
// Roles are taken from both realm_access.roles & resource_access.{client}.roles
return jwt -> {
final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());
final var resourceAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("resource_access", Map.of());
// We assume here you have "spring-addons-confidential" and "spring-addons-public" clients configured with "client roles" mapper in Keycloak
final var confidentialClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("spring-addons-confidential", Map.of());
final var confidentialClientRoles = (Collection<String>) confidentialClientAccess.getOrDefault("roles", List.of());
final var publicClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("spring-addons-public", Map.of());
final var publicClientRoles = (Collection<String>) publicClientAccess.getOrDefault("roles", List.of());
// Merge the 3 sources of roles and map it to spring-security authorities
return Stream.concat(
realmRoles.stream(),
Stream.concat(confidentialClientRoles.stream(), publicClientRoles.stream()))
.map(SimpleGrantedAuthority::new).toList();
};
}
// spring-boot looks for a Converter<Jwt, ? extends AbstractAuthenticationToken> bean
// that is a converter from Jwt to something extending AbstractAuthenticationToken (and not AbstractAuthenticationToken itself)
// In this conf, we use JwtAuthenticationToken as AbstractAuthenticationToken implementation
public interface Jwt2AuthenticationConverter extends Converter<Jwt, JwtAuthenticationToken> {
}
#Bean
public Jwt2AuthenticationConverter authenticationConverter(Jwt2AuthoritiesConverter authoritiesConverter) {
return jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt));
}
// Give lower precedence to security filter-chains without "securityMatcher" so that the filter-chains with a "securityMatcher" get a chance to be matched
#Order(Ordered.LOWEST_PRECEDENCE)
#Bean
public SecurityFilterChain apiFilterChain(
HttpSecurity http,
ServerProperties serverProperties,
Converter<Jwt, ? extends AbstractAuthenticationToken> authenticationConverter) throws Exception {
// Enable OAuth2 with custom authorities mapping
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(authenticationConverter);
// As the authentication bean is the one expected by spring-boot,
// an alternative would be to use just
// http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
// Enable anonymous
http.anonymous();
// Enable and configure CORS
http.cors().configurationSource(corsConfigurationSource());
// State-less session (state in access-token only)
// with Disable CSRF because of disabled sessions
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.csrf().disable();
// Return 401 (unauthorized) instead of 302 (redirect to login) when authorization is missing or invalid
http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
});
// If SSL enabled, disable http (https only)
if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
http.requiresChannel().anyRequest().requiresSecure();
}
// Route security: authenticated to all routes but actuator and Swagger-UI
http.authorizeRequests()
.antMatchers("/actuator/health/readiness", "/actuator/health/liveness", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated();
return http.build();
}
private CorsConfigurationSource corsConfigurationSource() {
// Very permissive CORS config...
final var configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setExposedHeaders(Arrays.asList("*"));
// Limited to API routes (neither actuator nor Swagger-UI)
final var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/greet/**", configuration);
return source;
}
}
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://localhost:8443/realms/master
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://localhost:8443/realms/master/protocol/openid-connect/certs
As mentioned in preamble, this is quite more verbose than spring-addons starters, it's not ready for multi-tenancy and each time CORS policy changes (new API routes for instance) or when the claims source for authorities change (new OAuth2 client with client-roles mapping or other OIDC provider than Keycloak), you'll have to edit source-code and re-publish your app...
Use the standard Spring Security OAuth2 client instead of a specific Keycloak adapter and SecurityFilterChain instead of WebSecurityAdapter.
Something like this:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true)
class OAuth2SecurityConfig {
#Bean
fun customOauth2FilterChain(http: HttpSecurity): SecurityFilterChain {
log.info("Configure HttpSecurity with OAuth2")
http {
oauth2ResourceServer {
jwt { jwtAuthenticationConverter = CustomBearerJwtAuthenticationConverter() }
}
oauth2Login {}
csrf { disable() }
authorizeRequests {
// Kubernetes
authorize("/readiness", permitAll)
authorize("/liveness", permitAll)
authorize("/actuator/health/**", permitAll)
// ...
// everything else needs at least a valid login, roles are checked at method level
authorize(anyRequest, authenticated)
}
}
return http.build()
}
And then in application.yml:
spring:
security:
oauth2:
client:
provider:
abc:
issuer-uri: https://keycloak.../auth/realms/foo
registration:
abc:
client-secret: ...
provider: abc
client-id: foo
scope: [ openid, profile, email ]
resourceserver:
jwt:
issuer-uri: https://keycloak.../auth/realms/foo
Using Keycloak adapters is not possible because the KeycloakWebSecurityConfigurerAdapter inherited from the WebSecurityConfigurerAdapter class, which was deprecated in Spring Security and subsequently removed in the newer release.
I have published a detailed article on integrating Keycloak with Spring Boot 3.0 on Medium, which provides a step-by-step guide on how to integrate Keycloak with Spring Boot 3.0.
This guide is particularly helpful for those who are new to integrating Keycloak with Spring Boot 3.0 or migrating to Spring Boot 3.0 from an older version.
You can check out the article (https://medium.com/geekculture/using-keycloak-with-spring-boot-3-0-376fa9f60e0b) for a comprehensive explanation of the integration process.
Hope this helps! If you have any questions, further clarifications or suggestions, Please feel free to leave a comment.
We are implementing a Spring Cloud Gateway application (with Webflux) that is mediating the OAuth2 authentication with Keycloak.
SCG checks if the Spring Session is active: if not, redirects to Keycloak login page and handles the response from the IDP. This process is executed out-of-the-box by the framework itself.
Our needs is to intercept the IDP Keycloak response in order to retrieve a field from the response payload.
Do you have any advices that will help us to accomplish this behavior?
Thanks!
You can implement ServerAuthenticationSuccessHandler:
#Component
public class AuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
private ServerRedirectStrategy redirectStrategy;
public AuthenticationSuccessHandler(AuthenticationService authenticationService) {
redirectStrategy = new DefaultServerRedirectStrategy();
}
#Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
if(authentication instanceof OAuth2AuthenticationToken) {
//Your logic here to retrieve oauth2 user info
}
ServerWebExchange exchange = webFilterExchange.getExchange();
URI location = URI.create(httpRequest.getURI().getHost());
return redirectStrategy.sendRedirect(exchange, location);
}
}
And update your security configuration to include success handler:
#Configuration
public class SecurityConfiguration {
private AuthenticationSuccessHandler authSuccessHandler;
public SecurityConfiguration(AuthenticationSuccessHandler authSuccessHandler) {
this.authSuccessHandler = authSuccessHandler;
}
#Bean
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchange -> exchange
//other security configs
.anyExchange().authenticated()
.and()
.oauth2Login(oauth2 -> oauth2
.authenticationSuccessHandler(authSuccessHandler)
);
return http.build();
}
}
I read this post about using multiple JWT Decoders in Spring Security flow which seems easy, except that I'm using Spring Webflux and not Spring WebMVC , which has the convenient WebSecurityConfigurerAdapter that you can extend to add multiple AuthenticationProvider instances. With Webflux you no longer extend some class to configure security.
So what's the problem while trying to replicate this with Webflux? This . As you can read there Webflux doesn't use AuthenticationProvider , you have to declare a ReactiveAuthenticationManager instead. The problem is I don't know how to make Spring use multiple authentication managers, each of them using its own ReactiveJwtDecoder.
My first authentication manager would be the one spring creates automatically using this property:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${scacap.auth0.issuer}
And my second Authentication Manager would be a custom one I'm declaring in my Security #Configuration:
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
#EnableConfigurationProperties(JwkProperties::class)
internal class SecurityConfiguration {
#Bean
fun securityFilter(
http: ServerHttpSecurity,
scalableAuthenticationManager: JwtReactiveAuthenticationManager
): SecurityWebFilterChain {
http.csrf().disable()
.authorizeExchange()
.anyExchange().authenticated()
.and()
.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(Auth0AuthenticationConverter())
return http.build()
}
#Bean
fun customAuthenticationManager(jwkProperties: JwkProperties): JwtReactiveAuthenticationManager {
val decoder = NimbusReactiveJwtDecoder.withJwkSource { Flux.fromIterable(jwkProperties.jwkSet.keys) }.build()
return JwtReactiveAuthenticationManager(decoder).also {
it.setJwtAuthenticationConverter(ScalableAuthenticationConverter())
}
}
}
I am debugging and it seems only one authentication manager is being picked so only auth0 tokens can be validated, but I also want to validate tokens with my own JWKS
Okay, so this is what I ended up doing:
Instead of trying someway to pass several AuthenticationManagers to Spring Security flow, I created one wrapper which I call DualAuthenticationManager. This way for Spring there is only one manager and I do the orchestration inside my wrapper like firstManager.authenticate(auth).onErrorResume { secondManager.authenticate(auth) }.
It ended up being shorter than I thought it would be. It's all in a #Bean function in my security #Configuration . And each manager has it's own converter function so I can create my UserToken model with two different JWTs :)
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
#EnableConfigurationProperties(*[JwtProperties::class, Auth0Properties::class])
internal class SecurityConfiguration(
private val jwtProperties: JwtProperties,
private val auth0Properties: Auth0Properties
) {
#Bean
fun securityFilter(
http: ServerHttpSecurity,
dualAuthManager: ReactiveAuthenticationManager
): SecurityWebFilterChain {
http.csrf().disable()
.authorizeExchange()
.pathMatchers("/actuator/**").permitAll()
.pathMatchers("/user/**").hasAuthority(Authorities.USER)
.anyExchange().authenticated()
.and()
.oauth2ResourceServer().jwt()
.authenticationManager(dualAuthManager)
return http.build()
}
#Bean
fun dualAuthManager(): ReactiveAuthenticationManager {
val firstManager = fromOidcIssuerLocation(auth0Properties.issuer).let { decoder ->
JwtReactiveAuthenticationManager(decoder).also {
it.setJwtAuthenticationConverter(FirstAuthenticationConverter())
}
}
val secondManager = withJwkSource { fromIterable(jwtProperties.jwkSet.keys) }.build().let { decoder ->
JwtReactiveAuthenticationManager(decoder).also {
it.setJwtAuthenticationConverter(SecondAuthenticationConverter())
}
}
return ReactiveAuthenticationManager { auth ->
firstManager.authenticate(auth).onErrorResume { secondManager.authenticate(auth) }
}
}
}
This is how my converters look:
class FirstAuthenticationConverter : Converter<Jwt, Mono<AbstractAuthenticationToken>> {
override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> {
val authorities = jwt.getClaimAsStringList(AUTHORITIES) ?: emptyList()
val userId = jwt.getClaimAsString(PERSON_ID)
val email = jwt.getClaimAsString(EMAIL)
return Mono.just(
UsernamePasswordAuthenticationToken(
UserToken(jwt.tokenValue, UserTokenType.FIRST, userId, email),
null,
authorities.map { SimpleGrantedAuthority(it) }
)
)
}
}
Then in my controller I get the object I built in the converter by doing:
#AuthenticationPrincipal userToken: UserToken
TL;DR: How to assign users custom roles/authorities on Resource server side (that means without JWT) based on their access_token?
The whole story: I have a working Auth server and a client (which is SPA), which can obtain access_token from the Auth server. With that access_token the client can request data on my Resource server (which is separated from Auth server). The Resource server can get username from Auth server using the access_token.
I can access the username in code by injection Authentication object into method like this:
#RequestMapping("/ping")
fun pingPong(auth: Authentication): String = "pong, " + auth.name
My question is how to add my custom roles or authorities (auth.authorities - there is only USER_ROLE) to this object which would be managed on the Resource server, not Auth server, based on the username.
I have tried several ways to do it but none has helped. The most promising was this:
#Configuration
#EnableWebSecurity
#EnableResourceServer
class ResourceServerConfigurer(val userDetailsService: MyUserDetailsService) : ResourceServerConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.userDetailsService(userDetailsService) // userDetailsService is autowired
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers("/", "/index.html").permitAll()
.anyRequest().authenticated()
}
}
And my custom UserDetailsService:
#Service
class UserDetailsService : org.springframework.security.core.userdetails.UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
return org.springframework.security.core.userdetails.User(username, "password", getAuthorities(username))
}
private fun getAuthorities(user: String): Set<GrantedAuthority> {
val authorities = HashSet<GrantedAuthority>()
authorities.addAll(listOf(
SimpleGrantedAuthority("ROLE_ONE"), //let's grant some roles to everyone
SimpleGrantedAuthority("ROLE_TWO")))
return authorities
}
}
Everything worked (I mean I was successfully authenticated) except that I still had only ROLE_USER. Next what I tried was providing a custom implementation of AbstractUserDetailsAuthenticationProvider:
#Bean
fun authenticationProvider(): AbstractUserDetailsAuthenticationProvider {
return object : AbstractUserDetailsAuthenticationProvider() {
override fun retrieveUser(username: String, authentication: UsernamePasswordAuthenticationToken): UserDetails {
return User(username, "password", getAuthorities(username))
}
private fun getAuthorities(user: String): Set<GrantedAuthority> {
val authorities = HashSet<GrantedAuthority>()
authorities.addAll(listOf(
SimpleGrantedAuthority("ROLE_ONE"),
SimpleGrantedAuthority("ROLE_TWO")))
return authorities
}
override fun additionalAuthenticationChecks(userDetails: UserDetails, authentication: UsernamePasswordAuthenticationToken?) {
}
}
}
with same result, only the ROLE_USER was present.
I would really appreciate any ideas from you guys how add some roles to the Authentication object after the access_token was validated and username obtained from Auth server.
Solution by OP.
First of all I needed to provide custom PrincipalExtractor and AuthoritiesExtractor implementations. But to make Spring use them it is necessary in configuration NOT to use security.oauth2.resource.token-info-uri but security.oauth2.resource.user-info-uri instead (I really didn't expect this to be one of the roots of my problem).
Finally the security config must be done in ResourceServerConfigurerAdapter, not in WebSecurityConfigurerAdapter.
The final code looks like this:
#SpringBootApplication
#RestController
class MyApplication {
#RequestMapping("/ping")
fun pingPong(user: Authentication): String {
return "pong, " + user.name + " - " + user.authorities.joinToString()
}
}
#Configuration
#EnableWebSecurity
#EnableResourceServer
class ResourceServerConfigurer : ResourceServerConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers("/", "/index.html").permitAll()
.anyRequest().authenticated()
}
#Bean
fun principalExtractor() = PrincipalExtractor {
return#PrincipalExtractor it["name"]
}
#Bean
fun authoritiesExtractor() = AuthoritiesExtractor {
return#AuthoritiesExtractor AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ONE,ROLE_TWO")
}
}
fun main(args: Array<String>) {
SpringApplication.run(MyApplication::class.java, *args)
}
How do you setup a separate Spring OAuth2 ResourceServer only, that uses and 3rd party AuthorizationServer
All examples I see always implement the ResourceServer and AuthorizationServer in same application.
I don't want to implement the AuthorizationServer as someone else is going to provide this.
Have tried with no luck
#Configuration
#EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter
And application.yml includes
security:
oauth2:
resource:
userInfoUri: https://...../userinfo
Adding to my question some further details::
In my understanding - with OAuth there are 4 players:
resource owner: a person
resource server: server exposing a protected API (protected by the authentication server)
authentication server: the server that handles issuing access tokens to clients
client: an application (say website) accessing the resource server API's after resource owner have given consent
I have tried various tutorials, but all seem to implement their own Authorisation server
http://www.swisspush.org/security/2016/10/17/oauth2-in-depth-introduction-for-enterprises
https://gigsterous.github.io/engineering/2017/03/01/spring-boot-4.html
or are examples of implementing the client player
http://www.baeldung.com/spring-security-openid-connect
https://spring.io/guides/tutorials/spring-boot-oauth2/
My Question is:
How do I implement just the Resource Server which secures my REST API, via a 3rd party authentication server, nothing more.
I have work this out - all you need is:
#SpringBootApplication
#EnableResourceServer
public class ResourceServer {
public static void main(String[] args) {
SpringApplication.run(ResourceServer.class, args);
}
}
With the application.yml as posted in the original question of:
security:
oauth2:
resource:
userInfoUri: https://........userinfo
I've created two sample separate applications, one of them acting as oauth client, and another one acting as a resource server, and both of them are using an external authentication server (which is facebook in this example).
The scenario in the example is as follows, the user opens app1 (oauth client) and gets redirected to first page, and once he clicks login, he'll be redirected to facebook login, and after a successful login, he will get back to the first page. If he clicked on the first button, a call to an api within the same application will be made, and will appear beside message 1 label, and if he clicked on the second button, a call to an api within app2 (resource server) will be made, and the message will be displayed beside message 2 label.
If you checked the logs, you will find the api call going from app1 to app2 containing the access token in the request parameters.
Logs for app1 calling app2
Please find the source code on the git repository here
This is the configuration for app1 (oauth client)
app1 web security config
#Configuration
#EnableOAuth2Sso
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests().antMatchers("/", "/login**", "/webjars/**", "/error**").permitAll()
.anyRequest().authenticated().and().logout().logoutSuccessUrl("/").permitAll().and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("GET");
config.addAllowedMethod("POST");
config.addAllowedMethod("PUT");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
app1 application properties
security:
oauth2:
client:
clientId: <your client id>
clientSecret: <your client secret>
accessTokenUri: https://graph.facebook.com/oauth/access_token
userAuthorizationUri: https://www.facebook.com/dialog/oauth?redirect_url=https://localhost:8443/
tokenName: access_token
authenticationScheme: query
clientAuthenticationScheme: form
registered-redirect-uri: https://localhost:8443/
pre-established-redirect-uri: https://localhost:8443/
resource:
userInfoUri: https://graph.facebook.com/me
logging:
level:
org.springframework.security: DEBUG
This is the configuration for app2 (resource server)
app2 resource server config
#Configuration
#EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
String[] ignoredPaths = new String[] { "/error", "/login", "/doLogut", "/home", "/pageNotFound", "/css/**",
"/js/**", "/fonts/**", "/img/**" };
#Value("${security.oauth2.resource.user-info-uri}")
private String userInfoUri;
#Value("${security.oauth2.client.client-id}")
private String clientId;
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(ignoredPaths).permitAll().anyRequest().authenticated();
}
#Primary
#Bean
public UserInfoTokenServices tokenService() {
final UserInfoTokenServices tokenService = new UserInfoTokenServices(userInfoUri, clientId);
return tokenService;
}
}
app2 application properties
security:
oauth2:
resource:
userInfoUri: https://graph.facebook.com/me
client:
client-id: <your client id>
logging:
level:
org.springframework.security: DEBUG
This is where app1 controller calls an api on app2 (hi2 api)
#RestController
#CrossOrigin(origins = "*", allowedHeaders = "*")
public class UserController {
#Autowired
OAuth2RestTemplate restTemplate;
#RequestMapping("/user")
public Principal user(Principal principal) {
return principal;
}
#RequestMapping("/hi")
public String hi(Principal principal) {
return "Hi, " + principal.getName();
}
#RequestMapping("/hi2")
public String hi2(Principal principal) {
final String greeting = restTemplate.getForObject("http://127.0.0.1:8082/api/hello", String.class);
System.out.println(greeting);
return greeting;
}
}