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)
}
Related
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)
}
I have written a Spring Security Class. But somehow it is not working as expected. I am trying to hit the Rest APIs via a Postman by selecting the Basic Auth method. And here is the scenario.
Correct username and password --> Works (I get 200 responses)
Incorrect username/password --> Works (I get 401 responses)
Select No Auth in Postman --> Doesn't Work (I should get 401, but it allows the request to pass through)
Now for #1 and #2 it works fine. Its the #3 that is the troublesome part. My Security code is written like:
#Configuration
#EnableWebSecurity
class SecurityConfig {
#Value("\${spring.security.user.name}")
private val userName : String? = null
#Value("\${spring.user.password}")
private val password : String? = null
#Autowired
lateinit var appAuthenticationEntryPoint: AppAuthenticationEntryPoint
#Bean
fun passwordEncoder(): PasswordEncoder {
return MyPasswordDelegation().createDelegatingPasswordEncoder()
}
#Bean
#Throws(Exception::class)
fun userDetailsService(): InMemoryUserDetailsManager? {
val userDetails : UserDetails = User.withUsername(userName).password(passwordEncoder().encode(password)).roles("USER").build()
return InMemoryUserDetailsManager(userDetails)
}
#Throws(Exception::class)
#Bean
fun filterChain(httpSecurity : HttpSecurity): SecurityFilterChain {
httpSecurity.csrf().disable()
// Allow only HTTPS Requests
httpSecurity.requiresChannel {
channel -> channel.anyRequest().requiresSecure()
}.authorizeRequests {
authorize -> authorize.antMatchers("/app-download/**").fullyAuthenticated()
.and()
.httpBasic()
.and()
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint)
}
return httpSecurity.build()
}
}
Can you please tell me what am I doing wrong here?
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'm trying to rewrite a previous example with JWT's built with a custom JWT Filter into a simplified version based on Springs new authorization server and this example:
https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/jwt/login
The example sets up an InMemoryUserDetailsManager with a single user → user,password and an "app" authority so I assume it is designed to handle roles/authorities?
Everything works fine (as explained in the examples README) if I use the provided SecurityFilterChain
But if I change this:
...
http.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
Into this
...
http.authorizeHttpRequests((authorize) -> authorize
.antMatchers("/").hasRole("app")
//.antMatchers("/").hasAuthority("app")
.anyRequest().authenticated()
)
I get a 403 Status back
The authority gets added to the JWT as expected like this:
..
"scope": "app"
}
Apart from the antMatchers given above, my code is exactly as clone from the Spring Security example
What am I missing here?
OK, read the specs ;-)
Accoring to https://docs.spring.io/spring-security/reference/reactive/oauth2/resource-server/jwt.html
Authorities gets prefixed with a SCOPE_
So this partly fixes the problem
.antMatchers("/").hasAuthority("SCOPE_app")
I still havent figured out how to use hasRoles?
To use hasRole, you need to have authorities which start with ROLE_. What you could do is register a converter which would read roles from JWT and add them as GrantedAuthority.
public class RolesClaimConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter wrappedConverter;
public RolesClaimConverter(JwtGrantedAuthoritiesConverter conv) {
wrappedConverter = conv;
}
#Override
public AbstractAuthenticationToken convert(#NonNull Jwt jwt) {
// get authorities from wrapped converter
var grantedAuthorities = new ArrayList<>(wrappedConverter.convert(jwt));
// get role authorities
var roles = (List<String>) jwt.getClaims().get("roles");
if (roles != null) {
for (String role : roles) {
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
}
return new JwtAuthenticationToken(jwt, grantedAuthorities);
}
}
Then register your converter in your security configuration
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer(resourceServer -> resourceServer
.jwt()
.jwtAuthenticationConverter(
new RolesClaimConverter(
new JwtGrantedAuthoritiesConverter()
)
)
)
// other configuration
;
return http.build();
}
And that's it. All you need to do now is to pass a list of roles as a claim when creating JWT and you can use .antMatchers("/").hasRole("app") and #PreAuthorize("hasRole('app')") in your code.
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