Skip to content

Commit

Permalink
Add in support for opaque tokens.
Browse files Browse the repository at this point in the history
id_tokens are never opaque, they are by definition in JWT format.
https://www.pivotaltracker.com/story/show/114126761
[#114126761]
  • Loading branch information
fhanik committed Apr 7, 2016
1 parent 97f3389 commit d31bd1e
Show file tree
Hide file tree
Showing 9 changed files with 572 additions and 196 deletions.
Expand Up @@ -51,4 +51,5 @@ public class ClaimConstants {
public static final String ROLES = "roles"; public static final String ROLES = "roles";
public static final String PROFILE = "profile"; public static final String PROFILE = "profile";
public static final String USER_ATTRIBUTES = "user_attributes"; public static final String USER_ATTRIBUTES = "user_attributes";
public static final String REVOCABLE = "revocable";
} }
Expand Up @@ -78,6 +78,8 @@ public class Claims {
private String profile; private String profile;
@JsonProperty(ClaimConstants.USER_ATTRIBUTES) @JsonProperty(ClaimConstants.USER_ATTRIBUTES)
private String userAttributes; private String userAttributes;
@JsonProperty(ClaimConstants.REVOCABLE)
private boolean revocable;


public String getUserId() { public String getUserId() {
return userId; return userId;
Expand Down Expand Up @@ -300,4 +302,11 @@ public String getUserAttributes() {
public void setUserAttributes(String userAttributes) { public void setUserAttributes(String userAttributes) {
this.userAttributes = userAttributes; this.userAttributes = userAttributes;
} }
public boolean isRevocable() {
return revocable;
}

public void setRevocable(boolean revocable) {
this.revocable = revocable;
}
} }
Expand Up @@ -42,4 +42,6 @@ public CompositeAccessToken(OAuth2AccessToken accessToken) {
super(accessToken); super(accessToken);
} }




} }
Expand Up @@ -16,7 +16,12 @@ public class RevocableToken {


public enum TokenType { public enum TokenType {
ID_TOKEN, ACCESS_TOKEN, REFRESH_TOKEN ID_TOKEN, ACCESS_TOKEN, REFRESH_TOKEN
}; }

public enum TokenFormat {
JWT, OPAQUE
}



private String tokenId; private String tokenId;
private String clientId; private String clientId;
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml
Expand Up @@ -376,6 +376,10 @@
<property name="refreshTokenValidity" value="${jwt.token.policy.global.refreshTokenValiditySeconds:2592000}" /> <property name="refreshTokenValidity" value="${jwt.token.policy.global.refreshTokenValiditySeconds:2592000}" />
</bean> </bean>


<bean id="revocableTokenProvisioning" class="org.cloudfoundry.identity.uaa.oauth.token.JdbcRevocableTokenProvisioning">
<constructor-arg index="0" ref="jdbcTemplate"/>
</bean>

<bean id="tokenServices" class="org.cloudfoundry.identity.uaa.oauth.UaaTokenServices"> <bean id="tokenServices" class="org.cloudfoundry.identity.uaa.oauth.UaaTokenServices">
<property name="clientDetailsService" ref="jdbcClientDetailsService" /> <property name="clientDetailsService" ref="jdbcClientDetailsService" />
<property name="userDatabase" ref="userDatabase" /> <property name="userDatabase" ref="userDatabase" />
Expand All @@ -384,6 +388,7 @@
<property name="approvalStore" ref="approvalStore" /> <property name="approvalStore" ref="approvalStore" />
<property name="tokenPolicy" ref="globalTokenPolicy" /> <property name="tokenPolicy" ref="globalTokenPolicy" />
<property name="excludedClaims" ref="excludedClaims"/> <property name="excludedClaims" ref="excludedClaims"/>
<property name="tokenProvisioning" ref="revocableTokenProvisioning"/>
</bean> </bean>


<bean id="excludedClaims" class="java.util.LinkedHashSet"> <bean id="excludedClaims" class="java.util.LinkedHashSet">
Expand Down
Expand Up @@ -15,7 +15,9 @@
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; 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.preprocessResponse;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; 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.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
Expand Down Expand Up @@ -70,7 +72,8 @@ public void checkToken() throws Exception {
fieldWithPath("auth_time").type(NUMBER).description("Only applicable for user tokens").optional(), fieldWithPath("auth_time").type(NUMBER).description("Only applicable for user tokens").optional(),
fieldWithPath("zid").description("Zone ID"), 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("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") getMockMvc().perform(post("/check_token")
Expand All @@ -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") 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)); ), requestParameters, responseFields));
} }

} }
Expand Up @@ -13,6 +13,7 @@
package org.cloudfoundry.identity.uaa.mock.token; package org.cloudfoundry.identity.uaa.mock.token;


import com.fasterxml.jackson.core.type.TypeReference; 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.UaaAuthentication;
import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails; import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails;
import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal;
Expand All @@ -24,7 +25,10 @@
import org.cloudfoundry.identity.uaa.oauth.UaaTokenServices; import org.cloudfoundry.identity.uaa.oauth.UaaTokenServices;
import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants;
import org.cloudfoundry.identity.uaa.oauth.jwt.Jwt; 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.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.IdentityProvider;
import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning;
import org.cloudfoundry.identity.uaa.provider.PasswordPolicy; import org.cloudfoundry.identity.uaa.provider.PasswordPolicy;
Expand Down Expand Up @@ -57,7 +61,6 @@
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.codec.Base64; 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.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.common.util.OAuth2Utils; import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
Expand Down Expand Up @@ -96,7 +99,9 @@
import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils; import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.utils;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.stringContainsInOrder; import static org.hamcrest.Matchers.stringContainsInOrder;
import static org.hamcrest.core.Is.is; import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.StringStartsWith.startsWith; import static org.hamcrest.core.StringStartsWith.startsWith;
Expand All @@ -109,6 +114,7 @@
import static org.junit.Assert.assertTrue; 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.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 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.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
Expand Down Expand Up @@ -2372,24 +2378,8 @@ public void testGetTokenScopesNotInAuthentication() throws Exception {


@Test @Test
public void testRevocablePasswordGrantTokenForDefaultZone() throws Exception { public void testRevocablePasswordGrantTokenForDefaultZone() throws Exception {
String username = new RandomValueStringGenerator().generate()+"@test.org";
String tokenKey = "access_token"; String tokenKey = "access_token";
String clientId = "testclient" + new RandomValueStringGenerator().generate(); Map<String,Object> tokenResponse = testRevocablePasswordGrantTokenForDefaultZone(new HashedMap());
String scopes = "cloud_controller.read";
setUpClients(clientId, scopes, scopes, "password,client_credentials", true, TEST_REDIRECT_URI, Arrays.asList(OriginKeys.UAA));
setUpUser(username);

Map<String,Object> 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<String, Object>>() {
});
assertNotNull("Token must be present", tokenResponse.get(tokenKey)); assertNotNull("Token must be present", tokenResponse.get(tokenKey));
assertTrue("Token must be a string", tokenResponse.get(tokenKey) instanceof String); assertTrue("Token must be a string", tokenResponse.get(tokenKey) instanceof String);
String token = (String)tokenResponse.get(tokenKey); String token = (String)tokenResponse.get(tokenKey);
Expand All @@ -2400,6 +2390,76 @@ public void testRevocablePasswordGrantTokenForDefaultZone() throws Exception {
assertTrue("Token revocation signature must have data", StringUtils.hasText((String) claims.get(ClaimConstants.REVOCATION_SIGNATURE))); assertTrue("Token revocation signature must have data", StringUtils.hasText((String) claims.get(ClaimConstants.REVOCATION_SIGNATURE)));
} }


@Test
public void testPasswordGrantTokenForDefaultZone_Opaque() throws Exception {
Map<String,String> parameters = new HashedMap();
parameters.put("token_format", "opaque");
String tokenKey = "access_token";
Map<String,Object> 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<String, Object> claims = JsonUtils.readValue(jwt.getClaims(), new TypeReference<Map<String, Object>>(){});
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<String,String> parameters = new HashedMap();
parameters.put("revocable", "true");
String tokenKey = "access_token";
Map<String,Object> 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<String, Object> claims = JsonUtils.readValue(jwt.getClaims(), new TypeReference<Map<String, Object>>(){});
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<String,Object> testRevocablePasswordGrantTokenForDefaultZone(Map<String, String> 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<String,String> 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<Map<String, Object>>() {});

}



private ScimUser setUpUser(String username) { private ScimUser setUpUser(String username) {
ScimUser scimUser = new ScimUser(); ScimUser scimUser = new ScimUser();
scimUser.setUserName(username); scimUser.setUserName(username);
Expand Down

0 comments on commit d31bd1e

Please sign in to comment.