Skip to content

Commit

Permalink
Document and implement user token revocation
Browse files Browse the repository at this point in the history
  • Loading branch information
fhanik committed Jan 22, 2016
1 parent 92579ad commit 3574c39
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 13 deletions.
43 changes: 43 additions & 0 deletions docs/UAA-APIs.rst
Expand Up @@ -656,6 +656,49 @@ Notes:

.. _oauth2 token endpoint:


OAuth2 Token Revocation Service/Client: ``GET /oauth/token/revoke/client/{client-id}``
--------------------------------------------------------------------------------------

An endpoint that allows all tokens for a specific client to be revoked
* Request: uses token authorization and requires `uaa.admin` scope::

GET /oauth/token/revoke/client/{client-id} HTTP/1.1
Host: server.example.com
Authorization: Bearer <uaa.admin> token

* Successful Response::

HTTP/1.1 200 OK

* Error Response::

HTTP/1.1 401 Unauthorized - Authentication is not sufficient
HTTP/1.1 403 Forbidden - Authenticated, but uaa.admin scope is not present
HTTP/1.1 404 Not Found - Client ID is invalid

OAuth2 Token Revocal Service/User: ``GET /oauth/token/revoke/user/{user-id}``
-----------------------------------------------------------------------------

An endpoint that allows all tokens for a specific user to be revoked
* Request: uses token authorization and requires `uaa.admin` scope::

GET /oauth/token/revoke/client/{client-id} HTTP/1.1
Host: server.example.com
Authorization: Bearer <uaa.admin> token

* Successful Response::

HTTP/1.1 200 OK

* Error Response::

HTTP/1.1 401 Unauthorized - Authentication is not sufficient
HTTP/1.1 403 Forbidden - Authenticated, but uaa.admin scope is not present
HTTP/1.1 404 Not Found - User ID is invalid



OpenID User Info Endpoint: ``GET /userinfo``
--------------------------------------------

Expand Down
Expand Up @@ -25,6 +25,7 @@
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.security.oauth2.provider.NoSuchClientException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
Expand All @@ -33,6 +34,8 @@
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import static org.springframework.http.HttpStatus.OK;

@Controller
public class TokenRevocationEndpoint {

Expand All @@ -48,24 +51,26 @@ public TokenRevocationEndpoint(MultitenantJdbcClientDetailsService clientDetails
}

@RequestMapping("/oauth/token/revoke/user/{userId}")
public void revokeTokensForUser(@PathVariable String userId) {
public ResponseEntity<Void> revokeTokensForUser(@PathVariable String userId) {
logger.debug("Revoking tokens for user: "+userId);
ScimUser user = userProvisioning.retrieve(userId);
user.setSalt(generator.generate());
userProvisioning.update(userId, user);
logger.debug("Tokens revoked for user: "+userId);
return new ResponseEntity<>(OK);
}

@RequestMapping("/oauth/token/revoke/user/{clientId}")
public void revokeTokensForClient(@PathVariable String clientId) {
@RequestMapping("/oauth/token/revoke/client/{clientId}")
public ResponseEntity<Void> revokeTokensForClient(@PathVariable String clientId) {
logger.debug("Revoking tokens for client: " + clientId);
BaseClientDetails client = (BaseClientDetails)clientDetailsService.loadClientByClientId(clientId);
client.addAdditionalInformation(ClientConstants.TOKEN_SALT,generator.generate());
clientDetailsService.updateClientDetails(client);
logger.debug("Tokens revoked for client: " + clientId);
return new ResponseEntity<>(OK);
}

@ExceptionHandler(ScimResourceNotFoundException.class)
@ExceptionHandler({ScimResourceNotFoundException.class, NoSuchClientException.class})
public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
InvalidTokenException e404 = new InvalidTokenException("Resource not found") {
Expand Down
19 changes: 19 additions & 0 deletions uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml
Expand Up @@ -41,6 +41,25 @@
<oauth:password authentication-manager-ref="compositeAuthenticationManager" />
</oauth:authorization-server>

<bean id="tokenRevocationEndpoint" class="org.cloudfoundry.identity.uaa.oauth.TokenRevocationEndpoint">
<constructor-arg name="clientDetailsService" ref="jdbcClientDetailsService"/>
<constructor-arg name="userProvisioning" ref="scimUserProvisioning"/>
</bean>

<http name="tokenRevocationFilter"
pattern="/oauth/token/revoke/**"
create-session="stateless"
authentication-manager-ref="emptyAuthenticationManager"
entry-point-ref="oauthAuthenticationEntryPoint"
xmlns="http://www.springframework.org/schema/security" use-expressions="true">
<intercept-url pattern="/oauth/token/revoke/client/**" access="#oauth2.hasScope('uaa.admin')" />
<intercept-url pattern="/oauth/token/revoke/user/**" access="#oauth2.hasScope('uaa.admin')" />
<custom-filter ref="resourceAgnosticAuthenticationFilter" position="PRE_AUTH_FILTER" />
<access-denied-handler ref="oauthAccessDeniedHandler" />
<expression-handler ref="oauthWebExpressionHandler" />
<csrf disabled="true"/>
</http>

<!-- Owner password flow for external authentication (SAML) -->
<!-- Pattern: /oauth/token parameters:{grant_type=password,passcode= -->
<http name="tokenEndpointSecurityForPasscodes" request-matcher-ref="passcodeTokenMatcher" create-session="stateless" use-expressions="false"
Expand Down
Expand Up @@ -16,16 +16,16 @@
import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication;
import org.cloudfoundry.identity.uaa.authentication.UaaAuthenticationDetails;
import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal;
import org.cloudfoundry.identity.uaa.oauth.UaaAuthorizationEndpoint;
import org.cloudfoundry.identity.uaa.constants.OriginKeys;
import org.cloudfoundry.identity.uaa.mock.InjectedMockContextTest;
import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils;
import org.cloudfoundry.identity.uaa.oauth.DisableIdTokenResponseTypeFilter;
import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants;
import org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants;
import org.cloudfoundry.identity.uaa.oauth.SignerProvider;
import org.cloudfoundry.identity.uaa.oauth.UaaAuthorizationEndpoint;
import org.cloudfoundry.identity.uaa.oauth.UaaTokenServices;
import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants;
import org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants;
import org.cloudfoundry.identity.uaa.provider.IdentityProvider;
import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning;
import org.cloudfoundry.identity.uaa.provider.PasswordPolicy;
import org.cloudfoundry.identity.uaa.provider.UaaIdentityProviderDefinition;
import org.cloudfoundry.identity.uaa.scim.ScimGroup;
Expand All @@ -42,7 +42,6 @@
import org.cloudfoundry.identity.uaa.user.UaaUserDatabase;
import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.cloudfoundry.identity.uaa.util.SetServerNameRequestPostProcessor;
import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning;
Expand Down Expand Up @@ -92,12 +91,10 @@
import java.util.TreeSet;
import java.util.UUID;

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.contains;
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.hasItem;
import static org.hamcrest.Matchers.isIn;
import static org.hamcrest.Matchers.stringContainsInOrder;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.StringStartsWith.startsWith;
Expand Down Expand Up @@ -495,7 +492,7 @@ public void test_Oauth_Authorize_API_Endpoint() throws Exception {
String userScopes = "";
setUpUser(username, userScopes, OriginKeys.UAA, IdentityZoneHolder.get().getId());

String cfAccessToken = MockMvcUtils.utils().getUserOAuthAccessToken(
String cfAccessToken = utils().getUserOAuthAccessToken(
getMockMvc(),
"cf",
"",
Expand Down Expand Up @@ -1878,6 +1875,123 @@ public void testGetClientCredentialsTokenForDefaultIdentityZone() throws Excepti
assertNotNull(claims.get(ClaimConstants.AZP));
}

@Test
public void test_Revoke_Client_And_User_Tokens() throws Exception {
String adminToken =
utils().getClientCredentialsOAuthAccessToken(
getMockMvc(),
"admin",
"adminsecret",
null,
null
);

BaseClientDetails client = new BaseClientDetails(
new RandomValueStringGenerator().generate(),
"",
"openid",
"client_credentials,password",
"clients.read");
client.setClientSecret("secret");

utils().createClient(getMockMvc(), adminToken, client);

//this is the token we will revoke
String readClientsToken =
utils().getClientCredentialsOAuthAccessToken(
getMockMvc(),
client.getClientId(),
client.getClientSecret(),
null,
null
);

//ensure our token works
getMockMvc().perform(
get("/oauth/clients")
.header("Authorization", "Bearer "+readClientsToken)
).andExpect(status().isOk());

//ensure we can't get to the endpoint without authentication
getMockMvc().perform(
get("/oauth/token/revoke/client/"+client.getClientId())
).andExpect(status().isUnauthorized());

//ensure we can't get to the endpoint without correct scope
getMockMvc().perform(
get("/oauth/token/revoke/client/"+client.getClientId())
.header("Authorization", "Bearer "+readClientsToken)
).andExpect(status().isForbidden());

//ensure that we have the correct error for invalid client id
getMockMvc().perform(
get("/oauth/token/revoke/client/notfound"+new RandomValueStringGenerator().generate())
.header("Authorization", "Bearer "+adminToken)
).andExpect(status().isNotFound());

//we revoke the tokens for that client
getMockMvc().perform(
get("/oauth/token/revoke/client/"+client.getClientId())
.header("Authorization", "Bearer "+adminToken)
).andExpect(status().isOk());

//we should fail attempting to use the token
getMockMvc().perform(
get("/oauth/clients")
.header("Authorization", "Bearer "+readClientsToken)
)
.andExpect(status().isUnauthorized())
.andExpect(content().string(containsString("\"error\":\"invalid_token\"")));


ScimUser user = new ScimUser(null,
new RandomValueStringGenerator().generate(),
"Given Name",
"Family Name");
user.setPrimaryEmail(user.getUserName()+"@test.org");
user.setPassword("password");

user = utils().createUser(getMockMvc(), adminToken, user);
user.setPassword("password");

String userInfoToken = utils().getUserOAuthAccessToken(
getMockMvc(),
client.getClientId(),
client.getClientSecret(),
user.getUserName(),
user.getPassword(),
"openid"
);

//ensure our token works
getMockMvc().perform(
get("/userinfo")
.header("Authorization", "Bearer "+userInfoToken)
).andExpect(status().isOk());

//we revoke the tokens for that user
getMockMvc().perform(
get("/oauth/token/revoke/user/"+user.getId()+"notfound")
.header("Authorization", "Bearer "+adminToken)
).andExpect(status().isNotFound());


//we revoke the tokens for that user
getMockMvc().perform(
get("/oauth/token/revoke/user/"+user.getId())
.header("Authorization", "Bearer "+adminToken)
).andExpect(status().isOk());

getMockMvc().perform(
get("/userinfo")
.header("Authorization", "Bearer "+userInfoToken)
)
.andExpect(status().isUnauthorized())
.andExpect(content().string(containsString("\"error\":\"invalid_token\"")));


}

@Test
public void testGetClientCredentials_WithAuthoritiesExcluded_ForDefaultIdentityZone() throws Exception {
Set<String> originalExclude = getWebApplicationContext().getBean(UaaTokenServices.class).getExcludedClaims();
Expand Down

0 comments on commit 3574c39

Please sign in to comment.