parent
3c7aa4243f
commit
aa767ec8bf
|
@ -15,11 +15,17 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.client.oidc.authentication;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
|
@ -31,7 +37,10 @@ import org.springframework.util.Assert;
|
|||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
@ -61,17 +70,55 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<Client
|
|||
put(MacAlgorithm.HS512, "HmacSHA512");
|
||||
}
|
||||
};
|
||||
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER =
|
||||
new ClaimTypeConverter(createDefaultClaimTypeConverters());
|
||||
private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
|
||||
private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = OidcIdTokenValidator::new;
|
||||
private Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver = clientRegistration -> SignatureAlgorithm.RS256;
|
||||
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory =
|
||||
clientRegistration -> DEFAULT_CLAIM_TYPE_CONVERTER;
|
||||
|
||||
/**
|
||||
* Returns the default {@link Converter}'s used for type conversion of claim values for an {@link OidcIdToken}.
|
||||
*
|
||||
* @return a {@link Map} of {@link Converter}'s keyed by {@link IdTokenClaimNames claim name}
|
||||
*/
|
||||
public static Map<String, Converter<Object, ?>> createDefaultClaimTypeConverters() {
|
||||
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
|
||||
Converter<Object, ?> instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class));
|
||||
Converter<Object, ?> urlConverter = getConverter(TypeDescriptor.valueOf(URL.class));
|
||||
Converter<Object, ?> collectionStringConverter = getConverter(
|
||||
TypeDescriptor.collection(Collection.class, TypeDescriptor.valueOf(String.class)));
|
||||
|
||||
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
|
||||
claimTypeConverters.put(IdTokenClaimNames.ISS, urlConverter);
|
||||
claimTypeConverters.put(IdTokenClaimNames.AUD, collectionStringConverter);
|
||||
claimTypeConverters.put(IdTokenClaimNames.EXP, instantConverter);
|
||||
claimTypeConverters.put(IdTokenClaimNames.IAT, instantConverter);
|
||||
claimTypeConverters.put(IdTokenClaimNames.AUTH_TIME, instantConverter);
|
||||
claimTypeConverters.put(IdTokenClaimNames.AMR, collectionStringConverter);
|
||||
claimTypeConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
|
||||
claimTypeConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
|
||||
claimTypeConverters.put(StandardClaimNames.UPDATED_AT, instantConverter);
|
||||
return claimTypeConverters;
|
||||
}
|
||||
|
||||
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
|
||||
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
|
||||
return source -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor, targetDescriptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JwtDecoder createDecoder(ClientRegistration clientRegistration) {
|
||||
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
|
||||
return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
|
||||
NimbusJwtDecoder jwtDecoder = buildDecoder(clientRegistration);
|
||||
OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
|
||||
jwtDecoder.setJwtValidator(jwtValidator);
|
||||
jwtDecoder.setJwtValidator(this.jwtValidatorFactory.apply(clientRegistration));
|
||||
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter =
|
||||
this.claimTypeConverterFactory.apply(clientRegistration);
|
||||
if (claimTypeConverter != null) {
|
||||
jwtDecoder.setClaimSetConverter(claimTypeConverter);
|
||||
}
|
||||
return jwtDecoder;
|
||||
});
|
||||
}
|
||||
|
@ -163,4 +210,16 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<Client
|
|||
Assert.notNull(jwsAlgorithmResolver, "jwsAlgorithmResolver cannot be null");
|
||||
this.jwsAlgorithmResolver = jwsAlgorithmResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the factory that provides a {@link Converter} used for type conversion of claim values for an {@link OidcIdToken}.
|
||||
* The default is {@link ClaimTypeConverter} for all {@link ClientRegistration clients}.
|
||||
*
|
||||
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used for type conversion
|
||||
* of claim values for a specific {@link ClientRegistration client}
|
||||
*/
|
||||
public final void setClaimTypeConverterFactory(Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
|
||||
Assert.notNull(claimTypeConverterFactory, "claimTypeConverterFactory cannot be null");
|
||||
this.claimTypeConverterFactory = claimTypeConverterFactory;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,11 +15,17 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.client.oidc.authentication;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
|
@ -31,7 +37,10 @@ import org.springframework.util.Assert;
|
|||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
@ -61,17 +70,55 @@ public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecod
|
|||
put(MacAlgorithm.HS512, "HmacSHA512");
|
||||
}
|
||||
};
|
||||
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER =
|
||||
new ClaimTypeConverter(createDefaultClaimTypeConverters());
|
||||
private final Map<String, ReactiveJwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
|
||||
private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = OidcIdTokenValidator::new;
|
||||
private Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver = clientRegistration -> SignatureAlgorithm.RS256;
|
||||
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory =
|
||||
clientRegistration -> DEFAULT_CLAIM_TYPE_CONVERTER;
|
||||
|
||||
/**
|
||||
* Returns the default {@link Converter}'s used for type conversion of claim values for an {@link OidcIdToken}.
|
||||
*
|
||||
* @return a {@link Map} of {@link Converter}'s keyed by {@link IdTokenClaimNames claim name}
|
||||
*/
|
||||
public static Map<String, Converter<Object, ?>> createDefaultClaimTypeConverters() {
|
||||
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
|
||||
Converter<Object, ?> instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class));
|
||||
Converter<Object, ?> urlConverter = getConverter(TypeDescriptor.valueOf(URL.class));
|
||||
Converter<Object, ?> collectionStringConverter = getConverter(
|
||||
TypeDescriptor.collection(Collection.class, TypeDescriptor.valueOf(String.class)));
|
||||
|
||||
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
|
||||
claimTypeConverters.put(IdTokenClaimNames.ISS, urlConverter);
|
||||
claimTypeConverters.put(IdTokenClaimNames.AUD, collectionStringConverter);
|
||||
claimTypeConverters.put(IdTokenClaimNames.EXP, instantConverter);
|
||||
claimTypeConverters.put(IdTokenClaimNames.IAT, instantConverter);
|
||||
claimTypeConverters.put(IdTokenClaimNames.AUTH_TIME, instantConverter);
|
||||
claimTypeConverters.put(IdTokenClaimNames.AMR, collectionStringConverter);
|
||||
claimTypeConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
|
||||
claimTypeConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
|
||||
claimTypeConverters.put(StandardClaimNames.UPDATED_AT, instantConverter);
|
||||
return claimTypeConverters;
|
||||
}
|
||||
|
||||
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
|
||||
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
|
||||
return source -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor, targetDescriptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveJwtDecoder createDecoder(ClientRegistration clientRegistration) {
|
||||
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
|
||||
return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
|
||||
NimbusReactiveJwtDecoder jwtDecoder = buildDecoder(clientRegistration);
|
||||
OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
|
||||
jwtDecoder.setJwtValidator(jwtValidator);
|
||||
jwtDecoder.setJwtValidator(this.jwtValidatorFactory.apply(clientRegistration));
|
||||
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter =
|
||||
this.claimTypeConverterFactory.apply(clientRegistration);
|
||||
if (claimTypeConverter != null) {
|
||||
jwtDecoder.setClaimSetConverter(claimTypeConverter);
|
||||
}
|
||||
return jwtDecoder;
|
||||
});
|
||||
}
|
||||
|
@ -163,4 +210,16 @@ public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecod
|
|||
Assert.notNull(jwsAlgorithmResolver, "jwsAlgorithmResolver cannot be null");
|
||||
this.jwsAlgorithmResolver = jwsAlgorithmResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the factory that provides a {@link Converter} used for type conversion of claim values for an {@link OidcIdToken}.
|
||||
* The default is {@link ClaimTypeConverter} for all {@link ClientRegistration clients}.
|
||||
*
|
||||
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used for type conversion
|
||||
* of claim values for a specific {@link ClientRegistration client}
|
||||
*/
|
||||
public final void setClaimTypeConverterFactory(Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
|
||||
Assert.notNull(claimTypeConverterFactory, "claimTypeConverterFactory cannot be null");
|
||||
this.claimTypeConverterFactory = claimTypeConverterFactory;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -15,25 +15,34 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.client.oidc.userinfo;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
||||
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link ReactiveOAuth2UserService} that supports OpenID Connect 1.0 Provider's.
|
||||
*
|
||||
|
@ -50,8 +59,36 @@ public class OidcReactiveOAuth2UserService implements
|
|||
|
||||
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
|
||||
|
||||
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER =
|
||||
new ClaimTypeConverter(createDefaultClaimTypeConverters());
|
||||
|
||||
private ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = new DefaultReactiveOAuth2UserService();
|
||||
|
||||
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory =
|
||||
clientRegistration -> DEFAULT_CLAIM_TYPE_CONVERTER;
|
||||
|
||||
/**
|
||||
* Returns the default {@link Converter}'s used for type conversion of claim values for an {@link OidcUserInfo}.
|
||||
|
||||
* @since 5.2
|
||||
* @return a {@link Map} of {@link Converter}'s keyed by {@link StandardClaimNames claim name}
|
||||
*/
|
||||
public static Map<String, Converter<Object, ?>> createDefaultClaimTypeConverters() {
|
||||
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
|
||||
Converter<Object, ?> instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class));
|
||||
|
||||
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
|
||||
claimTypeConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
|
||||
claimTypeConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
|
||||
claimTypeConverters.put(StandardClaimNames.UPDATED_AT, instantConverter);
|
||||
return claimTypeConverters;
|
||||
}
|
||||
|
||||
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
|
||||
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
|
||||
return source -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor, targetDescriptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<OidcUser> loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||
Assert.notNull(userRequest, "userRequest cannot be null");
|
||||
|
@ -76,8 +113,10 @@ public class OidcReactiveOAuth2UserService implements
|
|||
if (!OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
return this.oauth2UserService.loadUser(userRequest)
|
||||
.map(OAuth2User::getAttributes)
|
||||
.map(claims -> convertClaims(claims, userRequest.getClientRegistration()))
|
||||
.map(OidcUserInfo::new)
|
||||
.doOnNext(userInfo -> {
|
||||
String subject = userInfo.getSubject();
|
||||
|
@ -88,8 +127,29 @@ public class OidcReactiveOAuth2UserService implements
|
|||
});
|
||||
}
|
||||
|
||||
private Map<String, Object> convertClaims(Map<String, Object> claims, ClientRegistration clientRegistration) {
|
||||
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter =
|
||||
this.claimTypeConverterFactory.apply(clientRegistration);
|
||||
return claimTypeConverter != null ?
|
||||
claimTypeConverter.convert(claims) :
|
||||
DEFAULT_CLAIM_TYPE_CONVERTER.convert(claims);
|
||||
}
|
||||
|
||||
public void setOauth2UserService(ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService) {
|
||||
Assert.notNull(oauth2UserService, "oauth2UserService cannot be null");
|
||||
this.oauth2UserService = oauth2UserService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the factory that provides a {@link Converter} used for type conversion of claim values for an {@link OidcUserInfo}.
|
||||
* The default is {@link ClaimTypeConverter} for all {@link ClientRegistration clients}.
|
||||
*
|
||||
* @since 5.2
|
||||
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used for type conversion
|
||||
* of claim values for a specific {@link ClientRegistration client}
|
||||
*/
|
||||
public final void setClaimTypeConverterFactory(Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
|
||||
Assert.notNull(claimTypeConverterFactory, "claimTypeConverterFactory cannot be null");
|
||||
this.claimTypeConverterFactory = claimTypeConverterFactory;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -15,15 +15,21 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.client.oidc.userinfo;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
|
||||
|
@ -32,10 +38,14 @@ import org.springframework.util.Assert;
|
|||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link OAuth2UserService} that supports OpenID Connect 1.0 Provider's.
|
||||
|
@ -50,9 +60,35 @@ import java.util.Set;
|
|||
*/
|
||||
public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
||||
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
|
||||
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER =
|
||||
new ClaimTypeConverter(createDefaultClaimTypeConverters());
|
||||
private final Set<String> userInfoScopes = new HashSet<>(
|
||||
Arrays.asList(OidcScopes.PROFILE, OidcScopes.EMAIL, OidcScopes.ADDRESS, OidcScopes.PHONE));
|
||||
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = new DefaultOAuth2UserService();
|
||||
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory =
|
||||
clientRegistration -> DEFAULT_CLAIM_TYPE_CONVERTER;
|
||||
|
||||
/**
|
||||
* Returns the default {@link Converter}'s used for type conversion of claim values for an {@link OidcUserInfo}.
|
||||
|
||||
* @since 5.2
|
||||
* @return a {@link Map} of {@link Converter}'s keyed by {@link StandardClaimNames claim name}
|
||||
*/
|
||||
public static Map<String, Converter<Object, ?>> createDefaultClaimTypeConverters() {
|
||||
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
|
||||
Converter<Object, ?> instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class));
|
||||
|
||||
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
|
||||
claimTypeConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
|
||||
claimTypeConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
|
||||
claimTypeConverters.put(StandardClaimNames.UPDATED_AT, instantConverter);
|
||||
return claimTypeConverters;
|
||||
}
|
||||
|
||||
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
|
||||
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
|
||||
return source -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor, targetDescriptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||
|
@ -60,7 +96,16 @@ public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcU
|
|||
OidcUserInfo userInfo = null;
|
||||
if (this.shouldRetrieveUserInfo(userRequest)) {
|
||||
OAuth2User oauth2User = this.oauth2UserService.loadUser(userRequest);
|
||||
userInfo = new OidcUserInfo(oauth2User.getAttributes());
|
||||
|
||||
Map<String, Object> claims;
|
||||
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter =
|
||||
this.claimTypeConverterFactory.apply(userRequest.getClientRegistration());
|
||||
if (claimTypeConverter != null) {
|
||||
claims = claimTypeConverter.convert(oauth2User.getAttributes());
|
||||
} else {
|
||||
claims = DEFAULT_CLAIM_TYPE_CONVERTER.convert(oauth2User.getAttributes());
|
||||
}
|
||||
userInfo = new OidcUserInfo(claims);
|
||||
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
||||
|
||||
|
@ -132,4 +177,17 @@ public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcU
|
|||
Assert.notNull(oauth2UserService, "oauth2UserService cannot be null");
|
||||
this.oauth2UserService = oauth2UserService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the factory that provides a {@link Converter} used for type conversion of claim values for an {@link OidcUserInfo}.
|
||||
* The default is {@link ClaimTypeConverter} for all {@link ClientRegistration clients}.
|
||||
*
|
||||
* @since 5.2
|
||||
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used for type conversion
|
||||
* of claim values for a specific {@link ClientRegistration client}
|
||||
*/
|
||||
public final void setClaimTypeConverterFactory(Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
|
||||
Assert.notNull(claimTypeConverterFactory, "claimTypeConverterFactory cannot be null");
|
||||
this.claimTypeConverterFactory = claimTypeConverterFactory;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,15 +17,20 @@ package org.springframework.security.oauth2.client.oidc.authentication;
|
|||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
@ -49,6 +54,20 @@ public class OidcIdTokenDecoderFactoryTests {
|
|||
this.idTokenDecoderFactory = new OidcIdTokenDecoderFactory();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDefaultClaimTypeConvertersWhenCalledThenDefaultsAreCorrect() {
|
||||
Map<String, Converter<Object, ?>> claimTypeConverters = OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters();
|
||||
assertThat(claimTypeConverters).containsKey(IdTokenClaimNames.ISS);
|
||||
assertThat(claimTypeConverters).containsKey(IdTokenClaimNames.AUD);
|
||||
assertThat(claimTypeConverters).containsKey(IdTokenClaimNames.EXP);
|
||||
assertThat(claimTypeConverters).containsKey(IdTokenClaimNames.IAT);
|
||||
assertThat(claimTypeConverters).containsKey(IdTokenClaimNames.AUTH_TIME);
|
||||
assertThat(claimTypeConverters).containsKey(IdTokenClaimNames.AMR);
|
||||
assertThat(claimTypeConverters).containsKey(StandardClaimNames.EMAIL_VERIFIED);
|
||||
assertThat(claimTypeConverters).containsKey(StandardClaimNames.PHONE_NUMBER_VERIFIED);
|
||||
assertThat(claimTypeConverters).containsKey(StandardClaimNames.UPDATED_AT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setJwtValidatorFactoryWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.idTokenDecoderFactory.setJwtValidatorFactory(null))
|
||||
|
@ -61,6 +80,12 @@ public class OidcIdTokenDecoderFactoryTests {
|
|||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setClaimTypeConverterFactoryWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.idTokenDecoderFactory.setClaimTypeConverterFactory(null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDecoderWhenClientRegistrationNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(null))
|
||||
|
@ -141,4 +166,19 @@ public class OidcIdTokenDecoderFactoryTests {
|
|||
|
||||
verify(customJwsAlgorithmResolver).apply(same(clientRegistration));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDecoderWhenCustomClaimTypeConverterFactorySetThenApplied() {
|
||||
Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> customClaimTypeConverterFactory = mock(Function.class);
|
||||
this.idTokenDecoderFactory.setClaimTypeConverterFactory(customClaimTypeConverterFactory);
|
||||
|
||||
ClientRegistration clientRegistration = this.registration.build();
|
||||
|
||||
when(customClaimTypeConverterFactory.apply(same(clientRegistration)))
|
||||
.thenReturn(new ClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters()));
|
||||
|
||||
this.idTokenDecoderFactory.createDecoder(clientRegistration);
|
||||
|
||||
verify(customClaimTypeConverterFactory).apply(same(clientRegistration));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,15 +17,20 @@ package org.springframework.security.oauth2.client.oidc.authentication;
|
|||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
@ -49,6 +54,20 @@ public class ReactiveOidcIdTokenDecoderFactoryTests {
|
|||
this.idTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDefaultClaimTypeConvertersWhenCalledThenDefaultsAreCorrect() {
|
||||
Map<String, Converter<Object, ?>> claimTypeConverters = ReactiveOidcIdTokenDecoderFactory.createDefaultClaimTypeConverters();
|
||||
assertThat(claimTypeConverters).containsKey(IdTokenClaimNames.ISS);
|
||||
assertThat(claimTypeConverters).containsKey(IdTokenClaimNames.AUD);
|
||||
assertThat(claimTypeConverters).containsKey(IdTokenClaimNames.EXP);
|
||||
assertThat(claimTypeConverters).containsKey(IdTokenClaimNames.IAT);
|
||||
assertThat(claimTypeConverters).containsKey(IdTokenClaimNames.AUTH_TIME);
|
||||
assertThat(claimTypeConverters).containsKey(IdTokenClaimNames.AMR);
|
||||
assertThat(claimTypeConverters).containsKey(StandardClaimNames.EMAIL_VERIFIED);
|
||||
assertThat(claimTypeConverters).containsKey(StandardClaimNames.PHONE_NUMBER_VERIFIED);
|
||||
assertThat(claimTypeConverters).containsKey(StandardClaimNames.UPDATED_AT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setJwtValidatorFactoryWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.idTokenDecoderFactory.setJwtValidatorFactory(null))
|
||||
|
@ -61,6 +80,12 @@ public class ReactiveOidcIdTokenDecoderFactoryTests {
|
|||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setClaimTypeConverterFactoryWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.idTokenDecoderFactory.setClaimTypeConverterFactory(null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDecoderWhenClientRegistrationNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(null))
|
||||
|
@ -141,4 +166,19 @@ public class ReactiveOidcIdTokenDecoderFactoryTests {
|
|||
|
||||
verify(customJwsAlgorithmResolver).apply(same(clientRegistration));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDecoderWhenCustomClaimTypeConverterFactorySetThenApplied() {
|
||||
Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> customClaimTypeConverterFactory = mock(Function.class);
|
||||
this.idTokenDecoderFactory.setClaimTypeConverterFactory(customClaimTypeConverterFactory);
|
||||
|
||||
ClientRegistration clientRegistration = this.registration.build();
|
||||
|
||||
when(customClaimTypeConverterFactory.apply(same(clientRegistration)))
|
||||
.thenReturn(new ClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters()));
|
||||
|
||||
this.idTokenDecoderFactory.createDecoder(clientRegistration);
|
||||
|
||||
verify(customClaimTypeConverterFactory).apply(same(clientRegistration));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -21,6 +21,7 @@ import org.junit.Test;
|
|||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
||||
|
@ -28,6 +29,7 @@ import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
|||
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
||||
|
@ -41,11 +43,11 @@ import java.time.Instant;
|
|||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
|
@ -76,6 +78,20 @@ public class OidcReactiveOAuth2UserServiceTests {
|
|||
this.userService.setOauth2UserService(this.oauth2UserService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDefaultClaimTypeConvertersWhenCalledThenDefaultsAreCorrect() {
|
||||
Map<String, Converter<Object, ?>> claimTypeConverters = OidcReactiveOAuth2UserService.createDefaultClaimTypeConverters();
|
||||
assertThat(claimTypeConverters).containsKey(StandardClaimNames.EMAIL_VERIFIED);
|
||||
assertThat(claimTypeConverters).containsKey(StandardClaimNames.PHONE_NUMBER_VERIFIED);
|
||||
assertThat(claimTypeConverters).containsKey(StandardClaimNames.UPDATED_AT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setClaimTypeConverterFactoryWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.userService.setClaimTypeConverterFactory(null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenUserInfoUriNullThenUserInfoNotRetrieved() {
|
||||
this.registration.userInfoUri(null);
|
||||
|
@ -141,6 +157,28 @@ public class OidcReactiveOAuth2UserServiceTests {
|
|||
assertThat(this.userService.loadUser(userRequest()).block().getName()).isEqualTo("rob");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenCustomClaimTypeConverterFactorySetThenApplied() {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put(StandardClaimNames.SUB, "sub123");
|
||||
attributes.put("user", "rob");
|
||||
OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"),
|
||||
attributes, "user");
|
||||
when(this.oauth2UserService.loadUser(any())).thenReturn(Mono.just(oauth2User));
|
||||
|
||||
OidcUserRequest userRequest = userRequest();
|
||||
|
||||
Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> customClaimTypeConverterFactory = mock(Function.class);
|
||||
this.userService.setClaimTypeConverterFactory(customClaimTypeConverterFactory);
|
||||
|
||||
when(customClaimTypeConverterFactory.apply(same(userRequest.getClientRegistration())))
|
||||
.thenReturn(new ClaimTypeConverter(OidcReactiveOAuth2UserService.createDefaultClaimTypeConverters()));
|
||||
|
||||
this.userService.loadUser(userRequest).block().getUserInfo();
|
||||
|
||||
verify(customClaimTypeConverterFactory).apply(same(userRequest.getClientRegistration()));
|
||||
}
|
||||
|
||||
private OidcUserRequest userRequest() {
|
||||
return new OidcUserRequest(this.registration.build(), this.accessToken, this.idToken);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -15,14 +15,6 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.client.oidc.userinfo;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
|
@ -31,7 +23,7 @@ import org.junit.Before;
|
|||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
|
@ -40,6 +32,7 @@ import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserServ
|
|||
import org.springframework.security.oauth2.core.AuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
|
@ -47,9 +40,20 @@ import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
|
|||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.security.oauth2.client.registration.TestClientRegistrations.clientRegistration;
|
||||
import static org.springframework.security.oauth2.core.TestOAuth2AccessTokens.scopes;
|
||||
|
||||
|
@ -92,12 +96,26 @@ public class OidcUserServiceTests {
|
|||
this.server.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDefaultClaimTypeConvertersWhenCalledThenDefaultsAreCorrect() {
|
||||
Map<String, Converter<Object, ?>> claimTypeConverters = OidcUserService.createDefaultClaimTypeConverters();
|
||||
assertThat(claimTypeConverters).containsKey(StandardClaimNames.EMAIL_VERIFIED);
|
||||
assertThat(claimTypeConverters).containsKey(StandardClaimNames.PHONE_NUMBER_VERIFIED);
|
||||
assertThat(claimTypeConverters).containsKey(StandardClaimNames.UPDATED_AT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setOauth2UserServiceWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.userService.setOauth2UserService(null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setClaimTypeConverterFactoryWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.userService.setClaimTypeConverterFactory(null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenUserRequestIsNullThenThrowIllegalArgumentException() {
|
||||
this.exception.expect(IllegalArgumentException.class);
|
||||
|
@ -355,6 +373,35 @@ public class OidcUserServiceTests {
|
|||
assertThat(request.getBody().readUtf8()).isEqualTo("access_token=" + this.accessToken.getTokenValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUserWhenCustomClaimTypeConverterFactorySetThenApplied() {
|
||||
String userInfoResponse = "{\n" +
|
||||
" \"sub\": \"subject1\",\n" +
|
||||
" \"name\": \"first last\",\n" +
|
||||
" \"given_name\": \"first\",\n" +
|
||||
" \"family_name\": \"last\",\n" +
|
||||
" \"preferred_username\": \"user1\",\n" +
|
||||
" \"email\": \"user1@example.com\"\n" +
|
||||
"}\n";
|
||||
this.server.enqueue(jsonResponse(userInfoResponse));
|
||||
|
||||
String userInfoUri = this.server.url("/user").toString();
|
||||
|
||||
ClientRegistration clientRegistration = this.clientRegistrationBuilder
|
||||
.userInfoUri(userInfoUri)
|
||||
.build();
|
||||
|
||||
Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> customClaimTypeConverterFactory = mock(Function.class);
|
||||
this.userService.setClaimTypeConverterFactory(customClaimTypeConverterFactory);
|
||||
|
||||
when(customClaimTypeConverterFactory.apply(same(clientRegistration)))
|
||||
.thenReturn(new ClaimTypeConverter(OidcUserService.createDefaultClaimTypeConverters()));
|
||||
|
||||
this.userService.loadUser(new OidcUserRequest(clientRegistration, this.accessToken, this.idToken));
|
||||
|
||||
verify(customClaimTypeConverterFactory).apply(same(clientRegistration));
|
||||
}
|
||||
|
||||
private MockResponse jsonResponse(String json) {
|
||||
return new MockResponse()
|
||||
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -15,14 +15,12 @@
|
|||
*/
|
||||
package org.springframework.security.oauth2.core;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -49,7 +47,7 @@ public interface ClaimAccessor {
|
|||
*/
|
||||
default Boolean containsClaim(String claim) {
|
||||
Assert.notNull(claim, "claim cannot be null");
|
||||
return this.getClaims().containsKey(claim);
|
||||
return getClaims().containsKey(claim);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,11 +57,8 @@ public interface ClaimAccessor {
|
|||
* @return the claim value or {@code null} if it does not exist or is equal to {@code null}
|
||||
*/
|
||||
default String getClaimAsString(String claim) {
|
||||
if (!this.containsClaim(claim)) {
|
||||
return null;
|
||||
}
|
||||
Object claimValue = this.getClaims().get(claim);
|
||||
return (claimValue != null ? claimValue.toString() : null);
|
||||
return !containsClaim(claim) ? null :
|
||||
ClaimConversionService.getSharedInstance().convert(getClaims().get(claim), String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -73,7 +68,8 @@ public interface ClaimAccessor {
|
|||
* @return the claim value or {@code null} if it does not exist
|
||||
*/
|
||||
default Boolean getClaimAsBoolean(String claim) {
|
||||
return (this.containsClaim(claim) ? Boolean.valueOf(this.getClaimAsString(claim)) : null);
|
||||
return !containsClaim(claim) ? null :
|
||||
ClaimConversionService.getSharedInstance().convert(getClaims().get(claim), Boolean.class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,23 +79,16 @@ public interface ClaimAccessor {
|
|||
* @return the claim value or {@code null} if it does not exist
|
||||
*/
|
||||
default Instant getClaimAsInstant(String claim) {
|
||||
if (!this.containsClaim(claim)) {
|
||||
if (!containsClaim(claim)) {
|
||||
return null;
|
||||
}
|
||||
Object claimValue = this.getClaims().get(claim);
|
||||
if (Long.class.isAssignableFrom(claimValue.getClass()) ||
|
||||
Integer.class.isAssignableFrom(claimValue.getClass()) ||
|
||||
Double.class.isAssignableFrom(claimValue.getClass())) {
|
||||
return Instant.ofEpochSecond(((Number) claimValue).longValue());
|
||||
Object claimValue = getClaims().get(claim);
|
||||
Instant convertedValue = ClaimConversionService.getSharedInstance().convert(claimValue, Instant.class);
|
||||
if (convertedValue == null) {
|
||||
throw new IllegalArgumentException("Unable to convert claim '" + claim +
|
||||
"' of type '" + claimValue.getClass() + "' to Instant.");
|
||||
}
|
||||
if (Date.class.isAssignableFrom(claimValue.getClass())) {
|
||||
return ((Date) claimValue).toInstant();
|
||||
}
|
||||
if (Instant.class.isAssignableFrom(claimValue.getClass())) {
|
||||
return (Instant) claimValue;
|
||||
}
|
||||
throw new IllegalArgumentException("Unable to convert claim '" + claim +
|
||||
"' of type '" + claimValue.getClass() + "' to Instant.");
|
||||
return convertedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -109,14 +98,16 @@ public interface ClaimAccessor {
|
|||
* @return the claim value or {@code null} if it does not exist
|
||||
*/
|
||||
default URL getClaimAsURL(String claim) {
|
||||
if (!this.containsClaim(claim)) {
|
||||
if (!containsClaim(claim)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new URL(this.getClaimAsString(claim));
|
||||
} catch (MalformedURLException ex) {
|
||||
throw new IllegalArgumentException("Unable to convert claim '" + claim + "' to URL: " + ex.getMessage(), ex);
|
||||
Object claimValue = getClaims().get(claim);
|
||||
URL convertedValue = ClaimConversionService.getSharedInstance().convert(claimValue, URL.class);
|
||||
if (convertedValue == null) {
|
||||
throw new IllegalArgumentException("Unable to convert claim '" + claim +
|
||||
"' of type '" + claimValue.getClass() + "' to URL.");
|
||||
}
|
||||
return convertedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -126,13 +117,22 @@ public interface ClaimAccessor {
|
|||
* @param claim the name of the claim
|
||||
* @return the claim value or {@code null} if it does not exist or cannot be assigned to a {@code Map}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
default Map<String, Object> getClaimAsMap(String claim) {
|
||||
if (!this.containsClaim(claim) || !Map.class.isAssignableFrom(this.getClaims().get(claim).getClass())) {
|
||||
if (!containsClaim(claim)) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> claimValues = new HashMap<>();
|
||||
((Map<?, ?>) this.getClaims().get(claim)).forEach((k, v) -> claimValues.put(k.toString(), v));
|
||||
return claimValues;
|
||||
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
|
||||
final TypeDescriptor targetDescriptor = TypeDescriptor.map(
|
||||
Map.class, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Object.class));
|
||||
Object claimValue = getClaims().get(claim);
|
||||
Map<String, Object> convertedValue = (Map<String, Object>) ClaimConversionService.getSharedInstance().convert(
|
||||
claimValue, sourceDescriptor, targetDescriptor);
|
||||
if (convertedValue == null) {
|
||||
throw new IllegalArgumentException("Unable to convert claim '" + claim +
|
||||
"' of type '" + claimValue.getClass() + "' to Map.");
|
||||
}
|
||||
return convertedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -142,12 +142,21 @@ public interface ClaimAccessor {
|
|||
* @param claim the name of the claim
|
||||
* @return the claim value or {@code null} if it does not exist or cannot be assigned to a {@code List}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
default List<String> getClaimAsStringList(String claim) {
|
||||
if (!this.containsClaim(claim) || !List.class.isAssignableFrom(this.getClaims().get(claim).getClass())) {
|
||||
if (!containsClaim(claim)) {
|
||||
return null;
|
||||
}
|
||||
List<String> claimValues = new ArrayList<>();
|
||||
((List<?>) this.getClaims().get(claim)).forEach(e -> claimValues.add(e.toString()));
|
||||
return claimValues;
|
||||
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
|
||||
final TypeDescriptor targetDescriptor = TypeDescriptor.collection(
|
||||
List.class, TypeDescriptor.valueOf(String.class));
|
||||
Object claimValue = getClaims().get(claim);
|
||||
List<String> convertedValue = (List<String>) ClaimConversionService.getSharedInstance().convert(
|
||||
claimValue, sourceDescriptor, targetDescriptor);
|
||||
if (convertedValue == null) {
|
||||
throw new IllegalArgumentException("Unable to convert claim '" + claim +
|
||||
"' of type '" + claimValue.getClass() + "' to List.");
|
||||
}
|
||||
return convertedValue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.converter;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.converter.ConverterRegistry;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
|
||||
/**
|
||||
* A {@link ConversionService} configured with converters
|
||||
* that provide type conversion for claim values.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.2
|
||||
* @see GenericConversionService
|
||||
* @see ClaimAccessor
|
||||
*/
|
||||
public final class ClaimConversionService extends GenericConversionService {
|
||||
private static volatile ClaimConversionService sharedInstance;
|
||||
|
||||
private ClaimConversionService() {
|
||||
addConverters(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a shared instance of {@code ClaimConversionService}.
|
||||
*
|
||||
* @return a shared instance of {@code ClaimConversionService}
|
||||
*/
|
||||
public static ClaimConversionService getSharedInstance() {
|
||||
ClaimConversionService sharedInstance = ClaimConversionService.sharedInstance;
|
||||
if (sharedInstance == null) {
|
||||
synchronized (ClaimConversionService.class) {
|
||||
sharedInstance = ClaimConversionService.sharedInstance;
|
||||
if (sharedInstance == null) {
|
||||
sharedInstance = new ClaimConversionService();
|
||||
ClaimConversionService.sharedInstance = sharedInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the converters that provide type conversion for claim values
|
||||
* to the provided {@link ConverterRegistry}.
|
||||
*
|
||||
* @param converterRegistry the registry of converters to add to
|
||||
*/
|
||||
public static void addConverters(ConverterRegistry converterRegistry) {
|
||||
converterRegistry.addConverter(new ObjectToStringConverter());
|
||||
converterRegistry.addConverter(new ObjectToBooleanConverter());
|
||||
converterRegistry.addConverter(new ObjectToInstantConverter());
|
||||
converterRegistry.addConverter(new ObjectToURLConverter());
|
||||
converterRegistry.addConverter(new ObjectToListStringConverter());
|
||||
converterRegistry.addConverter(new ObjectToMapStringObjectConverter());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.converter;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link Converter} that provides type conversion for claim values.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.2
|
||||
* @see Converter
|
||||
*/
|
||||
public final class ClaimTypeConverter implements Converter<Map<String, Object>, Map<String, Object>> {
|
||||
private final Map<String, Converter<Object, ?>> claimTypeConverters;
|
||||
|
||||
/**
|
||||
* Constructs a {@code ClaimTypeConverter} using the provided parameters.
|
||||
*
|
||||
* @param claimTypeConverters a {@link Map} of {@link Converter}(s) keyed by claim name
|
||||
*/
|
||||
public ClaimTypeConverter(Map<String, Converter<Object, ?>> claimTypeConverters) {
|
||||
Assert.notEmpty(claimTypeConverters, "claimTypeConverters cannot be empty");
|
||||
Assert.noNullElements(claimTypeConverters.values().toArray(), "Converter(s) cannot be null");
|
||||
this.claimTypeConverters = Collections.unmodifiableMap(new LinkedHashMap<>(claimTypeConverters));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> convert(Map<String, Object> claims) {
|
||||
if (CollectionUtils.isEmpty(claims)) {
|
||||
return claims;
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>(claims);
|
||||
this.claimTypeConverters.forEach((claimName, typeConverter) -> {
|
||||
if (claims.containsKey(claimName)) {
|
||||
Object claim = claims.get(claimName);
|
||||
Object mappedClaim = typeConverter.convert(claim);
|
||||
if (mappedClaim != null) {
|
||||
result.put(claimName, mappedClaim);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.converter;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.GenericConverter;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 5.2
|
||||
*/
|
||||
final class ObjectToBooleanConverter implements GenericConverter {
|
||||
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return Collections.singleton(new ConvertiblePair(Object.class, Boolean.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
if (source instanceof Boolean) {
|
||||
return source;
|
||||
}
|
||||
return Boolean.valueOf(source.toString());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.converter;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.GenericConverter;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 5.2
|
||||
*/
|
||||
final class ObjectToInstantConverter implements GenericConverter {
|
||||
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return Collections.singleton(new ConvertiblePair(Object.class, Instant.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
if (source instanceof Instant) {
|
||||
return source;
|
||||
}
|
||||
if (source instanceof Date) {
|
||||
return ((Date) source).toInstant();
|
||||
}
|
||||
if (source instanceof Number) {
|
||||
return Instant.ofEpochSecond(((Number) source).longValue());
|
||||
}
|
||||
try {
|
||||
return Instant.ofEpochSecond(Long.parseLong(source.toString()));
|
||||
} catch (Exception ex) {
|
||||
// Ignore
|
||||
}
|
||||
try {
|
||||
return Instant.parse(source.toString());
|
||||
} catch (Exception ex) {
|
||||
// Ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.converter;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.ConditionalGenericConverter;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 5.2
|
||||
*/
|
||||
final class ObjectToListStringConverter implements ConditionalGenericConverter {
|
||||
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
Set<ConvertiblePair> convertibleTypes = new LinkedHashSet<>();
|
||||
convertibleTypes.add(new ConvertiblePair(Object.class, List.class));
|
||||
convertibleTypes.add(new ConvertiblePair(Object.class, Collection.class));
|
||||
return convertibleTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
if (targetType.getElementTypeDescriptor() == null ||
|
||||
targetType.getElementTypeDescriptor().getType().equals(String.class) ||
|
||||
sourceType == null ||
|
||||
ClassUtils.isAssignable(sourceType.getType(), targetType.getElementTypeDescriptor().getType())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
if (source instanceof List) {
|
||||
List<?> sourceList = (List<?>) source;
|
||||
if (!sourceList.isEmpty() && sourceList.get(0) instanceof String) {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
if (source instanceof Collection) {
|
||||
return ((Collection<?>) source).stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(Objects::toString)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
return Collections.singletonList(source.toString());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.converter;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.ConditionalGenericConverter;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 5.2
|
||||
*/
|
||||
final class ObjectToMapStringObjectConverter implements ConditionalGenericConverter {
|
||||
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return Collections.singleton(new ConvertiblePair(Object.class, Map.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
if (targetType.getElementTypeDescriptor() == null ||
|
||||
targetType.getMapKeyTypeDescriptor().getType().equals(String.class)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
if (!(source instanceof Map)) {
|
||||
return null;
|
||||
}
|
||||
Map<?, ?> sourceMap = (Map<?, ?>) source;
|
||||
if (!sourceMap.isEmpty() && sourceMap.keySet().iterator().next() instanceof String) {
|
||||
return source;
|
||||
}
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
sourceMap.forEach((k, v) -> result.put(k.toString(), v));
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.converter;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.GenericConverter;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 5.2
|
||||
*/
|
||||
final class ObjectToStringConverter implements GenericConverter {
|
||||
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return Collections.singleton(new ConvertiblePair(Object.class, String.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
return source == null ? null : source.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.converter;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.GenericConverter;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 5.2
|
||||
*/
|
||||
final class ObjectToURLConverter implements GenericConverter {
|
||||
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return Collections.singleton(new ConvertiblePair(Object.class, URL.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
if (source instanceof URL) {
|
||||
return source;
|
||||
}
|
||||
try {
|
||||
return new URI(source.toString()).toURL();
|
||||
} catch (Exception ex) {
|
||||
// Ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.converter;
|
||||
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ClaimConversionService}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.2
|
||||
*/
|
||||
public class ClaimConversionServiceTests {
|
||||
private final ConversionService conversionService = ClaimConversionService.getSharedInstance();
|
||||
|
||||
@Test
|
||||
public void convertStringWhenNullThenReturnNull() {
|
||||
assertThat(this.conversionService.convert(null, String.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertStringWhenStringThenReturnSame() {
|
||||
assertThat(this.conversionService.convert("string", String.class)).isSameAs("string");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertStringWhenNumberThenConverts() {
|
||||
assertThat(this.conversionService.convert(1234, String.class)).isEqualTo("1234");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertBooleanWhenNullThenReturnNull() {
|
||||
assertThat(this.conversionService.convert(null, Boolean.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertBooleanWhenBooleanThenReturnSame() {
|
||||
assertThat(this.conversionService.convert(Boolean.TRUE, Boolean.class)).isSameAs(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertBooleanWhenStringTrueThenConverts() {
|
||||
assertThat(this.conversionService.convert("true", Boolean.class)).isEqualTo(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertBooleanWhenNotConvertibleThenReturnBooleanFalse() {
|
||||
assertThat(this.conversionService.convert("not-convertible-boolean", Boolean.class)).isEqualTo(Boolean.FALSE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertInstantWhenNullThenReturnNull() {
|
||||
assertThat(this.conversionService.convert(null, Instant.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertInstantWhenInstantThenReturnSame() {
|
||||
Instant instant = Instant.now();
|
||||
assertThat(this.conversionService.convert(instant, Instant.class)).isSameAs(instant);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertInstantWhenDateThenConverts() {
|
||||
Instant instant = Instant.now();
|
||||
assertThat(this.conversionService.convert(Date.from(instant), Instant.class)).isEqualTo(instant);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertInstantWhenNumberThenConverts() {
|
||||
Instant instant = Instant.now();
|
||||
assertThat(this.conversionService.convert(instant.getEpochSecond(), Instant.class))
|
||||
.isEqualTo(instant.truncatedTo(ChronoUnit.SECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertInstantWhenStringThenConverts() {
|
||||
Instant instant = Instant.now();
|
||||
assertThat(this.conversionService.convert(String.valueOf(instant.getEpochSecond()), Instant.class))
|
||||
.isEqualTo(instant.truncatedTo(ChronoUnit.SECONDS));
|
||||
assertThat(this.conversionService.convert(String.valueOf(instant.toString()), Instant.class)).isEqualTo(instant);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertInstantWhenNotConvertibleThenReturnNull() {
|
||||
assertThat(this.conversionService.convert("not-convertible-instant", Instant.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertUrlWhenNullThenReturnNull() {
|
||||
assertThat(this.conversionService.convert(null, URL.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertUrlWhenUrlThenReturnSame() throws Exception {
|
||||
URL url = new URL("https://localhost");
|
||||
assertThat(this.conversionService.convert(url, URL.class)).isSameAs(url);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertUrlWhenStringThenConverts() throws Exception {
|
||||
String urlString = "https://localhost";
|
||||
URL url = new URL(urlString);
|
||||
assertThat(this.conversionService.convert(urlString, URL.class)).isEqualTo(url);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertUrlWhenNotConvertibleThenReturnNull() {
|
||||
assertThat(this.conversionService.convert("not-convertible-url", URL.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertCollectionStringWhenNullThenReturnNull() {
|
||||
assertThat(this.conversionService.convert(null, Collection.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertCollectionStringWhenListStringThenReturnSame() {
|
||||
List<String> list = Lists.list("1", "2", "3", "4");
|
||||
assertThat(this.conversionService.convert(list, Collection.class)).isSameAs(list);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertCollectionStringWhenListNumberThenConverts() {
|
||||
assertThat(this.conversionService.convert(Lists.list(1, 2, 3, 4), Collection.class))
|
||||
.isEqualTo(Lists.list("1", "2", "3", "4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertCollectionStringWhenNotConvertibleThenReturnSingletonList() {
|
||||
String string = "not-convertible-collection";
|
||||
assertThat(this.conversionService.convert(string, Collection.class))
|
||||
.isEqualTo(Collections.singletonList(string));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertListStringWhenNullThenReturnNull() {
|
||||
assertThat(this.conversionService.convert(null, List.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertListStringWhenListStringThenReturnSame() {
|
||||
List<String> list = Lists.list("1", "2", "3", "4");
|
||||
assertThat(this.conversionService.convert(list, List.class)).isSameAs(list);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertListStringWhenListNumberThenConverts() {
|
||||
assertThat(this.conversionService.convert(Lists.list(1, 2, 3, 4), List.class))
|
||||
.isEqualTo(Lists.list("1", "2", "3", "4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertListStringWhenNotConvertibleThenReturnSingletonList() {
|
||||
String string = "not-convertible-list";
|
||||
assertThat(this.conversionService.convert(string, List.class))
|
||||
.isEqualTo(Collections.singletonList(string));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertMapStringObjectWhenNullThenReturnNull() {
|
||||
assertThat(this.conversionService.convert(null, Map.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertMapStringObjectWhenMapStringObjectThenReturnSame() {
|
||||
Map<String, Object> mapStringObject = new HashMap<String, Object>() {
|
||||
{
|
||||
put("key1", "value1");
|
||||
put("key2", "value2");
|
||||
put("key3", "value3");
|
||||
}
|
||||
};
|
||||
assertThat(this.conversionService.convert(mapStringObject, Map.class)).isSameAs(mapStringObject);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertMapStringObjectWhenMapIntegerObjectThenConverts() {
|
||||
Map<String, Object> mapStringObject = new HashMap<String, Object>() {
|
||||
{
|
||||
put("1", "value1");
|
||||
put("2", "value2");
|
||||
put("3", "value3");
|
||||
}
|
||||
};
|
||||
Map<Integer, Object> mapIntegerObject = new HashMap<Integer, Object>() {
|
||||
{
|
||||
put(1, "value1");
|
||||
put(2, "value2");
|
||||
put(3, "value3");
|
||||
}
|
||||
};
|
||||
assertThat(this.conversionService.convert(mapIntegerObject, Map.class)).isEqualTo(mapStringObject);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertMapStringObjectWhenNotConvertibleThenReturnNull() {
|
||||
List<String> notConvertibleList = Lists.list("1", "2", "3", "4");
|
||||
assertThat(this.conversionService.convert(notConvertibleList, Map.class)).isNull();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.converter;
|
||||
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link ClaimTypeConverter}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.2
|
||||
*/
|
||||
public class ClaimTypeConverterTests {
|
||||
private static final String STRING_CLAIM = "string-claim";
|
||||
private static final String BOOLEAN_CLAIM = "boolean-claim";
|
||||
private static final String INSTANT_CLAIM = "instant-claim";
|
||||
private static final String URL_CLAIM = "url-claim";
|
||||
private static final String COLLECTION_STRING_CLAIM = "collection-string-claim";
|
||||
private static final String LIST_STRING_CLAIM = "list-string-claim";
|
||||
private static final String MAP_STRING_OBJECT_CLAIM = "map-string-object-claim";
|
||||
private ClaimTypeConverter claimTypeConverter;
|
||||
|
||||
@Before
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setup() {
|
||||
Converter<Object, ?> stringConverter = getConverter(TypeDescriptor.valueOf(String.class));
|
||||
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
|
||||
Converter<Object, ?> instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class));
|
||||
Converter<Object, ?> urlConverter = getConverter(TypeDescriptor.valueOf(URL.class));
|
||||
Converter<Object, ?> collectionStringConverter = getConverter(
|
||||
TypeDescriptor.collection(Collection.class, TypeDescriptor.valueOf(String.class)));
|
||||
Converter<Object, ?> listStringConverter = getConverter(
|
||||
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));
|
||||
Converter<Object, ?> mapStringObjectConverter = getConverter(
|
||||
TypeDescriptor.map(Map.class, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Object.class)));
|
||||
|
||||
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
|
||||
claimTypeConverters.put(STRING_CLAIM, stringConverter);
|
||||
claimTypeConverters.put(BOOLEAN_CLAIM, booleanConverter);
|
||||
claimTypeConverters.put(INSTANT_CLAIM, instantConverter);
|
||||
claimTypeConverters.put(URL_CLAIM, urlConverter);
|
||||
claimTypeConverters.put(COLLECTION_STRING_CLAIM, collectionStringConverter);
|
||||
claimTypeConverters.put(LIST_STRING_CLAIM, listStringConverter);
|
||||
claimTypeConverters.put(MAP_STRING_OBJECT_CLAIM, mapStringObjectConverter);
|
||||
this.claimTypeConverter = new ClaimTypeConverter(claimTypeConverters);
|
||||
}
|
||||
|
||||
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
|
||||
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
|
||||
return source -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor, targetDescriptor);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenConvertersNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new ClaimTypeConverter(null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenConvertersHasNullConverterThenThrowIllegalArgumentException() {
|
||||
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
|
||||
claimTypeConverters.put("claim1", null);
|
||||
assertThatThrownBy(() -> new ClaimTypeConverter(claimTypeConverters))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenClaimsEmptyThenReturnSame() {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
assertThat(this.claimTypeConverter.convert(claims)).isSameAs(claims);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenAllClaimsRequireConversionThenConvertAll() throws Exception {
|
||||
Instant instant = Instant.now();
|
||||
URL url = new URL("https://localhost");
|
||||
List<Number> listNumber = Lists.list(1, 2, 3, 4);
|
||||
List<String> listString = Lists.list("1", "2", "3", "4");
|
||||
Map<Integer, Object> mapIntegerObject = new HashMap<>();
|
||||
mapIntegerObject.put(1, "value1");
|
||||
Map<String, Object> mapStringObject = new HashMap<>();
|
||||
mapStringObject.put("1", "value1");
|
||||
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put(STRING_CLAIM, Boolean.TRUE);
|
||||
claims.put(BOOLEAN_CLAIM, "true");
|
||||
claims.put(INSTANT_CLAIM, instant.toString());
|
||||
claims.put(URL_CLAIM, url.toExternalForm());
|
||||
claims.put(COLLECTION_STRING_CLAIM, listNumber);
|
||||
claims.put(LIST_STRING_CLAIM, listNumber);
|
||||
claims.put(MAP_STRING_OBJECT_CLAIM, mapIntegerObject);
|
||||
|
||||
claims = this.claimTypeConverter.convert(claims);
|
||||
|
||||
assertThat(claims.get(STRING_CLAIM)).isEqualTo("true");
|
||||
assertThat(claims.get(BOOLEAN_CLAIM)).isEqualTo(Boolean.TRUE);
|
||||
assertThat(claims.get(INSTANT_CLAIM)).isEqualTo(instant);
|
||||
assertThat(claims.get(URL_CLAIM)).isEqualTo(url);
|
||||
assertThat(claims.get(COLLECTION_STRING_CLAIM)).isEqualTo(listString);
|
||||
assertThat(claims.get(LIST_STRING_CLAIM)).isEqualTo(listString);
|
||||
assertThat(claims.get(MAP_STRING_OBJECT_CLAIM)).isEqualTo(mapStringObject);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenNoClaimsRequireConversionThenConvertNone() throws Exception {
|
||||
String string = "value";
|
||||
Boolean bool = Boolean.TRUE;
|
||||
Instant instant = Instant.now();
|
||||
URL url = new URL("https://localhost");
|
||||
List<String> listString = Lists.list("1", "2", "3", "4");
|
||||
Map<String, Object> mapStringObject = new HashMap<>();
|
||||
mapStringObject.put("1", "value1");
|
||||
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put(STRING_CLAIM, string);
|
||||
claims.put(BOOLEAN_CLAIM, bool);
|
||||
claims.put(INSTANT_CLAIM, instant);
|
||||
claims.put(URL_CLAIM, url);
|
||||
claims.put(COLLECTION_STRING_CLAIM, listString);
|
||||
claims.put(LIST_STRING_CLAIM, listString);
|
||||
claims.put(MAP_STRING_OBJECT_CLAIM, mapStringObject);
|
||||
|
||||
claims = this.claimTypeConverter.convert(claims);
|
||||
|
||||
assertThat(claims.get(STRING_CLAIM)).isSameAs(string);
|
||||
assertThat(claims.get(BOOLEAN_CLAIM)).isSameAs(bool);
|
||||
assertThat(claims.get(INSTANT_CLAIM)).isSameAs(instant);
|
||||
assertThat(claims.get(URL_CLAIM)).isSameAs(url);
|
||||
assertThat(claims.get(COLLECTION_STRING_CLAIM)).isSameAs(listString);
|
||||
assertThat(claims.get(LIST_STRING_CLAIM)).isSameAs(listString);
|
||||
assertThat(claims.get(MAP_STRING_OBJECT_CLAIM)).isSameAs(mapStringObject);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertWhenConverterNotAvailableThenDoesNotConvert() {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("claim1", "value1");
|
||||
|
||||
claims = this.claimTypeConverter.convert(claims);
|
||||
|
||||
assertThat(claims.get("claim1")).isSameAs("value1");
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -16,47 +16,49 @@
|
|||
|
||||
package org.springframework.security.oauth2.jwt;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
|
||||
import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Converts a JWT claim set, claim by claim. Can be configured with custom converters
|
||||
* by claim name.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.1
|
||||
* @see ClaimTypeConverter
|
||||
*/
|
||||
public final class MappedJwtClaimSetConverter
|
||||
implements Converter<Map<String, Object>, Map<String, Object>> {
|
||||
|
||||
private static final Converter<Object, Collection<String>> AUDIENCE_CONVERTER = new AudienceConverter();
|
||||
private static final Converter<Object, String> ISSUER_CONVERTER = new IssuerConverter();
|
||||
private static final Converter<Object, String> STRING_CONVERTER = new StringConverter();
|
||||
private static final Converter<Object, Instant> TEMPORAL_CONVERTER = new InstantConverter();
|
||||
|
||||
private final Map<String, Converter<Object, ?>> claimConverters;
|
||||
public final class MappedJwtClaimSetConverter implements Converter<Map<String, Object>, Map<String, Object>> {
|
||||
private final static ConversionService CONVERSION_SERVICE = ClaimConversionService.getSharedInstance();
|
||||
private final static TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
|
||||
private final static TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
|
||||
private final static TypeDescriptor INSTANT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Instant.class);
|
||||
private final static TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
|
||||
private final Map<String, Converter<Object, ?>> claimTypeConverters;
|
||||
private final Converter<Map<String, Object>, Map<String, Object>> delegate;
|
||||
|
||||
/**
|
||||
* Constructs a {@link MappedJwtClaimSetConverter} with the provided arguments
|
||||
*
|
||||
* This will completely replace any set of default converters.
|
||||
*
|
||||
* @param claimConverters The {@link Map} of converters to use
|
||||
* @param claimTypeConverters The {@link Map} of converters to use
|
||||
*/
|
||||
public MappedJwtClaimSetConverter(Map<String, Converter<Object, ?>> claimConverters) {
|
||||
Assert.notNull(claimConverters, "claimConverters cannot be null");
|
||||
this.claimConverters = new HashMap<>(claimConverters);
|
||||
public MappedJwtClaimSetConverter(Map<String, Converter<Object, ?>> claimTypeConverters) {
|
||||
Assert.notNull(claimTypeConverters, "claimTypeConverters cannot be null");
|
||||
this.claimTypeConverters = claimTypeConverters;
|
||||
this.delegate = new ClaimTypeConverter(claimTypeConverters);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -78,29 +80,65 @@ public final class MappedJwtClaimSetConverter
|
|||
* Collections.singletonMap(JwtClaimNames.SUB, new UserDetailsServiceJwtSubjectConverter()));
|
||||
* </pre>
|
||||
*
|
||||
* To completely replace the underlying {@link Map} of converters, {@see MappedJwtClaimSetConverter(Map)}.
|
||||
* To completely replace the underlying {@link Map} of converters, see {@link MappedJwtClaimSetConverter#MappedJwtClaimSetConverter(Map)}.
|
||||
*
|
||||
* @param claimConverters
|
||||
* @param claimTypeConverters
|
||||
* @return An instance of {@link MappedJwtClaimSetConverter} that contains the converters provided,
|
||||
* plus any defaults that were not overridden.
|
||||
*/
|
||||
public static MappedJwtClaimSetConverter withDefaults
|
||||
(Map<String, Converter<Object, ?>> claimConverters) {
|
||||
Assert.notNull(claimConverters, "claimConverters cannot be null");
|
||||
public static MappedJwtClaimSetConverter withDefaults(Map<String, Converter<Object, ?>> claimTypeConverters) {
|
||||
Assert.notNull(claimTypeConverters, "claimTypeConverters cannot be null");
|
||||
|
||||
Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
|
||||
Converter<Object, ?> collectionStringConverter = getConverter(
|
||||
TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
|
||||
|
||||
Map<String, Converter<Object, ?>> claimNameToConverter = new HashMap<>();
|
||||
claimNameToConverter.put(JwtClaimNames.AUD, AUDIENCE_CONVERTER);
|
||||
claimNameToConverter.put(JwtClaimNames.EXP, TEMPORAL_CONVERTER);
|
||||
claimNameToConverter.put(JwtClaimNames.IAT, TEMPORAL_CONVERTER);
|
||||
claimNameToConverter.put(JwtClaimNames.ISS, ISSUER_CONVERTER);
|
||||
claimNameToConverter.put(JwtClaimNames.JTI, STRING_CONVERTER);
|
||||
claimNameToConverter.put(JwtClaimNames.NBF, TEMPORAL_CONVERTER);
|
||||
claimNameToConverter.put(JwtClaimNames.SUB, STRING_CONVERTER);
|
||||
claimNameToConverter.putAll(claimConverters);
|
||||
claimNameToConverter.put(JwtClaimNames.AUD, collectionStringConverter);
|
||||
claimNameToConverter.put(JwtClaimNames.EXP, MappedJwtClaimSetConverter::convertInstant);
|
||||
claimNameToConverter.put(JwtClaimNames.IAT, MappedJwtClaimSetConverter::convertInstant);
|
||||
claimNameToConverter.put(JwtClaimNames.ISS, MappedJwtClaimSetConverter::convertIssuer);
|
||||
claimNameToConverter.put(JwtClaimNames.JTI, stringConverter);
|
||||
claimNameToConverter.put(JwtClaimNames.NBF, MappedJwtClaimSetConverter::convertInstant);
|
||||
claimNameToConverter.put(JwtClaimNames.SUB, stringConverter);
|
||||
claimNameToConverter.putAll(claimTypeConverters);
|
||||
|
||||
return new MappedJwtClaimSetConverter(claimNameToConverter);
|
||||
}
|
||||
|
||||
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
|
||||
return source -> CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
|
||||
}
|
||||
|
||||
private static Instant convertInstant(Object source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
Instant result = (Instant) CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, INSTANT_TYPE_DESCRIPTOR);
|
||||
if (result == null) {
|
||||
throw new IllegalStateException("Could not coerce " + source + " into an Instant");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String convertIssuer(Object source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
URL result = (URL) CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, URL_TYPE_DESCRIPTOR);
|
||||
if (result != null) {
|
||||
return result.toExternalForm();
|
||||
}
|
||||
if (source instanceof String && ((String) source).contains(":")) {
|
||||
try {
|
||||
return new URI((String) source).toString();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Could not coerce " + source + " into a URI String", ex);
|
||||
}
|
||||
}
|
||||
return (String) CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, STRING_TYPE_DESCRIPTOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
|
@ -108,17 +146,10 @@ public final class MappedJwtClaimSetConverter
|
|||
public Map<String, Object> convert(Map<String, Object> claims) {
|
||||
Assert.notNull(claims, "claims cannot be null");
|
||||
|
||||
Map<String, Object> mappedClaims = new HashMap<>(claims);
|
||||
Map<String, Object> mappedClaims = this.delegate.convert(claims);
|
||||
|
||||
for (Map.Entry<String, Converter<Object, ?>> entry : this.claimConverters.entrySet()) {
|
||||
String claimName = entry.getKey();
|
||||
Converter<Object, ?> converter = entry.getValue();
|
||||
if (converter != null) {
|
||||
Object claim = claims.get(claimName);
|
||||
Object mappedClaim = converter.convert(claim);
|
||||
mappedClaims.compute(claimName, (key, value) -> mappedClaim);
|
||||
}
|
||||
}
|
||||
mappedClaims = removeClaims(mappedClaims);
|
||||
mappedClaims = addClaims(mappedClaims);
|
||||
|
||||
Instant issuedAt = (Instant) mappedClaims.get(JwtClaimNames.IAT);
|
||||
Instant expiresAt = (Instant) mappedClaims.get(JwtClaimNames.EXP);
|
||||
|
@ -129,100 +160,18 @@ public final class MappedJwtClaimSetConverter
|
|||
return mappedClaims;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces an <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-4.1.3">Audience</a> claim
|
||||
* into a {@link Collection<String>}, ignoring null values, and throwing an error if its coercion efforts fail.
|
||||
*/
|
||||
private static class AudienceConverter implements Converter<Object, Collection<String>> {
|
||||
|
||||
@Override
|
||||
public Collection<String> convert(Object source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (source instanceof Collection) {
|
||||
return ((Collection<?>) source).stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(Objects::toString)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return Arrays.asList(source.toString());
|
||||
}
|
||||
private Map<String, Object> removeClaims(Map<String, Object> claims) {
|
||||
return claims.entrySet().stream()
|
||||
.filter(e -> e.getValue() != null)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces an <a target="_blank" href="https://tools.ietf.org/html/rfc7519#section-4.1.1">Issuer</a> claim
|
||||
* into a {@link URL}, ignoring null values, and throwing an error if its coercion efforts fail.
|
||||
*/
|
||||
private static class IssuerConverter implements Converter<Object, String> {
|
||||
|
||||
@Override
|
||||
public String convert(Object source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (source instanceof URL) {
|
||||
return ((URL) source).toExternalForm();
|
||||
}
|
||||
|
||||
if (source instanceof String && ((String) source).contains(":")) {
|
||||
try {
|
||||
return URI.create((String) source).toString();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Could not coerce " + source + " into a URI String", e);
|
||||
}
|
||||
}
|
||||
|
||||
return source.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces a claim into an {@link Instant}, ignoring null values, and throwing an error
|
||||
* if its coercion efforts fail.
|
||||
*/
|
||||
private static class InstantConverter implements Converter<Object, Instant> {
|
||||
@Override
|
||||
public Instant convert(Object source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (source instanceof Instant) {
|
||||
return (Instant) source;
|
||||
}
|
||||
|
||||
if (source instanceof Date) {
|
||||
return ((Date) source).toInstant();
|
||||
}
|
||||
|
||||
if (source instanceof Number) {
|
||||
return Instant.ofEpochSecond(((Number) source).longValue());
|
||||
}
|
||||
|
||||
try {
|
||||
return Instant.ofEpochSecond(Long.parseLong(source.toString()));
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Could not coerce " + source + " into an Instant", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces a claim into a {@link String}, ignoring null values, and throwing an error if its
|
||||
* coercion efforts fail.
|
||||
*/
|
||||
private static class StringConverter implements Converter<Object, String> {
|
||||
@Override
|
||||
public String convert(Object source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return source.toString();
|
||||
}
|
||||
private Map<String, Object> addClaims(Map<String, Object> claims) {
|
||||
Map<String, Object> result = new HashMap<>(claims);
|
||||
this.claimTypeConverters.entrySet().stream()
|
||||
.filter(e -> !claims.containsKey(e.getKey()))
|
||||
.filter(e -> e.getValue().convert(null) != null)
|
||||
.forEach(e -> result.put(e.getKey(), e.getValue().convert(null)));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue