-
Notifications
You must be signed in to change notification settings - Fork 6k
Externalize coercion in ClaimAccessor #6799
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
Conversation
Map<String, Object> source = new HashMap<>(); | ||
source.put(JwtClaimNames.EXP, exp); |
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.
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 pointing this out. Did you mean the iat
claim? This was to remain backward compatible with NimbusJwtDecoderJwkSupport
, though there are perhaps other ways to achieve this.
@@ -140,30 +138,6 @@ public void convertWhenUsingCustomConverterThenAllOtherDefaultsAreStillUsed() th | |||
assertThat(target.get(JwtClaimNames.SUB)).isEqualTo("1234"); | |||
} | |||
|
|||
@Test | |||
public void convertWhenConverterReturnsNullThenClaimIsRemoved() { |
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.
} | ||
|
||
@Test | ||
public void convertWhenConverterReturnsValueWhenEntryIsMissingThenEntryIsAdded() { |
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.
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'm not keen on removing these test until we come to an agreement on #6800. This PR isn't about changing how MappedJwtClaimSetConverter
behaves.
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.
Glad to see us taking this step forward! I've left some feedback inline.
* | ||
* @return an {@code Object} to {@code String} {@link Converter} | ||
*/ | ||
public static Converter<Object, String> stringConverter() { |
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.
What are your thoughts around
public static final Converter<Object, String> STRING_CONVERTER = ObjectConverters::asString
It seems like a constant lookup would perform faster than a method invocation.
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 seems like a constant lookup would perform faster than a method invocation
Not sure about this? Do you think the static method approach would be too slow?
Also keep in mind that this follows the same pattern as RsaKeyConverters.pkcs8()
and RsaKeyConverters.x509
and being consistent is key.
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 reason in RsaKeyConverters
is so that each gets its own instance of KeyFactory
, but I didn't see any state in the ObjectConverters
.
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.
There is a bit of a tradeoff. Holding static values improves performance a bit as it won't need to be garbage collected. However, it cannot be garbage collected when there continues to be a reference to it, so that means there is additional memory overhead.
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.
@rwinch It's not clear to me whether you are requesting changes here? If you are, can you please provide more detail? With the current design, garbage collection won't happen after the class is loaded. For example, using stringConverter()
as an example, which is static
and it references the static
method asString
. Are you suggesting that stringConverter()
be changed to non-static, more like a factory method?
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 didn't really suggest one way or the other, but pointed out the tradeoffs.
With the current design, garbage collection won't happen after the class is loaded.
It will happen every time stringConverter()
is used and the reference is no longer held. For example, here GC could happen as soon as foo()
completed:
private void foo() {
Converter<Object, String> toString = stringConverter();
...
}
If foo()
is invoked a lot then there is going to be a lot of GC that can happen.
On the flip side ObjectConverters
creates static members and returns the same instance, there would not be GC happening and multiple invocations of stringConverter()
would return the same object. This is a plus. However, the static values will never be GC'd which is a tradeoff if we anticipate the values only being used at startup.
Given this is likely to be used outside of startup, I would suggest using the static members as @jzheaux suggested.
@@ -49,7 +46,7 @@ | |||
*/ | |||
default Boolean containsClaim(String claim) { |
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.
Now that these are converted up front, what are your thoughts on adding a method:
default <T> T getClaim(String claim)
It'd be really nice for users to be able to do something like:
List<String> scopes = jwt.getClaim("scope");
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'm not sure how we would implement it with the latest implementation that uses a ClaimConversionService
. We would need to supply the targetType
using ClaimConversionService.getSharedInstance().convert(claimValue, targetType)
, which must be captured via the return type of caller. Off the top of my head, I'm not aware how we can capture the generic return type? If it's even possible?
Either way, this would be a feature enhancement as it's not required for this PR. Feel free to log a ticket if you would like this enhancement.
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.
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.
Agreed that it can be a separate issue. Yes, @rwinch this would simply do:
default <T> T getClaim(String claim) {
return (T) this.getClaims().get(claim);
}
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 now understand what you are suggesting @jzheaux. As @rwinch mentioned
the idea would be that it presumes it is already converted to the type
I agree that this would be a nice improvement as long as the user is aware that the claim has already been converted otherwise a ClassCastException
will occur.
I'll add a new ticket for 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.
See #6947
((List<?>) this.getClaims().get(claim)).forEach(e -> claimValues.add(e.toString())); | ||
return claimValues; | ||
Object claimValue = getClaims().get(claim); | ||
List<String> convertedValue = ObjectConverters.listStringConverter().convert(claimValue); |
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 Optional is just a bit more readable in situations like these:
return Optional.ofNullable(ObjectConverters.listStringConverter().convert(claimValue))
.orElseThrow(() -> new IllegalArgumentException...
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 this is a matter of preference. The current implementation is easily readable as well and it has the benefit of not creating an extra object (Optional
) as part of the check.
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 agree this is a matter of preference. Personally, I find Optional
harder to read. For me this is especially the case when doing more than one thing on a single line. It is unfortunate, but this often happens because the way IDEs try to format the code with method chaining.
Note that using Optional
throughout the code base adds quite a bit of object creation which means it can impact GC.
Map<String, Object> source = new HashMap<>(); | ||
source.put(JwtClaimNames.EXP, exp); |
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 pointing this out. Did you mean the iat
claim? This was to remain backward compatible with NimbusJwtDecoderJwkSupport
, though there are perhaps other ways to achieve this.
} | ||
|
||
@Test | ||
public void convertWhenConverterReturnsValueWhenEntryIsMissingThenEntryIsAdded() { |
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'm not keen on removing these test until we come to an agreement on #6800. This PR isn't about changing how MappedJwtClaimSetConverter
behaves.
@@ -819,56 +817,6 @@ JwtDecoder jwtDecoder() { | |||
``` | |||
This will keep all the defaults, except it will override the default claim converter for `sub`. | |||
|
|||
[[oauth2resourceserver-claimsetmapping-add]] |
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 doesn't seem to me that this PR is about changing how MappedClaimSetConverter
works. We should defer changing its documentation to maybe #6800.
* @since 5.2 | ||
* @see Converter | ||
*/ | ||
public final class ObjectConverters { |
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.
Have we looked into using built in Spring types (i.e. ConversionService)? It seems we should probably try to use that over writing the conversion ourselves.
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.
Yes, I did originally check Converter
impl's in framework when I first wrote ClaimAccessor
but did not find anything that met the needs. I will check once again to make sure.
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.
@rwinch I checked again and the reason I did not reuse Converter
(s) in framework is because most (if not all) Converter
implementations in org.springframework.core.convert.support
are package-private.
Also, I don't feel it's necessary to reuse ConversionService
(or GenericConversionService
) as it provides additional API's that I won't use so I'd like to keep things as simple as possible.
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 worry about adding these converters as we will likely need to support more complex things like timestamps that already have support for conversion. What's more is this is rather general, so it means that as we need more types of conversions we need to support them directly vs relying on ConversionService. Perhaps we could still leverage ConversionService, but the API itself accept a Converter that was adapted from ConversionService
* | ||
* @return an {@code Object} to {@code String} {@link Converter} | ||
*/ | ||
public static Converter<Object, String> stringConverter() { |
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.
There is a bit of a tradeoff. Holding static values improves performance a bit as it won't need to be garbage collected. However, it cannot be garbage collected when there continues to be a reference to it, so that means there is additional memory overhead.
f112b72
to
bc5c91d
Compare
@rwinch @jzheaux I applied all changes and it's ready for next review. FYI, I force pushed as I had to remove code related to #6800 that should be excluded. @rwinch I also made use of |
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 commented inline. I'm not sure we need changes, but am going to mark as request changes until we discuss the point.
@@ -61,17 +70,55 @@ | |||
put(MacAlgorithm.HS512, "HmacSHA512"); | |||
} | |||
}; | |||
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER = |
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.
Thinking out loud a bit, but this API doesn't account for the fact that conversion may be a blocking operation. For example, if we have an InputStream
that needs converted to an Object (i.e. reading in JSON) that is blocking and the API does not return a reactive type.
I'm wondering if in practice this is a concern in this situation since we have likely already done all the IO (it probably is converting from a String not an InputStream
). What do we think the likelihood is that we will need to do a blocking operation during conversion? Are there other reasons we might need to block during conversion?
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'm wondering if in practice this is a concern in this situation since we have likely already done all the IO
The source
of claims passed into the Converter
should have already been parsed from it's JSON representation. For the ID Token use case, Nimbus's JSON parser would have read/parsed all claims in the raw ID Token (including custom claims) into an instance of JWTClaimsSet
. Even for cases where there is a custom claim and it's a JSON object, Nimbus would parse it into a Map
.
Are there other reasons we might need to block during conversion?
At this point, I do not see a use case where a blocking operation could happen with the default NimbusReactiveJwtDecoder
.
|
||
return new MappedJwtClaimSetConverter(claimNameToConverter); | ||
} | ||
|
||
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) { | ||
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class); | ||
return source -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor, targetDescriptor); |
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.
Since ClaimAccessor
is an interface, its methods need to retrieve the instance each time.
However, could this class instead keep it as state (e.g. private final ConversionService conversionService = ClaimConversionService.getSharedInstance()
)? I believe it would make things more readable.
|
||
return new MappedJwtClaimSetConverter(claimNameToConverter); | ||
} | ||
|
||
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) { | ||
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class); |
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.
Could we move these TypeDescriptor
declarations out to the class level, e.g. private static final TypeDescriptor OBJECT = TypeDescriptor.valueOf(Object.class)
?
Thanks @jzheaux. I applied your feedback. |
Fixes gh-6245