001/******************************************************************************* 002 * Copyright 2017 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 *******************************************************************************/ 018/** 019 * 020 */ 021package org.mitre.oauth2.service.impl; 022 023import static org.mitre.openid.connect.request.ConnectRequestParameters.CODE_CHALLENGE; 024import static org.mitre.openid.connect.request.ConnectRequestParameters.CODE_CHALLENGE_METHOD; 025import static org.mitre.openid.connect.request.ConnectRequestParameters.CODE_VERIFIER; 026 027import java.nio.charset.StandardCharsets; 028import java.security.MessageDigest; 029import java.security.NoSuchAlgorithmException; 030import java.util.Collection; 031import java.util.Date; 032import java.util.HashSet; 033import java.util.List; 034import java.util.Set; 035import java.util.UUID; 036 037import org.mitre.data.AbstractPageOperationTemplate; 038import org.mitre.data.DefaultPageCriteria; 039import org.mitre.oauth2.model.AuthenticationHolderEntity; 040import org.mitre.oauth2.model.ClientDetailsEntity; 041import org.mitre.oauth2.model.OAuth2AccessTokenEntity; 042import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; 043import org.mitre.oauth2.model.PKCEAlgorithm; 044import org.mitre.oauth2.model.SystemScope; 045import org.mitre.oauth2.repository.AuthenticationHolderRepository; 046import org.mitre.oauth2.repository.OAuth2TokenRepository; 047import org.mitre.oauth2.service.ClientDetailsEntityService; 048import org.mitre.oauth2.service.OAuth2TokenEntityService; 049import org.mitre.oauth2.service.SystemScopeService; 050import org.mitre.openid.connect.model.ApprovedSite; 051import org.mitre.openid.connect.service.ApprovedSiteService; 052import org.slf4j.Logger; 053import org.slf4j.LoggerFactory; 054import org.springframework.beans.factory.annotation.Autowired; 055import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; 056import org.springframework.security.core.AuthenticationException; 057import org.springframework.security.oauth2.common.exceptions.InvalidClientException; 058import org.springframework.security.oauth2.common.exceptions.InvalidRequestException; 059import org.springframework.security.oauth2.common.exceptions.InvalidScopeException; 060import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; 061import org.springframework.security.oauth2.provider.OAuth2Authentication; 062import org.springframework.security.oauth2.provider.OAuth2Request; 063import org.springframework.security.oauth2.provider.TokenRequest; 064import org.springframework.security.oauth2.provider.token.TokenEnhancer; 065import org.springframework.stereotype.Service; 066import org.springframework.transaction.annotation.Transactional; 067 068import com.google.common.base.Strings; 069import com.google.common.collect.Sets; 070import com.nimbusds.jose.util.Base64URL; 071import com.nimbusds.jwt.JWTClaimsSet; 072import com.nimbusds.jwt.PlainJWT; 073 074 075/** 076 * @author jricher 077 * 078 */ 079@Service("defaultOAuth2ProviderTokenService") 080public class DefaultOAuth2ProviderTokenService implements OAuth2TokenEntityService { 081 082 /** 083 * Logger for this class 084 */ 085 private static final Logger logger = LoggerFactory.getLogger(DefaultOAuth2ProviderTokenService.class); 086 087 @Autowired 088 private OAuth2TokenRepository tokenRepository; 089 090 @Autowired 091 private AuthenticationHolderRepository authenticationHolderRepository; 092 093 @Autowired 094 private ClientDetailsEntityService clientDetailsService; 095 096 @Autowired 097 private TokenEnhancer tokenEnhancer; 098 099 @Autowired 100 private SystemScopeService scopeService; 101 102 @Autowired 103 private ApprovedSiteService approvedSiteService; 104 105 106 @Override 107 public Set<OAuth2AccessTokenEntity> getAllAccessTokensForUser(String id) { 108 109 Set<OAuth2AccessTokenEntity> all = tokenRepository.getAllAccessTokens(); 110 Set<OAuth2AccessTokenEntity> results = Sets.newLinkedHashSet(); 111 112 for (OAuth2AccessTokenEntity token : all) { 113 if (clearExpiredAccessToken(token) != null && token.getAuthenticationHolder().getAuthentication().getName().equals(id)) { 114 results.add(token); 115 } 116 } 117 118 return results; 119 } 120 121 122 @Override 123 public Set<OAuth2RefreshTokenEntity> getAllRefreshTokensForUser(String id) { 124 Set<OAuth2RefreshTokenEntity> all = tokenRepository.getAllRefreshTokens(); 125 Set<OAuth2RefreshTokenEntity> results = Sets.newLinkedHashSet(); 126 127 for (OAuth2RefreshTokenEntity token : all) { 128 if (clearExpiredRefreshToken(token) != null && token.getAuthenticationHolder().getAuthentication().getName().equals(id)) { 129 results.add(token); 130 } 131 } 132 133 return results; 134 } 135 136 @Override 137 public OAuth2AccessTokenEntity getAccessTokenById(Long id) { 138 return clearExpiredAccessToken(tokenRepository.getAccessTokenById(id)); 139 } 140 141 @Override 142 public OAuth2RefreshTokenEntity getRefreshTokenById(Long id) { 143 return clearExpiredRefreshToken(tokenRepository.getRefreshTokenById(id)); 144 } 145 146 /** 147 * Utility function to delete an access token that's expired before returning it. 148 * @param token the token to check 149 * @return null if the token is null or expired, the input token (unchanged) if it hasn't 150 */ 151 private OAuth2AccessTokenEntity clearExpiredAccessToken(OAuth2AccessTokenEntity token) { 152 if (token == null) { 153 return null; 154 } else if (token.isExpired()) { 155 // immediately revoke expired token 156 logger.debug("Clearing expired access token: " + token.getValue()); 157 revokeAccessToken(token); 158 return null; 159 } else { 160 return token; 161 } 162 } 163 164 /** 165 * Utility function to delete a refresh token that's expired before returning it. 166 * @param token the token to check 167 * @return null if the token is null or expired, the input token (unchanged) if it hasn't 168 */ 169 private OAuth2RefreshTokenEntity clearExpiredRefreshToken(OAuth2RefreshTokenEntity token) { 170 if (token == null) { 171 return null; 172 } else if (token.isExpired()) { 173 // immediately revoke expired token 174 logger.debug("Clearing expired refresh token: " + token.getValue()); 175 revokeRefreshToken(token); 176 return null; 177 } else { 178 return token; 179 } 180 } 181 182 @Override 183 @Transactional(value="defaultTransactionManager") 184 public OAuth2AccessTokenEntity createAccessToken(OAuth2Authentication authentication) throws AuthenticationException, InvalidClientException { 185 if (authentication != null && authentication.getOAuth2Request() != null) { 186 // look up our client 187 OAuth2Request request = authentication.getOAuth2Request(); 188 189 ClientDetailsEntity client = clientDetailsService.loadClientByClientId(request.getClientId()); 190 191 if (client == null) { 192 throw new InvalidClientException("Client not found: " + request.getClientId()); 193 } 194 195 196 // handle the PKCE code challenge if present 197 if (request.getExtensions().containsKey(CODE_CHALLENGE)) { 198 String challenge = (String) request.getExtensions().get(CODE_CHALLENGE); 199 PKCEAlgorithm alg = PKCEAlgorithm.parse((String) request.getExtensions().get(CODE_CHALLENGE_METHOD)); 200 201 String verifier = request.getRequestParameters().get(CODE_VERIFIER); 202 203 if (alg.equals(PKCEAlgorithm.plain)) { 204 // do a direct string comparison 205 if (!challenge.equals(verifier)) { 206 throw new InvalidRequestException("Code challenge and verifier do not match"); 207 } 208 } else if (alg.equals(PKCEAlgorithm.S256)) { 209 // hash the verifier 210 try { 211 MessageDigest digest = MessageDigest.getInstance("SHA-256"); 212 String hash = Base64URL.encode(digest.digest(verifier.getBytes(StandardCharsets.US_ASCII))).toString(); 213 if (!challenge.equals(hash)) { 214 throw new InvalidRequestException("Code challenge and verifier do not match"); 215 } 216 } catch (NoSuchAlgorithmException e) { 217 logger.error("Unknown algorithm for PKCE digest", e); 218 } 219 } 220 221 } 222 223 224 OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity();//accessTokenFactory.createNewAccessToken(); 225 226 // attach the client 227 token.setClient(client); 228 229 // inherit the scope from the auth, but make a new set so it is 230 //not unmodifiable. Unmodifiables don't play nicely with Eclipselink, which 231 //wants to use the clone operation. 232 Set<SystemScope> scopes = scopeService.fromStrings(request.getScope()); 233 234 // remove any of the special system scopes 235 scopes = scopeService.removeReservedScopes(scopes); 236 237 token.setScope(scopeService.toStrings(scopes)); 238 239 // make it expire if necessary 240 if (client.getAccessTokenValiditySeconds() != null && client.getAccessTokenValiditySeconds() > 0) { 241 Date expiration = new Date(System.currentTimeMillis() + (client.getAccessTokenValiditySeconds() * 1000L)); 242 token.setExpiration(expiration); 243 } 244 245 // attach the authorization so that we can look it up later 246 AuthenticationHolderEntity authHolder = new AuthenticationHolderEntity(); 247 authHolder.setAuthentication(authentication); 248 authHolder = authenticationHolderRepository.save(authHolder); 249 250 token.setAuthenticationHolder(authHolder); 251 252 // attach a refresh token, if this client is allowed to request them and the user gets the offline scope 253 if (client.isAllowRefresh() && token.getScope().contains(SystemScopeService.OFFLINE_ACCESS)) { 254 OAuth2RefreshTokenEntity savedRefreshToken = createRefreshToken(client, authHolder); 255 256 token.setRefreshToken(savedRefreshToken); 257 } 258 259 //Add approved site reference, if any 260 OAuth2Request originalAuthRequest = authHolder.getAuthentication().getOAuth2Request(); 261 262 if (originalAuthRequest.getExtensions() != null && originalAuthRequest.getExtensions().containsKey("approved_site")) { 263 264 Long apId = Long.parseLong((String) originalAuthRequest.getExtensions().get("approved_site")); 265 ApprovedSite ap = approvedSiteService.getById(apId); 266 267 token.setApprovedSite(ap); 268 } 269 270 OAuth2AccessTokenEntity enhancedToken = (OAuth2AccessTokenEntity) tokenEnhancer.enhance(token, authentication); 271 272 OAuth2AccessTokenEntity savedToken = saveAccessToken(enhancedToken); 273 274 if (savedToken.getRefreshToken() != null) { 275 tokenRepository.saveRefreshToken(savedToken.getRefreshToken()); // make sure we save any changes that might have been enhanced 276 } 277 278 return savedToken; 279 } 280 281 throw new AuthenticationCredentialsNotFoundException("No authentication credentials found"); 282 } 283 284 285 private OAuth2RefreshTokenEntity createRefreshToken(ClientDetailsEntity client, AuthenticationHolderEntity authHolder) { 286 OAuth2RefreshTokenEntity refreshToken = new OAuth2RefreshTokenEntity(); //refreshTokenFactory.createNewRefreshToken(); 287 JWTClaimsSet.Builder refreshClaims = new JWTClaimsSet.Builder(); 288 289 290 // make it expire if necessary 291 if (client.getRefreshTokenValiditySeconds() != null) { 292 Date expiration = new Date(System.currentTimeMillis() + (client.getRefreshTokenValiditySeconds() * 1000L)); 293 refreshToken.setExpiration(expiration); 294 refreshClaims.expirationTime(expiration); 295 } 296 297 // set a random identifier 298 refreshClaims.jwtID(UUID.randomUUID().toString()); 299 300 // TODO: add issuer fields, signature to JWT 301 302 PlainJWT refreshJwt = new PlainJWT(refreshClaims.build()); 303 refreshToken.setJwt(refreshJwt); 304 305 //Add the authentication 306 refreshToken.setAuthenticationHolder(authHolder); 307 refreshToken.setClient(client); 308 309 310 311 // save the token first so that we can set it to a member of the access token (NOTE: is this step necessary?) 312 OAuth2RefreshTokenEntity savedRefreshToken = tokenRepository.saveRefreshToken(refreshToken); 313 return savedRefreshToken; 314 } 315 316 @Override 317 @Transactional(value="defaultTransactionManager") 318 public OAuth2AccessTokenEntity refreshAccessToken(String refreshTokenValue, TokenRequest authRequest) throws AuthenticationException { 319 320 if (Strings.isNullOrEmpty(refreshTokenValue)) { 321 // throw an invalid token exception if there's no refresh token value at all 322 throw new InvalidTokenException("Invalid refresh token: " + refreshTokenValue); 323 } 324 325 OAuth2RefreshTokenEntity refreshToken = clearExpiredRefreshToken(tokenRepository.getRefreshTokenByValue(refreshTokenValue)); 326 327 if (refreshToken == null) { 328 // throw an invalid token exception if we couldn't find the token 329 throw new InvalidTokenException("Invalid refresh token: " + refreshTokenValue); 330 } 331 332 ClientDetailsEntity client = refreshToken.getClient(); 333 334 AuthenticationHolderEntity authHolder = refreshToken.getAuthenticationHolder(); 335 336 // make sure that the client requesting the token is the one who owns the refresh token 337 ClientDetailsEntity requestingClient = clientDetailsService.loadClientByClientId(authRequest.getClientId()); 338 if (!client.getClientId().equals(requestingClient.getClientId())) { 339 tokenRepository.removeRefreshToken(refreshToken); 340 throw new InvalidClientException("Client does not own the presented refresh token"); 341 } 342 343 //Make sure this client allows access token refreshing 344 if (!client.isAllowRefresh()) { 345 throw new InvalidClientException("Client does not allow refreshing access token!"); 346 } 347 348 // clear out any access tokens 349 if (client.isClearAccessTokensOnRefresh()) { 350 tokenRepository.clearAccessTokensForRefreshToken(refreshToken); 351 } 352 353 if (refreshToken.isExpired()) { 354 tokenRepository.removeRefreshToken(refreshToken); 355 throw new InvalidTokenException("Expired refresh token: " + refreshTokenValue); 356 } 357 358 OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity(); 359 360 // get the stored scopes from the authentication holder's authorization request; these are the scopes associated with the refresh token 361 Set<String> refreshScopesRequested = new HashSet<>(refreshToken.getAuthenticationHolder().getAuthentication().getOAuth2Request().getScope()); 362 Set<SystemScope> refreshScopes = scopeService.fromStrings(refreshScopesRequested); 363 // remove any of the special system scopes 364 refreshScopes = scopeService.removeReservedScopes(refreshScopes); 365 366 Set<String> scopeRequested = authRequest.getScope() == null ? new HashSet<String>() : new HashSet<>(authRequest.getScope()); 367 Set<SystemScope> scope = scopeService.fromStrings(scopeRequested); 368 369 // remove any of the special system scopes 370 scope = scopeService.removeReservedScopes(scope); 371 372 if (scope != null && !scope.isEmpty()) { 373 // ensure a proper subset of scopes 374 if (refreshScopes != null && refreshScopes.containsAll(scope)) { 375 // set the scope of the new access token if requested 376 token.setScope(scopeService.toStrings(scope)); 377 } else { 378 String errorMsg = "Up-scoping is not allowed."; 379 logger.error(errorMsg); 380 throw new InvalidScopeException(errorMsg); 381 } 382 } else { 383 // otherwise inherit the scope of the refresh token (if it's there -- this can return a null scope set) 384 token.setScope(scopeService.toStrings(refreshScopes)); 385 } 386 387 token.setClient(client); 388 389 if (client.getAccessTokenValiditySeconds() != null) { 390 Date expiration = new Date(System.currentTimeMillis() + (client.getAccessTokenValiditySeconds() * 1000L)); 391 token.setExpiration(expiration); 392 } 393 394 if (client.isReuseRefreshToken()) { 395 // if the client re-uses refresh tokens, do that 396 token.setRefreshToken(refreshToken); 397 } else { 398 // otherwise, make a new refresh token 399 OAuth2RefreshTokenEntity newRefresh = createRefreshToken(client, authHolder); 400 token.setRefreshToken(newRefresh); 401 402 // clean up the old refresh token 403 tokenRepository.removeRefreshToken(refreshToken); 404 } 405 406 token.setAuthenticationHolder(authHolder); 407 408 tokenEnhancer.enhance(token, authHolder.getAuthentication()); 409 410 tokenRepository.saveAccessToken(token); 411 412 return token; 413 414 } 415 416 @Override 417 public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException { 418 419 OAuth2AccessTokenEntity accessToken = clearExpiredAccessToken(tokenRepository.getAccessTokenByValue(accessTokenValue)); 420 421 if (accessToken == null) { 422 throw new InvalidTokenException("Invalid access token: " + accessTokenValue); 423 } else { 424 return accessToken.getAuthenticationHolder().getAuthentication(); 425 } 426 } 427 428 429 /** 430 * Get an access token from its token value. 431 */ 432 @Override 433 public OAuth2AccessTokenEntity readAccessToken(String accessTokenValue) throws AuthenticationException { 434 OAuth2AccessTokenEntity accessToken = clearExpiredAccessToken(tokenRepository.getAccessTokenByValue(accessTokenValue)); 435 if (accessToken == null) { 436 throw new InvalidTokenException("Access token for value " + accessTokenValue + " was not found"); 437 } else { 438 return accessToken; 439 } 440 } 441 442 /** 443 * Get an access token by its authentication object. 444 */ 445 @Override 446 public OAuth2AccessTokenEntity getAccessToken(OAuth2Authentication authentication) { 447 // TODO: implement this against the new service (#825) 448 throw new UnsupportedOperationException("Unable to look up access token from authentication object."); 449 } 450 451 /** 452 * Get a refresh token by its token value. 453 */ 454 @Override 455 public OAuth2RefreshTokenEntity getRefreshToken(String refreshTokenValue) throws AuthenticationException { 456 OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenByValue(refreshTokenValue); 457 if (refreshToken == null) { 458 throw new InvalidTokenException("Refresh token for value " + refreshTokenValue + " was not found"); 459 } 460 else { 461 return refreshToken; 462 } 463 } 464 465 /** 466 * Revoke a refresh token and all access tokens issued to it. 467 */ 468 @Override 469 @Transactional(value="defaultTransactionManager") 470 public void revokeRefreshToken(OAuth2RefreshTokenEntity refreshToken) { 471 tokenRepository.clearAccessTokensForRefreshToken(refreshToken); 472 tokenRepository.removeRefreshToken(refreshToken); 473 } 474 475 /** 476 * Revoke an access token. 477 */ 478 @Override 479 @Transactional(value="defaultTransactionManager") 480 public void revokeAccessToken(OAuth2AccessTokenEntity accessToken) { 481 tokenRepository.removeAccessToken(accessToken); 482 } 483 484 485 /* (non-Javadoc) 486 * @see org.mitre.oauth2.service.OAuth2TokenEntityService#getAccessTokensForClient(org.mitre.oauth2.model.ClientDetailsEntity) 487 */ 488 @Override 489 public List<OAuth2AccessTokenEntity> getAccessTokensForClient(ClientDetailsEntity client) { 490 return tokenRepository.getAccessTokensForClient(client); 491 } 492 493 /* (non-Javadoc) 494 * @see org.mitre.oauth2.service.OAuth2TokenEntityService#getRefreshTokensForClient(org.mitre.oauth2.model.ClientDetailsEntity) 495 */ 496 @Override 497 public List<OAuth2RefreshTokenEntity> getRefreshTokensForClient(ClientDetailsEntity client) { 498 return tokenRepository.getRefreshTokensForClient(client); 499 } 500 501 /** 502 * Clears out expired tokens and any abandoned authentication objects 503 */ 504 @Override 505 public void clearExpiredTokens() { 506 logger.debug("Cleaning out all expired tokens"); 507 508 new AbstractPageOperationTemplate<OAuth2AccessTokenEntity>("clearExpiredAccessTokens") { 509 @Override 510 public Collection<OAuth2AccessTokenEntity> fetchPage() { 511 return tokenRepository.getAllExpiredAccessTokens(new DefaultPageCriteria()); 512 } 513 514 @Override 515 public void doOperation(OAuth2AccessTokenEntity item) { 516 revokeAccessToken(item); 517 } 518 }.execute(); 519 520 new AbstractPageOperationTemplate<OAuth2RefreshTokenEntity>("clearExpiredRefreshTokens") { 521 @Override 522 public Collection<OAuth2RefreshTokenEntity> fetchPage() { 523 return tokenRepository.getAllExpiredRefreshTokens(new DefaultPageCriteria()); 524 } 525 526 @Override 527 public void doOperation(OAuth2RefreshTokenEntity item) { 528 revokeRefreshToken(item); 529 } 530 }.execute(); 531 532 new AbstractPageOperationTemplate<AuthenticationHolderEntity>("clearExpiredAuthenticationHolders") { 533 @Override 534 public Collection<AuthenticationHolderEntity> fetchPage() { 535 return authenticationHolderRepository.getOrphanedAuthenticationHolders(new DefaultPageCriteria()); 536 } 537 538 @Override 539 public void doOperation(AuthenticationHolderEntity item) { 540 authenticationHolderRepository.remove(item); 541 } 542 }.execute(); 543 } 544 545 /* (non-Javadoc) 546 * @see org.mitre.oauth2.service.OAuth2TokenEntityService#saveAccessToken(org.mitre.oauth2.model.OAuth2AccessTokenEntity) 547 */ 548 @Override 549 @Transactional(value="defaultTransactionManager") 550 public OAuth2AccessTokenEntity saveAccessToken(OAuth2AccessTokenEntity accessToken) { 551 OAuth2AccessTokenEntity newToken = tokenRepository.saveAccessToken(accessToken); 552 553 // if the old token has any additional information for the return from the token endpoint, carry it through here after save 554 if (accessToken.getAdditionalInformation() != null && !accessToken.getAdditionalInformation().isEmpty()) { 555 newToken.getAdditionalInformation().putAll(accessToken.getAdditionalInformation()); 556 } 557 558 return newToken; 559 } 560 561 /* (non-Javadoc) 562 * @see org.mitre.oauth2.service.OAuth2TokenEntityService#saveRefreshToken(org.mitre.oauth2.model.OAuth2RefreshTokenEntity) 563 */ 564 @Override 565 @Transactional(value="defaultTransactionManager") 566 public OAuth2RefreshTokenEntity saveRefreshToken(OAuth2RefreshTokenEntity refreshToken) { 567 return tokenRepository.saveRefreshToken(refreshToken); 568 } 569 570 /** 571 * @return the tokenEnhancer 572 */ 573 public TokenEnhancer getTokenEnhancer() { 574 return tokenEnhancer; 575 } 576 577 /** 578 * @param tokenEnhancer the tokenEnhancer to set 579 */ 580 public void setTokenEnhancer(TokenEnhancer tokenEnhancer) { 581 this.tokenEnhancer = tokenEnhancer; 582 } 583 584 @Override 585 public OAuth2AccessTokenEntity getRegistrationAccessTokenForClient(ClientDetailsEntity client) { 586 List<OAuth2AccessTokenEntity> allTokens = getAccessTokensForClient(client); 587 588 for (OAuth2AccessTokenEntity token : allTokens) { 589 if ((token.getScope().contains(SystemScopeService.REGISTRATION_TOKEN_SCOPE) || token.getScope().contains(SystemScopeService.RESOURCE_TOKEN_SCOPE)) 590 && token.getScope().size() == 1) { 591 // if it only has the registration scope, then it's a registration token 592 return token; 593 } 594 } 595 596 return null; 597 } 598 599 600 601}