Skip to content

Commit aa767ec

Browse files
committed
Externalize coercion in ClaimAccessor
Fixes gh-6245
1 parent 3c7aa42 commit aa767ec

File tree

20 files changed

+1430
-200
lines changed

20 files changed

+1430
-200
lines changed

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java

+61-2
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,17 @@
1515
*/
1616
package org.springframework.security.oauth2.client.oidc.authentication;
1717

18+
import org.springframework.core.convert.TypeDescriptor;
19+
import org.springframework.core.convert.converter.Converter;
1820
import org.springframework.security.oauth2.client.registration.ClientRegistration;
1921
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
2022
import org.springframework.security.oauth2.core.OAuth2Error;
2123
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
24+
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
25+
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
26+
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
2227
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
28+
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
2329
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
2430
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
2531
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
@@ -31,7 +37,10 @@
3137
import org.springframework.util.StringUtils;
3238

3339
import javax.crypto.spec.SecretKeySpec;
40+
import java.net.URL;
3441
import java.nio.charset.StandardCharsets;
42+
import java.time.Instant;
43+
import java.util.Collection;
3544
import java.util.HashMap;
3645
import java.util.Map;
3746
import java.util.concurrent.ConcurrentHashMap;
@@ -61,17 +70,55 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<Client
6170
put(MacAlgorithm.HS512, "HmacSHA512");
6271
}
6372
};
73+
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER =
74+
new ClaimTypeConverter(createDefaultClaimTypeConverters());
6475
private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
6576
private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = OidcIdTokenValidator::new;
6677
private Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver = clientRegistration -> SignatureAlgorithm.RS256;
78+
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory =
79+
clientRegistration -> DEFAULT_CLAIM_TYPE_CONVERTER;
80+
81+
/**
82+
* Returns the default {@link Converter}'s used for type conversion of claim values for an {@link OidcIdToken}.
83+
*
84+
* @return a {@link Map} of {@link Converter}'s keyed by {@link IdTokenClaimNames claim name}
85+
*/
86+
public static Map<String, Converter<Object, ?>> createDefaultClaimTypeConverters() {
87+
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
88+
Converter<Object, ?> instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class));
89+
Converter<Object, ?> urlConverter = getConverter(TypeDescriptor.valueOf(URL.class));
90+
Converter<Object, ?> collectionStringConverter = getConverter(
91+
TypeDescriptor.collection(Collection.class, TypeDescriptor.valueOf(String.class)));
92+
93+
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
94+
claimTypeConverters.put(IdTokenClaimNames.ISS, urlConverter);
95+
claimTypeConverters.put(IdTokenClaimNames.AUD, collectionStringConverter);
96+
claimTypeConverters.put(IdTokenClaimNames.EXP, instantConverter);
97+
claimTypeConverters.put(IdTokenClaimNames.IAT, instantConverter);
98+
claimTypeConverters.put(IdTokenClaimNames.AUTH_TIME, instantConverter);
99+
claimTypeConverters.put(IdTokenClaimNames.AMR, collectionStringConverter);
100+
claimTypeConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
101+
claimTypeConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
102+
claimTypeConverters.put(StandardClaimNames.UPDATED_AT, instantConverter);
103+
return claimTypeConverters;
104+
}
105+
106+
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
107+
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
108+
return source -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor, targetDescriptor);
109+
}
67110

68111
@Override
69112
public JwtDecoder createDecoder(ClientRegistration clientRegistration) {
70113
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
71114
return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
72115
NimbusJwtDecoder jwtDecoder = buildDecoder(clientRegistration);
73-
OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
74-
jwtDecoder.setJwtValidator(jwtValidator);
116+
jwtDecoder.setJwtValidator(this.jwtValidatorFactory.apply(clientRegistration));
117+
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter =
118+
this.claimTypeConverterFactory.apply(clientRegistration);
119+
if (claimTypeConverter != null) {
120+
jwtDecoder.setClaimSetConverter(claimTypeConverter);
121+
}
75122
return jwtDecoder;
76123
});
77124
}
@@ -163,4 +210,16 @@ public final void setJwsAlgorithmResolver(Function<ClientRegistration, JwsAlgori
163210
Assert.notNull(jwsAlgorithmResolver, "jwsAlgorithmResolver cannot be null");
164211
this.jwsAlgorithmResolver = jwsAlgorithmResolver;
165212
}
213+
214+
/**
215+
* Sets the factory that provides a {@link Converter} used for type conversion of claim values for an {@link OidcIdToken}.
216+
* The default is {@link ClaimTypeConverter} for all {@link ClientRegistration clients}.
217+
*
218+
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used for type conversion
219+
* of claim values for a specific {@link ClientRegistration client}
220+
*/
221+
public final void setClaimTypeConverterFactory(Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
222+
Assert.notNull(claimTypeConverterFactory, "claimTypeConverterFactory cannot be null");
223+
this.claimTypeConverterFactory = claimTypeConverterFactory;
224+
}
166225
}

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java

+61-2
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,17 @@
1515
*/
1616
package org.springframework.security.oauth2.client.oidc.authentication;
1717

18+
import org.springframework.core.convert.TypeDescriptor;
19+
import org.springframework.core.convert.converter.Converter;
1820
import org.springframework.security.oauth2.client.registration.ClientRegistration;
1921
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
2022
import org.springframework.security.oauth2.core.OAuth2Error;
2123
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
24+
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
25+
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
26+
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
2227
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
28+
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
2329
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
2430
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
2531
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
@@ -31,7 +37,10 @@
3137
import org.springframework.util.StringUtils;
3238

3339
import javax.crypto.spec.SecretKeySpec;
40+
import java.net.URL;
3441
import java.nio.charset.StandardCharsets;
42+
import java.time.Instant;
43+
import java.util.Collection;
3544
import java.util.HashMap;
3645
import java.util.Map;
3746
import java.util.concurrent.ConcurrentHashMap;
@@ -61,17 +70,55 @@ public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecod
6170
put(MacAlgorithm.HS512, "HmacSHA512");
6271
}
6372
};
73+
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER =
74+
new ClaimTypeConverter(createDefaultClaimTypeConverters());
6475
private final Map<String, ReactiveJwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
6576
private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = OidcIdTokenValidator::new;
6677
private Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver = clientRegistration -> SignatureAlgorithm.RS256;
78+
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory =
79+
clientRegistration -> DEFAULT_CLAIM_TYPE_CONVERTER;
80+
81+
/**
82+
* Returns the default {@link Converter}'s used for type conversion of claim values for an {@link OidcIdToken}.
83+
*
84+
* @return a {@link Map} of {@link Converter}'s keyed by {@link IdTokenClaimNames claim name}
85+
*/
86+
public static Map<String, Converter<Object, ?>> createDefaultClaimTypeConverters() {
87+
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
88+
Converter<Object, ?> instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class));
89+
Converter<Object, ?> urlConverter = getConverter(TypeDescriptor.valueOf(URL.class));
90+
Converter<Object, ?> collectionStringConverter = getConverter(
91+
TypeDescriptor.collection(Collection.class, TypeDescriptor.valueOf(String.class)));
92+
93+
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
94+
claimTypeConverters.put(IdTokenClaimNames.ISS, urlConverter);
95+
claimTypeConverters.put(IdTokenClaimNames.AUD, collectionStringConverter);
96+
claimTypeConverters.put(IdTokenClaimNames.EXP, instantConverter);
97+
claimTypeConverters.put(IdTokenClaimNames.IAT, instantConverter);
98+
claimTypeConverters.put(IdTokenClaimNames.AUTH_TIME, instantConverter);
99+
claimTypeConverters.put(IdTokenClaimNames.AMR, collectionStringConverter);
100+
claimTypeConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
101+
claimTypeConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
102+
claimTypeConverters.put(StandardClaimNames.UPDATED_AT, instantConverter);
103+
return claimTypeConverters;
104+
}
105+
106+
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
107+
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
108+
return source -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor, targetDescriptor);
109+
}
67110

68111
@Override
69112
public ReactiveJwtDecoder createDecoder(ClientRegistration clientRegistration) {
70113
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
71114
return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
72115
NimbusReactiveJwtDecoder jwtDecoder = buildDecoder(clientRegistration);
73-
OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
74-
jwtDecoder.setJwtValidator(jwtValidator);
116+
jwtDecoder.setJwtValidator(this.jwtValidatorFactory.apply(clientRegistration));
117+
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter =
118+
this.claimTypeConverterFactory.apply(clientRegistration);
119+
if (claimTypeConverter != null) {
120+
jwtDecoder.setClaimSetConverter(claimTypeConverter);
121+
}
75122
return jwtDecoder;
76123
});
77124
}
@@ -163,4 +210,16 @@ public final void setJwsAlgorithmResolver(Function<ClientRegistration, JwsAlgori
163210
Assert.notNull(jwsAlgorithmResolver, "jwsAlgorithmResolver cannot be null");
164211
this.jwsAlgorithmResolver = jwsAlgorithmResolver;
165212
}
213+
214+
/**
215+
* Sets the factory that provides a {@link Converter} used for type conversion of claim values for an {@link OidcIdToken}.
216+
* The default is {@link ClaimTypeConverter} for all {@link ClientRegistration clients}.
217+
*
218+
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used for type conversion
219+
* of claim values for a specific {@link ClientRegistration client}
220+
*/
221+
public final void setClaimTypeConverterFactory(Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
222+
Assert.notNull(claimTypeConverterFactory, "claimTypeConverterFactory cannot be null");
223+
this.claimTypeConverterFactory = claimTypeConverterFactory;
224+
}
166225
}

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserService.java

+65-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,25 +15,34 @@
1515
*/
1616
package org.springframework.security.oauth2.client.oidc.userinfo;
1717

18-
import java.util.HashSet;
19-
import java.util.Set;
20-
18+
import org.springframework.core.convert.TypeDescriptor;
19+
import org.springframework.core.convert.converter.Converter;
2120
import org.springframework.security.core.GrantedAuthority;
21+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2222
import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService;
2323
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
2424
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
2525
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
2626
import org.springframework.security.oauth2.core.OAuth2Error;
27+
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
28+
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
2729
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
30+
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
2831
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
2932
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
3033
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
3134
import org.springframework.security.oauth2.core.user.OAuth2User;
3235
import org.springframework.util.Assert;
3336
import org.springframework.util.StringUtils;
34-
3537
import reactor.core.publisher.Mono;
3638

39+
import java.time.Instant;
40+
import java.util.HashMap;
41+
import java.util.HashSet;
42+
import java.util.Map;
43+
import java.util.Set;
44+
import java.util.function.Function;
45+
3746
/**
3847
* An implementation of an {@link ReactiveOAuth2UserService} that supports OpenID Connect 1.0 Provider's.
3948
*
@@ -50,8 +59,36 @@ public class OidcReactiveOAuth2UserService implements
5059

5160
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
5261

62+
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER =
63+
new ClaimTypeConverter(createDefaultClaimTypeConverters());
64+
5365
private ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = new DefaultReactiveOAuth2UserService();
5466

67+
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory =
68+
clientRegistration -> DEFAULT_CLAIM_TYPE_CONVERTER;
69+
70+
/**
71+
* Returns the default {@link Converter}'s used for type conversion of claim values for an {@link OidcUserInfo}.
72+
73+
* @since 5.2
74+
* @return a {@link Map} of {@link Converter}'s keyed by {@link StandardClaimNames claim name}
75+
*/
76+
public static Map<String, Converter<Object, ?>> createDefaultClaimTypeConverters() {
77+
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
78+
Converter<Object, ?> instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class));
79+
80+
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
81+
claimTypeConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
82+
claimTypeConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
83+
claimTypeConverters.put(StandardClaimNames.UPDATED_AT, instantConverter);
84+
return claimTypeConverters;
85+
}
86+
87+
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
88+
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
89+
return source -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor, targetDescriptor);
90+
}
91+
5592
@Override
5693
public Mono<OidcUser> loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
5794
Assert.notNull(userRequest, "userRequest cannot be null");
@@ -76,8 +113,10 @@ private Mono<OidcUserInfo> getUserInfo(OidcUserRequest userRequest) {
76113
if (!OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest)) {
77114
return Mono.empty();
78115
}
116+
79117
return this.oauth2UserService.loadUser(userRequest)
80118
.map(OAuth2User::getAttributes)
119+
.map(claims -> convertClaims(claims, userRequest.getClientRegistration()))
81120
.map(OidcUserInfo::new)
82121
.doOnNext(userInfo -> {
83122
String subject = userInfo.getSubject();
@@ -88,8 +127,29 @@ private Mono<OidcUserInfo> getUserInfo(OidcUserRequest userRequest) {
88127
});
89128
}
90129

130+
private Map<String, Object> convertClaims(Map<String, Object> claims, ClientRegistration clientRegistration) {
131+
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter =
132+
this.claimTypeConverterFactory.apply(clientRegistration);
133+
return claimTypeConverter != null ?
134+
claimTypeConverter.convert(claims) :
135+
DEFAULT_CLAIM_TYPE_CONVERTER.convert(claims);
136+
}
137+
91138
public void setOauth2UserService(ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService) {
92139
Assert.notNull(oauth2UserService, "oauth2UserService cannot be null");
93140
this.oauth2UserService = oauth2UserService;
94141
}
142+
143+
/**
144+
* Sets the factory that provides a {@link Converter} used for type conversion of claim values for an {@link OidcUserInfo}.
145+
* The default is {@link ClaimTypeConverter} for all {@link ClientRegistration clients}.
146+
*
147+
* @since 5.2
148+
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used for type conversion
149+
* of claim values for a specific {@link ClientRegistration client}
150+
*/
151+
public final void setClaimTypeConverterFactory(Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
152+
Assert.notNull(claimTypeConverterFactory, "claimTypeConverterFactory cannot be null");
153+
this.claimTypeConverterFactory = claimTypeConverterFactory;
154+
}
95155
}

0 commit comments

Comments
 (0)