Description
Context
We have a use-case for filtering the scopes that go into an access_token
, based on the Resource Owner's "roles" - e.g., if you have the role hr-user
you can have payslip.view
in the scopes of access tokens issued for you, but not the payslip.edit
scope - even if the Client is allowed to request it.
There is no way to easily change the OAuth2Authorization#authorizedScopes()
before it is created/saved.
The token itself, when it is a JWT, can be customized with an OAuth2TokenCustomizer<JwtEncodingContext>
that acts on the scope claim, but the token response has the full list of authorized scopes.
Expected Behavior
When the OAuth2Authorization
object is created and saved in the OAuth2Service
, either through OAuth2AuthorizationCodeRequestAuthenticationProvider
or OAuth2AuthorizationConsentAuthenticationProvider
, I want to be able to alter the scopes.
Current workaround
Currently, we work around this by creating a custom AuthenticationProvider
that wraps around both OAuth2AuthorizationCodeRequestAuthenticationProvider
and OAuth2AuthorizationConsentAuthenticationProvider
:
public class AppSsoAuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
// Either an OAuth2AuthorizationCodeRequestAuthenticationProvider or an OAuth2AuthorizationConsentAuthenticationProvider
private final AuthenticationProvider delegate;
private final OAuth2AuthorizationService authorizationService;
public AppSsoAuthorizationCodeRequestAuthenticationProvider(AuthenticationProvider delegate,
OAuth2AuthorizationService authorizationService) {
this.delegate = delegate;
this.authorizationService = authorizationService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// This may throw an OAuth2AuthorizationCodeRequestAuthenticationException; in
// this case we rethrow.
var authResult = delegate.authenticate(authentication);
// This does not happen with our supported cases, but in the case of the device
// grant type, OAuth2AuthorizationConsentAuthenticationProvider will return null.
if (authResult == null) {
return null;
}
// When an authorization request comes in, and is valid, BUT the user is not
// authenticated, an OAuth2AuthorizationCodeRequestAuthenticationToken is
// returned, that is marked as !authenticated. This is a special case signaling
// that the end-user must log-in first. In this case, we just follow through.
//
// The rest of the filter chain will save the incoming request in the session and
// redirect the user to the login page. Once they are logged in, the saved request
// will be replayed.
if (!authResult.isAuthenticated()) {
return authResult;
}
// Sometimes the authentication flow returns a
// OAuth2AuthorizationConsentAuthenticationToken when consent is required.
// In that case, we just follow through. Otherwise we grab the result.
if (!(authResult instanceof OAuth2AuthorizationCodeRequestAuthenticationToken authCodeAuthResult)) {
return authResult;
}
// We load the authorization from the repo, change the scopes, and re-save it.
var authCode = authCodeAuthResult.getAuthorizationCode();
var authorization = authorizationService.findByToken(authCode.getTokenValue(),
new OAuth2TokenType(OAuth2ParameterNames.CODE));
// Filter the scopes based on the principal
var filteredScopes = filterScopes(authorization.getAuthorizedScopes(), authResult.getPrincipal());
var newAuthorization = OAuth2Authorization.from(authorization).authorizedScopes(filteredScopes).build();
authorizationService.save(newAuthorization);
//@formatter:off
return new OAuth2AuthorizationCodeRequestAuthenticationToken(
authCodeAuthResult.getAuthorizationUri(),
authCodeAuthResult.getClientId(),
authResultPrincipalAuthentication,
authCodeAuthResult.getAuthorizationCode(),
authCodeAuthResult.getRedirectUri(),
authCodeAuthResult.getState(),
filteredScopes
);
//@formatter:on
}
@Override
public boolean supports(Class<?> authentication) {
return delegate.supports(authentication);
}
private Set<String> filterScopes(Set<String> authorizedScopes, Object principal) {
// business logic
}
}