-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Add BearerTokenAuthenticationConverter #14791
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will you please change BearerTokenAuthenticationFilter
to accept an AuthenticationConverter
? Also, OAuth2ResourceServerConfigurer
should configure this in the same way as BearerTokenResolver
. Will you please add that?
Finally, please add tests both for the new authentication converter and corresponding tests in OAuth2ResourceServerConfigurerTests
.
...ramework/security/oauth2/server/resource/web/AbstractBearerTokenAuthenticationConverter.java
Outdated
Show resolved
Hide resolved
Hi @jzheaux! Thanks for your feedback! I would like some advice on the best way to do this. I can add
But there are several problems.
Even if i declare method
And use it only when a custom
Not the most beautiful method, but it partially solves the problem. Next you will need to do something with
But I'm not sure about this solution. In addition, there may be a problem with |
e91cbc1
to
3317b0d
Compare
Hi @jzheaux! I have added
Now it works with authenticationConverter or with bearerTokenResolver, but not with both. If a
In
Tests work as is. |
Hi @jzheaux ! Maybe it’s worth adding |
@CrazyParanoid, thanks for your patience as I have been out of town. We certainly want to remain passive and don't want to change behavior unnecessarily. Also, though, we don't typically add filter components that cannot be used by existing filters. I'll take a look at the PR this week and see if I can find a way to simplify things. Thank you for all your research details. |
@franticticktick I realize it's been a while since you opened this PR. I've reviewed it and posted a PR to your repo that you can merge if you agree to the changes. We can see from there what other changes might be needed. |
e4fbf43
to
e39e608
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the updates, @franticticktick. This is coming together nicely. I've left some feedback inline.
return this; | ||
} | ||
|
||
public OAuth2ResourceServerConfigurer<H> authenticationConverter(AuthenticationConverter authenticationConverter) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though, bearerTokenResolver
doesn't have a JavaDoc, please add one for authenticationConverter
@@ -194,9 +201,16 @@ public OAuth2ResourceServerConfigurer<H> authenticationManagerResolver( | |||
return this; | |||
} | |||
|
|||
@Deprecated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a JavaDoc here that at least has the following:
/**
* @deprecated please use {@link #authenticationConverter} instead
*/
this.entryPoints.put(requestMatcher, authenticationEntryPoint); | ||
this.deniedHandlers.put(requestMatcher, this.accessDeniedHandler); | ||
this.ignoreCsrfRequestMatchers.add(requestMatcher); | ||
BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder | ||
.rootBeanDefinition(BearerTokenAuthenticationFilter.class); | ||
BeanMetadataElement authenticationManagerResolver = getAuthenticationManagerResolver(oauth2ResourceServer); | ||
filterBuilder.addConstructorArgValue(authenticationManagerResolver); | ||
filterBuilder.addPropertyValue(BEARER_TOKEN_RESOLVER, bearerTokenResolver); | ||
filterBuilder.addPropertyValue(AUTHENTICATION_CONVERTER, authenticationConverter); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typically in XML support, two properties that set the same underlying value cannot be specified together.
Will you please add a check that errors if both authentication-converter-ref
and bearer-token-resolver-ref
are specified? The error message would say something like "you cannot use bearer-token-ref
and authentication-converter-ref
in the same oauth2-resource-server
element".
In the Java DSL, the rule is "whatever is the last method called"; however, which one is the "last attribute" is not always clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added such a check. Also, I removed RootBeanDefinition
from getAuthenticationConverter
and getBearerTokenResolver
methods. We need to handle the case when bearerTokenResolver
and authenticationConverter
are null. This will mean that the user left the default configuration and we can set authenticationConverter
to RootBeanDefinition
.
BeanMetadataElement bearerTokenResolver = getBearerTokenResolver(oauth2ResourceServer);
BeanMetadataElement authenticationConverter = getAuthenticationConverter(oauth2ResourceServer);
if (bearerTokenResolver != null && authenticationConverter != null) {
throw new BeanDefinitionStoreException(
"You cannot use bearer-token-ref and authentication-converter-ref in the same oauth2-resource-server element");
}
if (bearerTokenResolver == null && authenticationConverter == null) {
authenticationConverter = new RootBeanDefinition(BearerTokenAuthenticationConverter.class);
}
After these checks we are guaranteed to have either bearerTokenResolver
or authenticationConverter
left. And we can create a requestMatcher
and add PropertyValue to the filterBuilder
.
@@ -64,10 +65,14 @@ final class OAuth2ResourceServerBeanDefinitionParser implements BeanDefinitionPa | |||
|
|||
static final String BEARER_TOKEN_RESOLVER_REF = "bearer-token-resolver-ref"; | |||
|
|||
static final String AUTHENTICATION_CONVERTER_REF = "authentication-converter-ref"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You will also need to add this to spring-security-6.5.rnc
and then run ./gradlew :spring-security-config:rncToXsd
to update the equivalent XSD. After that, there are some tests that will check that you have documented the attribute.
@@ -760,10 +762,44 @@ public void getBearerTokenResolverWhenResolverBeanAndAnotherOnTheDslThenTheDslOn | |||
} | |||
|
|||
@Test | |||
public void getBearerTokenResolverWhenNoResolverSpecifiedThenTheDefaultIsUsed() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this test stay? I prefer not to remove tests until the code it is testing is removed. This simplifies proving backward compatibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't work. We'll never get DefaultBearerTokenResolver
, since BearerTokenAuthenticationConverter
now does the main work. At the same time, we have BearerTokenResolverAuthenticationConverterAdapter
, which replaces any custom BearerTokenResolver
, of course, it can have DefaultBearerTokenResolver
, but only if the developer defines it himself. In other words, instead of DefaultBearerTokenResolver
, we now have BearerTokenAuthenticationConverter
.
* {@link BearerTokenAuthenticationToken} | ||
* | ||
* @author Max Batischev | ||
* @since 6.3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please update this to be 6.5
* Set the {@link AuthenticationConverter} to use. Defaults to | ||
* {@link BearerTokenAuthenticationConverter}. | ||
* @param authenticationConverter the {@code AuthenticationConverter} to use | ||
* @since 6.3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please change this to 6.5
@@ -287,13 +304,184 @@ public void constructorWhenNullAuthenticationManagerResolverThenThrowsException( | |||
// @formatter:on | |||
} | |||
|
|||
@Test | |||
public void doFilterWhenBearerTokenPresentAndConverterSetThenAuthenticates() throws ServletException, IOException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the additional test cleanup! Will you please place any tests or cleanup that aren't part of adding BearerTokenAuthenticationConverter
into a previous commit?
Hi @jzheaux , i need some more time to tidy up this PR. Also, I have a few questions, I'll write a new comment a bit later. |
Closes spring-projectsgh-14750 Signed-off-by: Max Batischev <mblancer@mail.ru>
Signed-off-by: Max Batischev <mblancer@mail.ru>
e39e608
to
ee09666
Compare
Hey @jzheaux , thanks for the warmth. Could you review my latest changes please? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the next revision here and for your patience. I've been letting this one metabolize to ensure the solution is getting us closer to being able to use AuthenticationFilter
while also honoring a separate initiative to make Authentication
instances immutable.
So, :), in addition to my latest inline feedback, will you please update the @since
attributes to 7.0 and use the 7.0 rnc?
@@ -181,7 +186,14 @@ public void setSecurityContextRepository(SecurityContextRepository securityConte | |||
*/ | |||
public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will you please mark this as deprecated?
this.logger.trace("Did not process request since did not find bearer token"); | ||
filterChain.doFilter(request, response); | ||
return; | ||
} | ||
|
||
BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token); | ||
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); | ||
if (authenticationRequest instanceof AbstractAuthenticationToken details) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit tricky to be having the authentication converter setting the details as well as the filter. Instead of doing it in both places, will you please add to BearerTokenAuthenticatinFilter
a private static adapter class and hold a default bearerTokenResolver and authenticationDetailsSource as members? This adapter would be the default authenticationConverter
for BearerTokenAuthenticationFilter
.
If BearerTokenAuthenticationFilter#setBearerTokenResolver
or #setAuthenticationDetailsSource
is called, it will set the adapter's members accordingly if authenticationConverter
is of the right type. Otherwise, the setters return an error like "setBearerTokenResolver and setAuthenticationConverter cannot be used together".
I think this makes sense because BearerTokenAuthenticationConverter
takes care of resolving the token and populating it with details. So it's a usage error to try and do either of those here once you've specified an AuthenticationConverter
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can't fully guarantee backward compatibility this way. I can make an adapter like this:
public static final class BearerTokenAuthenticationConverterAdapter implements AuthenticationConverter {
private final Log logger = LogFactory.getLog(this.getClass());
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
@Override
public Authentication convert(HttpServletRequest request) {
String token = this.bearerTokenResolver.resolve(request);
if (!StringUtils.hasText(token)) {
this.logger.trace("Did not process request since did not find bearer token");
return null;
}
AbstractAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token);
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
return authenticationRequest;
}
public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
this.authenticationDetailsSource = authenticationDetailsSource;
}
public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
this.bearerTokenResolver = bearerTokenResolver;
}
public BearerTokenResolver getBearerTokenResolver() {
return this.bearerTokenResolver;
}
}
It has both authenticationDetailsSource
and bearerTokenResolver
. At the same time, when calling the setAuthenticationDetailsSource
method, I can check the authenticationConverter
type:
@Deprecated
public void setAuthenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
if(this.authenticationConverter instanceof BearerTokenAuthenticationConverterAdapter adapter) {
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
adapter.setAuthenticationDetailsSource(authenticationDetailsSource);
} else {
throw new IllegalStateException("authenticationDetailsSource and authenticationConverter cannot be use together");
}
}
But there is one problem, if the user wanted to call the setAuthenticationDetailsSource
method without explicitly defining BearerTokenResolver
, he will get an error. Pay attention to the test OAuth2ResourceServerConfigurerTests.requestWhenCustomAuthenticationDetailsSourceThenUsed
.
@Test
public void requestWhenCustomAuthenticationDetailsSourceThenUsed() throws Exception {
this.spring.register(CustomAuthenticationDetailsSource.class, JwtDecoderConfig.class, BasicController.class)
.autowire();
JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
given(decoder.decode(anyString())).willReturn(JWT);
this.mvc.perform(get("/authenticated").with(bearerToken(JWT_TOKEN)))
.andExpect(status().isOk())
.andExpect(content().string(JWT_SUBJECT));
verifyBean(AuthenticationDetailsSource.class).buildDetails(any());
}
This test will not pass. authenticationDetailsSource
via withObjectPostProcessor
, but the bearerTokenResolver
was not explicitly set, which means that BearerTokenAuthenticationFilter
will use BearerTokenAuthenticationConverter
as the converter. In OAuth2ResourceServerConfigurer
:
AuthenticationConverter getAuthenticationConverter() {
if (this.authenticationConverter != null) {
return this.authenticationConverter;
}
if (this.context.getBeanNamesForType(AuthenticationConverter.class).length > 0) {
this.authenticationConverter = this.context.getBean(AuthenticationConverter.class);
}
else if (this.context.getBeanNamesForType(BearerTokenResolver.class).length > 0) {
BearerTokenResolver bearerTokenResolver = this.context.getBean(BearerTokenResolver.class);
BearerTokenAuthenticationFilter.BearerTokenAuthenticationConverterAdapter converter = new BearerTokenAuthenticationFilter.BearerTokenAuthenticationConverterAdapter();
converter.setBearerTokenResolver(bearerTokenResolver);
this.authenticationConverter = converter;
}
else {
this.authenticationConverter = new BearerTokenAuthenticationConverter();
}
return this.authenticationConverter;
}
Simply put, authenticationDetailsSource
can only be set if bearerTokenResolver
is explicitly specified via bean or via configurer. Otherwise, BearerTokenAuthenticationFilter
will use BearerTokenAuthenticationConverter
. Without explicitly specified bearerTokenResolver
, setting authenticationDetailsSource
is simply pointless. But we can do this:
@Deprecated
public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
logger.warn("If you specify an authenticationConverter it will not be used.");
BearerTokenAuthenticationConverterAdapter adapter = new BearerTokenAuthenticationConverterAdapter();
adapter.setBearerTokenResolver(bearerTokenResolver);
this.authenticationConverter = adapter;
}
@Deprecated
public void setAuthenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
logger.warn("If you specify an authenticationConverter it will not be used. " +
"Instead the DefaultBearerTokenResolver will be used in combination with the authenticationDetailsSource you specify.");
BearerTokenAuthenticationConverterAdapter adapter = new BearerTokenAuthenticationConverterAdapter();
adapter.setAuthenticationDetailsSource(authenticationDetailsSource);
this.authenticationConverter = adapter;
}
At the same time, we will preserve backward compatibility as much as possible. All tests will remain as is, we will warn the user that authenticationConverter will not be used. @jzheaux what do you think about this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The correct solution would be like this:
@Deprecated
public void setAuthenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
if(this.authenticationConverter instanceof BearerTokenAuthenticationConverterAdapter adapter) {
adapter.setAuthenticationDetailsSource(authenticationDetailsSource);
} else {
logger.warn("If you specify an authenticationConverter it will not be used. " +
"Instead the DefaultBearerTokenResolver will be used in combination with the authenticationDetailsSource you specify.");
BearerTokenAuthenticationConverterAdapter adapter = new BearerTokenAuthenticationConverterAdapter();
adapter.setAuthenticationDetailsSource(authenticationDetailsSource);
this.authenticationConverter = adapter;
}
}
And:
@Deprecated
public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
if(this.authenticationConverter instanceof BearerTokenAuthenticationConverterAdapter adapter) {
adapter.setBearerTokenResolver(bearerTokenResolver);
} else {
logger.warn("If you specify an authenticationConverter it will not be used.");
BearerTokenAuthenticationConverterAdapter adapter = new BearerTokenAuthenticationConverterAdapter();
adapter.setBearerTokenResolver(bearerTokenResolver);
this.authenticationConverter = adapter;
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, thanks, @franticticktick, for the research. Let's proceed with that, though please have the log be a TRACE log and use the language:
LogMessage.of(() -> String.format("Will override %s with %s", this.authenticationConverter, bearerTokenResolver))
and
LogMessage.of(() -> String.format("Will override %s with %s", this.authenticationConverter, authenticationDetailsSource))
The nice thing about log messages like this is that they read more like a sentence: "BearerTokenAuthenticationFilter will override ..."
I agree that the log, in combination with a deprecation message directing them to call BearerTokenAuthenticationConverter#setAuthenticationDetailsSource
, should be a strong enough signal.
@@ -208,7 +220,7 @@ public void setAuthenticationFailureHandler(final AuthenticationFailureHandler a | |||
/** | |||
* Set the {@link AuthenticationDetailsSource} to use. Defaults to | |||
* {@link WebAuthenticationDetailsSource}. | |||
* @param authenticationDetailsSource the {@code AuthenticationConverter} to use | |||
* @param authenticationDetailsSource the {@code AuthenticationDetailsSource} to use | |||
* @since 5.5 | |||
*/ | |||
public void setAuthenticationDetailsSource( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will you please deprecate this method since BearerTokenAuthenticationConverter
takes care of this?
Closes gh-14750