VYPR
Medium severity5.3NVD Advisory· Published Nov 27, 2017· Updated May 13, 2026

CVE-2017-8031

CVE-2017-8031

Description

An issue was discovered in Cloud Foundry Foundation cf-release (all versions prior to v279) and UAA (30.x versions prior to 30.6, 45.x versions prior to 45.4, 52.x versions prior to 52.1). In some cases, the UAA allows an authenticated user for a particular client to revoke client tokens for other users on the same client. This occurs only if the client is using opaque tokens or JWT tokens validated using the check_token endpoint. A malicious actor could cause denial of service.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.cloudfoundry.identity:cloudfoundry-identity-serverMaven
>= 4.6.0, < 4.7.14.7.1
org.cloudfoundry.identity:cloudfoundry-identity-serverMaven
>= 4.0.0, < 4.5.34.5.3
org.cloudfoundry.identity:cloudfoundry-identity-serverMaven
< 3.20.13.20.1

Affected products

3
  • cpe:2.3:a:cloudfoundry:cf-release:*:*:*:*:*:*:*:*
    Range: <=278
  • cpe:2.3:a:cloudfoundry:uaa-release:*:*:*:*:*:*:*:*+ 1 more
    • cpe:2.3:a:cloudfoundry:uaa-release:*:*:*:*:*:*:*:*range: >=30,<30.6
    • cpe:2.3:a:cloudfoundry:uaa-release:52:*:*:*:*:*:*:*

Patches

3
66166d17781a

Merge branch 'feature/token_self_revocation' into prerelease/4.7.x

https://github.com/cloudfoundry/uaaFilip HanikOct 23, 2017via ghsa
6 files changed · +197 6
  • server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/JdbcRevocableTokenProvisioning.java+1 1 modified
    @@ -55,7 +55,7 @@ public class JdbcRevocableTokenProvisioning implements RevocableTokenProvisionin
         protected AtomicLong lastExpiredCheck = new AtomicLong(0);
         protected long expirationCheckInterval = 30000; //30 seconds
     
    -    protected JdbcRevocableTokenProvisioning(JdbcTemplate jdbcTemplate) {
    +    public JdbcRevocableTokenProvisioning(JdbcTemplate jdbcTemplate) {
             this.rowMapper =  new RevocableTokenRowMapper();
             this.template = jdbcTemplate;
         }
    
  • server/src/main/java/org/cloudfoundry/identity/uaa/oauth/TokenRevocationEndpoint.java+5 2 modified
    @@ -16,6 +16,7 @@
     
     import org.apache.commons.logging.Log;
     import org.apache.commons.logging.LogFactory;
    +import org.cloudfoundry.identity.uaa.audit.event.SystemDeletable;
     import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal;
     import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants;
     import org.cloudfoundry.identity.uaa.oauth.token.RevocableToken;
    @@ -88,10 +89,12 @@ public ResponseEntity<Void> revokeTokensForUserAndClient(@PathVariable String us
         @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, IdentityZoneHolder.get().getId());
    +        String zoneId = IdentityZoneHolder.get().getId();
    +        BaseClientDetails client = (BaseClientDetails)clientDetailsService.loadClientByClientId(clientId, zoneId);
             client.addAdditionalInformation(ClientConstants.TOKEN_SALT,generator.generate());
    -        clientDetailsService.updateClientDetails(client, IdentityZoneHolder.get().getId());
    +        clientDetailsService.updateClientDetails(client, zoneId);
             logger.debug("Tokens revoked for client: " + clientId);
    +        ((SystemDeletable)tokenProvisioning).deleteByClient(clientId, zoneId);
             return new ResponseEntity<>(OK);
         }
     
    
  • server/src/test/java/org/cloudfoundry/identity/uaa/oauth/TokenRevocationEndpointTests.java+122 0 added
    @@ -0,0 +1,122 @@
    +/*
    + * ****************************************************************************
    + *     Cloud Foundry
    + *     Copyright (c) [2009-2017] Pivotal Software, Inc. All Rights Reserved.
    + *
    + *     This product is licensed to you under the Apache License, Version 2.0 (the "License").
    + *     You may not use this product except in compliance with the License.
    + *
    + *     This product includes a number of subcomponents with
    + *     separate copyright notices and license terms. Your use of these
    + *     subcomponents is subject to the terms and conditions of the
    + *     subcomponent's license, as noted in the LICENSE file.
    + * ****************************************************************************
    + */
    +
    +package org.cloudfoundry.identity.uaa.oauth;
    +
    +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.constants.OriginKeys;
    +import org.cloudfoundry.identity.uaa.oauth.token.JdbcRevocableTokenProvisioning;
    +import org.cloudfoundry.identity.uaa.oauth.token.RevocableToken;
    +import org.cloudfoundry.identity.uaa.resources.jdbc.JdbcPagingListFactory;
    +import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning;
    +import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimUserProvisioning;
    +import org.cloudfoundry.identity.uaa.test.JdbcTestBase;
    +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
    +import org.cloudfoundry.identity.uaa.zone.MultitenantJdbcClientDetailsService;
    +import org.junit.After;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.springframework.context.ApplicationEventPublisher;
    +import org.springframework.security.core.context.SecurityContextHolder;
    +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
    +import org.springframework.security.oauth2.provider.ClientDetails;
    +import org.springframework.security.oauth2.provider.OAuth2Request;
    +import org.springframework.security.oauth2.provider.client.BaseClientDetails;
    +
    +import java.util.Collections;
    +
    +import static org.cloudfoundry.identity.uaa.oauth.client.ClientConstants.TOKEN_SALT;
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertNotEquals;
    +import static org.mockito.Mockito.mock;
    +import static org.mockito.Mockito.spy;
    +
    +public class TokenRevocationEndpointTests extends JdbcTestBase {
    +
    +    private TokenRevocationEndpoint endpoint;
    +    private RandomValueStringGenerator generator;
    +    private BaseClientDetails client;
    +    private ApplicationEventPublisher publisher;
    +    private MultitenantJdbcClientDetailsService clientService;
    +
    +    @Before
    +    public void setupForTokenRevocation() throws Exception {
    +        String zoneId = IdentityZoneHolder.get().getId();
    +        generator = new RandomValueStringGenerator();
    +        String clientId = generator.generate().toLowerCase();
    +        client = new BaseClientDetails(clientId, "", "some.scopes", "client_credentials", "authorities");
    +        client.addAdditionalInformation(TOKEN_SALT, "pre-salt");
    +        clientService = spy(new MultitenantJdbcClientDetailsService(jdbcTemplate));
    +        clientService.addClientDetails(client, zoneId);
    +
    +        ScimUserProvisioning userProvisioning = new JdbcScimUserProvisioning(
    +            jdbcTemplate,
    +            new JdbcPagingListFactory(jdbcTemplate, limitSqlAdapter)
    +        );
    +        JdbcRevocableTokenProvisioning provisioning = spy(new JdbcRevocableTokenProvisioning(jdbcTemplate));
    +        endpoint = spy(new TokenRevocationEndpoint(clientService, userProvisioning, provisioning));
    +        publisher = mock(ApplicationEventPublisher.class);
    +
    +        SecurityContextHolder.getContext().setAuthentication(
    +            new UaaOauth2Authentication(
    +                "token-value",
    +                zoneId,
    +                mock(OAuth2Request.class),
    +                new UaaAuthentication(
    +                    new UaaPrincipal("id", "username", "username@test.com", OriginKeys.UAA, "", zoneId),
    +                    Collections.emptyList(),
    +                    mock(UaaAuthenticationDetails.class)
    +                )
    +            )
    +        );
    +
    +        provisioning.create(
    +            new RevocableToken()
    +                .setClientId(client.getClientId())
    +                .setTokenId("token-id")
    +                .setUserId(null)
    +                .setResponseType(RevocableToken.TokenType.ACCESS_TOKEN)
    +                .setValue("value")
    +                .setIssuedAt(System.currentTimeMillis()),
    +            zoneId
    +        );
    +    }
    +
    +    @After
    +    public void cleanup() throws Exception {
    +        SecurityContextHolder.clearContext();
    +        IdentityZoneHolder.clear();
    +    }
    +
    +    @Test
    +    public void revokeTokensForClient() throws Exception {
    +        assertEquals("pre-salt", getClient().getAdditionalInformation().get(TOKEN_SALT));
    +        assertEquals(1, clientTokenCount());
    +        endpoint.revokeTokensForClient(client.getClientId());
    +        assertNotEquals("pre-salt", getClient().getAdditionalInformation().get(TOKEN_SALT));
    +        assertEquals(0, clientTokenCount());
    +    }
    +
    +    public ClientDetails getClient() {
    +        return clientService.loadClientByClientId(client.getClientId());
    +    }
    +
    +    public int clientTokenCount() {
    +        return jdbcTemplate.queryForObject("select count(*) from revocable_tokens where client_id = ?", Integer.class, client.getClientId());
    +    }
    +
    +}
    \ No newline at end of file
    
  • uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml+1 1 modified
    @@ -63,7 +63,7 @@
                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') or @self.isClientTokenRevocationForSelf(request, 4)" />
    +        <intercept-url pattern="/oauth/token/revoke/client/**" access="#oauth2.hasScope('tokens.revoke')" />
             <intercept-url pattern="/oauth/token/revoke/user/**" access="#oauth2.hasScope('uaa.admin') or (#oauth2.hasScope('tokens.revoke') and @self.isUserTokenRevocationForSelf(request, 4))" />
             <intercept-url pattern="/oauth/token/revoke/**" access="#oauth2.hasScope('tokens.revoke') or @self.isTokenRevocationForSelf(request, 3)" method="DELETE"/>
             <intercept-url pattern="/**" access="denyAll" />
    
  • uaa/src/test/java/org/cloudfoundry/identity/uaa/login/TokenEndpointDocs.java+1 1 modified
    @@ -707,7 +707,7 @@ public void revokeAllTokens_forAClient() throws Exception {
                             true
                     );
             Snippet requestHeaders = requestHeaders(
    -            headerWithName("Authorization").description("Bearer token with uaa.admin or tokens.revoke scope. Any token with the matching client_id may also be used for self revocation."),
    +            headerWithName("Authorization").description("Bearer token with uaa.admin or tokens.revoke scope."),
                 IDENTITY_ZONE_ID_HEADER,
                 IDENTITY_ZONE_SUBDOMAIN_HEADER
             );
    
  • uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenRevocationEndpointTest.java+67 1 modified
    @@ -72,7 +72,7 @@ public void revokeOwnJWToken() throws Exception {
         }
     
         @Test
    -    public void revokeOtherClientToken() throws Exception {
    +    public void revokeOtherClientTokenByJti() throws Exception {
             String revokerClientId = generator.generate();
             String resourceClientId = generator.generate();
     
    @@ -128,6 +128,72 @@ public void revokeOtherClientToken() throws Exception {
             }
         }
     
    +    @Test
    +    public void revokeOtherClientTokenByClientId_tokensDotRevoke() throws Exception {
    +        revokeOtherClientTokenByClientId("tokens.revoke");
    +    }
    +
    +    @Test
    +    public void revokeOtherClientTokenByClientId_uaaDotAdmin() throws Exception {
    +        revokeOtherClientTokenByClientId("uaa.admin");
    +    }
    +
    +    public void revokeOtherClientTokenByClientId(String scope) throws Exception {
    +        String revokerClientId = generator.generate();
    +        String resourceClientId = generator.generate();
    +
    +        BaseClientDetails revokerClient =
    +            setUpClients(revokerClientId,
    +                         scope,
    +                         "openid",
    +                         "client_credentials,password",
    +                         true
    +            );
    +
    +
    +        BaseClientDetails targetClient =
    +            setUpClients(resourceClientId,
    +                         "uaa.none",
    +                         "openid",
    +                         "client_credentials,password",
    +                         true
    +            );
    +
    +
    +        //this is the token we will revoke
    +        String revokeAccessToken =
    +            getClientCredentialsOAuthAccessToken(
    +                getMockMvc(),
    +                revokerClient.getClientId(),
    +                SECRET,
    +                scope,
    +                null,
    +                false
    +            );
    +
    +        String tokenToBeRevoked =
    +            getClientCredentialsOAuthAccessToken(
    +                getMockMvc(),
    +                resourceClientId,
    +                SECRET,
    +                null,
    +                null,
    +                true
    +            );
    +
    +        getMockMvc().perform(delete("/oauth/token/revoke/client/" + resourceClientId)
    +                                 .header("Authorization", "Bearer " + revokeAccessToken))
    +            .andExpect(status().isOk());
    +
    +
    +        try {
    +            tokenProvisioning.retrieve(tokenToBeRevoked, IdentityZoneHolder.get().getId());
    +            fail("Token should have been deleted");
    +        } catch (EmptyResultDataAccessException e) {
    +            //expected
    +        }
    +    }
    +
         @Test
         public void revokeOtherClientTokenForbidden() throws Exception {
             String resourceClientId = generator.generate();
    
1e2a746968cd

Merge branch 'feature/token_self_revocation' into prerelease/3.20.x

https://github.com/cloudfoundry/uaaFilip HanikOct 23, 2017via ghsa
7 files changed · +229 30
  • server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/JdbcRevocableTokenProvisioning.java+7 1 modified
    @@ -46,6 +46,7 @@ public class JdbcRevocableTokenProvisioning implements RevocableTokenProvisionin
         protected final static String DELETE_QUERY = "DELETE FROM " + TABLE + " WHERE token_id=? and identity_zone_id=?";
         protected final static String DELETE_EXPIRED_QUERY = "DELETE FROM " + TABLE + " WHERE expires_at < ?";
         protected final static String DELETE_REFRESH_TOKEN_QUERY = "DELETE FROM " + TABLE + " WHERE user_id=? AND client_id=? AND response_type='" +REFRESH_TOKEN_RESPONSE_TYPE+ "' AND identity_zone_id=?";
    +    protected final static String DELETE_BY_CLIENT_QUERY = "DELETE FROM " + TABLE + " WHERE identity_zone_id=? and client_id=?";
         protected final static String DELETE_BY_ZONE_QUERY = "DELETE FROM " + TABLE + " WHERE identity_zone_id=?";
     
     
    @@ -56,7 +57,7 @@ public class JdbcRevocableTokenProvisioning implements RevocableTokenProvisionin
         protected AtomicLong lastExpiredCheck = new AtomicLong(0);
         protected long expirationCheckInterval = 30000; //30 seconds
     
    -    protected JdbcRevocableTokenProvisioning(JdbcTemplate jdbcTemplate) {
    +    public JdbcRevocableTokenProvisioning(JdbcTemplate jdbcTemplate) {
             this.rowMapper =  new RevocableTokenRowMapper();
             this.template = jdbcTemplate;
         }
    @@ -136,6 +137,11 @@ public RevocableToken delete(String id, int version) {
             return previous;
         }
     
    +    @Override
    +    public int deleteByClient(String clientId, String zoneId) {
    +        return template.update(DELETE_BY_CLIENT_QUERY, zoneId, clientId);
    +    }
    +
         @Override
         public int deleteByIdentityZone(String zoneId) {
             return template.update(DELETE_BY_ZONE_QUERY, IdentityZoneHolder.get().getId());
    
  • server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/RevocableTokenProvisioning.java+1 1 modified
    @@ -26,6 +26,6 @@ public interface RevocableTokenProvisioning extends ResourceManager<RevocableTok
     
         List<RevocableToken> getClientTokens(String clientId);
     
    -
    +    int deleteByClient(String clientId, String zoneId);
     
     }
    
  • server/src/main/java/org/cloudfoundry/identity/uaa/oauth/TokenRevocationEndpoint.java+3 0 modified
    @@ -23,6 +23,7 @@
     import org.cloudfoundry.identity.uaa.scim.ScimUser;
     import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning;
     import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceNotFoundException;
    +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
     import org.cloudfoundry.identity.uaa.zone.MultitenantJdbcClientDetailsService;
     import org.springframework.dao.EmptyResultDataAccessException;
     import org.springframework.http.ResponseEntity;
    @@ -75,10 +76,12 @@ public ResponseEntity<Void> revokeTokensForUser(@PathVariable String userId) {
         @RequestMapping("/oauth/token/revoke/client/{clientId}")
         public ResponseEntity<Void> revokeTokensForClient(@PathVariable String clientId) {
             logger.debug("Revoking tokens for client: " + clientId);
    +        String zoneId = IdentityZoneHolder.get().getId();
             BaseClientDetails client = (BaseClientDetails)clientDetailsService.loadClientByClientId(clientId);
             client.addAdditionalInformation(ClientConstants.TOKEN_SALT,generator.generate());
             clientDetailsService.updateClientDetails(client);
             logger.debug("Tokens revoked for client: " + clientId);
    +        tokenProvisioning.deleteByClient(clientId, zoneId);
             return new ResponseEntity<>(OK);
         }
     
    
  • server/src/test/java/org/cloudfoundry/identity/uaa/oauth/TokenRevocationEndpointTests.java+121 0 added
    @@ -0,0 +1,121 @@
    +/*
    + * ****************************************************************************
    + *     Cloud Foundry
    + *     Copyright (c) [2009-2017] Pivotal Software, Inc. All Rights Reserved.
    + *
    + *     This product is licensed to you under the Apache License, Version 2.0 (the "License").
    + *     You may not use this product except in compliance with the License.
    + *
    + *     This product includes a number of subcomponents with
    + *     separate copyright notices and license terms. Your use of these
    + *     subcomponents is subject to the terms and conditions of the
    + *     subcomponent's license, as noted in the LICENSE file.
    + * ****************************************************************************
    + */
    +
    +package org.cloudfoundry.identity.uaa.oauth;
    +
    +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.constants.OriginKeys;
    +import org.cloudfoundry.identity.uaa.oauth.token.JdbcRevocableTokenProvisioning;
    +import org.cloudfoundry.identity.uaa.oauth.token.RevocableToken;
    +import org.cloudfoundry.identity.uaa.resources.jdbc.JdbcPagingListFactory;
    +import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning;
    +import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimUserProvisioning;
    +import org.cloudfoundry.identity.uaa.test.JdbcTestBase;
    +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
    +import org.cloudfoundry.identity.uaa.zone.MultitenantJdbcClientDetailsService;
    +import org.junit.After;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.springframework.context.ApplicationEventPublisher;
    +import org.springframework.security.core.context.SecurityContextHolder;
    +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
    +import org.springframework.security.oauth2.provider.ClientDetails;
    +import org.springframework.security.oauth2.provider.OAuth2Request;
    +import org.springframework.security.oauth2.provider.client.BaseClientDetails;
    +
    +import java.util.Collections;
    +
    +import static org.cloudfoundry.identity.uaa.oauth.client.ClientConstants.TOKEN_SALT;
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertNotEquals;
    +import static org.mockito.Mockito.mock;
    +import static org.mockito.Mockito.spy;
    +
    +public class TokenRevocationEndpointTests extends JdbcTestBase {
    +
    +    private TokenRevocationEndpoint endpoint;
    +    private RandomValueStringGenerator generator;
    +    private BaseClientDetails client;
    +    private ApplicationEventPublisher publisher;
    +    private MultitenantJdbcClientDetailsService clientService;
    +
    +    @Before
    +    public void setupForTokenRevocation() throws Exception {
    +        String zoneId = IdentityZoneHolder.get().getId();
    +        generator = new RandomValueStringGenerator();
    +        String clientId = generator.generate().toLowerCase();
    +        client = new BaseClientDetails(clientId, "", "some.scopes", "client_credentials", "authorities");
    +        client.addAdditionalInformation(TOKEN_SALT, "pre-salt");
    +        clientService = spy(new MultitenantJdbcClientDetailsService(dataSource));
    +        clientService.addClientDetails(client);
    +
    +        ScimUserProvisioning userProvisioning = new JdbcScimUserProvisioning(
    +            jdbcTemplate,
    +            new JdbcPagingListFactory(jdbcTemplate, limitSqlAdapter)
    +        );
    +        JdbcRevocableTokenProvisioning provisioning = spy(new JdbcRevocableTokenProvisioning(jdbcTemplate));
    +        endpoint = spy(new TokenRevocationEndpoint(clientService, userProvisioning, provisioning));
    +        publisher = mock(ApplicationEventPublisher.class);
    +
    +        SecurityContextHolder.getContext().setAuthentication(
    +            new UaaOauth2Authentication(
    +                "token-value",
    +                zoneId,
    +                mock(OAuth2Request.class),
    +                new UaaAuthentication(
    +                    new UaaPrincipal("id", "username", "username@test.com", OriginKeys.UAA, "", zoneId),
    +                    Collections.emptyList(),
    +                    mock(UaaAuthenticationDetails.class)
    +                )
    +            )
    +        );
    +
    +        provisioning.create(
    +            new RevocableToken()
    +                .setClientId(client.getClientId())
    +                .setTokenId("token-id")
    +                .setUserId(null)
    +                .setResponseType(RevocableToken.TokenType.ACCESS_TOKEN)
    +                .setValue("value")
    +                .setIssuedAt(System.currentTimeMillis())
    +        );
    +    }
    +
    +    @After
    +    public void cleanup() throws Exception {
    +        SecurityContextHolder.clearContext();
    +        IdentityZoneHolder.clear();
    +    }
    +
    +    @Test
    +    public void revokeTokensForClient() throws Exception {
    +        assertEquals("pre-salt", getClient().getAdditionalInformation().get(TOKEN_SALT));
    +        assertEquals(1, clientTokenCount());
    +        endpoint.revokeTokensForClient(client.getClientId());
    +        assertNotEquals("pre-salt", getClient().getAdditionalInformation().get(TOKEN_SALT));
    +        assertEquals(0, clientTokenCount());
    +    }
    +
    +    public ClientDetails getClient() {
    +        return clientService.loadClientByClientId(client.getClientId());
    +    }
    +
    +    public int clientTokenCount() {
    +        return jdbcTemplate.queryForObject("select count(*) from revocable_tokens where client_id = ?", Integer.class, client.getClientId());
    +    }
    +
    +}
    \ No newline at end of file
    
  • uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml+1 1 modified
    @@ -69,7 +69,7 @@
                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') or @self.isClientTokenRevocationForSelf(request, 4)" />
    +        <intercept-url pattern="/oauth/token/revoke/client/**" access="#oauth2.hasScope('tokens.revoke')" />
             <intercept-url pattern="/oauth/token/revoke/user/**" access="#oauth2.hasScope('uaa.admin') or (#oauth2.hasScope('tokens.revoke') and @self.isUserTokenRevocationForSelf(request, 4))" />
             <intercept-url pattern="/oauth/token/revoke/**" access="#oauth2.hasScope('tokens.revoke') or @self.isTokenRevocationForSelf(request, 3)" method="DELETE"/>
             <intercept-url pattern="/**" access="denyAll" />
    
  • uaa/src/test/java/org/cloudfoundry/identity/uaa/login/TokenEndpointDocs.java+1 1 modified
    @@ -692,7 +692,7 @@ public void revokeAllTokens_forAClient() throws Exception {
                             true
                     );
             Snippet requestHeaders = requestHeaders(
    -            headerWithName("Authorization").description("Bearer token with uaa.admin or tokens.revoke scope. Any token with the matching client_id may also be used for self revocation."),
    +            headerWithName("Authorization").description("Bearer token with uaa.admin or tokens.revoke scope."),
                 IDENTITY_ZONE_ID_HEADER,
                 IDENTITY_ZONE_SUBDOMAIN_HEADER
             );
    
  • uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java+95 26 modified
    @@ -2948,50 +2948,119 @@ public void revokeOwnJWToken() throws Exception {
         }
     
         @Test
    -    public void revokeOtherClientToken() throws Exception {
    +    public void revokeOtherClientTokenByJti() throws Exception {
    +        String revokerClientId = generator.generate();
             String resourceClientId = generator.generate();
             BaseClientDetails resourceClient = new BaseClientDetails(
                     resourceClientId,
    -                "",
    +                "tokens.revoke",
                     "uaa.resource",
                     "client_credentials,password",
    -                "uaa.resource");
    +                "uaa.resource",
    +                "http://redirect.uri");
             resourceClient.setClientSecret("secret");
             createClient(getMockMvc(), adminToken, resourceClient);
     
    -        BaseClientDetails client = new BaseClientDetails(
    +        BaseClientDetails revokerClient = new BaseClientDetails(
                 generator.generate(),
    -            "",
    +            "uaa.none",
                 "openid",
                 "client_credentials,password",
    -            "tokens.revoke");
    -        client.setClientSecret("secret");
    -        createClient(getMockMvc(), adminToken, client);
    +            "tokens.revoke",
    +             "http://redirect.uri");
    +        revokerClient.setClientSecret("secret");
    +        createClient(getMockMvc(), adminToken, revokerClient);
     
             //this is the token we will revoke
             String revokeAccessToken =
    -                getClientCredentialsOAuthAccessToken(
    -                        getMockMvc(),
    -                        client.getClientId(),
    -                        client.getClientSecret(),
    -                        "tokens.revoke",
    -                        null,
    -                        false
    -                );
    +            getClientCredentialsOAuthAccessToken(
    +                getMockMvc(),
    +                revokerClient.getClientId(),
    +                SECRET,
    +                "tokens.revoke",
    +                null,
    +                false
    +            );
     
             String tokenToBeRevoked =
    -                getClientCredentialsOAuthAccessToken(
    -                        getMockMvc(),
    -                        resourceClientId,
    -                        resourceClient.getClientSecret(),
    -                        null,
    -                        null,
    -                        true
    -                );
    +            getClientCredentialsOAuthAccessToken(
    +                getMockMvc(),
    +                resourceClientId,
    +                SECRET,
    +                null,
    +                null,
    +                true
    +            );
     
             getMockMvc().perform(delete("/oauth/token/revoke/" + tokenToBeRevoked)
    -                .header("Authorization", "Bearer " + revokeAccessToken))
    -                .andExpect(status().isOk());
    +                                 .header("Authorization", "Bearer " + revokeAccessToken))
    +            .andExpect(status().isOk());
    +
    +
    +        try {
    +            tokenProvisioning.retrieve(tokenToBeRevoked);
    +            fail("Token should have been deleted");
    +        } catch (EmptyResultDataAccessException e) {
    +            //expected
    +        }
    +    }
    +
    +    @Test
    +    public void revokeOtherClientTokenByClientId_tokensDotRevoke() throws Exception {
    +        revokeOtherClientTokenByClientId("tokens.revoke");
    +    }
    +
    +    @Test
    +    public void revokeOtherClientTokenByClientId_uaaDotAdmin() throws Exception {
    +        revokeOtherClientTokenByClientId("uaa.admin");
    +    }
    +
    +    public void revokeOtherClientTokenByClientId(String scope) throws Exception {
    +        String revokerClientId = generator.generate();
    +        String resourceClientId = generator.generate();
    +
    +        BaseClientDetails revokerClient =
    +            setUpClients(revokerClientId,
    +                         scope,
    +                         "openid",
    +                         "client_credentials,password",
    +                         true
    +            );
    +
    +
    +        BaseClientDetails targetClient =
    +            setUpClients(resourceClientId,
    +                         "uaa.none",
    +                         "openid",
    +                         "client_credentials,password",
    +                         true
    +            );
    +
    +
    +        //this is the token we will revoke
    +        String revokeAccessToken =
    +            getClientCredentialsOAuthAccessToken(
    +                getMockMvc(),
    +                revokerClient.getClientId(),
    +                SECRET,
    +                scope,
    +                null,
    +                false
    +            );
    +
    +        String tokenToBeRevoked =
    +            getClientCredentialsOAuthAccessToken(
    +                getMockMvc(),
    +                resourceClientId,
    +                SECRET,
    +                null,
    +                null,
    +                true
    +            );
    +
    +        getMockMvc().perform(delete("/oauth/token/revoke/client/" + resourceClientId)
    +                                 .header("Authorization", "Bearer " + revokeAccessToken))
    +            .andExpect(status().isOk());
     
     
             try {
    
20808046de8b

Merge branch 'feature/token_self_revocation' into prerelease/4.5.x

https://github.com/cloudfoundry/uaaFilip HanikOct 23, 2017via ghsa
6 files changed · +233 42
  • server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/JdbcRevocableTokenProvisioning.java+1 1 modified
    @@ -55,7 +55,7 @@ public class JdbcRevocableTokenProvisioning implements RevocableTokenProvisionin
         protected AtomicLong lastExpiredCheck = new AtomicLong(0);
         protected long expirationCheckInterval = 30000; //30 seconds
     
    -    protected JdbcRevocableTokenProvisioning(JdbcTemplate jdbcTemplate) {
    +    public JdbcRevocableTokenProvisioning(JdbcTemplate jdbcTemplate) {
             this.rowMapper =  new RevocableTokenRowMapper();
             this.template = jdbcTemplate;
         }
    
  • server/src/main/java/org/cloudfoundry/identity/uaa/oauth/TokenRevocationEndpoint.java+5 2 modified
    @@ -16,6 +16,7 @@
     
     import org.apache.commons.logging.Log;
     import org.apache.commons.logging.LogFactory;
    +import org.cloudfoundry.identity.uaa.audit.event.SystemDeletable;
     import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal;
     import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants;
     import org.cloudfoundry.identity.uaa.oauth.token.RevocableToken;
    @@ -76,10 +77,12 @@ public ResponseEntity<Void> revokeTokensForUser(@PathVariable String userId) {
         @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, IdentityZoneHolder.get().getId());
    +        String zoneId = IdentityZoneHolder.get().getId();
    +        BaseClientDetails client = (BaseClientDetails)clientDetailsService.loadClientByClientId(clientId, zoneId);
             client.addAdditionalInformation(ClientConstants.TOKEN_SALT,generator.generate());
    -        clientDetailsService.updateClientDetails(client, IdentityZoneHolder.get().getId());
    +        clientDetailsService.updateClientDetails(client, zoneId);
             logger.debug("Tokens revoked for client: " + clientId);
    +        ((SystemDeletable)tokenProvisioning).deleteByClient(clientId, zoneId);
             return new ResponseEntity<>(OK);
         }
     
    
  • server/src/test/java/org/cloudfoundry/identity/uaa/oauth/TokenRevocationEndpointTests.java+122 0 added
    @@ -0,0 +1,122 @@
    +/*
    + * ****************************************************************************
    + *     Cloud Foundry
    + *     Copyright (c) [2009-2017] Pivotal Software, Inc. All Rights Reserved.
    + *
    + *     This product is licensed to you under the Apache License, Version 2.0 (the "License").
    + *     You may not use this product except in compliance with the License.
    + *
    + *     This product includes a number of subcomponents with
    + *     separate copyright notices and license terms. Your use of these
    + *     subcomponents is subject to the terms and conditions of the
    + *     subcomponent's license, as noted in the LICENSE file.
    + * ****************************************************************************
    + */
    +
    +package org.cloudfoundry.identity.uaa.oauth;
    +
    +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.constants.OriginKeys;
    +import org.cloudfoundry.identity.uaa.oauth.token.JdbcRevocableTokenProvisioning;
    +import org.cloudfoundry.identity.uaa.oauth.token.RevocableToken;
    +import org.cloudfoundry.identity.uaa.resources.jdbc.JdbcPagingListFactory;
    +import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning;
    +import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimUserProvisioning;
    +import org.cloudfoundry.identity.uaa.test.JdbcTestBase;
    +import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
    +import org.cloudfoundry.identity.uaa.zone.MultitenantJdbcClientDetailsService;
    +import org.junit.After;
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.springframework.context.ApplicationEventPublisher;
    +import org.springframework.security.core.context.SecurityContextHolder;
    +import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
    +import org.springframework.security.oauth2.provider.ClientDetails;
    +import org.springframework.security.oauth2.provider.OAuth2Request;
    +import org.springframework.security.oauth2.provider.client.BaseClientDetails;
    +
    +import java.util.Collections;
    +
    +import static org.cloudfoundry.identity.uaa.oauth.client.ClientConstants.TOKEN_SALT;
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertNotEquals;
    +import static org.mockito.Mockito.mock;
    +import static org.mockito.Mockito.spy;
    +
    +public class TokenRevocationEndpointTests extends JdbcTestBase {
    +
    +    private TokenRevocationEndpoint endpoint;
    +    private RandomValueStringGenerator generator;
    +    private BaseClientDetails client;
    +    private ApplicationEventPublisher publisher;
    +    private MultitenantJdbcClientDetailsService clientService;
    +
    +    @Before
    +    public void setupForTokenRevocation() throws Exception {
    +        String zoneId = IdentityZoneHolder.get().getId();
    +        generator = new RandomValueStringGenerator();
    +        String clientId = generator.generate().toLowerCase();
    +        client = new BaseClientDetails(clientId, "", "some.scopes", "client_credentials", "authorities");
    +        client.addAdditionalInformation(TOKEN_SALT, "pre-salt");
    +        clientService = spy(new MultitenantJdbcClientDetailsService(jdbcTemplate));
    +        clientService.addClientDetails(client, zoneId);
    +
    +        ScimUserProvisioning userProvisioning = new JdbcScimUserProvisioning(
    +            jdbcTemplate,
    +            new JdbcPagingListFactory(jdbcTemplate, limitSqlAdapter)
    +        );
    +        JdbcRevocableTokenProvisioning provisioning = spy(new JdbcRevocableTokenProvisioning(jdbcTemplate));
    +        endpoint = spy(new TokenRevocationEndpoint(clientService, userProvisioning, provisioning));
    +        publisher = mock(ApplicationEventPublisher.class);
    +
    +        SecurityContextHolder.getContext().setAuthentication(
    +            new UaaOauth2Authentication(
    +                "token-value",
    +                zoneId,
    +                mock(OAuth2Request.class),
    +                new UaaAuthentication(
    +                    new UaaPrincipal("id", "username", "username@test.com", OriginKeys.UAA, "", zoneId),
    +                    Collections.emptyList(),
    +                    mock(UaaAuthenticationDetails.class)
    +                )
    +            )
    +        );
    +
    +        provisioning.create(
    +            new RevocableToken()
    +                .setClientId(client.getClientId())
    +                .setTokenId("token-id")
    +                .setUserId(null)
    +                .setResponseType(RevocableToken.TokenType.ACCESS_TOKEN)
    +                .setValue("value")
    +                .setIssuedAt(System.currentTimeMillis()),
    +            zoneId
    +        );
    +    }
    +
    +    @After
    +    public void cleanup() throws Exception {
    +        SecurityContextHolder.clearContext();
    +        IdentityZoneHolder.clear();
    +    }
    +
    +    @Test
    +    public void revokeTokensForClient() throws Exception {
    +        assertEquals("pre-salt", getClient().getAdditionalInformation().get(TOKEN_SALT));
    +        assertEquals(1, clientTokenCount());
    +        endpoint.revokeTokensForClient(client.getClientId());
    +        assertNotEquals("pre-salt", getClient().getAdditionalInformation().get(TOKEN_SALT));
    +        assertEquals(0, clientTokenCount());
    +    }
    +
    +    public ClientDetails getClient() {
    +        return clientService.loadClientByClientId(client.getClientId());
    +    }
    +
    +    public int clientTokenCount() {
    +        return jdbcTemplate.queryForObject("select count(*) from revocable_tokens where client_id = ?", Integer.class, client.getClientId());
    +    }
    +
    +}
    \ No newline at end of file
    
  • uaa/src/main/webapp/WEB-INF/spring/oauth-endpoints.xml+1 1 modified
    @@ -63,7 +63,7 @@
                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') or @self.isClientTokenRevocationForSelf(request, 4)" />
    +        <intercept-url pattern="/oauth/token/revoke/client/**" access="#oauth2.hasScope('tokens.revoke')" />
             <intercept-url pattern="/oauth/token/revoke/user/**" access="#oauth2.hasScope('uaa.admin') or (#oauth2.hasScope('tokens.revoke') and @self.isUserTokenRevocationForSelf(request, 4))" />
             <intercept-url pattern="/oauth/token/revoke/**" access="#oauth2.hasScope('tokens.revoke') or @self.isTokenRevocationForSelf(request, 3)" method="DELETE"/>
             <intercept-url pattern="/**" access="denyAll" />
    
  • uaa/src/test/java/org/cloudfoundry/identity/uaa/login/TokenEndpointDocs.java+1 1 modified
    @@ -705,7 +705,7 @@ public void revokeAllTokens_forAClient() throws Exception {
                             true
                     );
             Snippet requestHeaders = requestHeaders(
    -            headerWithName("Authorization").description("Bearer token with uaa.admin or tokens.revoke scope. Any token with the matching client_id may also be used for self revocation."),
    +            headerWithName("Authorization").description("Bearer token with uaa.admin or tokens.revoke scope."),
                 IDENTITY_ZONE_ID_HEADER,
                 IDENTITY_ZONE_SUBDOMAIN_HEADER
             );
    
  • uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenMvcMockTests.java+103 37 modified
    @@ -3411,52 +3411,118 @@ public void revokeOwnJWToken() throws Exception {
         }
     
         @Test
    -    public void revokeOtherClientToken() throws Exception {
    +    public void revokeOtherClientTokenByJti() throws Exception {
    +        String revokerClientId = generator.generate();
             String resourceClientId = generator.generate();
    -        BaseClientDetails resourceClient = new BaseClientDetails(
    -                resourceClientId,
    -                "",
    -                "uaa.resource",
    -                "client_credentials,password",
    -                "uaa.resource",
    -                "http://redirect.uri");
    -        resourceClient.setClientSecret("secret");
    -        createClient(getMockMvc(), adminToken, resourceClient);
     
    -        BaseClientDetails client = new BaseClientDetails(
    -            generator.generate(),
    -            "",
    -            "openid",
    -            "client_credentials,password",
    -            "tokens.revoke",
    -             "http://redirect.uri");
    -        client.setClientSecret("secret");
    -        createClient(getMockMvc(), adminToken, client);
    +        BaseClientDetails revokerClient =
    +            setUpClients(revokerClientId,
    +                         "tokens.revoke",
    +                         "openid",
    +                         "client_credentials,password",
    +                         true
    +            );
    +
    +
    +        BaseClientDetails targetClient =
    +            setUpClients(resourceClientId,
    +                         "uaa.none",
    +                         "openid",
    +                         "client_credentials,password",
    +                         true
    +            );
    +
     
             //this is the token we will revoke
             String revokeAccessToken =
    -                getClientCredentialsOAuthAccessToken(
    -                        getMockMvc(),
    -                        client.getClientId(),
    -                        client.getClientSecret(),
    -                        "tokens.revoke",
    -                        null,
    -                        false
    -                );
    +            getClientCredentialsOAuthAccessToken(
    +                getMockMvc(),
    +                revokerClient.getClientId(),
    +                SECRET,
    +                "tokens.revoke",
    +                null,
    +                false
    +            );
     
             String tokenToBeRevoked =
    -                getClientCredentialsOAuthAccessToken(
    -                        getMockMvc(),
    -                        resourceClientId,
    -                        resourceClient.getClientSecret(),
    -                        null,
    -                        null,
    -                        true
    -                );
    +            getClientCredentialsOAuthAccessToken(
    +                getMockMvc(),
    +                resourceClientId,
    +                SECRET,
    +                null,
    +                null,
    +                true
    +            );
     
             getMockMvc().perform(delete("/oauth/token/revoke/" + tokenToBeRevoked)
    -                .header("Authorization", "Bearer " + revokeAccessToken))
    -                .andExpect(status().isOk());
    +                                 .header("Authorization", "Bearer " + revokeAccessToken))
    +            .andExpect(status().isOk());
    +
    +
    +        try {
    +            tokenProvisioning.retrieve(tokenToBeRevoked, IdentityZoneHolder.get().getId());
    +            fail("Token should have been deleted");
    +        } catch (EmptyResultDataAccessException e) {
    +            //expected
    +        }
    +    }
    +
    +    @Test
    +    public void revokeOtherClientTokenByClientId_tokensDotRevoke() throws Exception {
    +        revokeOtherClientTokenByClientId("tokens.revoke");
    +    }
    +
    +    @Test
    +    public void revokeOtherClientTokenByClientId_uaaDotAdmin() throws Exception {
    +        revokeOtherClientTokenByClientId("uaa.admin");
    +    }
    +
    +    public void revokeOtherClientTokenByClientId(String scope) throws Exception {
    +        String revokerClientId = generator.generate();
    +        String resourceClientId = generator.generate();
    +
    +        BaseClientDetails revokerClient =
    +            setUpClients(revokerClientId,
    +                         scope,
    +                         "openid",
    +                         "client_credentials,password",
    +                         true
    +            );
    +
    +
    +        BaseClientDetails targetClient =
    +            setUpClients(resourceClientId,
    +                         "uaa.none",
    +                         "openid",
    +                         "client_credentials,password",
    +                         true
    +            );
    +
    +
    +        //this is the token we will revoke
    +        String revokeAccessToken =
    +            getClientCredentialsOAuthAccessToken(
    +                getMockMvc(),
    +                revokerClient.getClientId(),
    +                SECRET,
    +                scope,
    +                null,
    +                false
    +            );
    +
    +        String tokenToBeRevoked =
    +            getClientCredentialsOAuthAccessToken(
    +                getMockMvc(),
    +                resourceClientId,
    +                SECRET,
    +                null,
    +                null,
    +                true
    +            );
    +
    +        getMockMvc().perform(delete("/oauth/token/revoke/client/" + resourceClientId)
    +                                 .header("Authorization", "Bearer " + revokeAccessToken))
    +            .andExpect(status().isOk());
     
     
             try {
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

12

News mentions

0

No linked articles in our index yet.