You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by co...@apache.org on 2026/07/28 16:07:07 UTC

(cxf) branch coheigea/oauth2-default-encrypting created (now 2e2895f9b65)

This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a change to branch coheigea/oauth2-default-encrypting
in repository https://gitbox.apache.org/repos/asf/cxf.git


      at 2e2895f9b65 Check if tokens have been revoked first in DefaultEncryptingOAuthDataProvider

This branch includes the following new commits:

     new 2e2895f9b65 Check if tokens have been revoked first in DefaultEncryptingOAuthDataProvider

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(cxf) 01/01: Check if tokens have been revoked first in DefaultEncryptingOAuthDataProvider

Posted by co...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch coheigea/oauth2-default-encrypting
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit 2e2895f9b65e19af3af2e25f930812386951b14e
Author: Colm O hEigeartaigh <co...@apache.org>
AuthorDate: Tue Jul 28 17:06:45 2026 +0100

    Check if tokens have been revoked first in DefaultEncryptingOAuthDataProvider
---
 .../DefaultEncryptingOAuthDataProvider.java        |   8 +
 .../DefaultEncryptingOAuthDataProviderTest.java    | 267 +++++++++++++++++++++
 2 files changed, 275 insertions(+)

diff --git a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/DefaultEncryptingOAuthDataProvider.java b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/DefaultEncryptingOAuthDataProvider.java
index 90b9aaf7358..946947fdd10 100644
--- a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/DefaultEncryptingOAuthDataProvider.java
+++ b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/provider/DefaultEncryptingOAuthDataProvider.java
@@ -100,6 +100,10 @@ public class DefaultEncryptingOAuthDataProvider extends AbstractOAuthDataProvide
     }
     @Override
     public ServerAccessToken getAccessToken(String accessToken) throws OAuthServiceException {
+        // Check if token has been revoked (RFC 7009 §2.1 compliance)
+        if (!tokens.contains(accessToken)) {
+            return null;
+        }
         try {
             return ModelEncryptionSupport.decryptAccessToken(this, accessToken, key);
         } catch (SecurityException ex) {
@@ -136,6 +140,10 @@ public class DefaultEncryptingOAuthDataProvider extends AbstractOAuthDataProvide
     }
     @Override
     protected RefreshToken getRefreshToken(String refreshTokenKey) {
+        // Check if token has been revoked (RFC 7009 §2.1 compliance)
+        if (!refreshTokens.contains(refreshTokenKey)) {
+            return null;
+        }
         try {
             return ModelEncryptionSupport.decryptRefreshToken(this, refreshTokenKey, key);
         } catch (SecurityException ex) {
diff --git a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/DefaultEncryptingOAuthDataProviderTest.java b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/DefaultEncryptingOAuthDataProviderTest.java
new file mode 100644
index 00000000000..24025b1fa85
--- /dev/null
+++ b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/provider/DefaultEncryptingOAuthDataProviderTest.java
@@ -0,0 +1,267 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cxf.rs.security.oauth2.provider;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.apache.cxf.rs.security.oauth2.common.AccessTokenRegistration;
+import org.apache.cxf.rs.security.oauth2.common.Client;
+import org.apache.cxf.rs.security.oauth2.common.ServerAccessToken;
+import org.apache.cxf.rs.security.oauth2.common.UserSubject;
+import org.apache.cxf.rs.security.oauth2.tokens.refresh.RefreshToken;
+import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
+import org.apache.cxf.rt.security.crypto.KeyProperties;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+
+public class DefaultEncryptingOAuthDataProviderTest {
+    private DefaultEncryptingOAuthDataProvider provider;
+
+    @Before
+    public void setUp() throws Exception {
+        // Initialize provider with AES encryption (128-bit key)
+        provider = new DefaultEncryptingOAuthDataProvider(new KeyProperties("AES", 128));
+        // Register supported scopes
+        provider.setSupportedScopes(Collections.singletonMap("read", "Read Scope"));
+        provider.setSupportedScopes(Collections.singletonMap("refreshToken", "RefreshToken"));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (provider != null) {
+            provider.close();
+        }
+    }
+
+    private Client createTestClient(String clientId, String userLogin) {
+        Client c = new Client();
+        c.setRedirectUris(Collections.singletonList("http://client/redirect"));
+        c.setClientId(clientId);
+        c.setClientSecret("secret");
+        c.setResourceOwnerSubject(new UserSubject(userLogin));
+        provider.setClient(c);
+        return c;
+    }
+
+    /**
+     * RFC 7009 §2.1 compliance test: Revoked access tokens must become inaccessible.
+     * 
+     * Scenario:
+     * 1. Create an access token
+     * 2. Verify it is accessible via getAccessToken()
+     * 3. Revoke the token
+     * 4. Verify it is NO LONGER accessible (returns null)
+     * 
+     * This test ensures the fix for the revocation bypass vulnerability
+     * where getAccessToken() was not checking the revoked tokens set.
+     */
+    @Test
+    public void testRevokedAccessTokenReturnsNull() {
+        Client c = createTestClient("test1", "alice");
+        
+        AccessTokenRegistration atr = new AccessTokenRegistration();
+        atr.setClient(c);
+        atr.setApprovedScope(Collections.singletonList("read"));
+        atr.setSubject(c.getResourceOwnerSubject());
+        
+        // Step 1: Create and verify token is accessible
+        ServerAccessToken token = provider.createAccessToken(atr);
+        assertNotNull("Token should be accessible immediately after creation",
+                      provider.getAccessToken(token.getTokenKey()));
+        
+        // Step 2: Revoke the token
+        provider.revokeToken(c, token.getTokenKey(), OAuthConstants.ACCESS_TOKEN);
+        
+        // Step 3: CRITICAL: Verify revoked token is no longer accessible
+        // Before the fix, this would return the decrypted token (VULNERABILITY)
+        // After the fix, this must return null (RFC 7009 §2.1 compliance)
+        assertNull("Revoked access token must return null (RFC 7009 §2.1)",
+                   provider.getAccessToken(token.getTokenKey()));
+    }
+
+    /**
+     * RFC 7009 §2.1 and RFC 7662 §2.2 compliance test: Revoked refresh tokens
+     * must become inaccessible.
+     * 
+     * Scenario:
+     * 1. Create an access token with refresh token capability
+     * 2. Verify refresh token is accessible
+     * 3. Revoke the refresh token
+     * 4. Verify it is NO LONGER accessible (returns null)
+     * 
+     * This test prevents the attack where a revoked refresh token
+     * could mint new access tokens.
+     */
+    @Test
+    public void testRevokedRefreshTokenReturnsNull() {
+        Client c = createTestClient("test2", "bob");
+        
+        AccessTokenRegistration atr = new AccessTokenRegistration();
+        atr.setClient(c);
+        atr.setApprovedScope(Arrays.asList("read", "refreshToken"));
+        atr.setSubject(c.getResourceOwnerSubject());
+        
+        // Step 1: Create access token with refresh token
+        ServerAccessToken at = provider.createAccessToken(atr);
+        String refreshTokenKey = at.getRefreshToken();
+        
+        // Step 2: Verify refresh token is accessible before revocation
+        RefreshToken rt = provider.getRefreshToken(refreshTokenKey);
+        assertNotNull("Refresh token should be accessible immediately after creation", rt);
+        
+        // Step 3: Revoke the refresh token
+        provider.revokeToken(c, refreshTokenKey, OAuthConstants.REFRESH_TOKEN);
+        
+        // Step 4: CRITICAL: Verify revoked refresh token is no longer accessible
+        // Before the fix, this would return the decrypted token (VULNERABILITY)
+        // After the fix, this must return null (RFC 7009 §2.1 compliance)
+        assertNull("Revoked refresh token must return null (RFC 7009 §2.1)",
+                   provider.getRefreshToken(refreshTokenKey));
+    }
+
+    /**
+     * Security test: Verify multiple tokens can be managed independently.
+     * 
+     * Scenario:
+     * 1. Create two access tokens for the same client
+     * 2. Revoke only the first token
+     * 3. Verify first token is null (revoked)
+     * 4. Verify second token is still accessible (not revoked)
+     * 
+     * This test ensures the revocation mechanism is token-specific
+     * and doesn't inadvertently revoke other tokens.
+     */
+    @Test
+    public void testSelectiveAccessTokenRevocation() {
+        Client c = createTestClient("test3", "charlie");
+        
+        // Create first token
+        AccessTokenRegistration atr1 = new AccessTokenRegistration();
+        atr1.setClient(c);
+        atr1.setApprovedScope(Collections.singletonList("read"));
+        atr1.setSubject(c.getResourceOwnerSubject());
+        ServerAccessToken token1 = provider.createAccessToken(atr1);
+        
+        // Create second token
+        AccessTokenRegistration atr2 = new AccessTokenRegistration();
+        atr2.setClient(c);
+        atr2.setApprovedScope(Collections.singletonList("read"));
+        atr2.setSubject(c.getResourceOwnerSubject());
+        ServerAccessToken token2 = provider.createAccessToken(atr2);
+        
+        // Verify both are initially accessible
+        assertNotNull("Token 1 should be accessible", 
+                      provider.getAccessToken(token1.getTokenKey()));
+        assertNotNull("Token 2 should be accessible", 
+                      provider.getAccessToken(token2.getTokenKey()));
+        
+        // Revoke only token 1
+        provider.revokeToken(c, token1.getTokenKey(), OAuthConstants.ACCESS_TOKEN);
+        
+        // Verify only token 1 is revoked
+        assertNull("Token 1 must be revoked", 
+                   provider.getAccessToken(token1.getTokenKey()));
+        assertNotNull("Token 2 must still be accessible after revoking Token 1", 
+                      provider.getAccessToken(token2.getTokenKey()));
+    }
+
+    /**
+     * RFC 7662 §2.2 compliance test: Token introspection consistency.
+     * 
+     * Scenario:
+     * 1. Create an access token
+     * 2. Revoke it
+     * 3. Attempt to retrieve it (simulates introspection)
+     * 4. Verify null is returned (service layer converts to active:false)
+     * 
+     * This ensures that TokenIntrospectionService will correctly report
+     * active:false for revoked tokens by returning null.
+     */
+    @Test
+    public void testIntrospectionReportRevocationCorrectly() {
+        Client c = createTestClient("test6", "frank");
+        
+        AccessTokenRegistration atr = new AccessTokenRegistration();
+        atr.setClient(c);
+        atr.setApprovedScope(Collections.singletonList("read"));
+        atr.setSubject(c.getResourceOwnerSubject());
+        
+        ServerAccessToken token = provider.createAccessToken(atr);
+        String tokenKey = token.getTokenKey();
+        
+        // Before revocation: introspection should find the token
+        ServerAccessToken beforeRevoke = provider.getAccessToken(tokenKey);
+        assertNotNull("Token should be found before revocation (introspection returns details)", 
+                      beforeRevoke);
+        
+        // Revoke the token
+        provider.revokeToken(c, tokenKey, OAuthConstants.ACCESS_TOKEN);
+        
+        // After revocation: introspection should return null
+        // (higher-level code like TokenIntrospectionService converts this to active:false)
+        ServerAccessToken afterRevoke = provider.getAccessToken(tokenKey);
+        assertNull("Token should return null after revocation (introspection reports inactive)", 
+                   afterRevoke);
+    }
+
+    /**
+     * Security test: Verify that refresh tokens cannot be reused after revocation.
+     * 
+     * Scenario:
+     * 1. Create an access token with refresh token
+     * 2. Use the refresh token to create a new access token (verify it works)
+     * 3. Revoke the refresh token
+     * 4. Attempt to use the refresh token again (verify it fails)
+     * 
+     * This test ensures refresh token revocation prevents any further use
+     * of that token for minting new access tokens.
+     */
+    @Test
+    public void testRevokedRefreshTokenCannotMintNewTokens() {
+        Client c = createTestClient("test5", "eve");
+        
+        AccessTokenRegistration atr = new AccessTokenRegistration();
+        atr.setClient(c);
+        atr.setApprovedScope(Arrays.asList("read", "refreshToken"));
+        atr.setSubject(c.getResourceOwnerSubject());
+        
+        // Create initial access token with refresh token
+        ServerAccessToken initialToken = provider.createAccessToken(atr);
+        String refreshTokenKey = initialToken.getRefreshToken();
+        
+        // Verify refresh token can be retrieved before revocation
+        RefreshToken rt = provider.getRefreshToken(refreshTokenKey);
+        assertNotNull("Refresh token should be accessible before revocation", rt);
+        
+        // Revoke the refresh token
+        provider.revokeToken(c, refreshTokenKey, OAuthConstants.REFRESH_TOKEN);
+        
+        // CRITICAL: Verify refresh token cannot be accessed after revocation
+        // This prevents using the revoked refresh token to mint new access tokens
+        assertNull("Revoked refresh token must return null, preventing token minting", 
+                   provider.getRefreshToken(refreshTokenKey));
+    }
+}