From d31bd1e1a54e23e80931886440f5462850527710 Mon Sep 17 00:00:00 2001 From: Filip Hanik Date: Thu, 7 Apr 2016 14:33:31 -0600 Subject: [PATCH] Add in support for opaque tokens. id_tokens are never opaque, they are by definition in JWT format. https://www.pivotaltracker.com/story/show/114126761 [#114126761] --- .../uaa/oauth/token/ClaimConstants.java | 1 + .../identity/uaa/oauth/token/Claims.java | 9 + .../uaa/oauth/token/CompositeAccessToken.java | 2 + .../uaa/oauth/token/RevocableToken.java | 7 +- .../identity/uaa/oauth/UaaTokenServices.java | 247 ++++++++--- .../uaa/oauth/UaaTokenServicesTests.java | 392 ++++++++++++------ .../webapp/WEB-INF/spring/oauth-endpoints.xml | 5 + .../mock/token/CheckTokenEndpointDocs.java | 9 +- .../uaa/mock/token/TokenMvcMockTests.java | 96 ++++- 9 files changed, 572 insertions(+), 196 deletions(-) diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/ClaimConstants.java b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/ClaimConstants.java index cb5c2dd16d7..7031ffc1586 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/ClaimConstants.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/ClaimConstants.java @@ -51,4 +51,5 @@ public class ClaimConstants { public static final String ROLES = "roles"; public static final String PROFILE = "profile"; public static final String USER_ATTRIBUTES = "user_attributes"; + public static final String REVOCABLE = "revocable"; } diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/Claims.java b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/Claims.java index 6daf3fbb126..1fca7d645e0 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/Claims.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/Claims.java @@ -78,6 +78,8 @@ public class Claims { private String profile; @JsonProperty(ClaimConstants.USER_ATTRIBUTES) private String userAttributes; + @JsonProperty(ClaimConstants.REVOCABLE) + private boolean revocable; public String getUserId() { return userId; @@ -300,4 +302,11 @@ public String getUserAttributes() { public void setUserAttributes(String userAttributes) { this.userAttributes = userAttributes; } + public boolean isRevocable() { + return revocable; + } + + public void setRevocable(boolean revocable) { + this.revocable = revocable; + } } diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/CompositeAccessToken.java b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/CompositeAccessToken.java index 4b2b56c0383..e06e7960790 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/CompositeAccessToken.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/CompositeAccessToken.java @@ -42,4 +42,6 @@ public CompositeAccessToken(OAuth2AccessToken accessToken) { super(accessToken); } + + } diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/RevocableToken.java b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/RevocableToken.java index 8b2ec3ec7eb..853dc8fc7f5 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/RevocableToken.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/RevocableToken.java @@ -16,7 +16,12 @@ public class RevocableToken { public enum TokenType { ID_TOKEN, ACCESS_TOKEN, REFRESH_TOKEN - }; + } + + public enum TokenFormat { + JWT, OPAQUE + } + private String tokenId; private String clientId; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java index aacbc12f633..a0dd8d1d6e5 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java @@ -15,6 +15,9 @@ import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.cloudfoundry.identity.uaa.approval.Approval; +import org.cloudfoundry.identity.uaa.approval.Approval.ApprovalStatus; +import org.cloudfoundry.identity.uaa.approval.ApprovalStore; import org.cloudfoundry.identity.uaa.audit.event.TokenIssuedEvent; import org.cloudfoundry.identity.uaa.authentication.Origin; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; @@ -22,23 +25,24 @@ import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; 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.CompositeAccessToken; -import org.cloudfoundry.identity.uaa.util.TokenValidation; -import org.cloudfoundry.identity.uaa.zone.TokenPolicy; -import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration; -import org.cloudfoundry.identity.uaa.approval.Approval; -import org.cloudfoundry.identity.uaa.approval.Approval.ApprovalStatus; -import org.cloudfoundry.identity.uaa.approval.ApprovalStore; +import org.cloudfoundry.identity.uaa.oauth.token.RevocableToken; +import org.cloudfoundry.identity.uaa.oauth.token.RevocableTokenProvisioning; import org.cloudfoundry.identity.uaa.user.UaaAuthority; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.util.TokenValidation; import org.cloudfoundry.identity.uaa.util.UaaTokenUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.cloudfoundry.identity.uaa.zone.TokenPolicy; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; @@ -46,13 +50,11 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.jwt.crypto.sign.SignatureVerifier; -import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException; import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken; import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken; import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2RefreshToken; -import org.springframework.security.oauth2.common.exceptions.InvalidClientException; import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; import org.springframework.security.oauth2.common.exceptions.InvalidScopeException; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; @@ -61,7 +63,6 @@ import org.springframework.security.oauth2.provider.AuthorizationRequest; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; -import org.springframework.security.oauth2.provider.ClientRegistrationException; import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.OAuth2Request; @@ -88,7 +89,36 @@ import java.util.Set; import java.util.UUID; -import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.*; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.ADDITIONAL_AZ_ATTR; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.AUD; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.AUTHORITIES; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.AUTH_TIME; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.AZP; +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.EMAIL; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EXP; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.FAMILY_NAME; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.GIVEN_NAME; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.GRANT_TYPE; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.IAT; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.ISS; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.JTI; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.NONCE; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.ORIGIN; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.PHONE_NUMBER; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.PROFILE; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.REVOCABLE; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.REVOCATION_SIGNATURE; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.ROLES; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.SCOPE; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.SUB; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.USER_ATTRIBUTES; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.USER_ID; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.USER_NAME; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.ZONE_ID; +import static org.cloudfoundry.identity.uaa.oauth.token.RevocableToken.TokenFormat.JWT; +import static org.cloudfoundry.identity.uaa.oauth.token.RevocableToken.TokenFormat.OPAQUE; import static org.cloudfoundry.identity.uaa.util.TokenValidation.validate; @@ -118,6 +148,8 @@ public class UaaTokenServices implements AuthorizationServerTokenServices, Resou private List validIdTokenScopes = Arrays.asList("openid"); private TokenPolicy tokenPolicy; + private RevocableTokenProvisioning tokenProvisioning; + private Set excludedClaims = Collections.EMPTY_SET; public Set getExcludedClaims() { @@ -132,6 +164,14 @@ public void setValidIdTokenScopes(List validIdTokenScopes) { this.validIdTokenScopes = validIdTokenScopes; } + public RevocableTokenProvisioning getTokenProvisioning() { + return tokenProvisioning; + } + + public void setTokenProvisioning(RevocableTokenProvisioning tokenProvisioning) { + this.tokenProvisioning = tokenProvisioning; + } + @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; @@ -148,6 +188,14 @@ public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenReque + request.getRequestParameters().get("grant_type")); } + if (refreshTokenValue.length()<=36) { + try { + refreshTokenValue = tokenProvisioning.retrieve(refreshTokenValue).getValue(); + } catch (EmptyResultDataAccessException x) { + throw new InvalidTokenException("Refresh token with ID:"+refreshTokenValue+" not found."); + } + } + Map claims = getClaimsForToken(refreshTokenValue); // TODO: Should reuse the access token you get after the first @@ -168,6 +216,12 @@ public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenReque String userid = (String) claims.get(USER_ID); + String accessTokenId = (String) claims.get(JTI); + accessTokenId = accessTokenId.replace("-r", ""); + + boolean revocable = claims.get(REVOCABLE) == null ? false : (Boolean)claims.get(REVOCABLE); + + // TODO: Need to add a lookup by id so that the refresh token does not // need to contain a name UaaUser user = userDatabase.retrieveUserById(userid); @@ -190,7 +244,7 @@ public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenReque // default request scopes to what is in the refresh token Set requestedScopes = request.getScope(); if (requestedScopes.isEmpty()) { - requestedScopes = new HashSet(tokenScopes); + requestedScopes = new HashSet<>(tokenScopes); } // The user may not request scopes that were not part of the refresh @@ -229,11 +283,12 @@ public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenReque int zoneAccessTokenValidity = getZoneAccessTokenValidity(); - OAuth2AccessToken accessToken = + CompositeAccessToken accessToken = createAccessToken( + accessTokenId, user.getId(), user, - claims.get(AUTH_TIME) != null ? new Date(((Long)claims.get(AUTH_TIME)) * 1000l) : null, + (claims.get(AUTH_TIME) != null) ? new Date(((Long) claims.get(AUTH_TIME)) * 1000l) : null, validity != null ? validity.intValue() : zoneAccessTokenValidity, null, requestedScopes, @@ -247,7 +302,8 @@ public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenReque revocableHashSignature, false, null, //TODO populate response types - null); + null, + revocable); return accessToken; } @@ -297,24 +353,25 @@ private void checkForApproval(String userid, } - private OAuth2AccessToken createAccessToken(String userId, - UaaUser user, - Date userAuthenticationTime, - int validitySeconds, - Collection clientScopes, - Set requestedScopes, - String clientId, - Set resourceIds, - String grantType, - String refreshToken, - String nonce, - Map additionalAuthorizationAttributes, - Set responseTypes, - String revocableHashSignature, - boolean forceIdTokenCreation, - Set externalGroupsForIdToken, - Map> userAttributesForIdToken) throws AuthenticationException { - String tokenId = UUID.randomUUID().toString(); + private CompositeAccessToken createAccessToken(String tokenId, + String userId, + UaaUser user, + Date userAuthenticationTime, + int validitySeconds, + Collection clientScopes, + Set requestedScopes, + String clientId, + Set resourceIds, + String grantType, + String refreshToken, + String nonce, + Map additionalAuthorizationAttributes, + Set responseTypes, + String revocableHashSignature, + boolean forceIdTokenCreation, + Set externalGroupsForIdToken, + Map> userAttributesForIdToken, + boolean revocable) throws AuthenticationException { CompositeAccessToken accessToken = new CompositeAccessToken(tokenId); if (validitySeconds > 0) { accessToken.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L))); @@ -350,7 +407,8 @@ private OAuth2AccessToken createAccessToken(String userId, resourceIds, grantType, refreshToken, - revocableHashSignature + revocableHashSignature, + revocable ); try { content = JsonUtils.writeValueAsString(jwtAccessToken); @@ -426,7 +484,8 @@ private void populateIdToken(CompositeAccessToken token, Set resourceIds, String grantType, String refreshToken, - String revocableHashSignature) { + String revocableHashSignature, + boolean revocable) { Map response = new LinkedHashMap(); @@ -442,6 +501,9 @@ private void populateIdToken(CompositeAccessToken token, response.put(CLIENT_ID, clientId); response.put(CID, clientId); response.put(AZP, clientId); //openId Connect + if (revocable) { + response.put(REVOCABLE, true); + } if (null != grantType) { response.put(GRANT_TYPE, grantType); @@ -514,8 +576,13 @@ public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) ClientDetails client = clientDetailsService.loadClientByClientId(authentication.getOAuth2Request().getClientId()); String revocableHashSignature = UaaTokenUtils.getRevocableTokenSignature(client, user); - OAuth2RefreshToken refreshToken = createRefreshToken(authentication, revocableHashSignature); + String tokenId = generateUniqueTokenId(); + String refreshTokenId = tokenId + "-r"; + boolean opaque = "opaque".equals(authentication.getOAuth2Request().getRequestParameters().get("token_format")); + boolean revocable = opaque || "true".equals(authentication.getOAuth2Request().getRequestParameters().get("revocable")); + + OAuth2RefreshToken refreshToken = createRefreshToken(refreshTokenId, authentication, revocableHashSignature, revocable); String clientId = authentication.getOAuth2Request().getClientId(); Set userScopes = authentication.getOAuth2Request().getScope(); @@ -554,8 +621,12 @@ public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) Integer validity = client.getAccessTokenValiditySeconds(); Set responseTypes = extractResponseTypes(authentication); - OAuth2AccessToken accessToken = + + + + CompositeAccessToken accessToken = createAccessToken( + tokenId, userId, user, userAuthenticationTime, @@ -572,9 +643,61 @@ public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) revocableHashSignature, wasIdTokenRequestedThroughAuthCodeScopeParameter, externalGroupsForIdToken, - userAttributesForIdToken); + userAttributesForIdToken, + revocable); + if (revocable) { + return persistRevocableToken(tokenId, refreshTokenId, accessToken, refreshToken, clientId, userId, opaque); + } else { + return accessToken; + } + } - return accessToken; + public CompositeAccessToken persistRevocableToken(String tokenId, String refreshTokenId, CompositeAccessToken token, OAuth2RefreshToken refreshToken, String clientId, String userId, boolean opaque) { + String scope = token.getScope().toString(); + if (StringUtils.hasText(scope) && scope.length()>1000) { + scope.substring(0,1000); + } + long now = System.currentTimeMillis(); + RevocableToken revocableAccessToken = new RevocableToken() + .setTokenId(tokenId) + .setClientId(clientId) + .setExpiresAt(token.getExpiration().getTime()) + .setIssuedAt(now) + .setFormat(opaque ? OPAQUE.name() : JWT.name()) + .setResponseType(RevocableToken.TokenType.ACCESS_TOKEN) + .setZoneId(IdentityZoneHolder.get().getId()) + .setUserId(userId) + .setScope(scope) + .setValue(token.getValue()); + tokenProvisioning.create(revocableAccessToken); + + if (refreshToken!=null) { + RevocableToken revocableRefreshToken = new RevocableToken() + .setTokenId(refreshTokenId) + .setClientId(clientId) + .setExpiresAt(((ExpiringOAuth2RefreshToken) refreshToken).getExpiration().getTime()) + .setIssuedAt(now) + .setFormat(opaque ? OPAQUE.name() : JWT.name()) + .setResponseType(RevocableToken.TokenType.REFRESH_TOKEN) + .setZoneId(IdentityZoneHolder.get().getId()) + .setUserId(userId) + .setScope(scope) + .setValue(refreshToken.getValue()); + tokenProvisioning.create(revocableRefreshToken); + } + + if (opaque) { + CompositeAccessToken result = new CompositeAccessToken(tokenId); + result.setIdTokenValue(token.getIdTokenValue()); + result.setExpiration(token.getExpiration()); + result.setAdditionalInformation(token.getAdditionalInformation()); + result.setScope(token.getScope()); + result.setTokenType(token.getTokenType()); + result.setRefreshToken(refreshToken==null ? null : new DefaultOAuth2RefreshToken(refreshTokenId)); + return result; + } else { + return token; + } } /** @@ -624,7 +747,10 @@ private Map getAdditionalAuthorizationAttributes(String authorit return null; } - private ExpiringOAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication, String revocableHashSignature) { + private ExpiringOAuth2RefreshToken createRefreshToken(String tokenId, + OAuth2Authentication authentication, + String revocableHashSignature, + boolean revocable) { String grantType = authentication.getOAuth2Request().getRequestParameters().get("grant_type"); if (!isRefreshTokenSupported(grantType)) { @@ -635,8 +761,8 @@ private ExpiringOAuth2RefreshToken createRefreshToken(OAuth2Authentication authe .getOAuth2Request().getRequestParameters().get("authorities")); int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request()); - ExpiringOAuth2RefreshToken token = new DefaultExpiringOAuth2RefreshToken(UUID.randomUUID().toString(), - new Date(System.currentTimeMillis() + (validitySeconds * 1000L))); + ExpiringOAuth2RefreshToken token = new DefaultExpiringOAuth2RefreshToken(tokenId, + new Date(System.currentTimeMillis() + (validitySeconds * 1000L))); String userId = getUserId(authentication); @@ -646,10 +772,15 @@ private ExpiringOAuth2RefreshToken createRefreshToken(OAuth2Authentication authe try { content = JsonUtils.writeValueAsString( createJWTRefreshToken( - token, user, authentication.getOAuth2Request().getScope(), + token, + tokenId, + user, + authentication.getOAuth2Request().getScope(), authentication.getOAuth2Request().getClientId(), - grantType, additionalAuthorizationAttributes,authentication.getOAuth2Request().getResourceIds(), - revocableHashSignature + grantType, + additionalAuthorizationAttributes,authentication.getOAuth2Request().getResourceIds(), + revocableHashSignature, + revocable ) ); } catch (JsonUtils.JsonUtilException e) { @@ -668,17 +799,19 @@ protected String getUserId(OAuth2Authentication authentication) { private Map createJWTRefreshToken( OAuth2RefreshToken token, + String tokenId, UaaUser user, Set scopes, String clientId, String grantType, Map additionalAuthorizationAttributes, Set resourceIds, - String revocableSignature) { + String revocableSignature, + boolean revocable) { Map response = new LinkedHashMap(); - response.put(JTI, UUID.randomUUID().toString()+"-r"); //-r for refresh token + response.put(JTI, tokenId); response.put(SUB, user.getId()); response.put(SCOPE, scopes); if (null != additionalAuthorizationAttributes) { @@ -697,6 +830,10 @@ protected String getUserId(OAuth2Authentication authentication) { response.put(ZONE_ID,IdentityZoneHolder.get().getId()); } + if (revocable) { + response.put(ClaimConstants.REVOCABLE, true); + } + if (null != grantType) { response.put(GRANT_TYPE, grantType); } @@ -715,6 +852,10 @@ protected String getUserId(OAuth2Authentication authentication) { return response; } + protected String generateUniqueTokenId() { + return UUID.randomUUID().toString().replace("-", ""); + } + /** * Check the current authorization request to indicate whether a refresh * token should be issued or not. @@ -765,6 +906,18 @@ public void setUserDatabase(UaaUserDatabase userDatabase) { @Override public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException { + if (StringUtils.isEmpty(accessToken)) { + throw new InvalidTokenException("Invalid access token value, must be at least 30 characters:"+accessToken); + } + + if (accessToken.length()<=36) { + try { + accessToken = tokenProvisioning.retrieve(accessToken).getValue(); + } catch (EmptyResultDataAccessException x) { + throw new InvalidTokenException("Revocable token with ID:"+accessToken+" not found."); + } + } + Map claims = getClaimsForToken(accessToken); // Check token expiry @@ -879,7 +1032,7 @@ private Set getAutoApprovedScopes(Object grantType, Collection t return UaaTokenUtils.retainAutoApprovedScopes(tokenScopes, autoApprovedScopes); } - private Map getClaimsForToken(String token) { + protected Map getClaimsForToken(String token) { TokenValidation tokenValidation = validate(token).throwIfInvalid(); Jwt tokenJwt = tokenValidation.getJwt(); Map claims = tokenValidation.getClaims(); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServicesTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServicesTests.java index 48cbb29f9c2..fe44f2efd7d 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServicesTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServicesTests.java @@ -12,23 +12,24 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.oauth; +import org.apache.commons.collections.map.HashedMap; +import org.cloudfoundry.identity.uaa.approval.Approval; +import org.cloudfoundry.identity.uaa.approval.Approval.ApprovalStatus; +import org.cloudfoundry.identity.uaa.approval.ApprovalStore; import org.cloudfoundry.identity.uaa.audit.AuditEvent; import org.cloudfoundry.identity.uaa.audit.AuditEventType; import org.cloudfoundry.identity.uaa.audit.event.TokenIssuedEvent; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; +import org.cloudfoundry.identity.uaa.constants.OriginKeys; +import org.cloudfoundry.identity.uaa.oauth.approval.InMemoryApprovalStore; import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; 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.CompositeAccessToken; -import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration; -import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; -import org.cloudfoundry.identity.uaa.zone.TokenPolicy; -import org.cloudfoundry.identity.uaa.constants.OriginKeys; -import org.cloudfoundry.identity.uaa.approval.Approval; -import org.cloudfoundry.identity.uaa.approval.Approval.ApprovalStatus; -import org.cloudfoundry.identity.uaa.approval.ApprovalStore; -import org.cloudfoundry.identity.uaa.oauth.approval.InMemoryApprovalStore; +import org.cloudfoundry.identity.uaa.oauth.token.RevocableToken; +import org.cloudfoundry.identity.uaa.oauth.token.RevocableTokenProvisioning; import org.cloudfoundry.identity.uaa.oauth.token.matchers.OAuth2RefreshTokenMatchers; import org.cloudfoundry.identity.uaa.test.MockAuthentication; import org.cloudfoundry.identity.uaa.test.TestApplicationEventPublisher; @@ -38,11 +39,16 @@ import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; +import org.cloudfoundry.identity.uaa.zone.TokenPolicy; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mockito.stubbing.Answer; +import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -57,6 +63,7 @@ import org.springframework.security.oauth2.provider.AuthorizationRequest; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.OAuth2RequestFactory; +import org.springframework.security.oauth2.provider.TokenRequest; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService; import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory; @@ -92,14 +99,18 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.core.AllOf.allOf; import static org.hamcrest.number.OrderingComparison.greaterThan; +import static org.hamcrest.number.OrderingComparison.lessThanOrEqualTo; import static org.hamcrest.text.IsEmptyString.isEmptyString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -118,7 +129,7 @@ public class UaaTokenServicesTests { public static final String REFRESH_TOKEN = "refresh_token"; public static final String AUTOAPPROVE = ClientConstants.AUTO_APPROVE; public static final String IMPLICIT = "implicit"; - public static final String CLIENT_AUTHORITIES = "read,update,write"; + public static final String CLIENT_AUTHORITIES = "read,update,write,openid"; public static final String CANNOT_READ_TOKEN_CLAIMS = "Cannot read token claims"; public static final String ISSUER_URI = "http://localhost:8080/uaa/oauth/token"; public static final String READ = "read"; @@ -142,6 +153,7 @@ public class UaaTokenServicesTests { UaaAuthority.authority("uaa.user"), UaaAuthority.authority("space.345.developer"), UaaAuthority.authority("space.123.admin"), + UaaAuthority.authority(OPENID), UaaAuthority.authority(READ), UaaAuthority.authority(WRITE)); @@ -191,6 +203,8 @@ public class UaaTokenServicesTests { private BaseClientDetails defaultClient; private OAuth2RequestFactory requestFactory; private TokenPolicy tokenPolicy; + private RevocableTokenProvisioning tokenProvisioning; + private final Map tokens = new HashMap<>(); public UaaTokenServicesTests() { @@ -216,18 +230,19 @@ public void setUp() throws Exception { mockAuthentication = new MockAuthentication(); SecurityContextHolder.getContext().setAuthentication(mockAuthentication); - requestedAuthScopes = Arrays.asList(READ, WRITE); - clientScopes = Arrays.asList(READ, WRITE); + requestedAuthScopes = Arrays.asList(READ, WRITE,OPENID); + clientScopes = Arrays.asList(READ, WRITE,OPENID); readScope = Arrays.asList(READ); writeScope = Arrays.asList(WRITE); - expandedScopes = Arrays.asList(READ, WRITE, DELETE); + expandedScopes = Arrays.asList(READ, WRITE, DELETE,OPENID); resourceIds = Arrays.asList(SCIM, CLIENTS); - expectedJson = "[\""+READ+"\",\""+WRITE+"\"]"; + expectedJson = "[\""+READ+"\",\""+WRITE+"\",\""+OPENID+"\"]"; + defaultClient = new BaseClientDetails( CLIENT_ID, SCIM+","+CLIENTS, - READ+","+WRITE, + READ+","+WRITE+","+OPENID, ALL_GRANTS_CSV, CLIENT_AUTHORITIES); @@ -237,6 +252,31 @@ public void setUp() throws Exception { defaultClient ) ); + + tokenProvisioning = mock(RevocableTokenProvisioning.class); + when(tokenProvisioning.create(anyObject())).thenAnswer((Answer) invocation -> { + RevocableToken arg = (RevocableToken)invocation.getArguments()[0]; + tokens.put(arg.getTokenId(), arg); + return arg; + }); + when(tokenProvisioning.update(anyString(), anyObject())).thenAnswer((Answer) invocation -> { + String id = (String)invocation.getArguments()[0]; + RevocableToken arg = (RevocableToken)invocation.getArguments()[1]; + arg.setTokenId(id); + tokens.put(arg.getTokenId(), arg); + return arg; + }); + when(tokenProvisioning.retrieve(anyString())).thenAnswer((Answer) invocation -> { + String id = (String)invocation.getArguments()[0]; + RevocableToken result = tokens.get(id); + if (result==null) { + throw new EmptyResultDataAccessException(1); + } + return result; + + }); + + requestFactory = new DefaultOAuth2RequestFactory(clientDetailsService); tokenServices.setClientDetailsService(clientDetailsService); tokenServices.setTokenPolicy(tokenPolicy); @@ -245,12 +285,14 @@ public void setUp() throws Exception { tokenServices.setUserDatabase(userDatabase); tokenServices.setApprovalStore(approvalStore); tokenServices.setApplicationEventPublisher(publisher); + tokenServices.setTokenProvisioning(tokenProvisioning); tokenServices.afterPropertiesSet(); } @After public void teardown() { IdentityZoneHolder.clear(); + tokens.clear(); } @Test(expected = InvalidTokenException.class) @@ -287,11 +329,30 @@ public void testCreateAccessTokenForAClient() { assertCommonClientAccessTokenProperties(accessToken); assertThat(accessToken, validFor(is(accessTokenValidity))); - assertThat(accessToken, issuerUri(is(ISSUER_URI))); - assertThat(accessToken, zoneId(is(IdentityZoneHolder.get().getId()))); + assertThat(accessToken, issuerUri(is(ISSUER_URI))); + assertThat(accessToken, zoneId(is(IdentityZoneHolder.get().getId()))); assertThat(accessToken.getRefreshToken(), is(nullValue())); - this.assertCommonEventProperties(accessToken, CLIENT_ID, expectedJson); + assertCommonEventProperties(accessToken, CLIENT_ID, expectedJson); + } + + + @Test + public void testCreateOpaqueAccessTokenForAClient() { + AuthorizationRequest authorizationRequest = new AuthorizationRequest(CLIENT_ID, clientScopes); + authorizationRequest.setResourceIds(new HashSet<>(resourceIds)); + Map azParameters = new HashMap<>(authorizationRequest.getRequestParameters()); + azParameters.put("token_format", "opaque"); + azParameters.put(GRANT_TYPE, CLIENT_CREDENTIALS); + authorizationRequest.setRequestParameters(azParameters); + + OAuth2Authentication authentication = new OAuth2Authentication(authorizationRequest.createOAuth2Request(), null); + + OAuth2AccessToken accessToken = tokenServices.createAccessToken(authentication); + + assertTrue("Token is not a composite token", accessToken instanceof CompositeAccessToken); + assertThat("Token value should be equal to or lesser than 36 characters", accessToken.getValue().length(), lessThanOrEqualTo(36)); + assertThat(accessToken.getRefreshToken(), is(nullValue())); } @Test @@ -346,16 +407,16 @@ public void testCreateAccessTokenAuthcodeGrant() { OAuth2AccessToken accessToken = tokenServices.createAccessToken(authentication); this.assertCommonUserAccessTokenProperties(accessToken); - assertThat(accessToken, issuerUri(is(ISSUER_URI))); - assertThat(accessToken, scope(is(requestedAuthScopes))); - assertThat(accessToken, validFor(is(60 * 60 * 12))); + assertThat(accessToken, issuerUri(is(ISSUER_URI))); + assertThat(accessToken, scope(is(requestedAuthScopes))); + assertThat(accessToken, validFor(is(60 * 60 * 12))); OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); - this.assertCommonUserRefreshTokenProperties(refreshToken); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); + this.assertCommonUserRefreshTokenProperties(refreshToken); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); - this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); + this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); } @Test @@ -403,16 +464,16 @@ public void testCreateAccessTokenPasswordGrant() { OAuth2AccessToken accessToken = tokenServices.createAccessToken(authentication); this.assertCommonUserAccessTokenProperties(accessToken); - assertThat(accessToken, issuerUri(is(ISSUER_URI))); - assertThat(accessToken, scope(is(requestedAuthScopes))); - assertThat(accessToken, validFor(is(60 * 60 * 12))); + assertThat(accessToken, issuerUri(is(ISSUER_URI))); + assertThat(accessToken, scope(is(requestedAuthScopes))); + assertThat(accessToken, validFor(is(60 * 60 * 12))); OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); - this.assertCommonUserRefreshTokenProperties(refreshToken); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); + this.assertCommonUserRefreshTokenProperties(refreshToken); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); - this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); + this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); } @Test @@ -428,16 +489,16 @@ public void testCreateRevocableAccessTokenPasswordGrant() { OAuth2AccessToken accessToken = tokenServices.createAccessToken(authentication); this.assertCommonUserAccessTokenProperties(accessToken); - assertThat(accessToken, issuerUri(is(ISSUER_URI))); - assertThat(accessToken, scope(is(requestedAuthScopes))); - assertThat(accessToken, validFor(is(60 * 60 * 12))); + assertThat(accessToken, issuerUri(is(ISSUER_URI))); + assertThat(accessToken, scope(is(requestedAuthScopes))); + assertThat(accessToken, validFor(is(60 * 60 * 12))); OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); - this.assertCommonUserRefreshTokenProperties(refreshToken); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); + this.assertCommonUserRefreshTokenProperties(refreshToken); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); - this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); + this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); } @Test @@ -455,9 +516,9 @@ public void testCreateAccessTokenRefreshGrant() throws InterruptedException { assertEquals(refreshedAccessToken.getRefreshToken().getValue(), accessToken.getRefreshToken().getValue()); this.assertCommonUserAccessTokenProperties(refreshedAccessToken); - assertThat(refreshedAccessToken, issuerUri(is(ISSUER_URI))); - assertThat(refreshedAccessToken, scope(is(requestedAuthScopes))); - assertThat(refreshedAccessToken, validFor(is(60 * 60 * 12))); + assertThat(refreshedAccessToken, issuerUri(is(ISSUER_URI))); + assertThat(refreshedAccessToken, scope(is(requestedAuthScopes))); + assertThat(refreshedAccessToken, validFor(is(60 * 60 * 12))); } @Test @@ -484,9 +545,9 @@ public void createAccessToken_usingRefreshGrant_inOtherZone() throws Exception { assertEquals(refreshedAccessToken.getRefreshToken().getValue(), accessToken.getRefreshToken().getValue()); this.assertCommonUserAccessTokenProperties(refreshedAccessToken); - assertThat(refreshedAccessToken, issuerUri(is("http://test-zone-subdomain.localhost:8080/uaa/oauth/token"))); - assertThat(refreshedAccessToken, scope(is(requestedAuthScopes))); - assertThat(refreshedAccessToken, validFor(is(3600))); + assertThat(refreshedAccessToken, issuerUri(is("http://test-zone-subdomain.localhost:8080/uaa/oauth/token"))); + assertThat(refreshedAccessToken, scope(is(requestedAuthScopes))); + assertThat(refreshedAccessToken, validFor(is(3600))); } private OAuth2AccessToken getOAuth2AccessToken() { @@ -510,6 +571,13 @@ private OAuth2AccessToken getOAuth2AccessToken() { .setExpiresAt(expiresAt.getTime()) .setStatus(ApprovalStatus.APPROVED) .setLastUpdatedAt(updatedAt.getTime())); + approvalStore.addApproval(new Approval() + .setUserId(userId) + .setClientId(CLIENT_ID) + .setScope(OPENID) + .setExpiresAt(expiresAt.getTime()) + .setStatus(ApprovalStatus.APPROVED) + .setLastUpdatedAt(updatedAt.getTime())); AuthorizationRequest authorizationRequest = new AuthorizationRequest(CLIENT_ID,requestedAuthScopes); authorizationRequest.setResourceIds(new HashSet<>(resourceIds)); @@ -541,16 +609,16 @@ public void testCreateAccessTokenRefreshGrantAllScopesAutoApproved() throws Inte OAuth2AccessToken accessToken = tokenServices.createAccessToken(authentication); this.assertCommonUserAccessTokenProperties(accessToken); - assertThat(accessToken, issuerUri(is(ISSUER_URI))); - assertThat(accessToken, scope(is(requestedAuthScopes))); - assertThat(accessToken, validFor(is(60 * 60 * 12))); + assertThat(accessToken, issuerUri(is(ISSUER_URI))); + assertThat(accessToken, scope(is(requestedAuthScopes))); + assertThat(accessToken, validFor(is(60 * 60 * 12))); OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); - this.assertCommonUserRefreshTokenProperties(refreshToken); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); + this.assertCommonUserRefreshTokenProperties(refreshToken); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); - this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); + this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); AuthorizationRequest refreshAuthorizationRequest = new AuthorizationRequest(CLIENT_ID,requestedAuthScopes); refreshAuthorizationRequest.setResourceIds(new HashSet<>(resourceIds)); @@ -564,7 +632,7 @@ public void testCreateAccessTokenRefreshGrantAllScopesAutoApproved() throws Inte this.assertCommonUserAccessTokenProperties(refreshedAccessToken); assertThat(refreshedAccessToken, issuerUri(is(ISSUER_URI))); - assertThat(refreshedAccessToken, scope(is(requestedAuthScopes))); + assertThat(refreshedAccessToken, scope(is(requestedAuthScopes))); assertThat(refreshedAccessToken, validFor(is(60 * 60 * 12))); assertThat(accessToken.getRefreshToken(), is(not(nullValue()))); } @@ -589,16 +657,16 @@ public void testCreateAccessTokenRefreshGrantSomeScopesAutoApprovedDowngradedReq OAuth2AccessToken accessToken = tokenServices.createAccessToken(authentication); this.assertCommonUserAccessTokenProperties(accessToken); - assertThat(accessToken, issuerUri(is(ISSUER_URI))); - assertThat(accessToken, scope(is(requestedAuthScopes))); - assertThat(accessToken, validFor(is(60 * 60 * 12))); + assertThat(accessToken, issuerUri(is(ISSUER_URI))); + assertThat(accessToken, scope(is(requestedAuthScopes))); + assertThat(accessToken, validFor(is(60 * 60 * 12))); OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); - this.assertCommonUserRefreshTokenProperties(refreshToken); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); + this.assertCommonUserRefreshTokenProperties(refreshToken); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); - this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); + this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); AuthorizationRequest refreshAuthorizationRequest = new AuthorizationRequest(CLIENT_ID,readScope); refreshAuthorizationRequest.setResourceIds(new HashSet<>(resourceIds)); @@ -636,6 +704,14 @@ public void testCreateAccessTokenRefreshGrantSomeScopesAutoApproved() throws Int .setStatus(ApprovalStatus.APPROVED) .setLastUpdatedAt(updatedAt.getTime())); + approvalStore.addApproval(new Approval() + .setUserId(userId) + .setClientId(CLIENT_ID) + .setScope(OPENID) + .setExpiresAt(expiresAt.getTime()) + .setStatus(ApprovalStatus.APPROVED) + .setLastUpdatedAt(updatedAt.getTime())); + AuthorizationRequest authorizationRequest = new AuthorizationRequest(CLIENT_ID,requestedAuthScopes); authorizationRequest.setResourceIds(new HashSet<>(resourceIds)); Map azParameters = new HashMap<>(authorizationRequest.getRequestParameters()); @@ -647,16 +723,16 @@ public void testCreateAccessTokenRefreshGrantSomeScopesAutoApproved() throws Int OAuth2AccessToken accessToken = tokenServices.createAccessToken(authentication); this.assertCommonUserAccessTokenProperties(accessToken); - assertThat(accessToken, issuerUri(is(ISSUER_URI))); - assertThat(accessToken, scope(is(requestedAuthScopes))); - assertThat(accessToken, validFor(is(60 * 60 * 12))); + assertThat(accessToken, issuerUri(is(ISSUER_URI))); + assertThat(accessToken, scope(is(requestedAuthScopes))); + assertThat(accessToken, validFor(is(60 * 60 * 12))); OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); - this.assertCommonUserRefreshTokenProperties(refreshToken); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); + this.assertCommonUserRefreshTokenProperties(refreshToken); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); - this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); + this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); AuthorizationRequest refreshAuthorizationRequest = new AuthorizationRequest(CLIENT_ID,requestedAuthScopes); refreshAuthorizationRequest.setResourceIds(new HashSet<>(resourceIds)); @@ -705,16 +781,16 @@ public void testCreateAccessTokenRefreshGrantNoScopesAutoApprovedIncompleteAppro OAuth2AccessToken accessToken = tokenServices.createAccessToken(authentication); this.assertCommonUserAccessTokenProperties(accessToken); - assertThat(accessToken, issuerUri(is(ISSUER_URI))); - assertThat(accessToken, scope(is(requestedAuthScopes))); - assertThat(accessToken, validFor(is(60 * 60 * 12))); + assertThat(accessToken, issuerUri(is(ISSUER_URI))); + assertThat(accessToken, scope(is(requestedAuthScopes))); + assertThat(accessToken, validFor(is(60 * 60 * 12))); OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); - this.assertCommonUserRefreshTokenProperties(refreshToken); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); + this.assertCommonUserRefreshTokenProperties(refreshToken); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); - this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); + this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); AuthorizationRequest refreshAuthorizationRequest = new AuthorizationRequest(CLIENT_ID,requestedAuthScopes); refreshAuthorizationRequest.setResourceIds(new HashSet<>(resourceIds)); @@ -763,16 +839,16 @@ public void testCreateAccessTokenRefreshGrantAllScopesAutoApprovedButApprovalDen OAuth2AccessToken accessToken = tokenServices.createAccessToken(authentication); this.assertCommonUserAccessTokenProperties(accessToken); - assertThat(accessToken, issuerUri(is(ISSUER_URI))); - assertThat(accessToken, scope(is(requestedAuthScopes))); - assertThat(accessToken, validFor(is(60 * 60 * 12))); + assertThat(accessToken, issuerUri(is(ISSUER_URI))); + assertThat(accessToken, scope(is(requestedAuthScopes))); + assertThat(accessToken, validFor(is(60 * 60 * 12))); OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); - this.assertCommonUserRefreshTokenProperties(refreshToken); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); + this.assertCommonUserRefreshTokenProperties(refreshToken); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); - this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); + this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); AuthorizationRequest refreshAuthorizationRequest = new AuthorizationRequest(CLIENT_ID,requestedAuthScopes); refreshAuthorizationRequest.setResourceIds(new HashSet<>(resourceIds)); @@ -801,7 +877,7 @@ public void testCreateAccessTokenImplicitGrant() { assertThat(accessToken, validFor(is(60 * 60 * 12))); assertThat(accessToken.getRefreshToken(), is(nullValue())); - this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); + this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); } @Test @@ -873,7 +949,7 @@ public void testCreateAccessWithNonExistingScopes() { assertThat(accessToken, validFor(is(60 * 60 * 12))); assertThat(accessToken.getRefreshToken(), is(nullValue())); - this.assertCommonEventProperties(accessToken, userId, buildJsonString(scopesThatDontExist)); + this.assertCommonEventProperties(accessToken, userId, buildJsonString(scopesThatDontExist)); } @Test @@ -905,11 +981,11 @@ public void createAccessToken_forUser_inanotherzone() { assertThat(accessToken.getRefreshToken(), is(not(nullValue()))); OAuth2RefreshToken refreshToken = accessToken.getRefreshToken(); - this.assertCommonUserRefreshTokenProperties(refreshToken); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is("http://test-zone-subdomain.localhost:8080/uaa/oauth/token"))); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(9600))); + this.assertCommonUserRefreshTokenProperties(refreshToken); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is("http://test-zone-subdomain.localhost:8080/uaa/oauth/token"))); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(9600))); - this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); + this.assertCommonEventProperties(accessToken, userId, buildJsonString(requestedAuthScopes)); } private String buildJsonString(List list) { @@ -1330,6 +1406,14 @@ public void testReadAccessToken() { .setExpiresAt(expiresAt.getTime()) .setStatus(ApprovalStatus.APPROVED) .setLastUpdatedAt(updatedAt.getTime())); + approvalStore.addApproval( + new Approval() + .setUserId(userId) + .setClientId(CLIENT_ID) + .setScope(OPENID) + .setExpiresAt(expiresAt.getTime()) + .setStatus(ApprovalStatus.APPROVED) + .setLastUpdatedAt(updatedAt.getTime())); OAuth2Authentication authentication = new OAuth2Authentication(authorizationRequest.createOAuth2Request(), userAuthentication); OAuth2AccessToken accessToken = tokenServices.createAccessToken(authentication); @@ -1397,6 +1481,57 @@ public void testLoadAuthenticationForAUser() { assertTrue(userAuth.isAuthenticated()); } + @Test + public void testLoad_Opaque_AuthenticationForAUser() { + defaultClient.addAdditionalInformation(ClientConstants.AUTO_APPROVE, true); + AuthorizationRequest authorizationRequest = new AuthorizationRequest(CLIENT_ID,requestedAuthScopes); + authorizationRequest.setResponseTypes(new HashSet(Arrays.asList(CompositeAccessToken.ID_TOKEN, "token"))); + authorizationRequest.setResourceIds(new HashSet<>(resourceIds)); + Map azParameters = new HashMap<>(authorizationRequest.getRequestParameters()); + azParameters.put(GRANT_TYPE, AUTHORIZATION_CODE); + azParameters.put("token_format", "opaque"); + authorizationRequest.setRequestParameters(azParameters); + Authentication userAuthentication = defaultUserAuthentication; + + OAuth2Authentication authentication = new OAuth2Authentication(authorizationRequest.createOAuth2Request(), userAuthentication); + OAuth2AccessToken accessToken = tokenServices.createAccessToken(authentication); + assertNotNull(accessToken); + assertTrue("Token should be composite token", accessToken instanceof CompositeAccessToken); + CompositeAccessToken composite = (CompositeAccessToken)accessToken; + assertThat("id_token should be JWT, thus longer than 36 characters", composite.getIdTokenValue().length(), greaterThan(36)); + assertThat("Opaque access token must be shorter than 37 characters", accessToken.getValue().length(), lessThanOrEqualTo(36)); + assertThat("Opaque refresh token must be shorter than 37 characters", accessToken.getRefreshToken().getValue().length(), lessThanOrEqualTo(36)); + + String accessTokenValue = tokenProvisioning.retrieve(composite.getValue()).getValue(); + Map accessTokenClaims = tokenServices.getClaimsForToken(accessTokenValue); + assertEquals(true, accessTokenClaims.get(ClaimConstants.REVOCABLE)); + + String refreshTokenValue = tokenProvisioning.retrieve(composite.getRefreshToken().getValue()).getValue(); + Map refreshTokenClaims = tokenServices.getClaimsForToken(refreshTokenValue); + assertEquals(true, refreshTokenClaims.get(ClaimConstants.REVOCABLE)); + + + OAuth2Authentication loadedAuthentication = tokenServices.loadAuthentication(accessToken.getValue()); + + assertEquals(USER_AUTHORITIES, loadedAuthentication.getAuthorities()); + assertEquals(username, loadedAuthentication.getName()); + UaaPrincipal uaaPrincipal = (UaaPrincipal)defaultUserAuthentication.getPrincipal(); + assertEquals(uaaPrincipal, loadedAuthentication.getPrincipal()); + assertNull(loadedAuthentication.getDetails()); + + Authentication userAuth = loadedAuthentication.getUserAuthentication(); + assertEquals(username, userAuth.getName()); + assertEquals(uaaPrincipal, userAuth.getPrincipal()); + assertTrue(userAuth.isAuthenticated()); + + Map params = new HashedMap(); + params.put("grant_type", "refresh_token"); + params.put("client_id",CLIENT_ID); + OAuth2AccessToken newAccessToken = tokenServices.refreshAccessToken(composite.getRefreshToken().getValue(), new TokenRequest(params, CLIENT_ID, Collections.EMPTY_SET, "refresh_token")); + System.out.println("newAccessToken = " + newAccessToken); + } + + @Test public void testLoadAuthenticationForAClient() { AuthorizationRequest authorizationRequest = new AuthorizationRequest(CLIENT_ID, requestedAuthScopes); @@ -1410,7 +1545,10 @@ public void testLoadAuthenticationForAClient() { OAuth2AccessToken accessToken = tokenServices.createAccessToken(authentication); OAuth2Authentication loadedAuthentication = tokenServices.loadAuthentication(accessToken.getValue()); - assertEquals(AuthorityUtils.commaSeparatedStringToAuthorityList(CLIENT_AUTHORITIES),loadedAuthentication.getAuthorities()); + assertThat("Client authorities match.", + loadedAuthentication.getAuthorities(), + containsInAnyOrder(AuthorityUtils.commaSeparatedStringToAuthorityList(CLIENT_AUTHORITIES).toArray()) + ); assertEquals(CLIENT_ID, loadedAuthentication.getName()); assertEquals(CLIENT_ID, loadedAuthentication.getPrincipal()); assertNull(loadedAuthentication.getDetails()); @@ -1453,16 +1591,16 @@ public void testCreateAccessTokenAuthcodeGrantAdditionalAuthorizationAttributes( OAuth2AccessToken token = tokenServices.createAccessToken(authentication); this.assertCommonUserAccessTokenProperties(token); - assertThat(token, issuerUri(is(ISSUER_URI))); - assertThat(token, scope(is(requestedAuthScopes))); - assertThat(token, validFor(is(60 * 60 * 12))); + assertThat(token, issuerUri(is(ISSUER_URI))); + assertThat(token, scope(is(requestedAuthScopes))); + assertThat(token, validFor(is(60 * 60 * 12))); OAuth2RefreshToken refreshToken = token.getRefreshToken(); - this.assertCommonUserRefreshTokenProperties(refreshToken); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); - assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); + this.assertCommonUserRefreshTokenProperties(refreshToken); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.issuerUri(is(ISSUER_URI))); + assertThat(refreshToken, OAuth2RefreshTokenMatchers.validFor(is(60 * 60 * 24 * 30))); - this.assertCommonEventProperties(token, userId, buildJsonString(requestedAuthScopes)); + this.assertCommonEventProperties(token, userId, buildJsonString(requestedAuthScopes)); Map azMap = new LinkedHashMap<>(); azMap.put("external_group", "domain\\group1"); @@ -1476,49 +1614,49 @@ private BaseClientDetails cloneClient(BaseClientDetails client) { @SuppressWarnings("unchecked") private void assertCommonClientAccessTokenProperties(OAuth2AccessToken accessToken) { - assertThat(accessToken, allOf(clientId(is(CLIENT_ID)), - userId(is(nullValue())), - subject(is(CLIENT_ID)), - username(is(nullValue())), - cid(is(CLIENT_ID)), - scope(is(clientScopes)), - audience(is(resourceIds)), - jwtId(not(isEmptyString())), - issuedAt(is(greaterThan(0))), - expiry(is(greaterThan(0))))); + assertThat(accessToken, allOf(clientId(is(CLIENT_ID)), + userId(is(nullValue())), + subject(is(CLIENT_ID)), + username(is(nullValue())), + cid(is(CLIENT_ID)), + scope(is(clientScopes)), + audience(is(resourceIds)), + jwtId(not(isEmptyString())), + issuedAt(is(greaterThan(0))), + expiry(is(greaterThan(0))))); } @SuppressWarnings({ "unused", "unchecked" }) private void assertCommonUserAccessTokenProperties(OAuth2AccessToken accessToken) { assertThat(accessToken, allOf(username(is(username)), - clientId(is(CLIENT_ID)), - subject(is(userId)), - audience(is(resourceIds)), - origin(is(OriginKeys.UAA)), - revocationSignature(is(not(nullValue()))), - cid(is(CLIENT_ID)), - userId(is(userId)), - email(is(email)), - jwtId(not(isEmptyString())), - issuedAt(is(greaterThan(0))), - expiry(is(greaterThan(0))) - )); + clientId(is(CLIENT_ID)), + subject(is(userId)), + audience(is(resourceIds)), + origin(is(OriginKeys.UAA)), + revocationSignature(is(not(nullValue()))), + cid(is(CLIENT_ID)), + userId(is(userId)), + email(is(email)), + jwtId(not(isEmptyString())), + issuedAt(is(greaterThan(0))), + expiry(is(greaterThan(0))) + )); } @SuppressWarnings("unchecked") private void assertCommonUserRefreshTokenProperties(OAuth2RefreshToken refreshToken) { assertThat(refreshToken, allOf(/*issuer(is(issuerUri)),*/ - OAuth2RefreshTokenMatchers.username(is(username)), - OAuth2RefreshTokenMatchers.clientId(is(CLIENT_ID)), - OAuth2RefreshTokenMatchers.subject(is(not(nullValue()))), - OAuth2RefreshTokenMatchers.audience(is(resourceIds)), - OAuth2RefreshTokenMatchers.origin(is(OriginKeys.UAA)), - OAuth2RefreshTokenMatchers.revocationSignature(is(not(nullValue()))), - OAuth2RefreshTokenMatchers.jwtId(not(isEmptyString())), - OAuth2RefreshTokenMatchers.issuedAt(is(greaterThan(0))), - OAuth2RefreshTokenMatchers.expiry(is(greaterThan(0))) - ) - ); + OAuth2RefreshTokenMatchers.username(is(username)), + OAuth2RefreshTokenMatchers.clientId(is(CLIENT_ID)), + OAuth2RefreshTokenMatchers.subject(is(not(nullValue()))), + OAuth2RefreshTokenMatchers.audience(is(resourceIds)), + OAuth2RefreshTokenMatchers.origin(is(OriginKeys.UAA)), + OAuth2RefreshTokenMatchers.revocationSignature(is(not(nullValue()))), + OAuth2RefreshTokenMatchers.jwtId(not(isEmptyString())), + OAuth2RefreshTokenMatchers.issuedAt(is(greaterThan(0))), + OAuth2RefreshTokenMatchers.expiry(is(greaterThan(0))) + ) + ); } private void assertCommonEventProperties(OAuth2AccessToken accessToken, String expectedPrincipalId, String expectedData) { diff --git a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml index 7a706d90167..5d4ddd93183 100755 --- a/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml +++ b/uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml @@ -376,6 +376,10 @@ + + + + @@ -384,6 +388,7 @@ + diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/CheckTokenEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/CheckTokenEndpointDocs.java index 191bc85d8cd..e0c0b5af519 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/CheckTokenEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/CheckTokenEndpointDocs.java @@ -15,7 +15,9 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.payload.JsonFieldType.*; +import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; @@ -70,7 +72,8 @@ public void checkToken() throws Exception { fieldWithPath("auth_time").type(NUMBER).description("Only applicable for user tokens").optional(), fieldWithPath("zid").description("Zone ID"), fieldWithPath("rev_sig").description("Revocation Signature - token revocation hash salted with at least client ID and client secret, and optionally various user values."), - fieldWithPath("origin").type(STRING).description("Only applicable for user tokens").optional() + fieldWithPath("origin").type(STRING).description("Only applicable for user tokens").optional(), + fieldWithPath("revocable").type(STRING).description("Set to true if this token is revocable").optional() ); getMockMvc().perform(post("/check_token") @@ -82,5 +85,5 @@ public void checkToken() throws Exception { headerWithName("Authorization").description("Uses basic authorization with base64(resource_server:shared_secret) assuming the caller (a resource server) is actually also a registered client and has `uaa.resource` authority") ), requestParameters, responseFields)); } - + } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java index 666a3a4db4b..4ca311e3da3 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java @@ -13,6 +13,7 @@ package org.cloudfoundry.identity.uaa.mock.token; import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.collections.map.HashedMap; import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; @@ -24,7 +25,10 @@ import org.cloudfoundry.identity.uaa.oauth.UaaTokenServices; import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; 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.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.provider.PasswordPolicy; @@ -57,7 +61,6 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.codec.Base64; -import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; @@ -96,7 +99,9 @@ import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.stringContainsInOrder; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.StringStartsWith.startsWith; @@ -109,6 +114,7 @@ import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -2372,24 +2378,8 @@ public void testGetTokenScopesNotInAuthentication() throws Exception { @Test public void testRevocablePasswordGrantTokenForDefaultZone() throws Exception { - String username = new RandomValueStringGenerator().generate()+"@test.org"; String tokenKey = "access_token"; - String clientId = "testclient" + new RandomValueStringGenerator().generate(); - String scopes = "cloud_controller.read"; - setUpClients(clientId, scopes, scopes, "password,client_credentials", true, TEST_REDIRECT_URI, Arrays.asList(OriginKeys.UAA)); - setUpUser(username); - - Map tokenResponse = - JsonUtils.readValue( - getMockMvc().perform(post("/oauth/token") - .param("username", username) - .param("password", "secret") - .header("Authorization", "Basic " + new String(Base64.encode((clientId + ":" + SECRET).getBytes()))) - .param(OAuth2Utils.RESPONSE_TYPE, "token") - .param(OAuth2Utils.GRANT_TYPE, "password") - .param(OAuth2Utils.CLIENT_ID, clientId)).andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(), new TypeReference>() { - }); + Map tokenResponse = testRevocablePasswordGrantTokenForDefaultZone(new HashedMap()); assertNotNull("Token must be present", tokenResponse.get(tokenKey)); assertTrue("Token must be a string", tokenResponse.get(tokenKey) instanceof String); String token = (String)tokenResponse.get(tokenKey); @@ -2400,6 +2390,76 @@ public void testRevocablePasswordGrantTokenForDefaultZone() throws Exception { assertTrue("Token revocation signature must have data", StringUtils.hasText((String) claims.get(ClaimConstants.REVOCATION_SIGNATURE))); } + @Test + public void testPasswordGrantTokenForDefaultZone_Opaque() throws Exception { + Map parameters = new HashedMap(); + parameters.put("token_format", "opaque"); + String tokenKey = "access_token"; + Map tokenResponse = testRevocablePasswordGrantTokenForDefaultZone(parameters); + assertNotNull("Token must be present", tokenResponse.get(tokenKey)); + assertTrue("Token must be a string", tokenResponse.get(tokenKey) instanceof String); + String token = (String)tokenResponse.get(tokenKey); + assertThat("Token must be shorter than 37 characters", token.length(), lessThanOrEqualTo(36)); + + RevocableToken revocableToken = getWebApplicationContext().getBean(RevocableTokenProvisioning.class).retrieve(token); + assertNotNull("Token should have been stored in the DB", revocableToken); + + Jwt jwt = JwtHelper.decode(revocableToken.getValue()); + Map claims = JsonUtils.readValue(jwt.getClaims(), new TypeReference>(){}); + assertNotNull("Revocable claim must exist", claims.get(ClaimConstants.REVOCABLE)); + assertTrue("Token revocable claim must be set to true", (Boolean)claims.get(ClaimConstants.REVOCABLE)); + } + + @Test + public void testPasswordGrantTokenForDefaultZone_Revocable() throws Exception { + Map parameters = new HashedMap(); + parameters.put("revocable", "true"); + String tokenKey = "access_token"; + Map tokenResponse = testRevocablePasswordGrantTokenForDefaultZone(parameters); + assertNotNull("Token must be present", tokenResponse.get(tokenKey)); + assertTrue("Token must be a string", tokenResponse.get(tokenKey) instanceof String); + String token = (String)tokenResponse.get(tokenKey); + assertThat("Token must be longer than 36 characters", token.length(), greaterThan(36)); + + Jwt jwt = JwtHelper.decode(token); + Map claims = JsonUtils.readValue(jwt.getClaims(), new TypeReference>(){}); + assertNotNull("Revocable claim must exist", claims.get(ClaimConstants.REVOCABLE)); + assertTrue("Token revocable claim must be set to true", (Boolean)claims.get(ClaimConstants.REVOCABLE)); + + RevocableToken revocableToken = getWebApplicationContext().getBean(RevocableTokenProvisioning.class).retrieve((String) claims.get(ClaimConstants.JTI)); + assertNotNull("Token should have been stored in the DB", revocableToken); + + + } + + + public Map testRevocablePasswordGrantTokenForDefaultZone(Map parameters) throws Exception { + String username = new RandomValueStringGenerator().generate()+"@test.org"; + String clientId = "testclient" + new RandomValueStringGenerator().generate(); + String scopes = "cloud_controller.read"; + setUpClients(clientId, scopes, scopes, "password,client_credentials", true, TEST_REDIRECT_URI, Arrays.asList(OriginKeys.UAA)); + setUpUser(username); + + MockHttpServletRequestBuilder post = post("/oauth/token") + .header("Authorization", "Basic " + new String(Base64.encode((clientId + ":" + SECRET).getBytes()))) + .param("username", username) + .param("password", "secret") + .param(OAuth2Utils.RESPONSE_TYPE, "token") + .param(OAuth2Utils.GRANT_TYPE, "password") + .param(OAuth2Utils.CLIENT_ID, clientId); + for (Map.Entry entry : parameters.entrySet()) { + post.param(entry.getKey(), entry.getValue()); + } + return JsonUtils.readValue( + getMockMvc().perform(post) + .andDo(print()) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), new TypeReference>() {}); + + } + + + private ScimUser setUpUser(String username) { ScimUser scimUser = new ScimUser(); scimUser.setUserName(username);