All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cloudfoundry.identity.uaa.util.TokenValidation Maven / Gradle / Ivy

/*******************************************************************************
 * 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.util; import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.identity.uaa.oauth.TokenRevokedException; import org.cloudfoundry.identity.uaa.oauth.jwt.Jwt; import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper; import org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants; import org.cloudfoundry.identity.uaa.oauth.token.RevocableToken; import org.cloudfoundry.identity.uaa.oauth.token.RevocableTokenProvisioning; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.flywaydb.core.internal.util.StringUtils; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.jwt.crypto.sign.InvalidSignatureException; import org.springframework.security.jwt.crypto.sign.SignatureVerifier; import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.util.Assert; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; import static java.util.Collections.emptySet; import static java.util.Optional.ofNullable; import static org.cloudfoundry.identity.uaa.oauth.client.ClientConstants.REQUIRED_USER_GROUPS; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.AUD; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.CID; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.CLIENT_ID; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EXP; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.ISS; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.REVOCATION_SIGNATURE; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.SCOPE; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.USER_ID; import static org.cloudfoundry.identity.uaa.util.UaaTokenUtils.isUserToken; public class TokenValidation { private static final Log logger = LogFactory.getLog(TokenValidation.class); private final Map claims; private final Jwt tokenJwt; private final String token; private final boolean decoded; // this is used to avoid checking claims on tokens that had errors when decoding private final List validationErrors = new ArrayList<>(); public static TokenValidation validate(String tokenJwtValue) { return new TokenValidation(tokenJwtValue); } private TokenValidation(String token) { this.token = token; Jwt tokenJwt; try { tokenJwt = JwtHelper.decode(token); } catch (Exception ex) { tokenJwt = null; validationErrors.add(new InvalidTokenException("Invalid token (could not decode): " + token, ex)); } this.tokenJwt = tokenJwt; String tokenJwtClaims; if(tokenJwt != null && StringUtils.hasText(tokenJwtClaims = tokenJwt.getClaims())) { Map claims; try { claims = JsonUtils.readValue(tokenJwtClaims, new TypeReference>() {}); } catch (JsonUtils.JsonUtilException ex) { claims = null; validationErrors.add(new InvalidTokenException("Invalid token (cannot read token claims): " + token, ex)); } this.claims = claims; } else { this.claims = new HashMap<>(); } this.decoded = isValid(); } public boolean isValid() { return validationErrors.size() == 0; } public List getValidationErrors() { return validationErrors; } @SuppressWarnings("CloneDoesntCallSuperClone") public TokenValidation clone() { return new TokenValidation(this); } private TokenValidation(TokenValidation source) { this.claims = source.claims == null ? null : new HashMap<>(source.claims); this.tokenJwt = source.tokenJwt; this.token = source.token; this.decoded = source.decoded; this.scopes = source.scopes; } public TokenValidation checkSignature(SignatureVerifier verifier) { if(!decoded) { return this; } try { tokenJwt.verifySignature(verifier); } catch (Exception ex) { logger.debug("Invalid token (could not verify signature)", ex); addError("Could not verify token signature.", new InvalidSignatureException(token)); } return this; } public TokenValidation checkIssuer(String issuer) { if(issuer == null) { return this; } if(!decoded || !claims.containsKey(ISS)) { addError("Token does not bear an ISS claim."); return this; } if(!equals(issuer, claims.get(ISS))) { addError("Invalid issuer (" + claims.get(ISS) + ") for token did not match expected: " + issuer); } return this; } public TokenValidation checkExpiry(Instant asOf) { if(!decoded || !claims.containsKey(EXP)) { addError("Token does not bear an EXP claim."); return this; } Object expClaim = claims.get(EXP); long expiry; try { expiry = (int) expClaim; if(asOf.getEpochSecond() > expiry) { addError("Token expired at " + expiry); } } catch (ClassCastException ex) { addError("Token bears an invalid or unparseable EXP claim.", ex); } return this; } public TokenValidation checkExpiry() { return checkExpiry(Instant.now()); } protected TokenValidation checkUser(Function getUser) { if(!decoded || !isUserToken(claims)) { addError("Token is not a user token."); return this; } if(!claims.containsKey(USER_ID)) { addError("Token does not bear a USER_ID claim."); return this; } String userId; Object userIdClaim = claims.get(USER_ID); try { userId = (String) userIdClaim; } catch (ClassCastException ex) { addError("Token bears an invalid or unparseable USER_ID claim.", ex); return this; } if(userId == null) { addError("Token has a null USER_ID claim."); } else { UaaUser user; try { user = getUser.apply(userId); Assert.notNull(user); } catch (UsernameNotFoundException ex) { user = null; addError("Token bears a non-existent user ID: " + userId, ex); } catch(InvalidTokenException ex) { user = null; validationErrors.add(ex); } if(user == null) { // Unlikely to occur, but since this is dependent on the implementation of an interface... addError("Found no data for user ID: " + userId); } else { List authorities = user.getAuthorities(); if (authorities == null) { addError("Invalid token (all scopes have been revoked)"); } else { List authoritiesValue = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); checkScopesWithin(authoritiesValue); } } } return this; } public TokenValidation checkScopesInclude(String... scopes) { return checkScopesInclude(Arrays.asList(scopes)); } public TokenValidation checkScopesInclude(Collection scopes) { getScopes().ifPresent(tokenScopes -> { String missingScopes = scopes.stream().filter(s -> !tokenScopes.contains(s)).collect(Collectors.joining(" ")); if(StringUtils.hasText(missingScopes)) { validationErrors.add(new InsufficientScopeException("Some expected scopes are missing: " + missingScopes)); } }); return this; } public TokenValidation checkScopesWithin(String... scopes) { return checkScopesWithin(Arrays.asList(scopes)); } public TokenValidation checkScopesWithin(Collection scopes) { getScopes().ifPresent(tokenScopes -> { Set scopePatterns = UaaStringUtils.constructWildcards(scopes); List missingScopes = tokenScopes.stream().filter(s -> !scopePatterns.stream().anyMatch(p -> p.matcher(s).matches())).collect(Collectors.toList()); if(!missingScopes.isEmpty()) { validationErrors.add(new InvalidTokenException("Some scopes have been revoked: " + missingScopes.stream().collect(Collectors.joining(" ")))); } }); return this; } public TokenValidation checkClientAndUser(ClientDetails client, UaaUser user) { TokenValidation validation = checkClient( cid -> { if (!equals(cid, client.getClientId())) { throw new InvalidTokenException("Token's client ID does not match expected value: " + client.getClientId()); } return client; }); if (isUserToken(claims)) { return validation .checkUser(uid -> { if (user == null) { throw new InvalidTokenException("Unable to validate user, no user found."); } else { if (!equals(uid, user.getId())) { throw new InvalidTokenException("Token does not have expected user ID."); } return user; } }) .checkRequiredUserGroups( ofNullable((Collection) client.getAdditionalInformation().get(REQUIRED_USER_GROUPS)).orElse(emptySet()), AuthorityUtils.authorityListToSet(user.getAuthorities()) ); } else { return validation; } } protected TokenValidation checkRequiredUserGroups(Collection requiredGroups, Collection userGroups) { if (!UaaTokenUtils.hasRequiredUserGroups(requiredGroups, userGroups)) { addError("User does not meet the client's required group criteria."); } return this; } protected TokenValidation checkClient(Function getClient) { if(!decoded || !claims.containsKey(CID)) { addError("Token bears no client ID."); return this; } if(claims.containsKey(CLIENT_ID) && !equals(claims.get(CID), claims.get(CLIENT_ID))) { addError("Token bears conflicting client ID claims."); return this; } String clientId; try { clientId = (String) claims.get(CID); } catch (ClassCastException ex) { addError("Token bears an invalid or unparseable CID claim.", ex); return this; } try { ClientDetails client = getClient.apply(clientId); Collection clientScopes; if (null == claims.get(USER_ID)) { // for client credentials tokens, we want to validate the client scopes clientScopes = ofNullable(client.getAuthorities()) .map(a -> a.stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())) .orElse(Collections.emptyList()); } else { clientScopes = client.getScope(); } checkScopesWithin(clientScopes); } catch(NoSuchClientException ex) { addError("The token refers to a non-existent client: " + clientId, ex); } catch(InvalidTokenException ex) { validationErrors.add(ex); } return this; } public TokenValidation checkRevocationSignature(List revocableSignatureList) { if(!decoded) { addError("Token does not bear a revocation hash."); return this; } if(!claims.containsKey(REVOCATION_SIGNATURE)) { // tokens issued before revocation signatures were implemented are still valid return this; } String revocableHashSignature; try { revocableHashSignature = (String)claims.get(REVOCATION_SIGNATURE); } catch (ClassCastException ex) { addError("Token bears an invalid or unparseable revocation signature.", ex); return this; } boolean hashMatched = false; for (String revocableSignature : revocableSignatureList) { if(revocableHashSignature.equals(revocableSignature)){ hashMatched = true; break; } } if(revocableHashSignature == null || !hashMatched) { validationErrors.add(new TokenRevokedException("revocable signature mismatch")); } return this; } public TokenValidation checkAudience(String... clients) { return checkAudience(Arrays.asList(clients)); } public TokenValidation checkAudience(Collection clients) { if (!decoded || !claims.containsKey(AUD)) { addError("The token does not bear an AUD claim."); return this; } Object audClaim = claims.get(AUD); List audience; if(audClaim instanceof String) { audience = Collections.singletonList((String) audClaim); } else if(audClaim == null) { audience = Collections.emptyList(); } else { try { audience = ((List) audClaim).stream() .map(s -> (String) s) .collect(Collectors.toList()); } catch (ClassCastException ex) { addError("The token's audience claim is invalid or unparseable.", ex); return this; } } List notInAudience = clients.stream().filter(c -> !audience.contains(c)).collect(Collectors.toList()); if(!notInAudience.isEmpty()) { String joinedAudiences = notInAudience.stream().map(c -> "".equals(c) ? "EMPTY_VALUE" : c ).collect(Collectors.joining(", ")); addError("Some parties were not in the token audience: " + joinedAudiences); } return this; } public TokenValidation checkRevocableTokenStore(RevocableTokenProvisioning revocableTokenProvisioning) { if(!decoded) { addError("The token could not be checked for revocation."); return this; } try { String tokenId; if(claims.containsKey(ClaimConstants.REVOCABLE) && (boolean) claims.get(ClaimConstants.REVOCABLE)) { if((tokenId = (String) claims.get(ClaimConstants.JTI)) == null) { addError("The token does not bear a token ID (JTI)."); return this; } RevocableToken revocableToken = null; try { revocableToken = revocableTokenProvisioning.retrieve(tokenId, IdentityZoneHolder.get().getId()); } catch(EmptyResultDataAccessException ex) { } if(revocableToken == null) { validationErrors.add(new TokenRevokedException("The token has been revoked: " + tokenId)); } } } catch(ClassCastException ex) { addError("The token's revocability or JTI claim is invalid or unparseable.", ex); return this; } return this; } private boolean addError(String msg, Exception cause) { return validationErrors.add(new InvalidTokenException(msg, cause)); } private boolean addError(String msg) { return addError(msg, null); } private static boolean equals(Object a, Object b) { if(a == null) return b == null; return a.equals(b); } private Optional> scopes = null; private Optional> getScopes() { if (scopes == null) { if (!decoded || !claims.containsKey(SCOPE)) { addError("The token does not bear a SCOPE claim."); return scopes = Optional.empty(); } Object scopeClaim = claims.get(SCOPE); if (scopeClaim == null) { // treat null scope claim the same as empty scope claim scopeClaim = new ArrayList<>(); } try { return scopes = Optional.of(((List) scopeClaim).stream() .map(s -> (String) s) .collect(Collectors.toList())); } catch (ClassCastException ex) { addError("The token's scope claim is invalid or unparseable.", ex); return scopes = Optional.empty(); } } return scopes; } public TokenValidation throwIfInvalid() { if(!isValid()) { throw validationErrors.get(0); } return this; } public Jwt getJwt() { return tokenJwt; } public Map getClaims() { return claims; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy