From 9cfb890207d5f037b274755efe9ff134f669da25 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Tue, 27 Jun 2017 11:45:19 -0400 Subject: [PATCH] Use id_token for user authentication Fixes gh-4410 --- config/spring-security-config.gradle | 1 + ...ionCodeAuthenticationFilterConfigurer.java | 74 +++++- .../oauth2/client/OAuth2LoginConfigurer.java | 15 +- .../security/jwt/JwtClaimAccessor.java | 12 +- .../spring-security-oauth2-client.gradle | 3 +- ...ionCodeAuthenticationProcessingFilter.java | 1 + ...thorizationCodeAuthenticationProvider.java | 42 +++- .../OAuth2AuthenticationToken.java | 16 +- .../DefaultProviderJwtDecoderRegistry.java | 55 ++++ .../jwt/ProviderJwtDecoderRegistry.java} | 15 +- .../registration/ClientRegistration.java | 17 ++ .../ClientRegistrationProperties.java | 9 + .../oauth2/client/user/OAuth2UserService.java | 7 +- .../AbstractOAuth2UserConverter.java | 57 ----- .../converter/CustomOAuth2UserConverter.java | 57 ----- .../user/converter/OAuth2UserConverter.java | 46 ---- .../user/nimbus/NimbusOAuth2UserService.java | 238 ++++++++++++------ .../security/oauth2/core/AbstractToken.java | 28 +++ .../security/oauth2/core/ClaimAccessor.java | 12 +- .../provider/DefaultProviderMetadata.java | 112 +++++++++ .../core/provider/ProviderMetadata.java} | 29 ++- .../oauth2/core/user/DefaultOAuth2User.java | 40 +-- .../security/oauth2/core/user/OAuth2User.java | 1 + .../oauth2/core/user/OAuth2UserAuthority.java | 91 +++++++ .../security/oauth2/oidc/core/Address.java | 42 ++++ .../security/oauth2/oidc/core/IdToken.java | 54 ++++ .../oauth2/oidc/core/IdTokenClaim.java | 54 ++++ .../oidc/core/IdTokenClaimAccessor.java | 88 +++++++ .../StandardClaim.java} | 9 +- .../oidc/core/StandardClaimAccessor.java | 118 +++++++++ .../security/oauth2/oidc/core/UserInfo.java | 69 +++++ .../oidc/core/endpoint/OidcParameter.java | 30 +++ .../oauth2/oidc/{ => core}/package-info.java | 2 +- .../oidc/core/user/DefaultOidcUser.java | 79 ++++++ .../UserInfo.java => core/user/OidcUser.java} | 80 ++---- .../oidc/core/user/OidcUserAuthority.java | 96 +++++++ .../oidc/{ => core}/user/package-info.java | 2 +- .../oauth2/oidc/user/DefaultUserInfo.java | 154 ------------ .../samples/OAuth2LoginApplicationTests.java | 8 +- .../client/OAuth2LoginAutoConfiguration.java | 32 +-- .../java/sample/user/GitHubOAuth2User.java | 6 +- .../META-INF/oauth2-clients-defaults.yml | 5 +- .../src/main/resources/application.yml | 5 +- 43 files changed, 1328 insertions(+), 583 deletions(-) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/jwt/DefaultProviderJwtDecoderRegistry.java rename oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/{user/converter/package-info.java => authentication/jwt/ProviderJwtDecoderRegistry.java} (63%) delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/AbstractOAuth2UserConverter.java delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/CustomOAuth2UserConverter.java delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/OAuth2UserConverter.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/provider/DefaultProviderMetadata.java rename oauth2/{oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/UserInfoConverter.java => oauth2-core/src/main/java/org/springframework/security/oauth2/core/provider/ProviderMetadata.java} (50%) create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2UserAuthority.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/Address.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdToken.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdTokenClaim.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdTokenClaimAccessor.java rename oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/{StandardClaimName.java => core/StandardClaim.java} (86%) create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/StandardClaimAccessor.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/UserInfo.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/endpoint/OidcParameter.java rename oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/{ => core}/package-info.java (92%) create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/DefaultOidcUser.java rename oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/{user/UserInfo.java => core/user/OidcUser.java} (53%) create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/OidcUserAuthority.java rename oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/{ => core}/user/package-info.java (92%) delete mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/DefaultUserInfo.java diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 146778ed23..917985c479 100644 --- a/config/spring-security-config.gradle +++ b/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') diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeAuthenticationFilterConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeAuthenticationFilterConfigurer.java index b8ec434b2b..386f47d87d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/AuthorizationCodeAuthenticationFilterConfigurer.java +++ b/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 authorizationCodeTokenExchanger; private OAuth2UserService userInfoService; - private Map> userInfoTypeConverters = new HashMap<>(); + private Map> customUserTypes = new HashMap<>(); + private Map userNameAttributeNames = new HashMap<>(); AuthorizationCodeAuthenticationFilterConfigurer() { @@ -71,10 +81,17 @@ final class AuthorizationCodeAuthenticationFilterConfigurer userInfoTypeConverter(Function userInfoConverter, URI userInfoUri) { - Assert.notNull(userInfoConverter, "userInfoConverter cannot be null"); + AuthorizationCodeAuthenticationFilterConfigurer customUserType(Class 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 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 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); + } + } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index 1d36f67d48..d4f669c723 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/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> exten return this.and(); } - public OAuth2LoginConfigurer userInfoTypeConverter(Function userInfoConverter, URI userInfoUri) { - Assert.notNull(userInfoConverter, "userInfoConverter cannot be null"); + public OAuth2LoginConfigurer customUserType(Class 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 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(); } diff --git a/oauth2/jwt-jose/src/main/java/org/springframework/security/jwt/JwtClaimAccessor.java b/oauth2/jwt-jose/src/main/java/org/springframework/security/jwt/JwtClaimAccessor.java index 03f81d59cb..98333335d4 100644 --- a/oauth2/jwt-jose/src/main/java/org/springframework/security/jwt/JwtClaimAccessor.java +++ b/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() { diff --git a/oauth2/oauth2-client/spring-security-oauth2-client.gradle b/oauth2/oauth2-client/spring-security-oauth2-client.gradle index b0834f886e..fd9e720231 100644 --- a/oauth2/oauth2-client/spring-security-oauth2-client.gradle +++ b/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' } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilter.java index 4fa10d6afe..b045649f3c 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProcessingFilter.java +++ b/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()); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java index 4ae4fd1665..6474266d96 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/AuthorizationCodeAuthenticationProvider.java +++ b/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 authorizationCodeTokenExchanger; + private final ProviderJwtDecoderRegistry providerJwtDecoderRegistry; private final OAuth2UserService userInfoService; private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); public AuthorizationCodeAuthenticationProvider( AuthorizationGrantTokenExchanger 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 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); + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java index d5119e3312..4404d21345 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java +++ b/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 OAuth 2.0 {@link Authentication}. * *

- * 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 Authentication is considered "authenticated" 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 UserInfo Endpoint. @@ -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 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; + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/jwt/DefaultProviderJwtDecoderRegistry.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/jwt/DefaultProviderJwtDecoderRegistry.java new file mode 100644 index 0000000000..c180ef874b --- /dev/null +++ b/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 ProviderMetadata + * is matched against the providerIdentifier parameter passed to {@link #getJwtDecoder(String)}. + * + * @author Joe Grandja + * @since 5.0 + */ +public class DefaultProviderJwtDecoderRegistry implements ProviderJwtDecoderRegistry { + private final Map jwtDecoders; + + public DefaultProviderJwtDecoderRegistry(Map 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 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); + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/jwt/ProviderJwtDecoderRegistry.java similarity index 63% rename from oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/package-info.java rename to oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/jwt/ProviderJwtDecoderRegistry.java index 2076982ef0..f0984bb226 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/package-info.java +++ b/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 OAuth 2.0 Provider. + * + * @author Joe Grandja + * @since 5.0 */ -package org.springframework.security.oauth2.client.user.converter; +public interface ProviderJwtDecoderRegistry { + + JwtDecoder getJwtDecoder(String providerIdentifier); + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index 3c333622ec..4195321060 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/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); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationProperties.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationProperties.java index 8a6c48be74..412148c221 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationProperties.java +++ b/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; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/OAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/OAuth2UserService.java index d4b9d8a964..b765d549e2 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/OAuth2UserService.java +++ b/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 UserInfo Endpoint * using the provided {@link OAuth2AuthenticationToken#getAccessToken()} - * and returning an {@link AuthenticatedPrincipal} in the form of an {@link OAuth2User} - * (for a standard OAuth 2.0 Provider) or {@link UserInfo} (for an OpenID Connect 1.0 Provider). + * 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 { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/AbstractOAuth2UserConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/AbstractOAuth2UserConverter.java deleted file mode 100644 index 0109c5627b..0000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/AbstractOAuth2UserConverter.java +++ /dev/null @@ -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 Function 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 implements Function { - private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); - - protected AbstractOAuth2UserConverter() { - } - - @Override - public final R apply(ClientHttpResponse clientHttpResponse) { - Map userAttributes; - - try { - userAttributes = (Map) 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 userAttributes); - -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/CustomOAuth2UserConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/CustomOAuth2UserConverter.java deleted file mode 100644 index 85b1b906d2..0000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/CustomOAuth2UserConverter.java +++ /dev/null @@ -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 Function 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 implements Function { - private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); - private final Class customType; - - public CustomOAuth2UserConverter(Class 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; - } -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/OAuth2UserConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/OAuth2UserConverter.java deleted file mode 100644 index 99d47874ac..0000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/OAuth2UserConverter.java +++ /dev/null @@ -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 { - private final String nameAttributeKey; - - public OAuth2UserConverter(String nameAttributeKey) { - Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty"); - this.nameAttributeKey = nameAttributeKey; - } - - @Override - protected OAuth2User apply(Map userAttributes) { - return new DefaultOAuth2User(userAttributes, this.nameAttributeKey); - } -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusOAuth2UserService.java index 5bcbc42d94..9d45a914e3 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/nimbus/NimbusOAuth2UserService.java +++ b/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 Nimbus OAuth 2.0 SDK internally. * *

- * This implementation uses a Map of converter's keyed by URI. - * The URI represents the UserInfo Endpoint address and the mapped Function - * is capable of converting the UserInfo Response to either an - * {@link OAuth2User} (for a standard OAuth 2.0 Provider) or - * {@link UserInfo} (for an OpenID Connect 1.0 Provider). + * This implementation may be configured with a Map of custom {@link OAuth2User} types + * keyed by URI, which represents the UserInfo Endpoint address. + * + *

+ * For {@link OAuth2User}'s registered at a standard OAuth 2.0 Provider, the attribute name + * for the "user's name" is required. This can be supplied via {@link #setUserNameAttributeNames(Map)}, + * keyed by URI, which represents the UserInfo Endpoint address. * * @author Joe Grandja * @since 5.0 * @see OAuth2AuthenticationToken - * @see AuthenticatedPrincipal * @see OAuth2User + * @see OidcUser * @see UserInfo * @see Nimbus OAuth 2.0 SDK */ public class NimbusOAuth2UserService implements OAuth2UserService { private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response"; - private final Map> userInfoTypeConverters; + private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + private Map userNameAttributeNames = Collections.unmodifiableMap(Collections.emptyMap()); + private Map> customUserTypes = Collections.unmodifiableMap(Collections.emptyMap()); - public NimbusOAuth2UserService(Map> userInfoTypeConverters) { - Assert.notEmpty(userInfoTypeConverters, "userInfoTypeConverters cannot be empty"); - this.userInfoTypeConverters = new HashMap<>(userInfoTypeConverters); + public NimbusOAuth2UserService() { } @Override - public OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException { - OAuth2User user; + public final OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException { + URI userInfoUri = this.getUserInfoUri(token); - try { - ClientRegistration clientRegistration = token.getClientRegistration(); - - URI userInfoUri; - try { - userInfoUri = new URI(clientRegistration.getProviderDetails().getUserInfoUri()); - } catch (Exception ex) { - throw new IllegalArgumentException("An error occurred parsing the userInfo URI: " + - clientRegistration.getProviderDetails().getUserInfoUri(), ex); - } - - Function userInfoConverter = this.userInfoTypeConverters.get(userInfoUri); - if (userInfoConverter == null) { - throw new IllegalArgumentException("There is no available User Info converter for " + userInfoUri.toString()); - } - - 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()); - } - - user = userInfoConverter.apply(new NimbusClientHttpResponse(httpResponse)); - - } 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); - } 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); + 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 userAttributes = this.getUserInfo(token); + UserInfo userInfo = new UserInfo(userAttributes); + + GrantedAuthority authority = new OidcUserAuthority(token.getIdToken(), userInfo); + Set 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 userAttributes = this.getUserInfo(token); + + GrantedAuthority authority = new OAuth2UserAuthority(userAttributes); + Set 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 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 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 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 { + httpResponse = httpRequest.send(); + } catch (IOException ex) { + throw new AuthenticationServiceException("An error occurred while sending the UserInfo Request: " + + ex.getMessage(), ex); + } + + if (httpResponse.getStatusCode() != HTTPResponse.SC_OK) { + UserInfoErrorResponse userInfoErrorResponse; + try { + 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); + } + 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()); + } + + try { + return (Map) this.jackson2HttpMessageConverter.read(Map.class, new NimbusClientHttpResponse(httpResponse)); + } catch (IOException 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); + } + } + + protected Map getUserNameAttributeNames() { + return this.userNameAttributeNames; + } + + public final void setUserNameAttributeNames(Map userNameAttributeNames) { + Assert.notEmpty(userNameAttributeNames, "userNameAttributeNames cannot be empty"); + this.userNameAttributeNames = Collections.unmodifiableMap(new HashMap<>(userNameAttributeNames)); + } + + protected Map> getCustomUserTypes() { + return this.customUserTypes; + } + + public final void setCustomUserTypes(Map> 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); + } + } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractToken.java index 39fca83d6e..8adf73fc3a 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractToken.java +++ b/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; + } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java index 0795c3ec44..ea67ec4739 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java +++ b/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); } } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/provider/DefaultProviderMetadata.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/provider/DefaultProviderMetadata.java new file mode 100644 index 0000000000..334e25a48e --- /dev/null +++ b/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; + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/UserInfoConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/provider/ProviderMetadata.java similarity index 50% rename from oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/UserInfoConverter.java rename to oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/provider/ProviderMetadata.java index 2544a9a35d..1856d5929d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/user/converter/UserInfoConverter.java +++ b/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 OAuth 2.0 Provider. * * @author Joe Grandja * @since 5.0 - * @see UserInfo - * @see ClientHttpResponse */ -public final class UserInfoConverter extends AbstractOAuth2UserConverter { +public interface ProviderMetadata { + + URL getIssuer(); + + URL getAuthorizationEndpoint(); + + URL getTokenEndpoint(); + + URL getUserInfoEndpoint(); + + URL getJwkSetUri(); - @Override - protected UserInfo apply(Map userAttributes) { - return new DefaultUserInfo(userAttributes); - } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java index 9139dd9b27..4a4f098c87 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java +++ b/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 authorities; - private final Map attributes; + private Map attributes; private final String nameAttributeKey; - public DefaultOAuth2User(Map attributes, String nameAttributeKey) { - this(Collections.emptySet(), attributes, nameAttributeKey); - } - public DefaultOAuth2User(Set authorities, Map 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 attributes) { + Assert.notEmpty(attributes, "attributes cannot be empty"); + this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes)); } private Set sortAuthorities(Set authorities) { - if (CollectionUtils.isEmpty(authorities)) { - return Collections.emptySet(); - } - SortedSet sortedAuthorities = new TreeSet<>((g1, g2) -> g1.getAuthority().compareTo(g2.getAuthority())); authorities.stream().forEach(sortedAuthorities::add); - return sortedAuthorities; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2User.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2User.java index 769cd3ba8b..5d516b1474 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2User.java +++ b/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 getAuthorities(); Map getAttributes(); + } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2UserAuthority.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2UserAuthority.java new file mode 100644 index 0000000000..a3d7a25f34 --- /dev/null +++ b/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 attributes; + + public OAuth2UserAuthority(Map attributes) { + this("ROLE_USER", attributes); + } + + public OAuth2UserAuthority(String authority, Map 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 getAttributes() { + return this.attributes; + } + + protected final void setAttributes(Map 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(); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/Address.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/Address.java new file mode 100644 index 0000000000..0470fc5ad7 --- /dev/null +++ b/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 OpenID Connect Core 1.0 specification + * that can be returned either in the UserInfo Response or the ID Token. + * + * @author Joe Grandja + * @since 5.0 + * @see Address Claim + * @see UserInfo Response + * @see ID Token + */ +public interface Address { + + String getFormatted(); + + String getStreetAddress(); + + String getLocality(); + + String getRegion(); + + String getPostalCode(); + + String getCountry(); + +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdToken.java new file mode 100644 index 0000000000..75e6c9a9e2 --- /dev/null +++ b/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 OpenID Connect Core 1.0 ID Token. + * + *

+ * The IdToken is a security token that contains "Claims" + * about the authentication of an End-User by an Authorization Server. + * + * @author Joe Grandja + * @since 5.0 + * @see AbstractToken + * @see IdTokenClaimAccessor + * @see StandardClaimAccessor + * @see ID Token + * @see Standard Claims + */ +public class IdToken extends AbstractToken implements IdTokenClaimAccessor { + private final Map claims; + + public IdToken(String tokenValue, Instant issuedAt, Instant expiresAt, Map claims) { + super(tokenValue, issuedAt, expiresAt); + Assert.notEmpty(claims, "claims cannot be empty"); + this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims)); + } + + @Override + public Map getClaims() { + return this.claims; + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdTokenClaim.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdTokenClaim.java new file mode 100644 index 0000000000..82f7c4ca1f --- /dev/null +++ b/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 "Claims" defined by the OpenID Connect Core 1.0 specification + * that can be returned in the ID Token. + * + * @author Joe Grandja + * @since 5.0 + * @see IdToken + * @see ID Token + */ + +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"; + +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdTokenClaimAccessor.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/IdTokenClaimAccessor.java new file mode 100644 index 0000000000..65e56597a3 --- /dev/null +++ b/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 "Claims" that can be returned in the ID Token + * 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 ID Token + * @see Standard Claims + * @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); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/StandardClaimName.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/StandardClaim.java similarity index 86% rename from oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/StandardClaimName.java rename to oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/StandardClaim.java index 858174e54b..02f5ec871d 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/StandardClaimName.java +++ b/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 OpenID Connect Core 1.0 specification - * and returned in either the UserInfo Response or in the ID Token. + * The "Standard Claims" defined by the OpenID Connect Core 1.0 specification + * that can be returned either in the UserInfo Response or the ID Token. * * @author Joe Grandja * @since 5.0 @@ -25,7 +25,7 @@ package org.springframework.security.oauth2.oidc; * @see UserInfo Response * @see ID Token */ -public interface StandardClaimName { +public interface StandardClaim { String SUB = "sub"; @@ -66,4 +66,5 @@ public interface StandardClaimName { String ADDRESS = "address"; String UPDATED_AT = "updated_at"; + } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/StandardClaimAccessor.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/StandardClaimAccessor.java new file mode 100644 index 0000000000..b47c06f7c7 --- /dev/null +++ b/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 "Standard Claims" that can be returned + * either in the UserInfo Response or the ID Token. + * + * @see ClaimAccessor + * @see StandardClaim + * @see UserInfo + * @see UserInfo Response + * @see ID Token + * @see Standard Claims + * @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); + } + +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/UserInfo.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/UserInfo.java new file mode 100644 index 0000000000..d47c6c9fb0 --- /dev/null +++ b/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 UserInfo Response that is returned + * from the OAuth 2.0 Protected Resource UserInfo Endpoint. + * + *

+ * The UserInfo contains a set of "Standard Claims" about the authentication of an End-User. + * + * @author Joe Grandja + * @since 5.0 + * @see StandardClaimAccessor + * @see UserInfo Response + * @see UserInfo Endpoint + * @see Standard Claims + */ +public class UserInfo implements StandardClaimAccessor { + private final Map claims; + + public UserInfo(Map claims) { + Assert.notEmpty(claims, "claims cannot be empty"); + this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims)); + } + + @Override + public Map 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(); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/endpoint/OidcParameter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/endpoint/OidcParameter.java new file mode 100644 index 0000000000..ff4aaa88a8 --- /dev/null +++ b/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 18.2 OAuth Parameters Registration + */ +public interface OidcParameter { + + String ID_TOKEN = "id_token"; + +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/package-info.java similarity index 92% rename from oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/package-info.java rename to oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/package-info.java index 3aad8e1a64..7cb31d5bcd 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/package-info.java +++ b/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 OpenID Connect Core 1.0. */ -package org.springframework.security.oauth2.oidc; +package org.springframework.security.oauth2.oidc.core; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/DefaultOidcUser.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/DefaultOidcUser.java new file mode 100644 index 0000000000..165f029d58 --- /dev/null +++ b/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}. + * + *

+ * The claim used for accessing the "name" of the + * user Principal 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 authorities, IdToken idToken) { + this(authorities, idToken, null); + } + + public DefaultOidcUser(Set 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 getClaims() { + return this.getAttributes(); + } + + @Override + public String getName() { + String name = this.getClaimAsString(NAME); + return (name != null ? name : super.getName()); + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/UserInfo.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/OidcUser.java similarity index 53% rename from oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/UserInfo.java rename to oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/OidcUser.java index cd5861c012..51197caec2 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/UserInfo.java +++ b/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 Principal * that is registered with an OpenID Connect 1.0 Provider. * *

- * The structure of the user Principal is defined by the - * UserInfo Endpoint, - * which is an OAuth 2.0 Protected Resource that returns a set of - * Claims - * about the authenticated End-User. + * An OidcUser contains "Claims" about the Authentication of the End-User. + * The claims are aggregated from the IdToken and optionally the UserInfo. * *

* 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 OpenID Connect Core 1.0 - * @see UserInfo Endpoint + * @see ID Token * @see Standard Claims */ -public interface UserInfo extends OAuth2User { +public interface OidcUser extends OAuth2User, IdTokenClaimAccessor { - String getSubject(); + Map 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(); - } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/OidcUserAuthority.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/OidcUserAuthority.java new file mode 100644 index 0000000000..9a0fa5e361 --- /dev/null +++ b/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; + } +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/package-info.java similarity index 92% rename from oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/package-info.java rename to oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/user/package-info.java index 357fb0e8fa..e2ed1f7358 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/package-info.java +++ b/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 OpenID Connect Core 1.0 representation of a user Principal. */ -package org.springframework.security.oauth2.oidc.user; +package org.springframework.security.oauth2.oidc.core.user; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/DefaultUserInfo.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/DefaultUserInfo.java deleted file mode 100644 index 8d2f1f6ac7..0000000000 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/user/DefaultUserInfo.java +++ /dev/null @@ -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}. - * - *

- * The key used for accessing the "name" of the - * Principal (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 attributes) { - this(Collections.emptySet(), attributes); - } - - public DefaultUserInfo(Set authorities, Map 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); - } -} diff --git a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java b/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java index c449e2c6b8..def112f0bd 100644 --- a/samples/boot/oauth2login/src/integration-test/java/org/springframework/security/samples/OAuth2LoginApplicationTests.java +++ b/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 authorities = new HashSet<>(); + authorities.add(authority); + + DefaultOAuth2User user = new DefaultOAuth2User(authorities, attributes, "email"); OAuth2UserService mock = mock(OAuth2UserService.class); when(mock.loadUser(any())).thenReturn(user); diff --git a/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2LoginAutoConfiguration.java b/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2LoginAutoConfiguration.java index 0ad37235ee..c933de3c86 100644 --- a/samples/boot/oauth2login/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2LoginAutoConfiguration.java +++ b/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 oauth2LoginConfigurer) throws Exception { + private void registerUserNameAttributeNames(OAuth2LoginConfigurer oauth2LoginConfigurer) throws Exception { Set 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 userInfoConverterType = ClassUtils.resolveClassName( - userInfoConverterTypeValue, this.getClass().getClassLoader()).asSubclass(Function.class); - Function userInfoConverter = null; - if (AbstractOAuth2UserConverter.class.isAssignableFrom(userInfoConverterType)) { - Constructor oauth2UserConverterConstructor = ClassUtils.getConstructorIfAvailable(userInfoConverterType, String.class); - if (oauth2UserConverterConstructor != null) { - String userInfoNameAttributeKey = this.environment.getProperty(fullClientPropertyKey + "." + USER_INFO_NAME_ATTR_KEY_PROPERTY); - userInfoConverter = (Function)oauth2UserConverterConstructor.newInstance(userInfoNameAttributeKey); - } - } - if (userInfoConverter == null) { - userInfoConverter = (Function)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)); } } } diff --git a/samples/boot/oauth2login/src/main/java/sample/user/GitHubOAuth2User.java b/samples/boot/oauth2login/src/main/java/sample/user/GitHubOAuth2User.java index 7ba81824ae..424defd589 100644 --- a/samples/boot/oauth2login/src/main/java/sample/user/GitHubOAuth2User.java +++ b/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 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 getAuthorities() { - return Collections.emptyList(); + return this.authorities; } @Override diff --git a/samples/boot/oauth2login/src/main/resources/META-INF/oauth2-clients-defaults.yml b/samples/boot/oauth2login/src/main/resources/META-INF/oauth2-clients-defaults.yml index 3158611615..6a1ab49bbe 100644 --- a/samples/boot/oauth2login/src/main/resources/META-INF/oauth2-clients-defaults.yml +++ b/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 diff --git a/samples/boot/oauth2login/src/main/resources/application.yml b/samples/boot/oauth2login/src/main/resources/application.yml index faa25818f7..3f0e19d85f 100644 --- a/samples/boot/oauth2login/src/main/resources/application.yml +++ b/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