Adding permissions to Authorization Code flow

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:

  1. Added API permissions
  2. Assigned permissions to users
  3. 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
  1. 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.

1 Like

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.

1 Like

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.

This topic was automatically closed 15 days after the last reply. New replies are no longer allowed.