@@ -1419,6 +1419,8 @@ describe('OAuthProvider', () => {
1419
1419
it ( 'should handle callback that returns only tokenProps or only grantProps' , async ( ) => {
1420
1420
// Create a provider with a callback that returns only tokenProps for auth code
1421
1421
// and only grantProps for refresh token
1422
+ // Note: With the enhanced implementation, when only grantProps is returned
1423
+ // without tokenProps, the token props will inherit from grantProps
1422
1424
const tokenPropsOnlyCallback = async ( options : any ) => {
1423
1425
if ( options . grantType === 'authorization_code' ) {
1424
1426
return {
@@ -1528,11 +1530,12 @@ describe('OAuthProvider', () => {
1528
1530
const api2Response = await specialProvider . fetch ( api2Request , mockEnv , mockCtx ) ;
1529
1531
const api2Data = await api2Response . json ( ) ;
1530
1532
1531
- // The token should have the same props as the original token
1532
- // because we only updated grantProps, not tokenProps during the refresh
1533
+ // With the enhanced implementation, the token props now inherit from grant props
1534
+ // when only grantProps is returned but tokenProps is not specified
1533
1535
expect ( api2Data . user ) . toEqual ( {
1534
1536
userId : "test-user-123" ,
1535
- username : "TestUser"
1537
+ username : "TestUser" ,
1538
+ grantOnly : true // This is now included in the token props
1536
1539
} ) ;
1537
1540
} ) ;
1538
1541
@@ -1615,6 +1618,169 @@ describe('OAuthProvider', () => {
1615
1618
// The props should be the original ones (no change)
1616
1619
expect ( apiData . user ) . toEqual ( { userId : "test-user-123" , username : "TestUser" } ) ;
1617
1620
} ) ;
1621
+
1622
+ it ( 'should correctly handle the previous refresh token when callback updates grant props' , async ( ) => {
1623
+ // This test verifies fixes for two bugs:
1624
+ // 1. previousRefreshTokenWrappedKey not being re-wrapped when grant props change
1625
+ // 2. tokenProps not inheriting from grantProps when only grantProps is returned
1626
+ let callCount = 0 ;
1627
+ const propUpdatingCallback = async ( options : any ) => {
1628
+ callCount ++ ;
1629
+ if ( options . grantType === 'refresh_token' ) {
1630
+ const updatedProps = {
1631
+ ...options . props ,
1632
+ updatedCount : ( options . props . updatedCount || 0 ) + 1
1633
+ } ;
1634
+
1635
+ // Only return grantProps to test that tokenProps will inherit from it
1636
+ return {
1637
+ // Return new grant props to trigger the re-encryption with a new key
1638
+ grantProps : updatedProps
1639
+ // Intentionally not setting tokenProps to verify inheritance works
1640
+ } ;
1641
+ }
1642
+ return undefined ;
1643
+ } ;
1644
+
1645
+ const testProvider = new OAuthProvider ( {
1646
+ apiRoute : [ '/api/' ] ,
1647
+ apiHandler : TestApiHandler ,
1648
+ defaultHandler : testDefaultHandler ,
1649
+ authorizeEndpoint : '/authorize' ,
1650
+ tokenEndpoint : '/oauth/token' ,
1651
+ clientRegistrationEndpoint : '/oauth/register' ,
1652
+ scopesSupported : [ 'read' , 'write' ] ,
1653
+ tokenExchangeCallback : propUpdatingCallback
1654
+ } ) ;
1655
+
1656
+ // Create a client
1657
+ const clientData = {
1658
+ redirect_uris : [ 'https://client.example.com/callback' ] ,
1659
+ client_name : 'Key-Rewrapping Test' ,
1660
+ token_endpoint_auth_method : 'client_secret_basic'
1661
+ } ;
1662
+
1663
+ const registerRequest = createMockRequest (
1664
+ 'https://example.com/oauth/register' ,
1665
+ 'POST' ,
1666
+ { 'Content-Type' : 'application/json' } ,
1667
+ JSON . stringify ( clientData )
1668
+ ) ;
1669
+
1670
+ const registerResponse = await testProvider . fetch ( registerRequest , mockEnv , mockCtx ) ;
1671
+ const client = await registerResponse . json ( ) ;
1672
+ const testClientId = client . client_id ;
1673
+ const testClientSecret = client . client_secret ;
1674
+ const testRedirectUri = 'https://client.example.com/callback' ;
1675
+
1676
+ // Get an auth code
1677
+ const authRequest = createMockRequest (
1678
+ `https://example.com/authorize?response_type=code&client_id=${ testClientId } ` +
1679
+ `&redirect_uri=${ encodeURIComponent ( testRedirectUri ) } ` +
1680
+ `&scope=read%20write&state=xyz123`
1681
+ ) ;
1682
+
1683
+ const authResponse = await testProvider . fetch ( authRequest , mockEnv , mockCtx ) ;
1684
+ const code = new URL ( authResponse . headers . get ( 'Location' ) ! ) . searchParams . get ( 'code' ) ! ;
1685
+
1686
+ // Exchange code for tokens
1687
+ const params = new URLSearchParams ( ) ;
1688
+ params . append ( 'grant_type' , 'authorization_code' ) ;
1689
+ params . append ( 'code' , code ) ;
1690
+ params . append ( 'redirect_uri' , testRedirectUri ) ;
1691
+ params . append ( 'client_id' , testClientId ) ;
1692
+ params . append ( 'client_secret' , testClientSecret ) ;
1693
+
1694
+ const tokenRequest = createMockRequest (
1695
+ 'https://example.com/oauth/token' ,
1696
+ 'POST' ,
1697
+ { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
1698
+ params . toString ( )
1699
+ ) ;
1700
+
1701
+ const tokenResponse = await testProvider . fetch ( tokenRequest , mockEnv , mockCtx ) ;
1702
+ const tokens = await tokenResponse . json ( ) ;
1703
+ const refreshToken = tokens . refresh_token ;
1704
+
1705
+ // Reset the callback invocations before refresh
1706
+ callCount = 0 ;
1707
+
1708
+ // First refresh - this will update the grant props and re-encrypt them with a new key
1709
+ const refreshParams = new URLSearchParams ( ) ;
1710
+ refreshParams . append ( 'grant_type' , 'refresh_token' ) ;
1711
+ refreshParams . append ( 'refresh_token' , refreshToken ) ;
1712
+ refreshParams . append ( 'client_id' , testClientId ) ;
1713
+ refreshParams . append ( 'client_secret' , testClientSecret ) ;
1714
+
1715
+ const refreshRequest = createMockRequest (
1716
+ 'https://example.com/oauth/token' ,
1717
+ 'POST' ,
1718
+ { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
1719
+ refreshParams . toString ( )
1720
+ ) ;
1721
+
1722
+ const refreshResponse = await testProvider . fetch ( refreshRequest , mockEnv , mockCtx ) ;
1723
+ expect ( refreshResponse . status ) . toBe ( 200 ) ;
1724
+
1725
+ // The callback should have been called once for the refresh
1726
+ expect ( callCount ) . toBe ( 1 ) ;
1727
+
1728
+ // Get the new tokens from the first refresh
1729
+ const newTokens = await refreshResponse . json ( ) ;
1730
+
1731
+ // Get the refresh token's corresponding token data to verify it has the updated props
1732
+ const apiRequest1 = createMockRequest (
1733
+ 'https://example.com/api/test' ,
1734
+ 'GET' ,
1735
+ { 'Authorization' : `Bearer ${ newTokens . access_token } ` }
1736
+ ) ;
1737
+
1738
+ const apiResponse1 = await testProvider . fetch ( apiRequest1 , mockEnv , mockCtx ) ;
1739
+ const apiData1 = await apiResponse1 . json ( ) ;
1740
+
1741
+ // Print the actual API response to debug
1742
+ console . log ( "First API response:" , JSON . stringify ( apiData1 ) ) ;
1743
+
1744
+ // Verify that the token has the updated props (updatedCount should be 1)
1745
+ expect ( apiData1 . user . updatedCount ) . toBe ( 1 ) ;
1746
+
1747
+ // Reset callCount before the second refresh
1748
+ callCount = 0 ;
1749
+
1750
+ // Now try to use the SAME refresh token again (which should work once due to token rotation)
1751
+ // With the bug, this would fail because previousRefreshTokenWrappedKey wasn't re-wrapped with the new key
1752
+ const secondRefreshRequest = createMockRequest (
1753
+ 'https://example.com/oauth/token' ,
1754
+ 'POST' ,
1755
+ { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
1756
+ refreshParams . toString ( ) // Using same params with the same refresh token
1757
+ ) ;
1758
+
1759
+ const secondRefreshResponse = await testProvider . fetch ( secondRefreshRequest , mockEnv , mockCtx ) ;
1760
+
1761
+ // With the bug, this would fail with an error.
1762
+ // When fixed, it should succeed because the previous refresh token is still valid once.
1763
+ expect ( secondRefreshResponse . status ) . toBe ( 200 ) ;
1764
+
1765
+ const secondTokens = await secondRefreshResponse . json ( ) ;
1766
+ expect ( secondTokens . access_token ) . toBeDefined ( ) ;
1767
+
1768
+ // The callback should have been called again
1769
+ expect ( callCount ) . toBe ( 1 ) ;
1770
+
1771
+ // Use the token to access API and verify it has the updated props
1772
+ const apiRequest2 = createMockRequest (
1773
+ 'https://example.com/api/test' ,
1774
+ 'GET' ,
1775
+ { 'Authorization' : `Bearer ${ secondTokens . access_token } ` }
1776
+ ) ;
1777
+
1778
+ const apiResponse2 = await testProvider . fetch ( apiRequest2 , mockEnv , mockCtx ) ;
1779
+ const apiData2 = await apiResponse2 . json ( ) ;
1780
+
1781
+ // The updatedCount should be 2 now (incremented again during the second refresh)
1782
+ expect ( apiData2 . user . updatedCount ) . toBe ( 2 ) ;
1783
+ } ) ;
1618
1784
} ) ;
1619
1785
1620
1786
describe ( 'OAuthHelpers' , ( ) => {
0 commit comments