Explorar o código

Use id_token for user authentication

Fixes gh-4410
Joe Grandja %!s(int64=8) %!d(string=hai) anos
pai
achega
9cfb890207
Modificáronse 43 ficheiros con 1319 adicións e 574 borrados
  1. 1 0
      config/spring-security-config.gradle
  2. 66 8
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeAuthenticationFilterConfigurer.java
  3. 10 5
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java
  4. 6 6
      oauth2/jwt-jose/src/main/java/org/springframework/security/jwt/JwtClaimAccessor.java
  5. 2 1
      oauth2/oauth2-client/spring-security-oauth2-client.gradle
  6. 1 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilter.java
  7. 33 9
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java
  8. 12 4
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java
  9. 55 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/jwt/DefaultProviderJwtDecoderRegistry.java
  10. 13 2
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/jwt/ProviderJwtDecoderRegistry.java
  11. 17 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java
  12. 9 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationProperties.java
  13. 4 3
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/OAuth2UserService.java
  14. 0 57
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/AbstractOAuth2UserConverter.java
  15. 0 57
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/CustomOAuth2UserConverter.java
  16. 0 46
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/OAuth2UserConverter.java
  17. 157 63
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusOAuth2UserService.java
  18. 28 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractToken.java
  19. 6 6
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java
  20. 112 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/provider/DefaultProviderMetadata.java
  21. 14 15
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/provider/ProviderMetadata.java
  22. 6 34
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java
  23. 1 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2User.java
  24. 91 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2UserAuthority.java
  25. 42 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/Address.java
  26. 54 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdToken.java
  27. 54 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdTokenClaim.java
  28. 88 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdTokenClaimAccessor.java
  29. 5 4
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/StandardClaim.java
  30. 118 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/StandardClaimAccessor.java
  31. 69 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/UserInfo.java
  32. 30 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/endpoint/OidcParameter.java
  33. 1 1
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/package-info.java
  34. 79 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/DefaultOidcUser.java
  35. 17 63
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/OidcUser.java
  36. 96 0
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/OidcUserAuthority.java
  37. 1 1
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/package-info.java
  38. 0 154
      oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/DefaultUserInfo.java
  39. 7 1
      samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java
  40. 6 26
      samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2LoginAutoConfiguration.java
  41. 4 2
      samples/boot/oauth2login/src/main/java/sample/user/GitHubOAuth2User.java
  42. 1 4
      samples/boot/oauth2login/src/main/resources/META-INF/oauth2-clients-defaults.yml
  43. 3 2
      samples/boot/oauth2login/src/main/resources/application.yml

+ 1 - 0
config/spring-security-config.gradle

@@ -13,6 +13,7 @@ dependencies {
 	optional project(':spring-security-ldap')
 	optional project(':spring-security-messaging')
 	optional project(':spring-security-oauth2-client')
+	optional project(':spring-security-jwt-jose')
 	optional project(':spring-security-openid')
 	optional project(':spring-security-web')
 	optional project(':spring-security-webflux')

+ 66 - 8
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeAuthenticationFilterConfigurer.java

@@ -15,25 +15,34 @@
  */
 package org.springframework.security.config.annotation.web.configurers.oauth2.client;
 
-import org.springframework.http.client.ClientHttpResponse;
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
 import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
+import org.springframework.security.jwt.JwtDecoder;
+import org.springframework.security.jwt.nimbus.NimbusJwtDecoderJwkSupport;
 import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter;
 import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProvider;
 import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
 import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger;
 import org.springframework.security.oauth2.client.authentication.nimbus.NimbusAuthorizationCodeTokenExchanger;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
 import org.springframework.security.oauth2.client.user.OAuth2UserService;
 import org.springframework.security.oauth2.client.user.nimbus.NimbusOAuth2UserService;
+import org.springframework.security.oauth2.client.authentication.jwt.DefaultProviderJwtDecoderRegistry;
+import org.springframework.security.oauth2.core.provider.DefaultProviderMetadata;
+import org.springframework.security.oauth2.client.authentication.jwt.ProviderJwtDecoderRegistry;
+import org.springframework.security.oauth2.core.provider.ProviderMetadata;
 import org.springframework.security.oauth2.core.user.OAuth2User;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.UriComponentsBuilder;
 
+import java.net.MalformedURLException;
 import java.net.URI;
+import java.net.URL;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.function.Function;
 
 /**
  * @author Joe Grandja
@@ -43,7 +52,8 @@ final class AuthorizationCodeAuthenticationFilterConfigurer<H extends HttpSecuri
 
 	private AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger;
 	private OAuth2UserService userInfoService;
-	private Map<URI, Function<ClientHttpResponse, ? extends OAuth2User>> userInfoTypeConverters = new HashMap<>();
+	private Map<URI, Class<? extends OAuth2User>> customUserTypes = new HashMap<>();
+	private Map<URI, String> userNameAttributeNames = new HashMap<>();
 
 
 	AuthorizationCodeAuthenticationFilterConfigurer() {
@@ -71,10 +81,17 @@ final class AuthorizationCodeAuthenticationFilterConfigurer<H extends HttpSecuri
 		return this;
 	}
 
-	AuthorizationCodeAuthenticationFilterConfigurer<H> userInfoTypeConverter(Function<ClientHttpResponse, ? extends OAuth2User> userInfoConverter, URI userInfoUri) {
-		Assert.notNull(userInfoConverter, "userInfoConverter cannot be null");
+	AuthorizationCodeAuthenticationFilterConfigurer<H> customUserType(Class<? extends OAuth2User> customUserType, URI userInfoUri) {
+		Assert.notNull(customUserType, "customUserType cannot be null");
 		Assert.notNull(userInfoUri, "userInfoUri cannot be null");
-		this.userInfoTypeConverters.put(userInfoUri, userInfoConverter);
+		this.customUserTypes.put(userInfoUri, customUserType);
+		return this;
+	}
+
+	AuthorizationCodeAuthenticationFilterConfigurer<H> userNameAttributeName(String userNameAttributeName, URI userInfoUri) {
+		Assert.hasText(userNameAttributeName, "userNameAttributeName cannot be empty");
+		Assert.notNull(userInfoUri, "userInfoUri cannot be null");
+		this.userNameAttributeNames.put(userInfoUri, userNameAttributeName);
 		return this;
 	}
 
@@ -89,7 +106,7 @@ final class AuthorizationCodeAuthenticationFilterConfigurer<H extends HttpSecuri
 	@Override
 	public void init(H http) throws Exception {
 		AuthorizationCodeAuthenticationProvider authenticationProvider = new AuthorizationCodeAuthenticationProvider(
-				this.getAuthorizationCodeTokenExchanger(), this.getUserInfoService());
+				this.getAuthorizationCodeTokenExchanger(), this.getProviderJwtDecoderRegistry(), this.getUserInfoService());
 		authenticationProvider = this.postProcess(authenticationProvider);
 		http.authenticationProvider(authenticationProvider);
 		super.init(http);
@@ -114,10 +131,51 @@ final class AuthorizationCodeAuthenticationFilterConfigurer<H extends HttpSecuri
 		return this.authorizationCodeTokenExchanger;
 	}
 
+	private ProviderJwtDecoderRegistry getProviderJwtDecoderRegistry() {
+		Map<ProviderMetadata, JwtDecoder> jwtDecoders = new HashMap<>();
+		ClientRegistrationRepository clientRegistrationRepository = OAuth2LoginConfigurer.getClientRegistrationRepository(this.getBuilder());
+		clientRegistrationRepository.getRegistrations().stream().forEach(registration -> {
+			ClientRegistration.ProviderDetails providerDetails = registration.getProviderDetails();
+			if (StringUtils.hasText(providerDetails.getJwkSetUri())) {
+				DefaultProviderMetadata providerMetadata = new DefaultProviderMetadata();
+				// Default the Issuer to the host of the Authorization Endpoint
+				providerMetadata.setIssuer(this.toURL(
+					UriComponentsBuilder
+						.fromHttpUrl(providerDetails.getAuthorizationUri())
+						.replacePath(null)
+						.toUriString()
+				));
+				providerMetadata.setAuthorizationEndpoint(this.toURL(providerDetails.getAuthorizationUri()));
+				providerMetadata.setTokenEndpoint(this.toURL(providerDetails.getTokenUri()));
+				providerMetadata.setUserInfoEndpoint(this.toURL(providerDetails.getUserInfoUri()));
+				providerMetadata.setJwkSetUri(this.toURL(providerDetails.getJwkSetUri()));
+				jwtDecoders.put(providerMetadata, new NimbusJwtDecoderJwkSupport(providerDetails.getJwkSetUri()));
+			}
+		});
+		return new DefaultProviderJwtDecoderRegistry(jwtDecoders);
+	}
+
 	private OAuth2UserService getUserInfoService() {
 		if (this.userInfoService == null) {
-			this.userInfoService = new NimbusOAuth2UserService(this.userInfoTypeConverters);
+			this.userInfoService = new NimbusOAuth2UserService();
+			if (!this.customUserTypes.isEmpty()) {
+				((NimbusOAuth2UserService)this.userInfoService).setCustomUserTypes(this.customUserTypes);
+			}
+			if (!this.userNameAttributeNames.isEmpty()) {
+				((NimbusOAuth2UserService)this.userInfoService).setUserNameAttributeNames(this.userNameAttributeNames);
+			}
 		}
 		return this.userInfoService;
 	}
+
+	private URL toURL(String urlStr) {
+		if (!StringUtils.hasText(urlStr)) {
+			return null;
+		}
+		try {
+			return new URL(urlStr);
+		} catch (MalformedURLException ex) {
+			throw new IllegalArgumentException("Failed to convert '" + urlStr + "' to a URL: " + ex.getMessage(), ex);
+		}
+	}
 }

+ 10 - 5
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java

@@ -16,7 +16,6 @@
 package org.springframework.security.config.annotation.web.configurers.oauth2.client;
 
 import org.springframework.context.ApplicationContext;
-import org.springframework.http.client.ClientHttpResponse;
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
 import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
 import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
@@ -35,7 +34,6 @@ import org.springframework.util.CollectionUtils;
 import java.net.URI;
 import java.util.Arrays;
 import java.util.Map;
-import java.util.function.Function;
 import java.util.stream.Collectors;
 
 /**
@@ -95,10 +93,17 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>> exten
 			return this.and();
 		}
 
-		public OAuth2LoginConfigurer<B> userInfoTypeConverter(Function<ClientHttpResponse, ? extends OAuth2User> userInfoConverter, URI userInfoUri) {
-			Assert.notNull(userInfoConverter, "userInfoConverter cannot be null");
+		public OAuth2LoginConfigurer<B> customUserType(Class<? extends OAuth2User> customUserType, URI userInfoUri) {
+			Assert.notNull(customUserType, "customUserType cannot be null");
 			Assert.notNull(userInfoUri, "userInfoUri cannot be null");
-			OAuth2LoginConfigurer.this.authorizationCodeAuthenticationFilterConfigurer.userInfoTypeConverter(userInfoConverter, userInfoUri);
+			OAuth2LoginConfigurer.this.authorizationCodeAuthenticationFilterConfigurer.customUserType(customUserType, userInfoUri);
+			return this.and();
+		}
+
+		public OAuth2LoginConfigurer<B> userNameAttributeName(String userNameAttributeName, URI userInfoUri) {
+			Assert.hasText(userNameAttributeName, "userNameAttributeName cannot be empty");
+			Assert.notNull(userInfoUri, "userInfoUri cannot be null");
+			OAuth2LoginConfigurer.this.authorizationCodeAuthenticationFilterConfigurer.userNameAttributeName(userNameAttributeName, userInfoUri);
 			return this.and();
 		}
 

+ 6 - 6
oauth2/jwt-jose/src/main/java/org/springframework/security/jwt/JwtClaimAccessor.java

@@ -17,7 +17,7 @@ package org.springframework.security.jwt;
 
 import org.springframework.security.oauth2.core.ClaimAccessor;
 
-import java.net.URI;
+import java.net.URL;
 import java.time.Instant;
 
 /**
@@ -33,17 +33,17 @@ import java.time.Instant;
  */
 public interface JwtClaimAccessor extends ClaimAccessor {
 
-	default URI getIssuer() {
-		return this.getClaimAsURI(JwtClaim.ISS);
+	default URL getIssuer() {
+		return this.getClaimAsURL(JwtClaim.ISS);
 	}
 
 	default String getSubject() {
 		return this.getClaimAsString(JwtClaim.SUB);
 	}
 
-	default String getAudience() {
-		// FIXME Should return String[]
-		return this.getClaimAsString(JwtClaim.AUD);
+	default String[] getAudience() {
+		// TODO Impl JwtClaim.AUD
+		return null;
 	}
 
 	default Instant getExpiresAt() {

+ 2 - 1
oauth2/oauth2-client/spring-security-oauth2-client.gradle

@@ -3,10 +3,11 @@ apply plugin: 'io.spring.convention.spring-module'
 dependencies {
 	compile project(':spring-security-core')
 	compile project(':spring-security-oauth2-core')
+	compile project(':spring-security-jwt-jose')
 	compile project(':spring-security-web')
 	compile springCoreDependency
-	compile 'com.nimbusds:oauth2-oidc-sdk'
 	compile 'org.springframework:spring-web'
+	compile 'com.nimbusds:oauth2-oidc-sdk'
 
 	provided 'javax.servlet:javax.servlet-api'
 }

+ 1 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilter.java

@@ -218,6 +218,7 @@ public class AuthorizationCodeAuthenticationProcessingFilter extends AbstractAut
 			this.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri());
 			this.tokenUri(clientRegistration.getProviderDetails().getTokenUri());
 			this.userInfoUri(clientRegistration.getProviderDetails().getUserInfoUri());
+			this.jwkSetUri(clientRegistration.getProviderDetails().getJwkSetUri());
 			this.clientName(clientRegistration.getClientName());
 			this.clientAlias(clientRegistration.getClientAlias());
 		}

+ 33 - 9
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java

@@ -22,10 +22,16 @@ import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
 import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
+import org.springframework.security.jwt.Jwt;
+import org.springframework.security.jwt.JwtDecoder;
+import org.springframework.security.oauth2.client.authentication.jwt.ProviderJwtDecoderRegistry;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.user.OAuth2UserService;
 import org.springframework.security.oauth2.core.AccessToken;
 import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
 import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.oauth2.oidc.core.IdToken;
+import org.springframework.security.oauth2.oidc.core.endpoint.OidcParameter;
 import org.springframework.util.Assert;
 
 import java.util.Collection;
@@ -68,16 +74,20 @@ import java.util.Collection;
  */
 public class AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
 	private final AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger;
+	private final ProviderJwtDecoderRegistry providerJwtDecoderRegistry;
 	private final OAuth2UserService userInfoService;
 	private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
 
 	public AuthorizationCodeAuthenticationProvider(
 			AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger,
+			ProviderJwtDecoderRegistry providerJwtDecoderRegistry,
 			OAuth2UserService userInfoService) {
 
 		Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null");
+		Assert.notNull(providerJwtDecoderRegistry, "providerJwtDecoderRegistry cannot be null");
 		Assert.notNull(userInfoService, "userInfoService cannot be null");
 		this.authorizationCodeTokenExchanger = authorizationCodeTokenExchanger;
+		this.providerJwtDecoderRegistry = providerJwtDecoderRegistry;
 		this.userInfoService = userInfoService;
 	}
 
@@ -85,6 +95,7 @@ public class AuthorizationCodeAuthenticationProvider implements AuthenticationPr
 	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
 		AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =
 				(AuthorizationCodeAuthenticationToken) authentication;
+		ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration();
 
 		TokenResponseAttributes tokenResponse =
 				this.authorizationCodeTokenExchanger.exchange(authorizationCodeAuthentication);
@@ -92,8 +103,20 @@ public class AuthorizationCodeAuthenticationProvider implements AuthenticationPr
 		AccessToken accessToken = new AccessToken(tokenResponse.getTokenType(),
 				tokenResponse.getTokenValue(), tokenResponse.getIssuedAt(),
 				tokenResponse.getExpiresAt(), tokenResponse.getScopes());
-		OAuth2AuthenticationToken accessTokenAuthentication = new OAuth2AuthenticationToken(
-				authorizationCodeAuthentication.getClientRegistration(), accessToken);
+
+		IdToken idToken = null;
+		if (tokenResponse.getAdditionalParameters().containsKey(OidcParameter.ID_TOKEN)) {
+			JwtDecoder jwtDecoder = this.providerJwtDecoderRegistry.getJwtDecoder(clientRegistration.getProviderDetails().getJwkSetUri());
+			if (jwtDecoder == null) {
+				throw new IllegalArgumentException("Unable to find a registered JwtDecoder for the provider '" + clientRegistration.getProviderDetails().getTokenUri() +
+					"'. Check to ensure you have configured the JwkSet URI property.");
+			}
+			Jwt jwt = jwtDecoder.decode((String)tokenResponse.getAdditionalParameters().get(OidcParameter.ID_TOKEN));
+			idToken = new IdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims());
+		}
+
+		OAuth2AuthenticationToken accessTokenAuthentication =
+			new OAuth2AuthenticationToken(clientRegistration, accessToken, idToken);
 		accessTokenAuthentication.setDetails(authorizationCodeAuthentication.getDetails());
 
 		OAuth2User user = this.userInfoService.loadUser(accessTokenAuthentication);
@@ -101,20 +124,21 @@ public class AuthorizationCodeAuthenticationProvider implements AuthenticationPr
 		Collection<? extends GrantedAuthority> authorities =
 				this.authoritiesMapper.mapAuthorities(user.getAuthorities());
 
-		OAuth2AuthenticationToken authenticationResult = new OAuth2AuthenticationToken(user, authorities,
-				accessTokenAuthentication.getClientRegistration(), accessTokenAuthentication.getAccessToken());
+		OAuth2AuthenticationToken authenticationResult = new OAuth2AuthenticationToken(
+			user, authorities, accessTokenAuthentication.getClientRegistration(),
+			accessTokenAuthentication.getAccessToken(), accessTokenAuthentication.getIdToken());
 		authenticationResult.setDetails(accessTokenAuthentication.getDetails());
 
 		return authenticationResult;
 	}
 
-	@Override
-	public boolean supports(Class<?> authentication) {
-		return AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
-	}
-
 	public final void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
 		Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null");
 		this.authoritiesMapper = authoritiesMapper;
 	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
+	}
 }

+ 12 - 4
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java

@@ -24,6 +24,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
 import org.springframework.security.oauth2.client.user.OAuth2UserService;
 import org.springframework.security.oauth2.core.AccessToken;
 import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.oauth2.oidc.core.IdToken;
 import org.springframework.util.Assert;
 
 import java.util.Collection;
@@ -33,7 +34,7 @@ import java.util.Collection;
  * that represents an <i>OAuth 2.0</i> {@link Authentication}.
  *
  * <p>
- * It associates an {@link OAuth2User}, {@link ClientRegistration} and an {@link AccessToken}.
+ * It associates an {@link OAuth2User}, {@link ClientRegistration}, {@link AccessToken} and optionally an {@link IdToken}.
  * This <code>Authentication</code> is considered <i>&quot;authenticated&quot;</i> if the {@link OAuth2User}
  * is provided in the respective constructor. This typically happens after the {@link OAuth2UserService}
  * retrieves the end-user's (resource owner) attributes from the <i>UserInfo Endpoint</i>.
@@ -43,19 +44,21 @@ import java.util.Collection;
  * @see OAuth2User
  * @see ClientRegistration
  * @see AccessToken
+ * @see IdToken
  */
 public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
 	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
 	private final OAuth2User principal;
 	private final ClientRegistration clientRegistration;
 	private final AccessToken accessToken;
+	private final IdToken idToken;
 
-	public OAuth2AuthenticationToken(ClientRegistration clientRegistration, AccessToken accessToken) {
-		this(null, AuthorityUtils.NO_AUTHORITIES, clientRegistration, accessToken);
+	public OAuth2AuthenticationToken(ClientRegistration clientRegistration, AccessToken accessToken, IdToken idToken) {
+		this(null, AuthorityUtils.NO_AUTHORITIES, clientRegistration, accessToken, idToken);
 	}
 
 	public OAuth2AuthenticationToken(OAuth2User principal, Collection<? extends GrantedAuthority> authorities,
-										ClientRegistration clientRegistration, AccessToken accessToken) {
+										ClientRegistration clientRegistration, AccessToken accessToken, IdToken idToken) {
 
 		super(authorities);
 		Assert.notNull(clientRegistration, "clientRegistration cannot be null");
@@ -63,6 +66,7 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
 		this.principal = principal;
 		this.clientRegistration = clientRegistration;
 		this.accessToken = accessToken;
+		this.idToken = idToken;
 		this.setAuthenticated(principal != null);
 	}
 
@@ -84,4 +88,8 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
 	public AccessToken getAccessToken() {
 		return this.accessToken;
 	}
+
+	public IdToken getIdToken() {
+		return this.idToken;
+	}
 }

+ 55 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/jwt/DefaultProviderJwtDecoderRegistry.java

@@ -0,0 +1,55 @@
+/*
+ * Copyright 2012-2017 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
+ *
+ *      http://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.client.authentication.jwt;
+
+import org.springframework.security.jwt.JwtDecoder;
+import org.springframework.security.oauth2.core.provider.ProviderMetadata;
+import org.springframework.util.Assert;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * The default implementation of a {@link ProviderJwtDecoderRegistry} that associates
+ * a {@link JwtDecoder} to a {@link ProviderMetadata}. The <code>ProviderMetadata</code>
+ * is matched against the <code>providerIdentifier</code> parameter passed to {@link #getJwtDecoder(String)}.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public class DefaultProviderJwtDecoderRegistry implements ProviderJwtDecoderRegistry {
+	private final Map<ProviderMetadata, JwtDecoder> jwtDecoders;
+
+	public DefaultProviderJwtDecoderRegistry(Map<ProviderMetadata, JwtDecoder> jwtDecoders) {
+		Assert.notNull(jwtDecoders, "jwtDecoders cannot be null");
+		this.jwtDecoders = Collections.unmodifiableMap(new HashMap<>(jwtDecoders));
+	}
+
+	@Override
+	public JwtDecoder getJwtDecoder(String providerIdentifier) {
+		Assert.hasText(providerIdentifier, "providerIdentifier cannot be empty");
+		Optional<ProviderMetadata> providerMetadataKey = this.jwtDecoders.keySet().stream().filter(providerMetadata ->
+			providerIdentifier.equals(providerMetadata.getIssuer().toString()) ||
+				providerIdentifier.equals(providerMetadata.getAuthorizationEndpoint().toString()) ||
+				providerIdentifier.equals(providerMetadata.getTokenEndpoint().toString()) ||
+				providerIdentifier.equals(providerMetadata.getUserInfoEndpoint().toString()) ||
+				providerIdentifier.equals(providerMetadata.getJwkSetUri().toString())
+		).findFirst();
+		return (providerMetadataKey.isPresent() ? this.jwtDecoders.get(providerMetadataKey.get()) : null);
+	}
+}

+ 13 - 2
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/package-info.java → oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/jwt/ProviderJwtDecoderRegistry.java

@@ -13,7 +13,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.springframework.security.oauth2.client.authentication.jwt;
+
+import org.springframework.security.jwt.JwtDecoder;
+
 /**
- * Support classes for converting to {@link org.springframework.security.oauth2.core.user.OAuth2User}.
+ * A registry for {@link JwtDecoder}'s that are associated to an <i>OAuth 2.0 Provider</i>.
+ *
+ * @author Joe Grandja
+ * @since 5.0
  */
-package org.springframework.security.oauth2.client.user.converter;
+public interface ProviderJwtDecoderRegistry {
+
+	JwtDecoder getJwtDecoder(String providerIdentifier);
+
+}

+ 17 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java

@@ -122,6 +122,7 @@ public class ClientRegistration {
 		private String authorizationUri;
 		private String tokenUri;
 		private String userInfoUri;
+		private String jwkSetUri;
 
 		protected ProviderDetails() {
 		}
@@ -149,6 +150,14 @@ public class ClientRegistration {
 		protected void setUserInfoUri(String userInfoUri) {
 			this.userInfoUri = userInfoUri;
 		}
+
+		public String getJwkSetUri() {
+			return this.jwkSetUri;
+		}
+
+		protected void setJwkSetUri(String jwkSetUri) {
+			this.jwkSetUri = jwkSetUri;
+		}
 	}
 
 	public static class Builder {
@@ -161,6 +170,7 @@ public class ClientRegistration {
 		protected String authorizationUri;
 		protected String tokenUri;
 		protected String userInfoUri;
+		protected String jwkSetUri;
 		protected String clientName;
 		protected String clientAlias;
 
@@ -180,6 +190,7 @@ public class ClientRegistration {
 			this.authorizationUri(clientRegistrationProperties.getAuthorizationUri());
 			this.tokenUri(clientRegistrationProperties.getTokenUri());
 			this.userInfoUri(clientRegistrationProperties.getUserInfoUri());
+			this.jwkSetUri(clientRegistrationProperties.getJwkSetUri());
 			this.clientName(clientRegistrationProperties.getClientName());
 			this.clientAlias(clientRegistrationProperties.getClientAlias());
 		}
@@ -227,6 +238,11 @@ public class ClientRegistration {
 			return this;
 		}
 
+		public Builder jwkSetUri(String jwkSetUri) {
+			this.jwkSetUri = jwkSetUri;
+			return this;
+		}
+
 		public Builder clientName(String clientName) {
 			this.clientName = clientName;
 			return this;
@@ -256,6 +272,7 @@ public class ClientRegistration {
 			providerDetails.setAuthorizationUri(this.authorizationUri);
 			providerDetails.setTokenUri(this.tokenUri);
 			providerDetails.setUserInfoUri(this.userInfoUri);
+			providerDetails.setJwkSetUri(this.jwkSetUri);
 			clientRegistration.setProviderDetails(providerDetails);
 
 			clientRegistration.setClientName(this.clientName);

+ 9 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationProperties.java

@@ -42,6 +42,7 @@ public class ClientRegistrationProperties {
 	private String authorizationUri;
 	private String tokenUri;
 	private String userInfoUri;
+	private String jwkSetUri;
 	private String clientName;
 	private String clientAlias;
 
@@ -118,6 +119,14 @@ public class ClientRegistrationProperties {
 		this.userInfoUri = userInfoUri;
 	}
 
+	public String getJwkSetUri() {
+		return this.jwkSetUri;
+	}
+
+	public void setJwkSetUri(String jwkSetUri) {
+		this.jwkSetUri = jwkSetUri;
+	}
+
 	public String getClientName() {
 		return this.clientName;
 	}

+ 4 - 3
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/OAuth2UserService.java

@@ -19,20 +19,21 @@ import org.springframework.security.core.AuthenticatedPrincipal;
 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
 import org.springframework.security.oauth2.core.user.OAuth2User;
-import org.springframework.security.oauth2.oidc.user.UserInfo;
+import org.springframework.security.oauth2.oidc.core.UserInfo;
+import org.springframework.security.oauth2.oidc.core.user.OidcUser;
 
 /**
  * Implementations of this interface are responsible for obtaining
  * the end-user's (resource owner) attributes from the <i>UserInfo Endpoint</i>
  * using the provided {@link OAuth2AuthenticationToken#getAccessToken()}
- * and returning an {@link AuthenticatedPrincipal} in the form of an {@link OAuth2User}
- * (for a standard <i>OAuth 2.0 Provider</i>) or {@link UserInfo} (for an <i>OpenID Connect 1.0 Provider</i>).
+ * and returning an {@link AuthenticatedPrincipal} in the form of an {@link OAuth2User}.
  *
  * @author Joe Grandja
  * @since 5.0
  * @see OAuth2AuthenticationToken
  * @see AuthenticatedPrincipal
  * @see OAuth2User
+ * @see OidcUser
  * @see UserInfo
  */
 public interface OAuth2UserService {

+ 0 - 57
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/AbstractOAuth2UserConverter.java

@@ -1,57 +0,0 @@
-/*
- * Copyright 2012-2017 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
- *
- *      http://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.client.user.converter;
-
-import org.springframework.http.client.ClientHttpResponse;
-import org.springframework.http.converter.HttpMessageConverter;
-import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
-import org.springframework.security.oauth2.core.user.OAuth2User;
-
-import java.io.IOException;
-import java.util.Map;
-import java.util.function.Function;
-
-/**
- * Base implementation of a <code>Function</code> that converts a {@link ClientHttpResponse}
- * to a specific type of {@link OAuth2User}.
- *
- * @author Joe Grandja
- * @since 5.0
- * @see OAuth2User
- * @see ClientHttpResponse
- */
-public abstract class AbstractOAuth2UserConverter<R extends OAuth2User> implements Function<ClientHttpResponse, R> {
-	private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
-
-	protected AbstractOAuth2UserConverter() {
-	}
-
-	@Override
-	public final R apply(ClientHttpResponse clientHttpResponse) {
-		Map<String, Object> userAttributes;
-
-		try {
-			userAttributes = (Map<String, Object>) this.jackson2HttpMessageConverter.read(Map.class, clientHttpResponse);
-		} catch (IOException ex) {
-			throw new IllegalArgumentException("An error occurred reading the UserInfo response: " + ex.getMessage(), ex);
-		}
-
-		return this.apply(userAttributes);
-	}
-
-	protected abstract R apply(Map<String, Object> userAttributes);
-
-}

+ 0 - 57
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/CustomOAuth2UserConverter.java

@@ -1,57 +0,0 @@
-/*
- * Copyright 2012-2017 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
- *
- *      http://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.client.user.converter;
-
-import org.springframework.http.client.ClientHttpResponse;
-import org.springframework.http.converter.HttpMessageConverter;
-import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
-import org.springframework.security.oauth2.core.user.OAuth2User;
-import org.springframework.util.Assert;
-
-import java.io.IOException;
-import java.util.function.Function;
-
-/**
- * A <code>Function</code> that converts a {@link ClientHttpResponse}
- * to a custom type of {@link OAuth2User}, as supplied via the constructor.
- *
- * @author Joe Grandja
- * @since 5.0
- * @see OAuth2User
- * @see ClientHttpResponse
- */
-public final class CustomOAuth2UserConverter<R extends OAuth2User> implements Function<ClientHttpResponse, R> {
-	private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
-	private final Class<R> customType;
-
-	public CustomOAuth2UserConverter(Class<R> customType) {
-		Assert.notNull(customType, "customType cannot be null");
-		this.customType = customType;
-	}
-
-	@Override
-	public R apply(ClientHttpResponse clientHttpResponse) {
-		R user;
-
-		try {
-			user = (R) this.jackson2HttpMessageConverter.read(this.customType, clientHttpResponse);
-		} catch (IOException ex) {
-			throw new IllegalArgumentException("An error occurred reading the UserInfo response: " + ex.getMessage(), ex);
-		}
-
-		return user;
-	}
-}

+ 0 - 46
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/OAuth2UserConverter.java

@@ -1,46 +0,0 @@
-/*
- * Copyright 2012-2017 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
- *
- *      http://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.client.user.converter;
-
-import org.springframework.http.client.ClientHttpResponse;
-import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
-import org.springframework.security.oauth2.core.user.OAuth2User;
-import org.springframework.util.Assert;
-
-import java.util.Map;
-
-/**
- * An implementation of a {@link AbstractOAuth2UserConverter} that converts
- * a {@link ClientHttpResponse} to a {@link OAuth2User}.
- *
- * @author Joe Grandja
- * @since 5.0
- * @see OAuth2User
- * @see ClientHttpResponse
- */
-public final class OAuth2UserConverter extends AbstractOAuth2UserConverter<OAuth2User> {
-	private final String nameAttributeKey;
-
-	public OAuth2UserConverter(String nameAttributeKey) {
-		Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty");
-		this.nameAttributeKey = nameAttributeKey;
-	}
-
-	@Override
-	protected OAuth2User apply(Map<String, Object> userAttributes) {
-		return new DefaultOAuth2User(userAttributes, this.nameAttributeKey);
-	}
-}

+ 157 - 63
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusOAuth2UserService.java

@@ -22,112 +22,206 @@ import com.nimbusds.oauth2.sdk.http.HTTPResponse;
 import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
 import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
 import com.nimbusds.openid.connect.sdk.UserInfoRequest;
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.PropertyAccessorFactory;
 import org.springframework.http.MediaType;
-import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
 import org.springframework.security.authentication.AuthenticationServiceException;
-import org.springframework.security.core.AuthenticatedPrincipal;
+import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.user.OAuth2UserService;
 import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
 import org.springframework.security.oauth2.core.user.OAuth2User;
-import org.springframework.security.oauth2.oidc.user.UserInfo;
+import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
+import org.springframework.security.oauth2.oidc.core.UserInfo;
+import org.springframework.security.oauth2.oidc.core.user.DefaultOidcUser;
+import org.springframework.security.oauth2.oidc.core.user.OidcUser;
+import org.springframework.security.oauth2.oidc.core.user.OidcUserAuthority;
 import org.springframework.util.Assert;
 
 import java.io.IOException;
 import java.net.URI;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.Function;
+import java.util.*;
 
 /**
  * An implementation of an {@link OAuth2UserService} that uses the <b>Nimbus OAuth 2.0 SDK</b> internally.
  *
  * <p>
- * This implementation uses a <code>Map</code> of converter's <i>keyed</i> by <code>URI</code>.
- * The <code>URI</code> represents the <i>UserInfo Endpoint</i> address and the mapped <code>Function</code>
- * is capable of converting the <i>UserInfo Response</i> to either an
- * {@link OAuth2User} (for a standard <i>OAuth 2.0 Provider</i>) or
- * {@link UserInfo} (for an <i>OpenID Connect 1.0 Provider</i>).
+ * This implementation may be configured with a <code>Map</code> of custom {@link OAuth2User} types
+ * <i>keyed</i> by <code>URI</code>, which represents the <i>UserInfo Endpoint</i> address.
+ *
+ * <p>
+ * For {@link OAuth2User}'s registered at a standard <i>OAuth 2.0 Provider</i>, the attribute name
+ * for the &quot;user's name&quot; is required. This can be supplied via {@link #setUserNameAttributeNames(Map)},
+ * <i>keyed</i> by <code>URI</code>, which represents the <i>UserInfo Endpoint</i> address.
  *
  * @author Joe Grandja
  * @since 5.0
  * @see OAuth2AuthenticationToken
- * @see AuthenticatedPrincipal
  * @see OAuth2User
+ * @see OidcUser
  * @see UserInfo
  * @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
  */
 public class NimbusOAuth2UserService implements OAuth2UserService {
 	private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
-	private final Map<URI, Function<ClientHttpResponse, ? extends OAuth2User>> userInfoTypeConverters;
+	private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
+	private Map<URI, String> userNameAttributeNames = Collections.unmodifiableMap(Collections.emptyMap());
+	private Map<URI, Class<? extends OAuth2User>> customUserTypes = Collections.unmodifiableMap(Collections.emptyMap());
 
-	public NimbusOAuth2UserService(Map<URI, Function<ClientHttpResponse, ? extends OAuth2User>> userInfoTypeConverters) {
-		Assert.notEmpty(userInfoTypeConverters, "userInfoTypeConverters cannot be empty");
-		this.userInfoTypeConverters = new HashMap<>(userInfoTypeConverters);
+	public NimbusOAuth2UserService() {
 	}
 
 	@Override
-	public OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
+	public final OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
+		URI userInfoUri = this.getUserInfoUri(token);
+
+		if (this.getCustomUserTypes().containsKey(userInfoUri)) {
+			return this.loadCustomUser(token);
+		}
+		if (token.getIdToken() != null) {
+			return this.loadOidcUser(token);
+		}
+
+		return this.loadOAuth2User(token);
+	}
+
+	protected OAuth2User loadOidcUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
+		// TODO Retrieving the UserInfo should be optional. Need to add the capability for opting in/out
+		Map<String, Object> userAttributes = this.getUserInfo(token);
+		UserInfo userInfo = new UserInfo(userAttributes);
+
+		GrantedAuthority authority = new OidcUserAuthority(token.getIdToken(), userInfo);
+		Set<GrantedAuthority> authorities = new HashSet<>();
+		authorities.add(authority);
+
+		return new DefaultOidcUser(authorities, token.getIdToken(), userInfo);
+	}
+
+	protected OAuth2User loadOAuth2User(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
+		URI userInfoUri = this.getUserInfoUri(token);
+		if (!this.getUserNameAttributeNames().containsKey(userInfoUri)) {
+			throw new IllegalArgumentException("The attribute name for the \"user's name\" is required for the OAuth2User " +
+				" retrieved from the UserInfo Endpoint -> " + userInfoUri.toString());
+		}
+		String userNameAttributeName = this.getUserNameAttributeNames().get(userInfoUri);
+
+		Map<String, Object> userAttributes = this.getUserInfo(token);
+
+		GrantedAuthority authority = new OAuth2UserAuthority(userAttributes);
+		Set<GrantedAuthority> authorities = new HashSet<>();
+		authorities.add(authority);
+
+		return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
+	}
+
+	protected OAuth2User loadCustomUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
+		URI userInfoUri = this.getUserInfoUri(token);
+		Class<? extends OAuth2User> customUserType = this.getCustomUserTypes().get(userInfoUri);
+
 		OAuth2User user;
+		try {
+			user = customUserType.newInstance();
+		} catch (ReflectiveOperationException ex) {
+			throw new IllegalArgumentException("An error occurred while attempting to instantiate the custom OAuth2User \"" +
+				customUserType.getName() + "\" -> " + ex.getMessage(), ex);
+		}
+
+		Map<String, Object> userAttributes = this.getUserInfo(token);
+		if (token.getIdToken() != null) {
+			userAttributes.putAll(token.getIdToken().getClaims());
+		}
+
+		BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(user);
+		wrapper.setAutoGrowNestedPaths(true);
+		wrapper.setPropertyValues(userAttributes);
+
+		return user;
+	}
+
+	protected Map<String, Object> getUserInfo(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
+		URI userInfoUri = this.getUserInfoUri(token);
+
+		BearerAccessToken accessToken = new BearerAccessToken(token.getAccessToken().getTokenValue());
+
+		UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken);
+		HTTPRequest httpRequest = userInfoRequest.toHTTPRequest();
+		httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE);
+		HTTPResponse httpResponse;
 
 		try {
-			ClientRegistration clientRegistration = token.getClientRegistration();
+			httpResponse = httpRequest.send();
+		} catch (IOException ex) {
+			throw new AuthenticationServiceException("An error occurred while sending the UserInfo Request: " +
+				ex.getMessage(), ex);
+		}
 
-			URI userInfoUri;
+		if (httpResponse.getStatusCode() != HTTPResponse.SC_OK) {
+			UserInfoErrorResponse userInfoErrorResponse;
 			try {
-				userInfoUri = new URI(clientRegistration.getProviderDetails().getUserInfoUri());
-			} catch (Exception ex) {
-				throw new IllegalArgumentException("An error occurred parsing the userInfo URI: " +
-					clientRegistration.getProviderDetails().getUserInfoUri(), ex);
+				userInfoErrorResponse = UserInfoErrorResponse.parse(httpResponse);
+			} catch (ParseException ex) {
+				OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
+					"An error occurred parsing the UserInfo Error response: " + ex.getMessage(), null);
+				throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
 			}
-
-			Function<ClientHttpResponse, ? extends OAuth2User> userInfoConverter = this.userInfoTypeConverters.get(userInfoUri);
-			if (userInfoConverter == null) {
-				throw new IllegalArgumentException("There is no available User Info converter for " + userInfoUri.toString());
+			ErrorObject errorObject = userInfoErrorResponse.getErrorObject();
+
+			StringBuilder errorDescription = new StringBuilder();
+			errorDescription.append("An error occurred while attempting to access the UserInfo Endpoint -> ");
+			errorDescription.append("Error details: [");
+			errorDescription.append("UserInfo Uri: ").append(userInfoUri.toString());
+			errorDescription.append(", Http Status: ").append(errorObject.getHTTPStatusCode());
+			if (errorObject.getCode() != null) {
+				errorDescription.append(", Error Code: ").append(errorObject.getCode());
 			}
-
-			BearerAccessToken accessToken = new BearerAccessToken(token.getAccessToken().getTokenValue());
-
-			// Request the User Info
-			UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken);
-			HTTPRequest httpRequest = userInfoRequest.toHTTPRequest();
-			httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE);
-			HTTPResponse httpResponse = httpRequest.send();
-
-			if (httpResponse.getStatusCode() != HTTPResponse.SC_OK) {
-				UserInfoErrorResponse userInfoErrorResponse = UserInfoErrorResponse.parse(httpResponse);
-				ErrorObject errorObject = userInfoErrorResponse.getErrorObject();
-
-				StringBuilder errorDescription = new StringBuilder();
-				errorDescription.append("An error occurred while attempting to access the UserInfo Endpoint -> ");
-				errorDescription.append("Error details: [");
-				errorDescription.append("UserInfo Uri: ").append(userInfoUri.toString());
-				errorDescription.append(", Http Status: ").append(errorObject.getHTTPStatusCode());
-				if (errorObject.getCode() != null) {
-					errorDescription.append(", Error Code: ").append(errorObject.getCode());
-				}
-				if (errorObject.getDescription() != null) {
-					errorDescription.append(", Error Description: ").append(errorObject.getDescription());
-				}
-				errorDescription.append("]");
-
-				OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, errorDescription.toString(), null);
-				throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+			if (errorObject.getDescription() != null) {
+				errorDescription.append(", Error Description: ").append(errorObject.getDescription());
 			}
+			errorDescription.append("]");
 
-			user = userInfoConverter.apply(new NimbusClientHttpResponse(httpResponse));
+			OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, errorDescription.toString(), null);
+			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+		}
 
-		} catch (ParseException ex) {
-			// This error occurs if the User Info Response is not well-formed or invalid
-			throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE), ex);
+		try {
+			return (Map<String, Object>) this.jackson2HttpMessageConverter.read(Map.class, new NimbusClientHttpResponse(httpResponse));
 		} catch (IOException ex) {
-			// This error occurs when there is a network-related issue
-			throw new AuthenticationServiceException("An error occurred while sending the User Info Request: " +
-				ex.getMessage(), ex);
+			OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
+				"An error occurred reading the UserInfo Success response: " + ex.getMessage(), null);
+			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
 		}
+	}
 
-		return user;
+	protected Map<URI, String> getUserNameAttributeNames() {
+		return this.userNameAttributeNames;
+	}
+
+	public final void setUserNameAttributeNames(Map<URI, String> userNameAttributeNames) {
+		Assert.notEmpty(userNameAttributeNames, "userNameAttributeNames cannot be empty");
+		this.userNameAttributeNames = Collections.unmodifiableMap(new HashMap<>(userNameAttributeNames));
+	}
+
+	protected Map<URI, Class<? extends OAuth2User>> getCustomUserTypes() {
+		return this.customUserTypes;
+	}
+
+	public final void setCustomUserTypes(Map<URI, Class<? extends OAuth2User>> customUserTypes) {
+		Assert.notEmpty(customUserTypes, "customUserTypes cannot be empty");
+		this.customUserTypes = Collections.unmodifiableMap(new HashMap<>(customUserTypes));
+	}
+
+	private URI getUserInfoUri(OAuth2AuthenticationToken token) {
+		ClientRegistration clientRegistration = token.getClientRegistration();
+		try {
+			return new URI(clientRegistration.getProviderDetails().getUserInfoUri());
+		} catch (Exception ex) {
+			throw new IllegalArgumentException("An error occurred parsing the UserInfo URI: " +
+				clientRegistration.getProviderDetails().getUserInfoUri(), ex);
+		}
 	}
 }

+ 28 - 0
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractToken.java

@@ -56,4 +56,32 @@ public abstract class AbstractToken implements Serializable {
 	public Instant getExpiresAt() {
 		return this.expiresAt;
 	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null || this.getClass() != obj.getClass()) {
+			return false;
+		}
+
+		AbstractToken that = (AbstractToken) obj;
+
+		if (!this.getTokenValue().equals(that.getTokenValue())) {
+			return false;
+		}
+		if (!this.getIssuedAt().equals(that.getIssuedAt())) {
+			return false;
+		}
+		return this.getExpiresAt().equals(that.getExpiresAt());
+	}
+
+	@Override
+	public int hashCode() {
+		int result = this.getTokenValue().hashCode();
+		result = 31 * result + this.getIssuedAt().hashCode();
+		result = 31 * result + this.getExpiresAt().hashCode();
+		return result;
+	}
 }

+ 6 - 6
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java

@@ -17,8 +17,8 @@ package org.springframework.security.oauth2.core;
 
 import org.springframework.util.Assert;
 
-import java.net.URI;
-import java.net.URISyntaxException;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.time.Instant;
 import java.util.Map;
 
@@ -56,14 +56,14 @@ public interface ClaimAccessor {
 		}
 	}
 
-	default URI getClaimAsURI(String claim) {
+	default URL getClaimAsURL(String claim) {
 		if (!this.containsClaim(claim)) {
 			return null;
 		}
 		try {
-			return new URI(this.getClaimAsString(claim));
-		} catch (URISyntaxException ex) {
-			throw new IllegalArgumentException("Unable to convert claim '" + claim + "' to URI: " + ex.getMessage(), ex);
+			return new URL(this.getClaimAsString(claim));
+		} catch (MalformedURLException ex) {
+			throw new IllegalArgumentException("Unable to convert claim '" + claim + "' to URL: " + ex.getMessage(), ex);
 		}
 	}
 }

+ 112 - 0
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/provider/DefaultProviderMetadata.java

@@ -0,0 +1,112 @@
+/*
+ * Copyright 2012-2017 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
+ *
+ *      http://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.provider;
+
+import java.net.URL;
+
+/**
+ * Default implementation of {@link ProviderMetadata}.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public class DefaultProviderMetadata implements ProviderMetadata {
+	private URL issuer;
+	private URL authorizationEndpoint;
+	private URL tokenEndpoint;
+	private URL userInfoEndpoint;
+	private URL jwkSetUri;
+
+	public DefaultProviderMetadata() {
+	}
+
+	@Override
+	public URL getIssuer() {
+		return issuer;
+	}
+
+	public void setIssuer(URL issuer) {
+		this.issuer = issuer;
+	}
+
+	@Override
+	public URL getAuthorizationEndpoint() {
+		return authorizationEndpoint;
+	}
+
+	public void setAuthorizationEndpoint(URL authorizationEndpoint) {
+		this.authorizationEndpoint = authorizationEndpoint;
+	}
+
+	@Override
+	public URL getTokenEndpoint() {
+		return tokenEndpoint;
+	}
+
+	public void setTokenEndpoint(URL tokenEndpoint) {
+		this.tokenEndpoint = tokenEndpoint;
+	}
+
+	@Override
+	public URL getUserInfoEndpoint() {
+		return userInfoEndpoint;
+	}
+
+	public void setUserInfoEndpoint(URL userInfoEndpoint) {
+		this.userInfoEndpoint = userInfoEndpoint;
+	}
+
+	@Override
+	public URL getJwkSetUri() {
+		return jwkSetUri;
+	}
+
+	public void setJwkSetUri(URL jwkSetUri) {
+		this.jwkSetUri = jwkSetUri;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null || this.getClass() != obj.getClass()) {
+			return false;
+		}
+
+		DefaultProviderMetadata that = (DefaultProviderMetadata) obj;
+
+		if (!this.getIssuer().equals(that.getIssuer())) {
+			return false;
+		}
+		if (!this.getAuthorizationEndpoint().equals(that.getAuthorizationEndpoint())) {
+			return false;
+		}
+		if (!this.getTokenEndpoint().equals(that.getTokenEndpoint())) {
+			return false;
+		}
+		return this.getUserInfoEndpoint().equals(that.getUserInfoEndpoint());
+	}
+
+	@Override
+	public int hashCode() {
+		int result = this.getIssuer().hashCode();
+		result = 31 * result + this.getAuthorizationEndpoint().hashCode();
+		result = 31 * result + this.getTokenEndpoint().hashCode();
+		result = 31 * result + this.getUserInfoEndpoint().hashCode();
+		return result;
+	}
+}

+ 14 - 15
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/UserInfoConverter.java → oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/provider/ProviderMetadata.java

@@ -13,27 +13,26 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.springframework.security.oauth2.client.user.converter;
+package org.springframework.security.oauth2.core.provider;
 
-import org.springframework.http.client.ClientHttpResponse;
-import org.springframework.security.oauth2.oidc.user.DefaultUserInfo;
-import org.springframework.security.oauth2.oidc.user.UserInfo;
-
-import java.util.Map;
+import java.net.URL;
 
 /**
- * An implementation of a {@link AbstractOAuth2UserConverter} that converts
- * a {@link ClientHttpResponse} to a {@link UserInfo}.
+ * Metadata describing the configuration information for an <i>OAuth 2.0 Provider</i>.
  *
  * @author Joe Grandja
  * @since 5.0
- * @see UserInfo
- * @see ClientHttpResponse
  */
-public final class UserInfoConverter extends AbstractOAuth2UserConverter<UserInfo> {
+public interface ProviderMetadata {
+
+	URL getIssuer();
+
+	URL getAuthorizationEndpoint();
+
+	URL getTokenEndpoint();
+
+	URL getUserInfoEndpoint();
+
+	URL getJwkSetUri();
 
-	@Override
-	protected UserInfo apply(Map<String, Object> userAttributes) {
-		return new DefaultUserInfo(userAttributes);
-	}
 }

+ 6 - 34
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java

@@ -18,9 +18,7 @@ package org.springframework.security.oauth2.core.user;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.SpringSecurityCoreVersion;
 import org.springframework.util.Assert;
-import org.springframework.util.CollectionUtils;
 
-import java.time.Instant;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -42,22 +40,18 @@ import java.util.stream.Collectors;
 public class DefaultOAuth2User implements OAuth2User {
 	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
 	private final Set<GrantedAuthority> authorities;
-	private final Map<String, Object> attributes;
+	private Map<String, Object> attributes;
 	private final String nameAttributeKey;
 
-	public DefaultOAuth2User(Map<String, Object> attributes, String nameAttributeKey) {
-		this(Collections.emptySet(), attributes, nameAttributeKey);
-	}
-
 	public DefaultOAuth2User(Set<GrantedAuthority> authorities, Map<String, Object> attributes, String nameAttributeKey) {
-		Assert.notNull(authorities, "authorities cannot be null");
+		Assert.notEmpty(authorities, "authorities cannot be empty");
 		Assert.notEmpty(attributes, "attributes cannot be empty");
 		Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty");
 		if (!attributes.containsKey(nameAttributeKey)) {
 			throw new IllegalArgumentException("Invalid nameAttributeKey: " + nameAttributeKey);
 		}
 		this.authorities = Collections.unmodifiableSet(this.sortAuthorities(authorities));
-		this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
+		this.setAttributes(attributes);
 		this.nameAttributeKey = nameAttributeKey;
 	}
 
@@ -76,37 +70,15 @@ public class DefaultOAuth2User implements OAuth2User {
 		return this.attributes;
 	}
 
-	protected String getAttributeAsString(String key) {
-		Object value = this.getAttributes().get(key);
-		return (value != null ? value.toString() : null);
-	}
-
-	protected Boolean getAttributeAsBoolean(String key) {
-		String value = this.getAttributeAsString(key);
-		return (value != null ? Boolean.valueOf(value) : null);
-	}
-
-	protected Instant getAttributeAsInstant(String key) {
-		String value = this.getAttributeAsString(key);
-		if (value == null) {
-			return null;
-		}
-		try {
-			return Instant.ofEpochSecond(Long.valueOf(value));
-		} catch (NumberFormatException ex) {
-			throw new IllegalArgumentException("Invalid long value: " + ex.getMessage(), ex);
-		}
+	protected final void setAttributes(Map<String, Object> attributes) {
+		Assert.notEmpty(attributes, "attributes cannot be empty");
+		this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
 	}
 
 	private Set<GrantedAuthority> sortAuthorities(Set<GrantedAuthority> authorities) {
-		if (CollectionUtils.isEmpty(authorities)) {
-			return Collections.emptySet();
-		}
-
 		SortedSet<GrantedAuthority> sortedAuthorities =
 			new TreeSet<>((g1, g2) -> g1.getAuthority().compareTo(g2.getAuthority()));
 		authorities.stream().forEach(sortedAuthorities::add);
-
 		return sortedAuthorities;
 	}
 

+ 1 - 0
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2User.java

@@ -52,4 +52,5 @@ public interface OAuth2User extends AuthenticatedPrincipal, Serializable {
 	Collection<? extends GrantedAuthority> getAuthorities();
 
 	Map<String, Object> getAttributes();
+
 }

+ 91 - 0
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2UserAuthority.java

@@ -0,0 +1,91 @@
+/*
+ * Copyright 2012-2017 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
+ *
+ *      http://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.user;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.util.Assert;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A {@link GrantedAuthority} that is associated with an {@link OAuth2User}.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see OAuth2User
+ */
+public class OAuth2UserAuthority implements GrantedAuthority {
+	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+	private final String authority;
+	private Map<String, Object> attributes;
+
+	public OAuth2UserAuthority(Map<String, Object> attributes) {
+		this("ROLE_USER", attributes);
+	}
+
+	public OAuth2UserAuthority(String authority, Map<String, Object> attributes) {
+		Assert.hasText(authority, "authority cannot be empty");
+		Assert.notEmpty(attributes, "attributes cannot be empty");
+		this.authority = authority;
+		this.setAttributes(attributes);
+	}
+
+	@Override
+	public String getAuthority() {
+		return this.authority;
+	}
+
+	public Map<String, Object> getAttributes() {
+		return this.attributes;
+	}
+
+	protected final void setAttributes(Map<String, Object> attributes) {
+		Assert.notEmpty(attributes, "attributes cannot be empty");
+		this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null || this.getClass() != obj.getClass()) {
+			return false;
+		}
+
+		OAuth2UserAuthority that = (OAuth2UserAuthority) obj;
+
+		if (!this.getAuthority().equals(that.getAuthority())) {
+			return false;
+		}
+		return this.getAttributes().equals(that.getAttributes());
+	}
+
+	@Override
+	public int hashCode() {
+		int result = this.getAuthority().hashCode();
+		result = 31 * result + this.getAttributes().hashCode();
+		return result;
+	}
+
+	@Override
+	public String toString() {
+		return this.getAuthority();
+	}
+}

+ 42 - 0
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/Address.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright 2012-2017 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
+ *
+ *      http://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.oidc.core;
+
+/**
+ * The Address Claim represents a physical mailing address defined by the <i>OpenID Connect Core 1.0</i> specification
+ * that can be returned either in the <i>UserInfo Response</i> or the <i>ID Token</i>.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#AddressClaim">Address Claim</a>
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse">UserInfo Response</a>
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
+ */
+public interface Address {
+
+	String getFormatted();
+
+	String getStreetAddress();
+
+	String getLocality();
+
+	String getRegion();
+
+	String getPostalCode();
+
+	String getCountry();
+
+}

+ 54 - 0
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdToken.java

@@ -0,0 +1,54 @@
+/*
+ * Copyright 2012-2017 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
+ *
+ *      http://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.oidc.core;
+
+import org.springframework.security.oauth2.core.AbstractToken;
+import org.springframework.util.Assert;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * An implementation of an {@link AbstractToken} representing an <i>OpenID Connect Core 1.0 ID Token</i>.
+ *
+ * <p>
+ * The <code>IdToken</code> is a security token that contains &quot;Claims&quot;
+ * about the authentication of an End-User by an Authorization Server.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see AbstractToken
+ * @see IdTokenClaimAccessor
+ * @see StandardClaimAccessor
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
+ */
+public class IdToken extends AbstractToken implements IdTokenClaimAccessor {
+	private final Map<String, Object> claims;
+
+	public IdToken(String tokenValue, Instant issuedAt, Instant expiresAt, Map<String, Object> claims) {
+		super(tokenValue, issuedAt, expiresAt);
+		Assert.notEmpty(claims, "claims cannot be empty");
+		this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
+	}
+
+	@Override
+	public Map<String, Object> getClaims() {
+		return this.claims;
+	}
+}

+ 54 - 0
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdTokenClaim.java

@@ -0,0 +1,54 @@
+/*
+ * Copyright 2012-2017 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
+ *
+ *      http://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.oidc.core;
+
+/**
+ * The &quot;Claims&quot; defined by the <i>OpenID Connect Core 1.0</i> specification
+ * that can be returned in the <i>ID Token</i>.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see IdToken
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
+ */
+
+public interface IdTokenClaim {
+
+	String ISS = "iss";
+
+	String SUB = "sub";
+
+	String AUD = "aud";
+
+	String EXP = "exp";
+
+	String IAT = "iat";
+
+	String AUTH_TIME = "auth_time";
+
+	String NONCE = "nonce";
+
+	String ACR = "acr";
+
+	String AMR = "amr";
+
+	String AZP = "azp";
+
+	String AT_HASH = "at_hash";
+
+	String C_HASH = "c_hash";
+
+}

+ 88 - 0
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdTokenClaimAccessor.java

@@ -0,0 +1,88 @@
+/*
+ * Copyright 2012-2017 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
+ *
+ *      http://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.oidc.core;
+
+import org.springframework.security.oauth2.core.ClaimAccessor;
+
+import java.net.URL;
+import java.time.Instant;
+
+/**
+ * A {@link ClaimAccessor} for the &quot;Claims&quot; that can be returned in the <i>ID Token</i>
+ * which provides information about the authentication of an End-User by an Authorization Server.
+ *
+ * @see ClaimAccessor
+ * @see StandardClaimAccessor
+ * @see StandardClaim
+ * @see IdTokenClaim
+ * @see IdToken
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public interface IdTokenClaimAccessor extends StandardClaimAccessor {
+
+	default URL getIssuer() {
+		return this.getClaimAsURL(IdTokenClaim.ISS);
+	}
+
+	default String getSubject() {
+		return this.getClaimAsString(IdTokenClaim.SUB);
+	}
+
+	default String[] getAudience() {
+		// TODO Impl IdTokenClaim.AUD
+		return null;
+	}
+
+	default Instant getExpiresAt() {
+		return this.getClaimAsInstant(IdTokenClaim.EXP);
+	}
+
+	default Instant getIssuedAt() {
+		return this.getClaimAsInstant(IdTokenClaim.IAT);
+	}
+
+	default Instant getAuthenticatedAt() {
+		return this.getClaimAsInstant(IdTokenClaim.AUTH_TIME);
+	}
+
+	default String getNonce() {
+		return this.getClaimAsString(IdTokenClaim.NONCE);
+	}
+
+	default String getAuthenticationContextClass() {
+		return this.getClaimAsString(IdTokenClaim.ACR);
+	}
+
+	default String[] getAuthenticationMethods() {
+		// TODO Impl IdTokenClaim.AMR
+		return null;
+	}
+
+	default String getAuthorizedParty() {
+		return this.getClaimAsString(IdTokenClaim.AZP);
+	}
+
+	default String getAccessTokenHash() {
+		return this.getClaimAsString(IdTokenClaim.AT_HASH);
+	}
+
+	default String getAuthorizationCodeHash() {
+		return this.getClaimAsString(IdTokenClaim.C_HASH);
+	}
+}

+ 5 - 4
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/StandardClaimName.java → oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/StandardClaim.java

@@ -13,11 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.springframework.security.oauth2.oidc;
+package org.springframework.security.oauth2.oidc.core;
 
 /**
- * The Standard Claims defined by the <i>OpenID Connect Core 1.0</i> specification
- * and returned in either the <i>UserInfo Response</i> or in the <i>ID Token</i>.
+ * The &quot;Standard Claims&quot; defined by the <i>OpenID Connect Core 1.0</i> specification
+ * that can be returned either in the <i>UserInfo Response</i> or the <i>ID Token</i>.
  *
  * @author Joe Grandja
  * @since 5.0
@@ -25,7 +25,7 @@ package org.springframework.security.oauth2.oidc;
  * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse">UserInfo Response</a>
  * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
  */
-public interface StandardClaimName {
+public interface StandardClaim {
 
 	String SUB = "sub";
 
@@ -66,4 +66,5 @@ public interface StandardClaimName {
 	String ADDRESS = "address";
 
 	String UPDATED_AT = "updated_at";
+
 }

+ 118 - 0
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/StandardClaimAccessor.java

@@ -0,0 +1,118 @@
+/*
+ * Copyright 2012-2017 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
+ *
+ *      http://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.oidc.core;
+
+import org.springframework.security.oauth2.core.ClaimAccessor;
+
+import java.time.Instant;
+
+/**
+ * A {@link ClaimAccessor} for the &quot;Standard Claims&quot; that can be returned
+ * either in the <i>UserInfo Response</i> or the <i>ID Token</i>.
+ *
+ * @see ClaimAccessor
+ * @see StandardClaim
+ * @see UserInfo
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse">UserInfo Response</a>
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
+ * @author Joe Grandja
+ * @since 5.0
+ */
+public interface StandardClaimAccessor extends ClaimAccessor {
+
+	default String getSubject() {
+		return this.getClaimAsString(StandardClaim.SUB);
+	}
+
+	default String getFullName() {
+		return this.getClaimAsString(StandardClaim.NAME);
+	}
+
+	default String getGivenName() {
+		return this.getClaimAsString(StandardClaim.GIVEN_NAME);
+	}
+
+	default String getFamilyName() {
+		return this.getClaimAsString(StandardClaim.FAMILY_NAME);
+	}
+
+	default String getMiddleName() {
+		return this.getClaimAsString(StandardClaim.MIDDLE_NAME);
+	}
+
+	default String getNickName() {
+		return this.getClaimAsString(StandardClaim.NICKNAME);
+	}
+
+	default String getPreferredUsername() {
+		return this.getClaimAsString(StandardClaim.PREFERRED_USERNAME);
+	}
+
+	default String getProfile() {
+		return this.getClaimAsString(StandardClaim.PROFILE);
+	}
+
+	default String getPicture() {
+		return this.getClaimAsString(StandardClaim.PICTURE);
+	}
+
+	default String getWebsite() {
+		return this.getClaimAsString(StandardClaim.WEBSITE);
+	}
+
+	default String getEmail() {
+		return this.getClaimAsString(StandardClaim.EMAIL);
+	}
+
+	default Boolean getEmailVerified() {
+		return this.getClaimAsBoolean(StandardClaim.EMAIL_VERIFIED);
+	}
+
+	default String getGender() {
+		return this.getClaimAsString(StandardClaim.GENDER);
+	}
+
+	default String getBirthdate() {
+		return this.getClaimAsString(StandardClaim.BIRTHDATE);
+	}
+
+	default String getZoneInfo() {
+		return this.getClaimAsString(StandardClaim.ZONEINFO);
+	}
+
+	default String getLocale() {
+		return this.getClaimAsString(StandardClaim.LOCALE);
+	}
+
+	default String getPhoneNumber() {
+		return this.getClaimAsString(StandardClaim.PHONE_NUMBER);
+	}
+
+	default Boolean getPhoneNumberVerified() {
+		return this.getClaimAsBoolean(StandardClaim.PHONE_NUMBER_VERIFIED);
+	}
+
+	default Address getAddress() {
+		// TODO Impl StandardClaim.ADDRESS
+		return null;
+	}
+
+	default Instant getUpdatedAt() {
+		return this.getClaimAsInstant(StandardClaim.UPDATED_AT);
+	}
+
+}

+ 69 - 0
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/UserInfo.java

@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012-2017 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
+ *
+ *      http://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.oidc.core;
+
+import org.springframework.util.Assert;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A representation of a <i>UserInfo Response</i> that is returned
+ * from the OAuth 2.0 Protected Resource <i>UserInfo Endpoint</i>.
+ *
+ * <p>
+ * The <code>UserInfo</code> contains a set of &quot;Standard Claims&quot; about the authentication of an End-User.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see StandardClaimAccessor
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse">UserInfo Response</a>
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfo">UserInfo Endpoint</a>
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
+ */
+public class UserInfo implements StandardClaimAccessor {
+	private final Map<String, Object> claims;
+
+	public UserInfo(Map<String, Object> claims) {
+		Assert.notEmpty(claims, "claims cannot be empty");
+		this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
+	}
+
+	@Override
+	public Map<String, Object> getClaims() {
+		return this.claims;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null || this.getClass() != obj.getClass()) {
+			return false;
+		}
+
+		UserInfo that = (UserInfo) obj;
+
+		return this.getClaims().equals(that.getClaims());
+	}
+
+	@Override
+	public int hashCode() {
+		return this.getClaims().hashCode();
+	}
+}

+ 30 - 0
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/endpoint/OidcParameter.java

@@ -0,0 +1,30 @@
+/*
+ * Copyright 2012-2017 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
+ *
+ *      http://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.oidc.core.endpoint;
+
+/**
+ * Standard parameters defined in the OAuth Parameters Registry
+ * and used by the authorization endpoint and token endpoint.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#OAuthParametersRegistry">18.2 OAuth Parameters Registration</a>
+ */
+public interface OidcParameter {
+
+	String ID_TOKEN = "id_token";
+
+}

+ 1 - 1
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/package-info.java → oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/package-info.java

@@ -16,4 +16,4 @@
 /**
  * Core classes and interfaces providing support for <i>OpenID Connect Core 1.0</i>.
  */
-package org.springframework.security.oauth2.oidc;
+package org.springframework.security.oauth2.oidc.core;

+ 79 - 0
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/DefaultOidcUser.java

@@ -0,0 +1,79 @@
+/*
+ * Copyright 2012-2017 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
+ *
+ *      http://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.oidc.core.user;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.security.oauth2.oidc.core.IdToken;
+import org.springframework.security.oauth2.oidc.core.IdTokenClaim;
+import org.springframework.security.oauth2.oidc.core.StandardClaim;
+import org.springframework.security.oauth2.oidc.core.UserInfo;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.springframework.security.oauth2.oidc.core.StandardClaim.NAME;
+
+/**
+ * The default implementation of an {@link OidcUser}.
+ *
+ * <p>
+ * The claim used for accessing the &quot;name&quot; of the
+ * user <code>Principal</code> via {@link #getClaims()}
+ * is {@link StandardClaim#NAME} or if not available
+ * will default to {@link IdTokenClaim#SUB}.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see OidcUser
+ * @see DefaultOAuth2User
+ * @see IdToken
+ * @see UserInfo
+ */
+public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
+	private final IdToken idToken;
+	private final UserInfo userInfo;
+
+	public DefaultOidcUser(Set<GrantedAuthority> authorities, IdToken idToken) {
+		this(authorities, idToken, null);
+	}
+
+	public DefaultOidcUser(Set<GrantedAuthority> authorities, IdToken idToken, UserInfo userInfo) {
+		super(authorities, idToken.getClaims(), IdTokenClaim.SUB);
+		this.idToken = idToken;
+		this.userInfo = userInfo;
+		if (userInfo != null) {
+			this.setAttributes(
+				Stream.of(this.getAttributes(), userInfo.getClaims())
+					.flatMap(m -> m.entrySet().stream())
+					.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (k1, k2) -> k1))
+			);
+		}
+	}
+
+	@Override
+	public Map<String, Object> getClaims() {
+		return this.getAttributes();
+	}
+
+	@Override
+	public String getName() {
+		String name = this.getClaimAsString(NAME);
+		return (name != null ? name : super.getName());
+	}
+}

+ 17 - 63
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/UserInfo.java → oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/OidcUser.java

@@ -13,24 +13,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.springframework.security.oauth2.oidc.user;
+package org.springframework.security.oauth2.oidc.core.user;
 
 import org.springframework.security.core.AuthenticatedPrincipal;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.oauth2.oidc.core.IdToken;
+import org.springframework.security.oauth2.oidc.core.IdTokenClaimAccessor;
+import org.springframework.security.oauth2.oidc.core.StandardClaimAccessor;
+import org.springframework.security.oauth2.oidc.core.UserInfo;
 
-import java.time.Instant;
+import java.util.Map;
 
 /**
  * A representation of a user <code>Principal</code>
  * that is registered with an <i>OpenID Connect 1.0 Provider</i>.
  *
  * <p>
- * The structure of the user <code>Principal</code> is defined by the
- * <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfo">UserInfo Endpoint</a>,
- * which is an <i>OAuth 2.0 Protected Resource</i> that returns a set of
- * <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Claims</a>
- * about the authenticated End-User.
+ * An <code>OidcUser</code> contains &quot;Claims&quot; about the Authentication of the End-User.
+ * The claims are aggregated from the <code>IdToken</code> and optionally the <code>UserInfo</code>.
  *
  * <p>
  * Implementation instances of this interface represent an {@link AuthenticatedPrincipal}
@@ -39,65 +40,18 @@ import java.time.Instant;
  *
  * @author Joe Grandja
  * @since 5.0
- * @see DefaultUserInfo
- * @see AuthenticatedPrincipal
+ * @see DefaultOidcUser
+ * @see OAuth2User
+ * @see IdToken
+ * @see UserInfo
+ * @see IdTokenClaimAccessor
+ * @see StandardClaimAccessor
  * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect Core 1.0</a>
- * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfo">UserInfo Endpoint</a>
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
  * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
  */
-public interface UserInfo extends OAuth2User {
+public interface OidcUser extends OAuth2User, IdTokenClaimAccessor {
 
-	String getSubject();
+	Map<String, Object> getClaims();
 
-	String getGivenName();
-
-	String getFamilyName();
-
-	String getMiddleName();
-
-	String getNickName();
-
-	String getPreferredUsername();
-
-	String getProfile();
-
-	String getPicture();
-
-	String getWebsite();
-
-	String getEmail();
-
-	Boolean getEmailVerified();
-
-	String getGender();
-
-	String getBirthdate();
-
-	String getZoneInfo();
-
-	String getLocale();
-
-	String getPhoneNumber();
-
-	Boolean getPhoneNumberVerified();
-
-	Address getAddress();
-
-	Instant getUpdatedAt();
-
-
-	interface Address {
-
-		String getFormatted();
-
-		String getStreetAddress();
-
-		String getLocality();
-
-		String getRegion();
-
-		String getPostalCode();
-
-		String getCountry();
-	}
 }

+ 96 - 0
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/OidcUserAuthority.java

@@ -0,0 +1,96 @@
+/*
+ * Copyright 2012-2017 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
+ *
+ *      http://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.oidc.core.user;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
+import org.springframework.security.oauth2.oidc.core.IdToken;
+import org.springframework.security.oauth2.oidc.core.UserInfo;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * A {@link GrantedAuthority} that is associated with an {@link OidcUser}.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see OidcUser
+ */
+public class OidcUserAuthority extends OAuth2UserAuthority {
+	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+	private final IdToken idToken;
+	private final UserInfo userInfo;
+
+	public OidcUserAuthority(IdToken idToken) {
+		this(idToken, null);
+	}
+
+	public OidcUserAuthority(IdToken idToken, UserInfo userInfo) {
+		this("ROLE_USER", idToken, userInfo);
+	}
+
+	public OidcUserAuthority(String authority, IdToken idToken, UserInfo userInfo) {
+		super(authority, idToken.getClaims());
+		this.idToken = idToken;
+		this.userInfo = userInfo;
+		if (userInfo != null) {
+			this.setAttributes(
+				Stream.of(this.getAttributes(), userInfo.getClaims())
+					.flatMap(m -> m.entrySet().stream())
+					.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (k1, k2) -> k1))
+			);
+		}
+	}
+
+	public IdToken getIdToken() {
+		return this.idToken;
+	}
+
+	public UserInfo getUserInfo() {
+		return this.userInfo;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null || this.getClass() != obj.getClass()) {
+			return false;
+		}
+		if (!super.equals(obj)) {
+			return false;
+		}
+
+		OidcUserAuthority that = (OidcUserAuthority) obj;
+
+		if (!this.getIdToken().equals(that.getIdToken())) {
+			return false;
+		}
+		return this.getUserInfo() != null ? this.getUserInfo().equals(that.getUserInfo()) : that.getUserInfo() == null;
+	}
+
+	@Override
+	public int hashCode() {
+		int result = super.hashCode();
+		result = 31 * result + this.getIdToken().hashCode();
+		result = 31 * result + (this.getUserInfo() != null ? this.getUserInfo().hashCode() : 0);
+		return result;
+	}
+}

+ 1 - 1
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/package-info.java → oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/package-info.java

@@ -16,4 +16,4 @@
 /**
  * Provides a model for an <i>OpenID Connect Core 1.0</i> representation of a user <code>Principal</code>.
  */
-package org.springframework.security.oauth2.oidc.user;
+package org.springframework.security.oauth2.oidc.core.user;

+ 0 - 154
oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/DefaultUserInfo.java

@@ -1,154 +0,0 @@
-/*
- * Copyright 2012-2017 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
- *
- *      http://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.oidc.user;
-
-import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
-import org.springframework.security.oauth2.oidc.StandardClaimName;
-
-import java.time.Instant;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Set;
-
-import static org.springframework.security.oauth2.oidc.StandardClaimName.*;
-
-/**
- * The default implementation of a {@link UserInfo}.
- *
- * <p>
- * The <i>key</i> used for accessing the &quot;name&quot; of the
- * <code>Principal</code> (user) via {@link #getAttributes()}
- * is {@link StandardClaimName#NAME} or if not available
- * will default to {@link StandardClaimName#SUB}.
- *
- * @author Joe Grandja
- * @since 5.0
- * @see UserInfo
- * @see DefaultOAuth2User
- */
-public class DefaultUserInfo extends DefaultOAuth2User implements UserInfo {
-
-	public DefaultUserInfo(Map<String, Object> attributes) {
-		this(Collections.emptySet(), attributes);
-	}
-
-	public DefaultUserInfo(Set<GrantedAuthority> authorities, Map<String, Object> attributes) {
-		super(authorities, attributes, SUB);
-	}
-
-	@Override
-	public String getSubject() {
-		return this.getAttributeAsString(SUB);
-	}
-
-	@Override
-	public String getName() {
-		String name = this.getAttributeAsString(NAME);
-		return (name != null ? name : super.getName());
-	}
-
-	@Override
-	public String getGivenName() {
-		return this.getAttributeAsString(GIVEN_NAME);
-	}
-
-	@Override
-	public String getFamilyName() {
-		return this.getAttributeAsString(FAMILY_NAME);
-	}
-
-	@Override
-	public String getMiddleName() {
-		return this.getAttributeAsString(MIDDLE_NAME);
-	}
-
-	@Override
-	public String getNickName() {
-		return this.getAttributeAsString(NICKNAME);
-	}
-
-	@Override
-	public String getPreferredUsername() {
-		return this.getAttributeAsString(PREFERRED_USERNAME);
-	}
-
-	@Override
-	public String getProfile() {
-		return this.getAttributeAsString(PROFILE);
-	}
-
-	@Override
-	public String getPicture() {
-		return this.getAttributeAsString(PICTURE);
-	}
-
-	@Override
-	public String getWebsite() {
-		return this.getAttributeAsString(WEBSITE);
-	}
-
-	@Override
-	public String getEmail() {
-		return this.getAttributeAsString(EMAIL);
-	}
-
-	@Override
-	public Boolean getEmailVerified() {
-		return this.getAttributeAsBoolean(EMAIL_VERIFIED);
-	}
-
-	@Override
-	public String getGender() {
-		return this.getAttributeAsString(GENDER);
-	}
-
-	@Override
-	public String getBirthdate() {
-		return this.getAttributeAsString(BIRTHDATE);
-	}
-
-	@Override
-	public String getZoneInfo() {
-		return this.getAttributeAsString(ZONEINFO);
-	}
-
-	@Override
-	public String getLocale() {
-		return this.getAttributeAsString(LOCALE);
-	}
-
-	@Override
-	public String getPhoneNumber() {
-		return this.getAttributeAsString(PHONE_NUMBER);
-	}
-
-	@Override
-	public Boolean getPhoneNumberVerified() {
-		return this.getAttributeAsBoolean(PHONE_NUMBER_VERIFIED);
-	}
-
-	@Override
-	public Address getAddress() {
-		// TODO Impl
-		return null;
-	}
-
-	@Override
-	public Instant getUpdatedAt() {
-		return this.getAttributeAsInstant(UPDATED_AT);
-	}
-}

+ 7 - 1
samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java

@@ -35,6 +35,7 @@ import org.springframework.http.HttpStatus;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter;
 import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
 import org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter;
@@ -48,6 +49,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
 import org.springframework.security.oauth2.core.endpoint.ResponseType;
 import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
 import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
 import org.springframework.test.context.junit4.SpringRunner;
 import org.springframework.web.util.UriComponents;
 import org.springframework.web.util.UriComponentsBuilder;
@@ -389,7 +391,11 @@ public class OAuth2LoginApplicationTests {
 			attributes.put("last-name", "Grandja");
 			attributes.put("email", "joeg@springsecurity.io");
 
-			DefaultOAuth2User user = new DefaultOAuth2User(attributes, "email");
+			GrantedAuthority authority = new OAuth2UserAuthority(attributes);
+			Set<GrantedAuthority> authorities = new HashSet<>();
+			authorities.add(authority);
+
+			DefaultOAuth2User user = new DefaultOAuth2User(authorities, attributes, "email");
 
 			OAuth2UserService mock = mock(OAuth2UserService.class);
 			when(mock.loadUser(any())).thenReturn(user);

+ 6 - 26
samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2LoginAutoConfiguration.java

@@ -24,21 +24,15 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
 import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.env.Environment;
-import org.springframework.http.client.ClientHttpResponse;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
-import org.springframework.security.oauth2.client.user.converter.AbstractOAuth2UserConverter;
-import org.springframework.security.oauth2.core.user.OAuth2User;
-import org.springframework.util.ClassUtils;
 
-import java.lang.reflect.Constructor;
 import java.net.URI;
 import java.util.Set;
-import java.util.function.Function;
 
 import static org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration.*;
 
@@ -54,8 +48,7 @@ import static org.springframework.boot.autoconfigure.security.oauth2.client.Clie
 @AutoConfigureAfter(ClientRegistrationAutoConfiguration.class)
 public class OAuth2LoginAutoConfiguration {
 	private static final String USER_INFO_URI_PROPERTY = "user-info-uri";
-	private static final String USER_INFO_CONVERTER_PROPERTY = "user-info-converter";
-	private static final String USER_INFO_NAME_ATTR_KEY_PROPERTY = "user-info-name-attribute-key";
+	private static final String USER_NAME_ATTR_NAME_PROPERTY = "user-name-attribute-name";
 
 	@EnableWebSecurity
 	protected static class OAuth2LoginSecurityConfiguration extends WebSecurityConfigurerAdapter {
@@ -75,11 +68,11 @@ public class OAuth2LoginAutoConfiguration {
 					.and()
 				.oauth2Login();
 
-			this.registerUserInfoTypeConverters(http.oauth2Login());
+			this.registerUserNameAttributeNames(http.oauth2Login());
 		}
 		// @formatter:on
 
-		private void registerUserInfoTypeConverters(OAuth2LoginConfigurer<HttpSecurity> oauth2LoginConfigurer) throws Exception {
+		private void registerUserNameAttributeNames(OAuth2LoginConfigurer<HttpSecurity> oauth2LoginConfigurer) throws Exception {
 			Set<String> clientPropertyKeys = resolveClientPropertyKeys(this.environment);
 			for (String clientPropertyKey : clientPropertyKeys) {
 				String fullClientPropertyKey = CLIENT_PROPERTY_PREFIX + "." + clientPropertyKey;
@@ -87,22 +80,9 @@ public class OAuth2LoginAutoConfiguration {
 					continue;
 				}
 				String userInfoUriValue = this.environment.getProperty(fullClientPropertyKey + "." + USER_INFO_URI_PROPERTY);
-				String userInfoConverterTypeValue = this.environment.getProperty(fullClientPropertyKey + "." + USER_INFO_CONVERTER_PROPERTY);
-				if (userInfoUriValue != null && userInfoConverterTypeValue != null) {
-					Class<? extends Function> userInfoConverterType = ClassUtils.resolveClassName(
-						userInfoConverterTypeValue, this.getClass().getClassLoader()).asSubclass(Function.class);
-					Function<ClientHttpResponse, ? extends OAuth2User> userInfoConverter = null;
-					if (AbstractOAuth2UserConverter.class.isAssignableFrom(userInfoConverterType)) {
-						Constructor<? extends Function> oauth2UserConverterConstructor = ClassUtils.getConstructorIfAvailable(userInfoConverterType, String.class);
-						if (oauth2UserConverterConstructor != null) {
-							String userInfoNameAttributeKey = this.environment.getProperty(fullClientPropertyKey + "." + USER_INFO_NAME_ATTR_KEY_PROPERTY);
-							userInfoConverter = (Function<ClientHttpResponse, ? extends OAuth2User>)oauth2UserConverterConstructor.newInstance(userInfoNameAttributeKey);
-						}
-					}
-					if (userInfoConverter == null) {
-						userInfoConverter = (Function<ClientHttpResponse, ? extends OAuth2User>)userInfoConverterType.newInstance();
-					}
-					oauth2LoginConfigurer.userInfoEndpoint().userInfoTypeConverter(userInfoConverter, new URI(userInfoUriValue));
+				String userNameAttributeNameValue = this.environment.getProperty(fullClientPropertyKey + "." + USER_NAME_ATTR_NAME_PROPERTY);
+				if (userInfoUriValue != null && userNameAttributeNameValue != null) {
+					oauth2LoginConfigurer.userInfoEndpoint().userNameAttributeName(userNameAttributeNameValue, URI.create(userInfoUriValue));
 				}
 			}
 		}

+ 4 - 2
samples/boot/oauth2login/src/main/java/sample/user/GitHubOAuth2User.java

@@ -16,17 +16,19 @@
 package sample.user;
 
 import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.oauth2.core.user.OAuth2User;
 
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
  * @author Joe Grandja
  */
 public class GitHubOAuth2User implements OAuth2User {
+	private List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
 	private String id;
 	private String name;
 	private String login;
@@ -37,7 +39,7 @@ public class GitHubOAuth2User implements OAuth2User {
 
 	@Override
 	public Collection<? extends GrantedAuthority> getAuthorities() {
-		return Collections.emptyList();
+		return this.authorities;
 	}
 
 	@Override

+ 1 - 4
samples/boot/oauth2login/src/main/resources/META-INF/oauth2-clients-defaults.yml

@@ -9,7 +9,7 @@ security:
         authorization-uri: "https://accounts.google.com/o/oauth2/auth"
         token-uri: "https://accounts.google.com/o/oauth2/token"
         user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo"
-        user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
+        jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs
         client-name: Google
         client-alias: google
       github:
@@ -20,7 +20,6 @@ security:
         authorization-uri: "https://github.com/login/oauth/authorize"
         token-uri: "https://github.com/login/oauth/access_token"
         user-info-uri: "https://api.github.com/user"
-        user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
         client-name: GitHub
         client-alias: github
       facebook:
@@ -31,7 +30,6 @@ security:
         authorization-uri: "https://www.facebook.com/v2.8/dialog/oauth"
         token-uri: "https://graph.facebook.com/v2.8/oauth/access_token"
         user-info-uri: "https://graph.facebook.com/me"
-        user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
         client-name: Facebook
         client-alias: facebook
       okta:
@@ -39,6 +37,5 @@ security:
         authorized-grant-type: authorization_code
         redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
         scopes: openid, email, profile
-        user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
         client-name: Okta
         client-alias: okta

+ 3 - 2
samples/boot/oauth2login/src/main/resources/application.yml

@@ -21,14 +21,15 @@ security:
       github:
         client-id: your-app-client-id
         client-secret: your-app-client-secret
-        user-info-name-attribute-key: "name"
+        user-name-attribute-name: "name"
       facebook:
         client-id: your-app-client-id
         client-secret: your-app-client-secret
-        user-info-name-attribute-key: "name"
+        user-name-attribute-name: "name"
       okta:
         client-id: your-app-client-id
         client-secret: your-app-client-secret
         authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize
         token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token
         user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo
+        jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys