I’m looking for a way to delegate permissions granted to a user into the access token.
This works just fine with client credentials, the permissions get parsed out in spring security and added to a SCOPE_perm:action. I’m receiving empty permissions when using authorization code flow. Am I missing something here? Seems weird since you can assign permissions to specific users. The goal is to do RBAC on a per-user basis - since they aren’t in the JWT, Spring Boot is not parsing them out creating a relevant authority.
Will this have to be implemented with a custom rule/hook?
I’m using Spring WebFlux for more context.
Hi @optimisticninja,
Welcome to the Auth0 Community!
I understand that you have encountered issues using the Authorization Code Flow to get permissions.
After testing the Authorization Code Flow, I was able to get a specific user’s permissions in the access token and can confirm that everything works as expected.
Given that, could you please make sure that you have:
- Added API permissions
- Assigned permissions to users
- Ensured that the
audience and scope parameters correspond to the API and permission(s) for the user in your /authorize request
https://YOUR_DOMAIN/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https://YOUR_APP/callback&
scope=SCOPE&
audience=API_AUDIENCE&
state=STATE
- Once completed and you have requested the
/oauth/token endpoint, the response will return the permissions in the access token.
Please let me know if you have any questions. I’d be happy to clarify.
Thank you.
Thanks! I was blindly assuming postman was behaving with the audience parameter since it was set but it wasn’t being passed, had to explicitly add the query param to the /authorize URL. Opaque tokens are gone from postman and now I have the solution for Spring. Much appreciated.
Hi @optimisticninja,
You’re most welcome! I’m happy to hear that it’s working now.
And yes, great observation, that’s correct; Opaque tokens are issued whenever the audience parameter is not provided as described in our Get Access Tokens docs.
Please don’t hesitate to let me know if there’s anything else I can do to help.
Thank you.
Actually, I have some recommendations for the Getting Started with Webflux API guide. I’ve done a full implementation from a purely backend standpoint.
First, there is an error in the properties added to application.properties/yml and the @Value in SecurityConfig.java
...
spring
security:
oauth2:
resourceserver:
jwt: # <----- Right here, it is jwk in the guide.
jwk-set-uri: https://YOUR_DOMAIN/.well-known/jwks.json
issuer-uri: https://YOUR_DOMAIN/
The dependencies added to build.gradle can be reduced to:
...
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
...
A purely backend approach that is stateless with logging authorization failures. The proxyTargetClass is important. In order to actually test security it is required. Any test using @WebFluxTest needs to exclude ReactiveSecurityAutoConfiguration.class and @Import(SecurityConfig.class). See here
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity(proxyTargetClass = true)
@Slf4j
@RequiredArgsConstructor
public class SecurityConfig {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuerUri;
@Bean
public SecurityWebFilterChain configure(ServerHttpSecurity http) {
return http.exceptionHandling()
.accessDeniedHandler(serverAccessDeniedHandler())
.and()
.authorizeExchange()
.pathMatchers(
HttpMethod.GET,
"/swagger-ui.html",
"/webjars/swagger-ui/*",
"/v3/api-docs/swagger-config",
"/v3/api-docs",
"/actuator/health",
"/posts",
"/posts/*")
.permitAll()
.anyExchange()
.authenticated()
.and()
.httpBasic()
.disable()
.formLogin()
.disable()
.csrf()
.disable()
.logout()
.disable()
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.oauth2ResourceServer()
.jwt()
.and()
.and()
.build();
}
private ServerAccessDeniedHandler serverAccessDeniedHandler() {
return (swe, e) -> {
var name = swe.getPrincipal().map(Principal::getName);
return swe.getPrincipal()
.cast(JwtAuthenticationToken.class)
.map(JwtAuthenticationToken::getAuthorities)
.map(
grantedAuthorities ->
grantedAuthorities.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(",")))
.zipWith(name)
.flatMap(
csvAndName -> {
StringBuilder sb = new StringBuilder();
sb.append("Authorization error [403]: Access Denied for ");
sb.append(csvAndName.getT2());
sb.append(": ");
if (csvAndName.getT1().length() > 0) {
sb.append("found " + csvAndName.getT1());
} else {
sb.append("no scopes found");
}
log.warn(sb.toString());
return Mono.fromRunnable(
() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN));
});
};
}
@Bean
public WebSessionManager webSessionManager() {
// Emulate SessionCreationPolicy.STATELESS
return exchange -> Mono.empty();
}
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders.fromOidcIssuerLocation(issuerUri);
}
}
Hi @optimisticninja,
Thank you for sharing your discoveries with the rest of the Community!
I’m sure many others will benefit from your recommendations.
Have a great rest of your day.
Thank you.