Context
In my project, I am supporting multiple ways of logging in: - "internal" user store (in-memory or in-database), - OAuth2 login - SAML login
These three ways of authenticating yield different Authentication objects: UsernamePasswordAuthenticationToken, OAuth2[Login]AuthenticationToken and Saml2Authentication.
I have custom rules for extracting information from the authentications (e.g. scopes, SAML attributes, etc).
I use the authentication results through AbstractAuthenticationEvent (e.g. log some specific information when authentication fails), and through the Security context (e.g. to display values mapped from the authentication to the user).
To minimize code duplication and switch-case when using those authentications, I rely on a custom interface, e.g. CustomAuthentication.
I register custom AuthenticationProviders which extend from (or delegate to) spring-security basic authentication providers. Those providers obtain the base Authentication object (through calling super.authenticate(), for example), perform custom logic, and return a subclass of the base Authentication.
For example:
class CustomSamlAuthentication extends Saml2Authentication implements CustomAuthentication {
[...]
}
public class CustomSamlAuthenticationProvider implements AuthenticationProvider {
private final OpenSamlAuthenticationProvider delegate;
public CustomSamlAuthenticationProvider() {
this.delegate = new OpenSamlAuthenticationProvider();
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Authentication authenticationResult = this.delegate.authenticate(authentication);
return new CustomSamlAuthentication((Saml2AuthenticationToken) authentication, (Saml2Authentication) authenticationResult);
}
@Override
public boolean supports(Class<?> authentication) {
return delegate.supports(authentication);
}
}
Alternatives would be to transform the authentication when we consume it, but that could be in multiple places. Also, some of the authentication information may be lost or more difficult to obtain by that time (e.g. which relying party registration was used to obtain this authentication?)
Expected Behavior
I want to get this result simply by providing two things:
- A custom Authentication implementation
- A custom AuthenticationProvider
This works very well with UserDetails and SAML, but it is not possible to accomplish this with OAuth2.
Current Behavior
To accomplish this with OAuth2, I need:
- A custom OAuth2LoginAuthenticationToken + custom OAuth2LoginAuthenticationProvider and/or OidcAuthorizationCodeAuthenticationProvider
- This is required for the authentication events to have the information I need
- A custom OAuth2LoginToken
- A custom OAuth2LoginAuthenticationFilter to override the behavior of the base class
See this sample project for a full implementation of the three use-cases.
Ideas
Two rough ideas:
- Updating the OAuth2LoginAuthenticationFilter to support a custom converter, from OAuth2LoginAuthenticationToken to OAuth2AuthenticationToken
- I could configure the login filter with "just" this converter. It is a bit involved but I don't have to track changes in the filter implementation over time
- Update the OAuth2LoginAuthenticationProvider and OidcAuthorizationCodeAutheProvider to take a OAuth2LoginAuthenticationToken and return a straight OAuth2AuthenticationToken as the authentication result ... but this probably has very wide implications.
I have not looked into reactive support for this.
Comment From: sjohnr
Hi @Kehrlann, thanks for the enhancement request. Since the OAuth2LoginAuthenticationFilter converts from an OAuth2LoginAuthenticationToken to an OAuth2AuthenticationToken (making it slightly different than the Saml2WebSsoAuthenticationFilter in terms of symmetry), I agree it looks possible to introduce a Converter<OAuth2LoginAuthenticationToken, OAuth2AuthenticationToken> to perform that work.
Comment From: robertPas
Is it by intent, that the converter can't be set via OAuth2LoginConfigurer, like e.g. the authorizationRequestRepository? Or is it just missing?
Comment From: sjohnr
@robertPas, yeah it's intentional. We typically wait to add something like this to the configurer because it was added for what seems to be an edge case. If numerous folks find they need to use that customization, we would probably pursue adding that to the configurer. You're welcome to open an enhancement suggestion for that with details on your use case. Others may agree with you that it's needed.
Comment From: EvgeniGordeev
@sjohnr is there a recommended way to call setAuthenticationResultConverter()? To avoid something like:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
DefaultSecurityFilterChain securityFilterChain = http.build();
securityFilterChain.getFilters().stream()
.filter(filter -> filter instanceof OAuth2LoginAuthenticationFilter)
.forEach(filter -> ((OAuth2LoginAuthenticationFilter) filter).setAuthenticationResultConverter(authenticationResultConverter()));
return securityFilterChain;
}
Comment From: Kehrlann
@EvgeniGordeev your example does feel a bit weird, and is outside of the HttpSecurity classic lifecycle (init/configure/...)
I would personally go for an ObjectPostProcessor.
Sample - this is in a custom AbstractHttpConfigurer so I have an init(HttpSercurity http) method, but it works in a bean too. Note that this works in Spring Security 6 (Boot 3+):
private static ObjectPostProcessor<OAuth2LoginAuthenticationFilter> postProcessor = new ObjectPostProcessor<>() {
@Override
public <O extends OAuth2LoginAuthenticationFilter> O postProcess(O object) {
object.setAuthenticationResultConverter(new MyConverter());
return object;
}
};
@Override
public void init(HttpSecurity http) throws Exception {
// @formatter:off
http
.oauth2Login(oauth2Config -> {
oauth2Config.withObjectPostProcessor(postProcessor);
// ... other oauth2login specific config ...
})
// ... custom config ...
;
// @formatter:on
}
If you are using Spring Security < 5.8, you may want to use the older DSL, like so::
private static ObjectPostProcessor<OAuth2LoginAuthenticationFilter> postProcessor = new ObjectPostProcessor<>() {
@Override
public <O extends OAuth2LoginAuthenticationFilter> O postProcess(O object) {
object.setAuthenticationResultConverter(new MyConverter());
return object;
}
};
@Override
public void init(HttpSecurity http) throws Exception {
// @formatter:off
http
.oauth2Login()
// ... custom config ...
.withObjectPostProcessor(postProcessor)
.and()
// ... custom config ...
;
// @formatter:on
}
Comment From: mkraszew
@Kehrlann HttpSecurity does not have withObjectPostProcessor method at all
Comment From: Kehrlann
@mkraszew Yes that is absolutely correct - the example above used the old configuration DSL (Spring Security 5.x).
For Security 6.x now want this to be in oauth2Login(...). I updated the example.
Comment From: pawelsalawa
For those looking for a way that works regardless of how unusual or complex your XML or Java config is, you can define your own converter as a bean, then inject the filter into it and call the setter for converter from @PostInit:
@Component
public class AccOAuth2ResultConverter implements Converter<OAuth2LoginAuthenticationToken, OAuth2AuthenticationToken> {
@Autowired
private OAuth2LoginAuthenticationFilter oAuth2LoginAuthenticationFilter;
public OAuth2AuthenticationToken convert(OAuth2LoginAuthenticationToken authenticationResult) {
// converter logic
}
@PostConstruct
public void init() {
oAuth2LoginAuthenticationFilter.setAuthenticationResultConverter(this);
}
}
This should really be just an attribute to <oauth2-login> tag. No reason for hiding it out of configuration. I wasted a lot of time trying to figure out how to configure it in a civilized way.