001/******************************************************************************* 002 * Copyright 2018 The MIT Internet Trust Consortium 003 * 004 * Portions copyright 2011-2013 The MITRE Corporation 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 *******************************************************************************/ 018package org.mitre.oauth2.introspectingfilter; 019 020import static org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod.SECRET_BASIC; 021 022import java.io.IOException; 023import java.net.URI; 024import java.util.Calendar; 025import java.util.Date; 026import java.util.HashMap; 027import java.util.HashSet; 028import java.util.Map; 029import java.util.Set; 030 031import org.apache.http.client.HttpClient; 032import org.apache.http.impl.client.HttpClientBuilder; 033import org.mitre.oauth2.introspectingfilter.service.IntrospectionAuthorityGranter; 034import org.mitre.oauth2.introspectingfilter.service.IntrospectionConfigurationService; 035import org.mitre.oauth2.introspectingfilter.service.impl.SimpleIntrospectionAuthorityGranter; 036import org.mitre.oauth2.model.RegisteredClient; 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039import org.springframework.http.HttpMethod; 040import org.springframework.http.client.ClientHttpRequest; 041import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 042import org.springframework.security.core.Authentication; 043import org.springframework.security.core.AuthenticationException; 044import org.springframework.security.oauth2.common.OAuth2AccessToken; 045import org.springframework.security.oauth2.common.util.OAuth2Utils; 046import org.springframework.security.oauth2.provider.OAuth2Authentication; 047import org.springframework.security.oauth2.provider.OAuth2Request; 048import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; 049import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; 050import org.springframework.util.LinkedMultiValueMap; 051import org.springframework.util.MultiValueMap; 052import org.springframework.web.client.RestClientException; 053import org.springframework.web.client.RestTemplate; 054 055import com.google.gson.JsonElement; 056import com.google.gson.JsonObject; 057import com.google.gson.JsonParser; 058import com.nimbusds.jose.util.Base64; 059 060/** 061 * This ResourceServerTokenServices implementation introspects incoming tokens at a 062 * server's introspection endpoint URL and passes an Authentication object along 063 * based on the response from the introspection endpoint. 064 * @author jricher 065 * 066 */ 067public class IntrospectingTokenService implements ResourceServerTokenServices { 068 069 private IntrospectionConfigurationService introspectionConfigurationService; 070 private IntrospectionAuthorityGranter introspectionAuthorityGranter = new SimpleIntrospectionAuthorityGranter(); 071 072 private int defaultExpireTime = 300000; // 5 minutes in milliseconds 073 private boolean forceCacheExpireTime = false; // force removal of cached tokens based on default expire time 074 private boolean cacheNonExpiringTokens = false; 075 private boolean cacheTokens = true; 076 077 private HttpComponentsClientHttpRequestFactory factory; 078 079 public IntrospectingTokenService() { 080 this(HttpClientBuilder.create().useSystemProperties().build()); 081 } 082 083 public IntrospectingTokenService(HttpClient httpClient) { 084 this.factory = new HttpComponentsClientHttpRequestFactory(httpClient); 085 } 086 087 // Inner class to store in the hash map 088 private class TokenCacheObject { 089 OAuth2AccessToken token; 090 OAuth2Authentication auth; 091 Date cacheExpire; 092 093 private TokenCacheObject(OAuth2AccessToken token, OAuth2Authentication auth) { 094 this.token = token; 095 this.auth = auth; 096 097 // we don't need to check the cacheTokens values, because this won't actually be added to the cache if cacheTokens is false 098 // if the token isn't null we use the token expire time 099 // if forceCacheExpireTime is also true, we also make sure that the token expire time is shorter than the default expire time 100 if ((this.token.getExpiration() != null) && (!forceCacheExpireTime || (forceCacheExpireTime && (this.token.getExpiration().getTime() - System.currentTimeMillis() <= defaultExpireTime)))) { 101 this.cacheExpire = this.token.getExpiration(); 102 } else { // if the token doesn't have an expire time, or if the using forceCacheExpireTime the token expire time is longer than the default, then use the default expire time 103 Calendar cal = Calendar.getInstance(); 104 cal.add(Calendar.MILLISECOND, defaultExpireTime); 105 this.cacheExpire = cal.getTime(); 106 } 107 } 108 } 109 110 private Map<String, TokenCacheObject> authCache = new HashMap<>(); 111 /** 112 * Logger for this class 113 */ 114 private static final Logger logger = LoggerFactory.getLogger(IntrospectingTokenService.class); 115 116 /** 117 * @return the introspectionConfigurationService 118 */ 119 public IntrospectionConfigurationService getIntrospectionConfigurationService() { 120 return introspectionConfigurationService; 121 } 122 123 /** 124 * @param introspectionConfigurationService the introspectionConfigurationService to set 125 */ 126 public void setIntrospectionConfigurationService(IntrospectionConfigurationService introspectionUrlProvider) { 127 this.introspectionConfigurationService = introspectionUrlProvider; 128 } 129 130 /** 131 * @param introspectionAuthorityGranter the introspectionAuthorityGranter to set 132 */ 133 public void setIntrospectionAuthorityGranter(IntrospectionAuthorityGranter introspectionAuthorityGranter) { 134 this.introspectionAuthorityGranter = introspectionAuthorityGranter; 135 } 136 137 /** 138 * @return the introspectionAuthorityGranter 139 */ 140 public IntrospectionAuthorityGranter getIntrospectionAuthorityGranter() { 141 return introspectionAuthorityGranter; 142 } 143 144 /** 145 * get the default cache expire time in milliseconds 146 * @return 147 */ 148 public int getDefaultExpireTime() { 149 return defaultExpireTime; 150 } 151 152 /** 153 * set the default cache expire time in milliseconds 154 * @param defaultExpireTime 155 */ 156 public void setDefaultExpireTime(int defaultExpireTime) { 157 this.defaultExpireTime = defaultExpireTime; 158 } 159 160 /** 161 * check if forcing a cache expire time maximum value 162 * @return the forceCacheExpireTime setting 163 */ 164 public boolean isForceCacheExpireTime() { 165 return forceCacheExpireTime; 166 } 167 168 /** 169 * set forcing a cache expire time maximum value 170 * @param forceCacheExpireTime 171 */ 172 public void setForceCacheExpireTime(boolean forceCacheExpireTime) { 173 this.forceCacheExpireTime = forceCacheExpireTime; 174 } 175 176 /** 177 * Are non-expiring tokens cached using the default cache time 178 * @return state of cacheNonExpiringTokens 179 */ 180 public boolean isCacheNonExpiringTokens() { 181 return cacheNonExpiringTokens; 182 } 183 184 /** 185 * should non-expiring tokens be cached using the default cache timeout 186 * @param cacheNonExpiringTokens 187 */ 188 public void setCacheNonExpiringTokens(boolean cacheNonExpiringTokens) { 189 this.cacheNonExpiringTokens = cacheNonExpiringTokens; 190 } 191 192 /** 193 * Is the service caching tokens, or is it hitting the introspection end point every time 194 * @return true is caching tokens locally, false hits the introspection end point every time 195 */ 196 public boolean isCacheTokens() { 197 return cacheTokens; 198 } 199 200 /** 201 * Configure if the client should cache tokens locally or not 202 * @param cacheTokens 203 */ 204 public void setCacheTokens(boolean cacheTokens) { 205 this.cacheTokens = cacheTokens; 206 } 207 208 /** 209 * Check to see if the introspection end point response for a token has been cached locally 210 * This call will return the token if it has been cached and is still valid according to 211 * the cache expire time on the TokenCacheObject. If a cached value has been found but is 212 * expired, either by default expire times or the token's own expire time, then the token is 213 * removed from the cache and null is returned. 214 * @param key is the token to check 215 * @return the cached TokenCacheObject or null 216 */ 217 private TokenCacheObject checkCache(String key) { 218 if (cacheTokens && authCache.containsKey(key)) { 219 TokenCacheObject tco = authCache.get(key); 220 221 if (tco != null && tco.cacheExpire != null && tco.cacheExpire.after(new Date())) { 222 return tco; 223 } else { 224 // if the token is expired, don't keep things around. 225 authCache.remove(key); 226 } 227 } 228 return null; 229 } 230 231 private OAuth2Request createStoredRequest(final JsonObject token) { 232 String clientId = token.get("client_id").getAsString(); 233 Set<String> scopes = new HashSet<>(); 234 if (token.has("scope")) { 235 scopes.addAll(OAuth2Utils.parseParameterList(token.get("scope").getAsString())); 236 } 237 Map<String, String> parameters = new HashMap<>(); 238 parameters.put("client_id", clientId); 239 parameters.put("scope", OAuth2Utils.formatParameterList(scopes)); 240 OAuth2Request storedRequest = new OAuth2Request(parameters, clientId, null, true, scopes, null, null, null, null); 241 return storedRequest; 242 } 243 244 private Authentication createUserAuthentication(JsonObject token) { 245 JsonElement userId = token.get("user_id"); 246 if(userId == null) { 247 userId = token.get("sub"); 248 if (userId == null) { 249 return null; 250 } 251 } 252 253 return new PreAuthenticatedAuthenticationToken(userId.getAsString(), token, introspectionAuthorityGranter.getAuthorities(token)); 254 } 255 256 private OAuth2AccessToken createAccessToken(final JsonObject token, final String tokenString) { 257 OAuth2AccessToken accessToken = new OAuth2AccessTokenImpl(token, tokenString); 258 return accessToken; 259 } 260 261 /** 262 * Validate a token string against the introspection endpoint, 263 * then parse it and store it in the local cache if caching is enabled. 264 * 265 * @param accessToken Token to pass to the introspection endpoint 266 * @return TokenCacheObject containing authentication and token if the token was valid, otherwise null 267 */ 268 private TokenCacheObject parseToken(String accessToken) { 269 270 // find out which URL to ask 271 String introspectionUrl; 272 RegisteredClient client; 273 try { 274 introspectionUrl = introspectionConfigurationService.getIntrospectionUrl(accessToken); 275 client = introspectionConfigurationService.getClientConfiguration(accessToken); 276 } catch (IllegalArgumentException e) { 277 logger.error("Unable to load introspection URL or client configuration", e); 278 return null; 279 } 280 // Use the SpringFramework RestTemplate to send the request to the 281 // endpoint 282 String validatedToken = null; 283 284 RestTemplate restTemplate; 285 MultiValueMap<String, String> form = new LinkedMultiValueMap<>(); 286 287 final String clientId = client.getClientId(); 288 final String clientSecret = client.getClientSecret(); 289 290 if (SECRET_BASIC.equals(client.getTokenEndpointAuthMethod())){ 291 // use BASIC auth if configured to do so 292 restTemplate = new RestTemplate(factory) { 293 294 @Override 295 protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException { 296 ClientHttpRequest httpRequest = super.createRequest(url, method); 297 httpRequest.getHeaders().add("Authorization", 298 String.format("Basic %s", Base64.encode(String.format("%s:%s", clientId, clientSecret)) )); 299 return httpRequest; 300 } 301 }; 302 } else { //Alternatively use form based auth 303 restTemplate = new RestTemplate(factory); 304 305 form.add("client_id", clientId); 306 form.add("client_secret", clientSecret); 307 } 308 309 form.add("token", accessToken); 310 311 try { 312 validatedToken = restTemplate.postForObject(introspectionUrl, form, String.class); 313 } catch (RestClientException rce) { 314 logger.error("validateToken", rce); 315 return null; 316 } 317 if (validatedToken != null) { 318 // parse the json 319 JsonElement jsonRoot = new JsonParser().parse(validatedToken); 320 if (!jsonRoot.isJsonObject()) { 321 return null; // didn't get a proper JSON object 322 } 323 324 JsonObject tokenResponse = jsonRoot.getAsJsonObject(); 325 326 if (tokenResponse.get("error") != null) { 327 // report an error? 328 logger.error("Got an error back: " + tokenResponse.get("error") + ", " + tokenResponse.get("error_description")); 329 return null; 330 } 331 332 if (!tokenResponse.get("active").getAsBoolean()) { 333 // non-valid token 334 logger.info("Server returned non-active token"); 335 return null; 336 } 337 // create an OAuth2Authentication 338 OAuth2Authentication auth = new OAuth2Authentication(createStoredRequest(tokenResponse), createUserAuthentication(tokenResponse)); 339 // create an OAuth2AccessToken 340 OAuth2AccessToken token = createAccessToken(tokenResponse, accessToken); 341 342 if (token.getExpiration() == null || token.getExpiration().after(new Date())) { 343 // Store them in the cache 344 TokenCacheObject tco = new TokenCacheObject(token, auth); 345 if (cacheTokens && (cacheNonExpiringTokens || token.getExpiration() != null)) { 346 authCache.put(accessToken, tco); 347 } 348 return tco; 349 } 350 } 351 352 // when the token is invalid for whatever reason 353 return null; 354 } 355 356 @Override 357 public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException { 358 // First check if the in memory cache has an Authentication object, and 359 // that it is still valid 360 // If Valid, return it 361 TokenCacheObject cacheAuth = checkCache(accessToken); 362 if (cacheAuth != null) { 363 return cacheAuth.auth; 364 } else { 365 cacheAuth = parseToken(accessToken); 366 if (cacheAuth != null) { 367 return cacheAuth.auth; 368 } else { 369 return null; 370 } 371 } 372 } 373 374 @Override 375 public OAuth2AccessToken readAccessToken(String accessToken) { 376 // First check if the in memory cache has a Token object, and that it is 377 // still valid 378 // If Valid, return it 379 TokenCacheObject cacheAuth = checkCache(accessToken); 380 if (cacheAuth != null) { 381 return cacheAuth.token; 382 } else { 383 cacheAuth = parseToken(accessToken); 384 if (cacheAuth != null) { 385 return cacheAuth.token; 386 } else { 387 return null; 388 } 389 } 390 } 391 392}