Expected Behavior
Easy way to set the SecurityContextRepository on the BearerTokenAuthenticationFilter when using OAuth2ResourceServerConfigurer
Current Behavior
You can't use OAuth2ResourceServerConfigurer and so have to manually set things up.
Context
With #10949 the BearerTokenAuthenticationFilter class can have a SecurityContextRepository set, however when the BearerTokenAuthenticationFilter is created by OAuth2ResourceServerConfigurer there's no easy way to set the SecurityContextRepository
It used to be the case before #10949 that the request authentication would get stored in the session, however with newer versions this isn't happening and it appears that to re-enable this we have to set the SecurityContextRepository.
Comment From: buckett
Ok, I've dug further into this and for my particular case I think I'm going to have to re-engineer more as JwtAuthenticationToken is annotated as @Transient which means that even if I set a SecurityContextRepository it doesn't save it in the session.
Comment From: sjohnr
I believe your analysis is correct @buckett, thanks for providing an update. Note that you can use an ObjectPostProcessor when necessary to set things that cannot be set via the DSL.
ObjectPostProcessor<BearerTokenAuthenticationFilter> postProcessor = new ObjectPostProcessor<>() {
@Override
public <O extends BearerTokenAuthenticationFilter> O postProcess(O filter) {
filter.setSecurityContextRepository(...);
return filter;
}
};
http.oauth2ResourceServer((oauth2) -> oauth2
.jwt(Customizer.withDefaults())
.withObjectPostProcessor(postProcessor)
);
I'm going to close this issue for now, based on your last comment.
Comment From: icyerasor
We had the very same problem and worked around it temporarily with an ugly hack.. @buckett - did you find a clean approach on how to achieve creating a session and saving an authentication to it after ResourceServer with BearerTokenAuth succeeds?
@sjohnr - thanks for providing the ObjectPostProcessor info.
Our Workaround:
..
.and().oauth2ResourceServer(oauth2 -> oauth2.withObjectPostProcessor(bearerTokenAuthPostProcessor).opaqueToken());
/**
* We first configure the BearerTokenAuthenticationFilter,
* so that it uses a HttpSessionSecurityContextRepository (instead of NullSecurityContextRepository).
* By default the BearerTokenAuthenticationFilter would not write anything to the session.
* <br/>
* Also we need to extend the default HttpSessionSecurityContextRepository,
* to swap the BearerTokenAuthentication before calling saveContext, so that it actually gets saved later.
*/
ObjectPostProcessor<BearerTokenAuthenticationFilter> bearerTokenAuthPostProcessor = new ObjectPostProcessor<>() {
@Override
public <O extends BearerTokenAuthenticationFilter> O postProcess(O filter) {
filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository() {
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
Authentication authentication = context.getAuthentication();
NonTransientBearerTokenAuthentication nonTransientBearerTokenAuthentication = new NonTransientBearerTokenAuthentication(
(OAuth2AuthenticatedPrincipal) authentication.getPrincipal(),
(OAuth2AccessToken) authentication.getCredentials(),
authentication.getAuthorities()
);
context.setAuthentication(nonTransientBearerTokenAuthentication);
super.saveContext(context, request, response);
}
});
return filter;
}
};
/**
* Copy of {@link org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication} without @Transient,
* so that @{@link HttpSessionSecurityContextRepository#saveContext(SecurityContext, HttpServletRequest, HttpServletResponse)}
* and later SaveToSessionResponseWrapper#saveContext
* do continue successfully, when HttpSessionSecurityContextRepository#isTransient is checked there.
*/
private static class NonTransientBearerTokenAuthentication extends AbstractOAuth2TokenAuthenticationToken<OAuth2AccessToken> {
private final Map<String, Object> attributes;
public NonTransientBearerTokenAuthentication(OAuth2AuthenticatedPrincipal principal, OAuth2AccessToken credentials,
Collection<? extends GrantedAuthority> authorities) {
super(credentials, principal, credentials, authorities);
Assert.isTrue(credentials.getTokenType() == OAuth2AccessToken.TokenType.BEARER, "credentials must be a bearer token");
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(principal.getAttributes()));
setAuthenticated(true);
}
@Override
public Map<String, Object> getTokenAttributes() {
return this.attributes;
}
}
Comment From: buckett
@icyerasor I've possibly forgotten something but we did something very similar, created a persistable JWT token (like your NonTransientBearerTokenAuthentication), but then we didn't look to have ended up using a post processor, but instead we have this class:
/**
*
* This was copied from Spring, but changed so that it returned a persistable JWT. This allows the authentication
* to be stored in the session.
*
* @author Rob Winch
* @author Josh Cummings
* @author Evgeniy Cheban
* @author Olivier Antoine
* @author Matthew Buckett
* @since 5.1
*/
public class PersistableJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
private String principalClaimName = JwtClaimNames.SUB;
@Override
public final AbstractAuthenticationToken convert(Jwt jwt) {
Collection<GrantedAuthority> authorities = extractAuthorities(jwt);
String principalClaimValue = jwt.getClaimAsString(this.principalClaimName);
return new PersistableJwtAuthenticationToken(jwt, authorities, principalClaimValue);
}
/**
* Extracts the {@link GrantedAuthority}s from scope attributes typically found in a
* {@link Jwt}
* @param jwt The token
* @return The collection of {@link GrantedAuthority}s found on the token
* @deprecated Since 5.2. Use your own custom converter instead
* @see JwtGrantedAuthoritiesConverter
* @see #setJwtGrantedAuthoritiesConverter(Converter)
*/
@Deprecated
protected Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
return this.jwtGrantedAuthoritiesConverter.convert(jwt);
}
/**
* Sets the {@link Converter Converter<Jwt, Collection<GrantedAuthority>>}
* to use. Defaults to {@link JwtGrantedAuthoritiesConverter}.
* @param jwtGrantedAuthoritiesConverter The converter
* @since 5.2
* @see JwtGrantedAuthoritiesConverter
*/
public void setJwtGrantedAuthoritiesConverter(
Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter) {
Assert.notNull(jwtGrantedAuthoritiesConverter, "jwtGrantedAuthoritiesConverter cannot be null");
this.jwtGrantedAuthoritiesConverter = jwtGrantedAuthoritiesConverter;
}
/**
* Sets the principal claim name. Defaults to {@link JwtClaimNames#SUB}.
* @param principalClaimName The principal claim name
* @since 5.4
*/
public void setPrincipalClaimName(String principalClaimName) {
Assert.hasText(principalClaimName, "principalClaimName cannot be empty");
this.principalClaimName = principalClaimName;
}
}
and then use this when configuring the security:
.oauth2ResourceServer().jwt().jwtAuthenticationConverter(new PersistableJwtAuthenticationConverter()).and()
I can't see anything else much that got changed in the commits.
Comment From: icyerasor
Okay, nice. Thx for providing that other workaround @buckett - makes more sense when using jwt anyway.