@@ -1758,5 +1758,86 @@ public async Task Should_Clear_Cookies_When_Logging_Out_Using_Custom_Cookie_Sche
17581758 }
17591759 }
17601760 }
1761+
1762+ [ Fact ]
1763+ public async Task Should_Call_OnMissingRefreshToken_After_Refresh_Fails ( )
1764+ {
1765+ var nonce = "" ;
1766+ var configuration = TestConfiguration . GetConfiguration ( ) ;
1767+ var domain = configuration [ "Auth0:Domain" ] ;
1768+ var clientId = configuration [ "Auth0:ClientId" ] ;
1769+ var onMissingRefreshTokenCalled = false ;
1770+
1771+ var mockHandler = new OidcMockBuilder ( )
1772+ . MockOpenIdConfig ( )
1773+ . MockJwks ( )
1774+ // Mock initial token with very short expiration (1 second) to trigger refresh on second request
1775+ . MockToken ( ( ) => JwtUtils . GenerateToken ( 1 , $ "https://{ domain } /", clientId , null , nonce , DateTime . UtcNow . AddSeconds ( 20 ) ) , ( me ) => me . HasGrantType ( "authorization_code" ) , 1 )
1776+ // Mock the refresh token endpoint to fail
1777+ . MockToken ( ( ) => JwtUtils . GenerateToken ( 1 , $ "https://{ domain } /", clientId , null , null , DateTime . UtcNow . AddSeconds ( 20 ) ) , ( me ) => me . HasGrantType ( "refresh_token" ) , 20 , true , HttpStatusCode . BadRequest )
1778+ . Build ( ) ;
1779+
1780+ using ( var server = TestServerBuilder . CreateServer ( opts =>
1781+ {
1782+ opts . ClientSecret = "123" ;
1783+ opts . Backchannel = new HttpClient ( mockHandler . Object ) ;
1784+ } , opts =>
1785+ {
1786+ opts . Audience = "123" ;
1787+ opts . Events = new Auth0WebAppWithAccessTokenEvents
1788+ {
1789+ OnMissingRefreshToken = ( context ) =>
1790+ {
1791+ onMissingRefreshTokenCalled = true ;
1792+ context . Response . Redirect ( "http://missing.rt/" ) ;
1793+ return Task . CompletedTask ;
1794+ }
1795+ } ;
1796+ opts . UseRefreshTokens = true ;
1797+ } ) )
1798+ {
1799+ using ( var client = server . CreateClient ( ) )
1800+ {
1801+ var loginResponse = ( await client . SendAsync ( $ "{ TestServerBuilder . Host } /{ TestServerBuilder . Login } ") ) ;
1802+ var setCookie = Assert . Single ( loginResponse . Headers , h => h . Key == "Set-Cookie" ) ;
1803+
1804+ var queryParameters = UriUtils . GetQueryParams ( loginResponse . Headers . Location ) ;
1805+
1806+ // Keep track of the nonce as we need to:
1807+ // - Send it to the `/oauth/token` endpoint
1808+ // - Include it in the generated ID Token
1809+ nonce = queryParameters [ "nonce" ] ;
1810+
1811+ // Keep track of the state as we need to:
1812+ // - Send it to the `/oauth/token` endpoint
1813+ var state = queryParameters [ "state" ] ;
1814+
1815+ var message = new HttpRequestMessage ( HttpMethod . Get , $ "{ TestServerBuilder . Host } /{ TestServerBuilder . Callback } ?state={ state } &nonce={ nonce } &code=123") ;
1816+
1817+ // Pass along the Set-Cookies to ensure `Nonce` and `Correlation` cookies are set.
1818+ var callbackResponse = ( await client . SendAsync ( message , setCookie . Value ) ) ;
1819+
1820+ // Wait for token to expire (1 second + some buffer)
1821+ await Task . Delay ( 2000 ) ;
1822+
1823+ // First request after token expires - this will trigger refresh (which will fail), clearing the refresh token
1824+ var firstResponse = await client . SendAsync ( $ "{ TestServerBuilder . Host } /{ TestServerBuilder . Process } ", callbackResponse . Headers . GetValues ( "Set-Cookie" ) ) ;
1825+ var firstContent = JObject . Parse ( await firstResponse . Content . ReadAsStringAsync ( ) ) ;
1826+
1827+ // Verify refresh token was cleared after failed refresh
1828+ firstContent . GetValue ( "RefreshToken" ) . Value < string > ( ) . Should ( ) . BeNull ( ) ;
1829+ onMissingRefreshTokenCalled . Should ( ) . BeFalse ( ) ;
1830+
1831+ // Second request - now OnMissingRefreshToken should be called since refresh token is missing
1832+ var secondResponse = await client . SendAsync ( $ "{ TestServerBuilder . Host } /{ TestServerBuilder . Process } ", firstResponse . Headers . GetValues ( "Set-Cookie" ) ) ;
1833+
1834+ // Verify OnMissingRefreshToken was called
1835+ onMissingRefreshTokenCalled . Should ( ) . BeTrue ( ) ;
1836+ secondResponse . Headers . Location . AbsoluteUri . Should ( ) . Be ( "http://missing.rt/" ) ;
1837+
1838+ mockHandler . Verify ( ) ;
1839+ }
1840+ }
1841+ }
17611842 }
17621843}
0 commit comments