diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2ClientAuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2ClientAuthenticationToken.java index 6cf0dd8bc5..0a98b8b54d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2ClientAuthenticationToken.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2ClientAuthenticationToken.java @@ -22,6 +22,9 @@ import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.AccessToken; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.util.Set; /** * An implementation of an {@link AbstractAuthenticationToken} @@ -70,4 +73,14 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken public AccessToken getAccessToken() { return this.accessToken; } + + public Set getAuthorizedScopes() { + // As per spec, in section 5.1 Successful Access Token Response + // https://tools.ietf.org/html/rfc6749#section-5.1 + // If AccessToken.scopes is empty, then default to the scopes + // originally requested by the client in the Authorization Request + return (!CollectionUtils.isEmpty(this.getAccessToken().getScopes()) ? + this.getAccessToken().getScopes() : + this.getClientRegistration().getScope()); + } } 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 91323026fd..25b84b211e 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 @@ -17,6 +17,7 @@ package org.springframework.security.oauth2.client.registration; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.oidc.core.OidcScope; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -340,7 +341,10 @@ public class ClientRegistration { Assert.notEmpty(this.scope, "scope cannot be empty"); Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty"); Assert.hasText(this.tokenUri, "tokenUri cannot be empty"); - Assert.hasText(this.userInfoUri, "userInfoUri cannot be empty"); + if (!this.scope.contains(OidcScope.OPENID)) { + // userInfoUri is optional for OIDC Clients + Assert.hasText(this.userInfoUri, "userInfoUri cannot be empty"); + } Assert.hasText(this.clientName, "clientName cannot be empty"); Assert.hasText(this.registrationId, "registrationId cannot be empty"); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/oidc/client/user/OidcUserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/oidc/client/user/OidcUserService.java index 2d539c274d..82a34d0c12 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/oidc/client/user/OidcUserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/oidc/client/user/OidcUserService.java @@ -21,13 +21,17 @@ import org.springframework.security.oauth2.client.authentication.OAuth2ClientAut import org.springframework.security.oauth2.client.user.OAuth2UserService; import org.springframework.security.oauth2.client.user.UserInfoRetriever; import org.springframework.security.oauth2.client.user.nimbus.NimbusUserInfoRetriever; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.oidc.client.authentication.OidcClientAuthenticationToken; +import org.springframework.security.oauth2.oidc.core.OidcScope; 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.OidcUserAuthority; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import java.util.Arrays; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -49,6 +53,8 @@ import java.util.Set; */ public class OidcUserService implements OAuth2UserService { private UserInfoRetriever userInfoRetriever = new NimbusUserInfoRetriever(); + private final Set userInfoScopes = new HashSet<>( + Arrays.asList(OidcScope.PROFILE, OidcScope.EMAIL, OidcScope.ADDRESS, OidcScope.PHONE)); @Override public OAuth2User loadUser(OAuth2ClientAuthenticationToken clientAuthentication) throws OAuth2AuthenticationException { @@ -57,8 +63,11 @@ public class OidcUserService implements OAuth2UserService { } OidcClientAuthenticationToken oidcClientAuthentication = (OidcClientAuthenticationToken)clientAuthentication; - Map userAttributes = this.getUserInfoRetriever().retrieve(oidcClientAuthentication); - UserInfo userInfo = new UserInfo(userAttributes); + UserInfo userInfo = null; + if (this.shouldRetrieveUserInfo(oidcClientAuthentication)) { + Map userAttributes = this.getUserInfoRetriever().retrieve(oidcClientAuthentication); + userInfo = new UserInfo(userAttributes); + } GrantedAuthority authority = new OidcUserAuthority(oidcClientAuthentication.getIdToken(), userInfo); Set authorities = new HashSet<>(); @@ -75,4 +84,28 @@ public class OidcUserService implements OAuth2UserService { Assert.notNull(userInfoRetriever, "userInfoRetriever cannot be null"); this.userInfoRetriever = userInfoRetriever; } + + private boolean shouldRetrieveUserInfo(OidcClientAuthenticationToken oidcClientAuthentication) { + // Auto-disabled if UserInfo Endpoint URI is not provided + if (StringUtils.isEmpty(oidcClientAuthentication.getClientRegistration().getProviderDetails() + .getUserInfoEndpoint().getUri())) { + + return false; + } + + // The Claims requested by the profile, email, address, and phone scope values + // are returned from the UserInfo Endpoint (as described in Section 5.3.2), + // when a response_type value is used that results in an Access Token being issued. + // However, when no Access Token is issued, which is the case for the response_type=id_token, + // the resulting Claims are returned in the ID Token. + // The Authorization Code Grant Flow, which is response_type=code, results in an Access Token being issued. + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals( + oidcClientAuthentication.getClientRegistration().getAuthorizationGrantType())) { + + // Return true if there is at least one match between the authorized scope(s) and UserInfo scope(s) + return oidcClientAuthentication.getAuthorizedScopes().stream().anyMatch(userInfoScopes::contains); + } + + return false; + } } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/OidcScope.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/OidcScope.java new file mode 100644 index 0000000000..c4f15f4193 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/oidc/core/OidcScope.java @@ -0,0 +1,45 @@ +/* + * 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.AccessToken; + +/** + * The scope values defined by the OpenID Connect Core 1.0 specification + * that can be used to request {@link StandardClaim Claims}. + *

+ * The scope(s) associated to an {@link AccessToken} determine what claims (resources) + * will be available when they are used to access OAuth 2.0 Protected Endpoints, + * such as the UserInfo Endpoint. + * + * @author Joe Grandja + * @since 5.0 + * @see StandardClaim + * @see Requesting Claims using Scope Values + */ +public interface OidcScope { + + String OPENID = "openid"; + + String PROFILE = "profile"; + + String EMAIL = "email"; + + String ADDRESS = "address"; + + String PHONE = "phone"; + +} diff --git a/samples/boot/oauth2login/src/main/java/sample/web/MainController.java b/samples/boot/oauth2login/src/main/java/sample/web/MainController.java index 7495c1f6ce..493a17cd55 100644 --- a/samples/boot/oauth2login/src/main/java/sample/web/MainController.java +++ b/samples/boot/oauth2login/src/main/java/sample/web/MainController.java @@ -21,12 +21,14 @@ import org.springframework.security.oauth2.client.authentication.OAuth2UserAuthe import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; +import java.util.Collections; import java.util.Map; /** @@ -46,13 +48,18 @@ public class MainController { @RequestMapping("/userinfo") public String userinfo(Model model, OAuth2UserAuthenticationToken authentication) { - Map userAttributes = this.webClient - .filter(oauth2Credentials(authentication)) - .get() - .uri(authentication.getClientAuthentication().getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri()) - .retrieve() - .bodyToMono(Map.class) - .block(); + Map userAttributes = Collections.emptyMap(); + String userInfoEndpointUri = authentication.getClientAuthentication().getClientRegistration() + .getProviderDetails().getUserInfoEndpoint().getUri(); + if (!StringUtils.isEmpty(userInfoEndpointUri)) { // userInfoEndpointUri is optional for OIDC Clients + userAttributes = this.webClient + .filter(oauth2Credentials(authentication)) + .get() + .uri(userInfoEndpointUri) + .retrieve() + .bodyToMono(Map.class) + .block(); + } model.addAttribute("userAttributes", userAttributes); return "userinfo"; }