Skip to content

No easy way to set SecurityContextRepository on OAuth2ResourceServerConfigurer #11919

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
buckett opened this issue Sep 29, 2022 · 5 comments
Closed
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: declined A suggestion or change that we don't feel we should currently apply

Comments

@buckett
Copy link

buckett commented Sep 29, 2022

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.

@buckett buckett added status: waiting-for-triage An issue we've not yet triaged type: enhancement A general enhancement labels Sep 29, 2022
@buckett
Copy link
Author

buckett commented Sep 29, 2022

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.

@sjohnr
Copy link
Contributor

sjohnr commented Sep 30, 2022

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.

@sjohnr sjohnr closed this as completed Sep 30, 2022
@sjohnr sjohnr added status: declined A suggestion or change that we don't feel we should currently apply and removed status: waiting-for-triage An issue we've not yet triaged type: enhancement A general enhancement labels Sep 30, 2022
@sjohnr sjohnr self-assigned this Sep 30, 2022
@sjohnr sjohnr added the in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) label Sep 30, 2022
@icyerasor
Copy link

icyerasor commented Nov 25, 2022

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;
		}
	}

@buckett
Copy link
Author

buckett commented Nov 25, 2022

@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&lt;Jwt, Collection&lt;GrantedAuthority&gt;&gt;}
	 * 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.

@icyerasor
Copy link

Okay, nice.
Thx for providing that other workaround @buckett - makes more sense when using jwt anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

No branches or pull requests

3 participants