Expected Behavior It would be nice if the following would be possible:

@RestController
public class Controller {
  @AuthenticationPrincipal(expression="claims['azp']") private String azp;

  @GetMapping("/azp")
  public ResponseEntity<String> azpEndpoint() {
    return ReponseEntity.ok(azp);
  }
}

Current Behavior

@RestController
public class Controller {

  @GetMapping("/azp")
  public ResponseEntity<String> azpEndpoint(@AuthenticationPrincipal(expression="claims['azp']") private String azp) {
    return ReponseEntity.ok(azp);
  }
}

Currently, the AuthenticationPrincipal and the expressions are only available if injected as method arguments into the controller enpoints.

Context We make use of code generation from swagger and OpenAPI specifications, which generate only the actual parameters of the endpoint as method arguments. We then implement these interfaces in our controllers.

This makes it impossible for us to use the method injection like this. Our current workaround is an additional service that extracts the required claims from the token as follows:

@Service
public class AuthenticationService {

    private final SecurityContextService securityContextService;

    public AuthenticationService(SecurityContextService securityContextService) {
        this.securityContextService = securityContextService;
    }

    public String getAzp() {
        SecurityContext ctx = securityContextService.getSecurityContext();

        Authentication authentication = ctx.getAuthentication();

        if (authentication instanceof BearerTokenAuthentication) {
            BearerTokenAuthentication bearerTokenAuthentication = (BearerTokenAuthentication) authentication;
            Object azp = bearerTokenAuthentication.getTokenAttributes().get("azp");
            /*
             * AZP should always be a string, but maybe the token is somehow malformed (by an attacker)
             */
            if (azp instanceof String) {
                return (String) azp;
            }
        }

        return null;
    }
}

Injecting the AuthenticationPrincipal (or expressions based on it) as a field would be quite convenient. This is already possible with some things, like the HttpServletRequest, so from an outside perspective without knowing the internals of spring-security, it seems like this should be possible.

Comment From: jzheaux

Thanks for the suggestion, @sonOfRa.

I think a concern to address here is thread-safety since multiple threads would share the same instance of your controller. How would Spring ensure that each thread gets its own azp?

The way it works with HttpServletRequest is that Spring injects a proxy to access a thread-local variable. Because HttpServletRequest is an interface, this works. String is final, though. Because of that, I think it would be quite tricky to get an arbitrary SpEL expression to work how you want it.

That said, I believe you can already do the following:

@RestController
public class Controller {
  @Autowired
  private HttpServletRequest request;

  @GetMapping("/azp")
  public ResponseEntity<String> azpEndpoint() {
    String azp = ((BearerTokenAuthentication) request.getUserPrincipal()).getAttribute("azp");
    return ReponseEntity.ok(azp);
  }
}

Or:

@Bean
@RequestScope 
BearerTokenAuthentication bearerToken(HttpServletRequest request) {
    return (BearerTokenAuthentication) request.getUserPrincipal();
}

// ...

@RestController
public class Controller {
  @Autowired
  private BearerTokenAuthentication bearerToken;

  @GetMapping("/azp")
  public ResponseEntity<String> azpEndpoint() {
    String azp = bearerToken.getAttribute("azp");
    return ReponseEntity.ok(azp);
  }
}

Or you can access the SecurityContext directly from the SecurityContextHolder, similarly to how you are solving the problem already.

I'm going to close this as I don't think it will work to use @AuthenticationPrincipal with field injection, specifically due to the fact that SpELs can resolve to final classes like String.

Feel free to continue the conversation if you feel like there's more to discuss, though.