/******************************************************************************* * Cloud Foundry * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). * You may not use this product except in compliance with the License. * * This product includes a number of subcomponents with * separate copyright notices and license terms. Your use of these * subcomponents is subject to the terms and conditions of the * subcomponent's license, as noted in the LICENSE file. *******************************************************************************/ package org.cloudfoundry.identity.uaa.oauth; import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; import org.cloudfoundry.identity.uaa.oauth.token.TokenConstants; import org.cloudfoundry.identity.uaa.security.DefaultSecurityContextAccessor; import org.cloudfoundry.identity.uaa.security.SecurityContextAccessor; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.util.UaaStringUtils; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.util.UaaTokenUtils; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.common.exceptions.InvalidClientException; import org.springframework.security.oauth2.common.exceptions.InvalidScopeException; import org.springframework.security.oauth2.common.exceptions.UnauthorizedClientException; import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.provider.AuthorizationRequest; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.OAuth2Request; import org.springframework.security.oauth2.provider.OAuth2RequestFactory; import org.springframework.security.oauth2.provider.TokenRequest; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import static java.util.Collections.emptySet; import static java.util.Collections.unmodifiableMap; import static java.util.Optional.ofNullable; import static org.cloudfoundry.identity.uaa.oauth.client.ClientConstants.REQUIRED_USER_GROUPS; import static org.springframework.security.oauth2.common.util.OAuth2Utils.GRANT_TYPE; /** * An {@link OAuth2RequestFactory} that applies various UAA-specific * rules to an authorization request, * validating it and setting the default values for requested scopes and resource ids. * * */ public class UaaAuthorizationRequestManager implements OAuth2RequestFactory { private final ClientDetailsService clientDetailsService; private Map<String, String> scopeToResource = Collections.singletonMap("openid", "openid"); private String scopeSeparator = "."; private SecurityContextAccessor securityContextAccessor = new DefaultSecurityContextAccessor(); private Collection<String> defaultScopes = new HashSet<String>(); public OAuth2RequestFactory getRequestFactory() { return requestFactory; } public void setRequestFactory(OAuth2RequestFactory requestFactory) { this.requestFactory = requestFactory; } private OAuth2RequestFactory requestFactory; private UaaUserDatabase uaaUserDatabase; private IdentityProviderProvisioning providerProvisioning; public UaaAuthorizationRequestManager(ClientDetailsService clientDetailsService, UaaUserDatabase userDatabase, IdentityProviderProvisioning providerProvisioning) { this.clientDetailsService = clientDetailsService; this.uaaUserDatabase = userDatabase; this.requestFactory = new DefaultOAuth2RequestFactory(clientDetailsService); this.providerProvisioning = providerProvisioning; } /** * Default requested scopes that are always added to a user token (and then removed if * the client doesn't have permission). * * @param defaultScopes the defaultScopes to set */ public void setDefaultScopes(Collection<String> defaultScopes) { this.defaultScopes = defaultScopes; } /** * A helper to pull stuff out of the current security context. * * @param securityContextAccessor the securityContextAccessor to set */ public void setSecurityContextAccessor(SecurityContextAccessor securityContextAccessor) { this.securityContextAccessor = securityContextAccessor; } /** * A map from scope name to resource id, for cases (like openid) that cannot * be extracted from the scope name. * * @param scopeToResource the map to use */ public void setScopesToResources(Map<String, String> scopeToResource) { this.scopeToResource = new HashMap<String, String>(scopeToResource); } /** * The string used to separate resource ids from feature names in requested scopes * (e.g. "cloud_controller.read"). * * @param scopeSeparator the scope separator to set. Default is period "." */ public void setScopeSeparator(String scopeSeparator) { this.scopeSeparator = scopeSeparator; } /** * Create an authorization request applying various UAA rules to the * authorizationParameters and the registered * client details. * <ul> * <li>For client_credentials grants, the default requested scopes are the client's * granted authorities</li> * <li>For other grant types the default requested scopes are the registered requested scopes in * the client details</li> * <li>Only requested scopes in those lists are valid, otherwise there is an exception * </li> * <li>If the requested scopes contain separators then resource ids are extracted as * the scope value up to the last index of the separator</li> * <li>Some requested scopes can be hard-wired to resource ids (like the open id * connect values), in which case the separator is ignored</li> * </ul> * */ @Override public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) { String clientId = authorizationParameters.get("client_id"); BaseClientDetails clientDetails = (BaseClientDetails)clientDetailsService.loadClientByClientId(clientId); validateParameters(authorizationParameters, clientDetails); Set<String> scopes = OAuth2Utils.parseParameterList(authorizationParameters.get(OAuth2Utils.SCOPE)); Set<String> responseTypes = OAuth2Utils.parseParameterList(authorizationParameters.get(OAuth2Utils.RESPONSE_TYPE)); String grantType = authorizationParameters.get(GRANT_TYPE); String state = authorizationParameters.get(OAuth2Utils.STATE); String redirectUri = authorizationParameters.get(OAuth2Utils.REDIRECT_URI); if ((scopes == null || scopes.isEmpty())) { if ("client_credentials".equals(grantType)) { // The client authorities should be a list of requestedScopes scopes = AuthorityUtils.authorityListToSet(clientDetails.getAuthorities()); } else { // The default for a user token is the requestedScopes registered with // the client scopes = clientDetails.getScope(); } } if (!"client_credentials".equals(grantType) && securityContextAccessor.isUser()) { String userId = securityContextAccessor.getUserId(); UaaUser uaaUser = uaaUserDatabase.retrieveUserById(userId); Collection<? extends GrantedAuthority> authorities = uaaUser.getAuthorities(); //validate scopes scopes = checkUserScopes(scopes, authorities, clientDetails); //check client IDP relationship - allowed providers checkClientIdpAuthorization(clientDetails, uaaUser); } Set<String> resourceIds = getResourceIds(clientDetails, scopes); clientDetails.setResourceIds(resourceIds); Map<String, String> actualParameters = new HashMap<>(authorizationParameters); AuthorizationRequest request = new AuthorizationRequest( actualParameters, null, clientId, scopes.isEmpty()?null:scopes, null, null, false, state, redirectUri, responseTypes ); if (!scopes.isEmpty()) { request.setScope(scopes); } request.setResourceIdsAndAuthoritiesFromClientDetails(clientDetails); return request; } /** * Apply UAA rules to validate the requested scopes scope. For client credentials * grants the valid requested scopes are actually in * the authorities of the client. * */ public void validateParameters(Map<String, String> parameters, ClientDetails clientDetails) { if (parameters.containsKey("scope")) { Set<String> validScope = clientDetails.getScope(); if ("client_credentials".equals(parameters.get("grant_type"))) { validScope = AuthorityUtils.authorityListToSet(clientDetails.getAuthorities()); } Set<Pattern> validWildcards = constructWildcards(validScope); Set<String> scopes = OAuth2Utils.parseParameterList(parameters.get("scope")); for (String scope : scopes) { if (!matches(validWildcards, scope)) { throw new InvalidScopeException(scope + " is invalid. Please use a valid scope name in the request"); } } } } protected void checkClientIdpAuthorization(BaseClientDetails client, UaaUser user) { List<String> allowedProviders = (List<String>)client.getAdditionalInformation().get(ClientConstants.ALLOWED_PROVIDERS); if (allowedProviders==null) { //null means any providers - no allowed providers means that we always allow it (backwards compatible) return; } else if (allowedProviders.isEmpty()){ throw new UnauthorizedClientException ("Client is not authorized for any identity providers."); } try { IdentityProvider provider = providerProvisioning.retrieveByOrigin(user.getOrigin(), user.getZoneId()); if (provider==null || !allowedProviders.contains(provider.getOriginKey())) { throw new DisallowedIdpException("Client is not authorized for specified user's identity provider."); } } catch (EmptyResultDataAccessException x) { //this should not happen...but if it does throw new UnauthorizedClientException ("User does not belong to a valid identity provider."); } } /** * Add or remove requested scopes derived from the current authenticated user's * authorities (if any) * * @param requestedScopes the initial set of requested scopes from the client registration * @param clientDetails * @param authorities the users authorities * @return modified requested scopes adapted according to the rules specified */ private Set<String> checkUserScopes(Set<String> requestedScopes, Collection<? extends GrantedAuthority> authorities, ClientDetails clientDetails) { Set<String> allowed = new LinkedHashSet<>(AuthorityUtils.authorityListToSet(authorities)); // Add in all default requestedScopes allowed.addAll(defaultScopes); // Find intersection of user authorities, default requestedScopes and client requestedScopes: Set<String> result = intersectScopes(new LinkedHashSet<>(requestedScopes), clientDetails.getScope(), allowed); // Check that a token with empty scope is not going to be granted if (result.isEmpty() && !clientDetails.getScope().isEmpty()) { throw new InvalidScopeException(requestedScopes + " is invalid. This user is not allowed any of the requested scopes"); } Collection<String> requiredUserGroups = ofNullable((Collection<String>) clientDetails.getAdditionalInformation().get(REQUIRED_USER_GROUPS)).orElse(emptySet()); if (!UaaTokenUtils.hasRequiredUserAuthorities(requiredUserGroups, authorities)) { throw new InvalidScopeException("User does not meet the client's required group criteria."); } return result; } protected Set<String> intersectScopes(Set<String> requestedScopes, Set<String> clientScopes, Set<String> userScopes) { Set<String> result = new HashSet<>(userScopes); Set<Pattern> clientWildcards = constructWildcards(clientScopes); for (Iterator<String> iter = result.iterator(); iter.hasNext();) { String scope = iter.next(); if (!matches(clientWildcards, scope)) { iter.remove(); } } Set<Pattern> requestedWildcards = constructWildcards(requestedScopes); // Weed out disallowed requestedScopes: for (Iterator<String> iter = result.iterator(); iter.hasNext();) { String scope = iter.next(); if (!matches(requestedWildcards, scope)) { iter.remove(); } } return result; } protected Set<Pattern> constructWildcards(Set<String> scopes) { return UaaStringUtils.constructWildcards(scopes); } protected boolean matches(Set<Pattern> wildcards, String scope) { return UaaStringUtils.matches(wildcards, scope); } private Set<String> getResourceIds(ClientDetails clientDetails, Set<String> scopes) { Set<String> resourceIds = new LinkedHashSet<String>(); //at a minimum - the resourceIds should contain the client this is intended for //http://openid.net/specs/openid-connect-core-1_0.html#IDToken if (clientDetails.getClientId()!=null) { resourceIds.add(clientDetails.getClientId()); } for (String scope : scopes) { if (scopeToResource.containsKey(scope)) { resourceIds.add(scopeToResource.get(scope)); } else if (scope.contains(scopeSeparator) && !scope.endsWith(scopeSeparator) && !scope.equals("uaa.none")) { String id = scope.substring(0, scope.lastIndexOf(scopeSeparator)); resourceIds.add(id); } } return resourceIds.isEmpty() ? clientDetails.getResourceIds() : resourceIds; } @Override public OAuth2Request createOAuth2Request(AuthorizationRequest request) { return requestFactory.createOAuth2Request(request); } @Override public OAuth2Request createOAuth2Request(ClientDetails client, TokenRequest tokenRequest) { return requestFactory.createOAuth2Request(client, tokenRequest); } @Override public TokenRequest createTokenRequest(Map<String, String> requestParameters, ClientDetails authenticatedClient) { ClientDetails targetClient = authenticatedClient; //clone so we can modify it requestParameters = new HashMap<>(requestParameters); String clientId = requestParameters.get(OAuth2Utils.CLIENT_ID); String grantType = requestParameters.get(GRANT_TYPE); if (clientId == null) { // if the clientId wasn't passed in in the map, we add pull it from the authenticated client object clientId = authenticatedClient.getClientId(); } else { if (TokenConstants.GRANT_TYPE_USER_TOKEN.equals(grantType)) { targetClient = clientDetailsService.loadClientByClientId(clientId); requestParameters.put(TokenConstants.USER_TOKEN_REQUESTING_CLIENT_ID, authenticatedClient.getClientId()); } else if (!clientId.equals(authenticatedClient.getClientId())) { // otherwise, make sure that they match throw new InvalidClientException("Given client ID does not match authenticated client"); } } Set<String> scopes = extractScopes(requestParameters, targetClient); Set<String> resourceIds = getResourceIds(targetClient, scopes); TokenRequest tokenRequest = new UaaTokenRequest(unmodifiableMap(requestParameters), authenticatedClient.getClientId(), scopes, grantType, resourceIds); return tokenRequest; } protected Set<String> extractScopes(Map<String, String> requestParameters, ClientDetails clientDetails) { boolean clientCredentials = "client_credentials".equals(requestParameters.get(GRANT_TYPE)); Set<String> scopes = OAuth2Utils.parseParameterList(requestParameters.get(OAuth2Utils.SCOPE)); if ((scopes == null || scopes.isEmpty())) { // If no scopes are specified in the incoming data, use the default values registered with the client // (the spec allows us to choose between this option and rejecting the request completely, so we'll take the // least obnoxious choice as a default). if (clientCredentials) { Set<String> authorities = new HashSet<>(); for (GrantedAuthority a : clientDetails.getAuthorities()) { authorities.add(a.getAuthority()); } scopes = authorities; } else { scopes = clientDetails.getScope(); } } if (!clientCredentials) { Set<String> userScopes = getUserScopes(); scopes = intersectScopes(scopes, clientDetails.getScope(), userScopes); } return scopes; } protected Set<String> getUserScopes() { Set<String> scopes = new HashSet<>(); if (securityContextAccessor.isUser()) { String userId = securityContextAccessor.getUserId(); Collection<? extends GrantedAuthority> authorities = uaaUserDatabase != null ? uaaUserDatabase.retrieveUserById(userId).getAuthorities() : securityContextAccessor.getAuthorities(); for (GrantedAuthority a : authorities) { scopes.add(a.getAuthority()); } } return scopes; } @Override public TokenRequest createTokenRequest(AuthorizationRequest authorizationRequest, String grantType) { return requestFactory.createTokenRequest(authorizationRequest, grantType); } public class UaaTokenRequest extends TokenRequest { private Set<String> resourceIds; Set<String> responseTypes; public UaaTokenRequest(Map<String, String> requestParameters, String clientId, Collection<String> scope, String grantType, Set<String> resourceIds) { super(requestParameters, clientId, scope, grantType); this.resourceIds = resourceIds; this.responseTypes = OAuth2Utils.parseParameterList(requestParameters.get(OAuth2Utils.RESPONSE_TYPE)); } @Override public OAuth2Request createOAuth2Request(ClientDetails client) { OAuth2Request request = super.createOAuth2Request(client); return new OAuth2Request( request.getRequestParameters(), client.getClientId(), client.getAuthorities(), true, request.getScope(), resourceIds, request.getRedirectUri(), responseTypes, request.getExtensions()); } } }