View Javadoc
1   /*
2    * Copyright 2008 Web Cohesion
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5    * the License. You may obtain a copy of the License at
6    * 
7    * https://www.apache.org/licenses/LICENSE-2.0
8    * 
9    * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10   * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11   * specific language governing permissions and limitations under the License.
12   */
13  
14  package org.springframework.security.oauth2.provider.token;
15  
16  import java.util.Date;
17  import java.util.Set;
18  import java.util.UUID;
19  
20  import org.springframework.beans.factory.InitializingBean;
21  import org.springframework.security.authentication.AuthenticationManager;
22  import org.springframework.security.core.Authentication;
23  import org.springframework.security.core.AuthenticationException;
24  import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken;
25  import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
26  import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken;
27  import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken;
28  import org.springframework.security.oauth2.common.OAuth2AccessToken;
29  import org.springframework.security.oauth2.common.OAuth2RefreshToken;
30  import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
31  import org.springframework.security.oauth2.common.exceptions.InvalidScopeException;
32  import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
33  import org.springframework.security.oauth2.provider.ClientDetails;
34  import org.springframework.security.oauth2.provider.ClientDetailsService;
35  import org.springframework.security.oauth2.provider.ClientRegistrationException;
36  import org.springframework.security.oauth2.provider.OAuth2Authentication;
37  import org.springframework.security.oauth2.provider.OAuth2Request;
38  import org.springframework.security.oauth2.provider.TokenRequest;
39  import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
40  import org.springframework.transaction.annotation.Transactional;
41  import org.springframework.util.Assert;
42  
43  /**
44   * Base implementation for token services using random UUID values for the access token and refresh token values. The
45   * main extension point for customizations is the {@link TokenEnhancer} which will be called after the access and
46   * refresh tokens have been generated but before they are stored.
47   * <p>
48   * Persistence is delegated to a {@code TokenStore} implementation and customization of the access token to a
49   * {@link TokenEnhancer}.
50   * 
51   * @author Ryan Heaton
52   * @author Luke Taylor
53   * @author Dave Syer
54   */
55  public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
56  		ConsumerTokenServices, InitializingBean {
57  
58  	private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.
59  
60  	private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.
61  
62  	private boolean supportRefreshToken = false;
63  
64  	private boolean reuseRefreshToken = true;
65  
66  	private TokenStore tokenStore;
67  
68  	private ClientDetailsService clientDetailsService;
69  
70  	private TokenEnhancer accessTokenEnhancer;
71  
72  	private AuthenticationManager authenticationManager;
73  
74  	/**
75  	 * Initialize these token services. If no random generator is set, one will be created.
76  	 */
77  	public void afterPropertiesSet() throws Exception {
78  		Assert.notNull(tokenStore, "tokenStore must be set");
79  	}
80  
81  	@Transactional
82  	public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
83  
84  		OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
85  		OAuth2RefreshToken refreshToken = null;
86  		if (existingAccessToken != null) {
87  			if (existingAccessToken.isExpired()) {
88  				if (existingAccessToken.getRefreshToken() != null) {
89  					refreshToken = existingAccessToken.getRefreshToken();
90  					// The token store could remove the refresh token when the
91  					// access token is removed, but we want to
92  					// be sure...
93  					tokenStore.removeRefreshToken(refreshToken);
94  				}
95  				tokenStore.removeAccessToken(existingAccessToken);
96  			}
97  			else {
98  				// Re-store the access token in case the authentication has changed
99  				tokenStore.storeAccessToken(existingAccessToken, authentication);
100 				return existingAccessToken;
101 			}
102 		}
103 
104 		// Only create a new refresh token if there wasn't an existing one
105 		// associated with an expired access token.
106 		// Clients might be holding existing refresh tokens, so we re-use it in
107 		// the case that the old access token
108 		// expired.
109 		if (refreshToken == null) {
110 			refreshToken = createRefreshToken(authentication);
111 		}
112 		// But the refresh token itself might need to be re-issued if it has
113 		// expired.
114 		else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
115 			ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
116 			if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
117 				refreshToken = createRefreshToken(authentication);
118 			}
119 		}
120 
121 		OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
122 		tokenStore.storeAccessToken(accessToken, authentication);
123 		// In case it was modified
124 		refreshToken = accessToken.getRefreshToken();
125 		if (refreshToken != null) {
126 			tokenStore.storeRefreshToken(refreshToken, authentication);
127 		}
128 		return accessToken;
129 
130 	}
131 
132 	@Transactional(noRollbackFor={InvalidTokenException.class, InvalidGrantException.class})
133 	public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)
134 			throws AuthenticationException {
135 
136 		if (!supportRefreshToken) {
137 			throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
138 		}
139 
140 		OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue);
141 		if (refreshToken == null) {
142 			throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
143 		}
144 
145 		OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken);
146 		if (this.authenticationManager != null && !authentication.isClientOnly()) {
147 			// The client has already been authenticated, but the user authentication might be old now, so give it a
148 			// chance to re-authenticate.
149 			Authentication user = new PreAuthenticatedAuthenticationToken(authentication.getUserAuthentication(), "", authentication.getAuthorities());
150 			user = authenticationManager.authenticate(user);
151 			Object details = authentication.getDetails();
152 			authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);
153 			authentication.setDetails(details);
154 		}
155 		String clientId = authentication.getOAuth2Request().getClientId();
156 		if (clientId == null || !clientId.equals(tokenRequest.getClientId())) {
157 			throw new InvalidGrantException("Wrong client for this refresh token: " + refreshTokenValue);
158 		}
159 
160 		// clear out any access tokens already associated with the refresh
161 		// token.
162 		tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
163 
164 		if (isExpired(refreshToken)) {
165 			tokenStore.removeRefreshToken(refreshToken);
166 			throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken);
167 		}
168 
169 		authentication = createRefreshedAuthentication(authentication, tokenRequest);
170 
171 		if (!reuseRefreshToken) {
172 			tokenStore.removeRefreshToken(refreshToken);
173 			refreshToken = createRefreshToken(authentication);
174 		}
175 
176 		OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
177 		tokenStore.storeAccessToken(accessToken, authentication);
178 		if (!reuseRefreshToken) {
179 			tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication);
180 		}
181 		return accessToken;
182 	}
183 
184 	public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
185 		return tokenStore.getAccessToken(authentication);
186 	}
187 
188 	/**
189 	 * Create a refreshed authentication.
190 	 * 
191 	 * @param authentication The authentication.
192 	 * @param request The scope for the refreshed token.
193 	 * @return The refreshed authentication.
194 	 * @throws InvalidScopeException If the scope requested is invalid or wider than the original scope.
195 	 */
196 	private OAuth2Authenticationork/security/oauth2/provider/OAuth2Authentication.html#OAuth2Authentication">OAuth2Authentication createRefreshedAuthentication(OAuth2Authentication authentication, TokenRequest request) {
197 		OAuth2Authentication narrowed = authentication;
198 		Set<String> scope = request.getScope();
199 		OAuth2Request clientAuth = authentication.getOAuth2Request().refresh(request);
200 		if (scope != null && !scope.isEmpty()) {
201 			Set<String> originalScope = clientAuth.getScope();
202 			if (originalScope == null || !originalScope.containsAll(scope)) {
203 				throw new InvalidScopeException("Unable to narrow the scope of the client authentication to " + scope
204 						+ ".", originalScope);
205 			}
206 			else {
207 				clientAuth = clientAuth.narrowScope(scope);
208 			}
209 		}
210 		narrowed = new OAuth2Authentication(clientAuth, authentication.getUserAuthentication());
211 		return narrowed;
212 	}
213 
214 	protected boolean isExpired(OAuth2RefreshToken refreshToken) {
215 		if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
216 			ExpiringOAuth2RefreshToken expiringToken = (ExpiringOAuth2RefreshToken) refreshToken;
217 			return expiringToken.getExpiration() == null
218 					|| System.currentTimeMillis() > expiringToken.getExpiration().getTime();
219 		}
220 		return false;
221 	}
222 
223 	public OAuth2AccessToken readAccessToken(String accessToken) {
224 		return tokenStore.readAccessToken(accessToken);
225 	}
226 
227 	public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException,
228 			InvalidTokenException {
229 		OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
230 		if (accessToken == null) {
231 			throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
232 		}
233 		else if (accessToken.isExpired()) {
234 			tokenStore.removeAccessToken(accessToken);
235 			throw new InvalidTokenException("Access token expired: " + accessTokenValue);
236 		}
237 
238 		OAuth2Authentication result = tokenStore.readAuthentication(accessToken);
239 		if (result == null) {
240 			// in case of race condition
241 			throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
242 		}
243 		if (clientDetailsService != null) {
244 			String clientId = result.getOAuth2Request().getClientId();
245 			try {
246 				clientDetailsService.loadClientByClientId(clientId);
247 			}
248 			catch (ClientRegistrationException e) {
249 				throw new InvalidTokenException("Client not valid: " + clientId, e);
250 			}
251 		}
252 		return result;
253 	}
254 
255 	public String getClientId(String tokenValue) {
256 		OAuth2Authentication authentication = tokenStore.readAuthentication(tokenValue);
257 		if (authentication == null) {
258 			throw new InvalidTokenException("Invalid access token: " + tokenValue);
259 		}
260 		OAuth2Request clientAuth = authentication.getOAuth2Request();
261 		if (clientAuth == null) {
262 			throw new InvalidTokenException("Invalid access token (no client id): " + tokenValue);
263 		}
264 		return clientAuth.getClientId();
265 	}
266 
267 	public boolean revokeToken(String tokenValue) {
268 		OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
269 		if (accessToken == null) {
270 			return false;
271 		}
272 		if (accessToken.getRefreshToken() != null) {
273 			tokenStore.removeRefreshToken(accessToken.getRefreshToken());
274 		}
275 		tokenStore.removeAccessToken(accessToken);
276 		return true;
277 	}
278 
279 	private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) {
280 		if (!isSupportRefreshToken(authentication.getOAuth2Request())) {
281 			return null;
282 		}
283 		int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request());
284 		String value = UUID.randomUUID().toString();
285 		if (validitySeconds > 0) {
286 			return new DefaultExpiringOAuth2RefreshToken(value, new Date(System.currentTimeMillis()
287 					+ (validitySeconds * 1000L)));
288 		}
289 		return new DefaultOAuth2RefreshToken(value);
290 	}
291 
292 	private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
293 		DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
294 		int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
295 		if (validitySeconds > 0) {
296 			token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
297 		}
298 		token.setRefreshToken(refreshToken);
299 		token.setScope(authentication.getOAuth2Request().getScope());
300 
301 		return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
302 	}
303 
304 	/**
305 	 * The access token validity period in seconds
306 	 * 
307 	 * @param clientAuth the current authorization request
308 	 * @return the access token validity period in seconds
309 	 */
310 	protected int getAccessTokenValiditySeconds(OAuth2Request clientAuth) {
311 		if (clientDetailsService != null) {
312 			ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
313 			Integer validity = client.getAccessTokenValiditySeconds();
314 			if (validity != null) {
315 				return validity;
316 			}
317 		}
318 		return accessTokenValiditySeconds;
319 	}
320 
321 	/**
322 	 * The refresh token validity period in seconds
323 	 * 
324 	 * @param clientAuth the current authorization request
325 	 * @return the refresh token validity period in seconds
326 	 */
327 	protected int getRefreshTokenValiditySeconds(OAuth2Request clientAuth) {
328 		if (clientDetailsService != null) {
329 			ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
330 			Integer validity = client.getRefreshTokenValiditySeconds();
331 			if (validity != null) {
332 				return validity;
333 			}
334 		}
335 		return refreshTokenValiditySeconds;
336 	}
337 
338 	/**
339 	 * Is a refresh token supported for this client (or the global setting if
340 	 * {@link #setClientDetailsService(ClientDetailsService) clientDetailsService} is not set.
341 	 * 
342 	 * @param clientAuth the current authorization request
343 	 * @return boolean to indicate if refresh token is supported
344 	 */
345 	protected boolean isSupportRefreshToken(OAuth2Request clientAuth) {
346 		if (clientDetailsService != null) {
347 			ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
348 			return client.getAuthorizedGrantTypes().contains("refresh_token");
349 		}
350 		return this.supportRefreshToken;
351 	}
352 
353 	/**
354 	 * An access token enhancer that will be applied to a new token before it is saved in the token store.
355 	 * 
356 	 * @param accessTokenEnhancer the access token enhancer to set
357 	 */
358 	public void setTokenEnhancer(TokenEnhancer accessTokenEnhancer) {
359 		this.accessTokenEnhancer = accessTokenEnhancer;
360 	}
361 
362 	/**
363 	 * The validity (in seconds) of the refresh token. If less than or equal to zero then the tokens will be
364 	 * non-expiring.
365 	 * 
366 	 * @param refreshTokenValiditySeconds The validity (in seconds) of the refresh token.
367 	 */
368 	public void setRefreshTokenValiditySeconds(int refreshTokenValiditySeconds) {
369 		this.refreshTokenValiditySeconds = refreshTokenValiditySeconds;
370 	}
371 
372 	/**
373 	 * The default validity (in seconds) of the access token. Zero or negative for non-expiring tokens. If a client
374 	 * details service is set the validity period will be read from the client, defaulting to this value if not defined
375 	 * by the client.
376 	 * 
377 	 * @param accessTokenValiditySeconds The validity (in seconds) of the access token.
378 	 */
379 	public void setAccessTokenValiditySeconds(int accessTokenValiditySeconds) {
380 		this.accessTokenValiditySeconds = accessTokenValiditySeconds;
381 	}
382 
383 	/**
384 	 * Whether to support the refresh token.
385 	 * 
386 	 * @param supportRefreshToken Whether to support the refresh token.
387 	 */
388 	public void setSupportRefreshToken(boolean supportRefreshToken) {
389 		this.supportRefreshToken = supportRefreshToken;
390 	}
391 
392 	/**
393 	 * Whether to reuse refresh tokens (until expired).
394 	 * 
395 	 * @param reuseRefreshToken Whether to reuse refresh tokens (until expired).
396 	 */
397 	public void setReuseRefreshToken(boolean reuseRefreshToken) {
398 		this.reuseRefreshToken = reuseRefreshToken;
399 	}
400 
401 	/**
402 	 * The persistence strategy for token storage.
403 	 * 
404 	 * @param tokenStore the store for access and refresh tokens.
405 	 */
406 	public void setTokenStore(TokenStore tokenStore) {
407 		this.tokenStore = tokenStore;
408 	}
409 
410 	/**
411 	 * An authentication manager that will be used (if provided) to check the user authentication when a token is
412 	 * refreshed.
413 	 * 
414 	 * @param authenticationManager the authenticationManager to set
415 	 */
416 	public void setAuthenticationManager(AuthenticationManager authenticationManager) {
417 		this.authenticationManager = authenticationManager;
418 	}
419 
420 	/**
421 	 * The client details service to use for looking up clients (if necessary). Optional if the access token expiry is
422 	 * set globally via {@link #setAccessTokenValiditySeconds(int)}.
423 	 * 
424 	 * @param clientDetailsService the client details service
425 	 */
426 	public void setClientDetailsService(ClientDetailsService clientDetailsService) {
427 		this.clientDetailsService = clientDetailsService;
428 	}
429 
430 }