1 /* 2 * Copyright 2002-2011 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * https://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.springframework.security.oauth2.provider.vote; 18 19 import java.util.Collection; 20 import java.util.Collections; 21 import java.util.Set; 22 23 import org.springframework.security.access.AccessDecisionVoter; 24 import org.springframework.security.access.AccessDeniedException; 25 import org.springframework.security.access.ConfigAttribute; 26 import org.springframework.security.core.Authentication; 27 import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException; 28 import org.springframework.security.oauth2.provider.AuthorizationRequest; 29 import org.springframework.security.oauth2.provider.OAuth2Authentication; 30 import org.springframework.security.oauth2.provider.OAuth2Request; 31 32 /** 33 * <p> 34 * Votes if any {@link ConfigAttribute#getAttribute()} starts with a prefix indicating that it is an OAuth2 scope. The 35 * default prefix string is <code>SCOPE_</code>, but this may be overridden to any value. Can also be used to deny 36 * access to an OAuth2 client by explicitly specifying an attribute value <code>DENY_OAUTH</code>. Typically you would 37 * want to explicitly deny access to all non-public resources that are not part of any scope. 38 * </p> 39 * 40 * <p> 41 * Abstains from voting if no configuration attribute commences with the scope prefix, or if the current 42 * <code>Authentication</code> is not a {@link OAuth2Authentication} or the current client authentication is not a 43 * {@link AuthorizationRequest} (which contains the scope data). Votes to grant access if there is an exact matching 44 * {@link AuthorizationRequest#getScope() authorized scope} to a <code>ConfigAttribute</code> starting with the scope 45 * prefix. Votes to deny access if there is no exact matching authorized scope to a <code>ConfigAttribute</code> 46 * starting with the scope prefix. 47 * </p> 48 * 49 * <p> 50 * All comparisons and prefixes are case insensitive so you can use (e.g.) <code>SCOPE_READ</code> for simple 51 * Facebook-like scope names that might be lower case in the resource definition, or 52 * <code>scope=https://my.company.com/scopes/read/</code> (<code>scopePrefix="scope="</code>) for Google-like URI scope 53 * names. 54 * </p> 55 * 56 * @author Dave Syer 57 * 58 */ 59 public class ScopeVoter implements AccessDecisionVoter<Object> { 60 61 private String scopePrefix = "SCOPE_"; 62 63 private String denyAccess = "DENY_OAUTH"; 64 65 private boolean throwException = true; 66 67 /** 68 * Flag to determine the behaviour on access denied. If set then we throw an {@link InsufficientScopeException} 69 * instead of returning {@link AccessDecisionVoter#ACCESS_DENIED}. This is unconventional for an access decision 70 * voter because it vetos the other voters in the chain, but it enables us to pass a message to the caller with 71 * information about the required scope. 72 * 73 * @param throwException the flag to set (default true) 74 */ 75 public void setThrowException(boolean throwException) { 76 this.throwException = throwException; 77 } 78 79 /** 80 * Allows the default role prefix of <code>SCOPE_</code> to be overridden. May be set to an empty value, although 81 * this is usually not desirable. 82 * 83 * @param scopePrefix the new prefix 84 */ 85 public void setScopePrefix(String scopePrefix) { 86 this.scopePrefix = scopePrefix; 87 } 88 89 /** 90 * The name of the config attribute that can be used to deny access to OAuth2 client. Defaults to 91 * <code>DENY_OAUTH</code>. 92 * 93 * @param denyAccess the deny access attribute value to set 94 */ 95 public void setDenyAccess(String denyAccess) { 96 this.denyAccess = denyAccess; 97 } 98 99 public boolean supports(ConfigAttribute attribute) { 100 if (denyAccess.equals(attribute.getAttribute()) || (attribute.getAttribute() != null) 101 && attribute.getAttribute().startsWith(scopePrefix)) { 102 return true; 103 } 104 else { 105 return false; 106 } 107 } 108 109 /** 110 * This implementation supports any type of class, because it does not query the presented secure object. 111 * 112 * @param clazz the secure object 113 * 114 * @return always <code>true</code> 115 */ 116 public boolean supports(Class<?> clazz) { 117 return true; 118 } 119 120 public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) { 121 122 int result = ACCESS_ABSTAIN; 123 124 if (!(authentication instanceof OAuth2Authentication)) { 125 return result; 126 127 } 128 129 for (ConfigAttribute attribute : attributes) { 130 if (denyAccess.equals(attribute.getAttribute())) { 131 return ACCESS_DENIED; 132 } 133 } 134 135 OAuth2Request clientAuthentication = ((OAuth2Authentication) authentication).getOAuth2Request(); 136 137 for (ConfigAttribute attribute : attributes) { 138 if (this.supports(attribute)) { 139 result = ACCESS_DENIED; 140 141 Set<String> scopes = clientAuthentication.getScope(); 142 for (String scope : scopes) { 143 if (attribute.getAttribute().toUpperCase().equals((scopePrefix + scope).toUpperCase())) { 144 return ACCESS_GRANTED; 145 } 146 } 147 if (result == ACCESS_DENIED && throwException) { 148 InsufficientScopeException failure = new InsufficientScopeException( 149 "Insufficient scope for this resource", Collections.singleton(attribute.getAttribute() 150 .substring(scopePrefix.length()))); 151 throw new AccessDeniedException(failure.getMessage(), failure); 152 } 153 } 154 } 155 156 return result; 157 } 158 159 }