Browse Source

Retrieving the UserInfo is conditional

Fixes gh-4451
Joe Grandja 8 years ago
parent
commit
ad91adf9dc

+ 13 - 0
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<String> 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());
+	}
 }

+ 5 - 1
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");
 		}

+ 35 - 2
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<String> 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<String, Object> userAttributes = this.getUserInfoRetriever().retrieve(oidcClientAuthentication);
-		UserInfo userInfo = new UserInfo(userAttributes);
+		UserInfo userInfo = null;
+		if (this.shouldRetrieveUserInfo(oidcClientAuthentication)) {
+			Map<String, Object> userAttributes = this.getUserInfoRetriever().retrieve(oidcClientAuthentication);
+			userInfo = new UserInfo(userAttributes);
+		}
 
 		GrantedAuthority authority = new OidcUserAuthority(oidcClientAuthentication.getIdToken(), userInfo);
 		Set<GrantedAuthority> 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;
+	}
 }

+ 45 - 0
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 <i>scope</i> values defined by the <i>OpenID Connect Core 1.0</i> specification
+ * that can be used to request {@link StandardClaim Claims}.
+ * <p>
+ * The scope(s) associated to an {@link AccessToken} determine what claims (resources)
+ * will be available when they are used to access <i>OAuth 2.0 Protected Endpoints</i>,
+ * such as the <i>UserInfo Endpoint</i>.
+ *
+ * @author Joe Grandja
+ * @since 5.0
+ * @see StandardClaim
+ * @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims">Requesting Claims using Scope Values</a>
+ */
+public interface OidcScope {
+
+	String OPENID = "openid";
+
+	String PROFILE = "profile";
+
+	String EMAIL = "email";
+
+	String ADDRESS = "address";
+
+	String PHONE = "phone";
+
+}

+ 14 - 7
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";
 	}