@@ -2469,6 +2469,73 @@ await attacker.PostAsync(
24692469 . Be ( HttpStatusCode . Unauthorized ) ;
24702470 }
24712471
2472+ [ Fact ]
2473+ public async Task Refresh_Hybrid_Maui_HeaderCredentials_ShouldSupportConsecutiveRefreshes ( )
2474+ {
2475+ _factory . Clock . Reset ( ) ;
2476+
2477+ var user = await _factory . CreateLoginUserAsync ( ) ;
2478+
2479+ using var client = CreateClient (
2480+ $ "maui-refresh-{ Guid . NewGuid ( ) : N} ") ;
2481+
2482+ SetClientProfile ( client , "Maui" ) ;
2483+
2484+ // 1. Login
2485+ using var login = await client . PostAsJsonAsync (
2486+ "/auth/login" ,
2487+ new
2488+ {
2489+ identifier = user . Identifier ,
2490+ secret = user . Secret
2491+ } ) ;
2492+
2493+ login . StatusCode . Should ( ) . Be ( HttpStatusCode . Found ) ;
2494+
2495+ var sessionId = ReadCredential ( login , "uas" ) ;
2496+ var refreshToken = ReadCredential ( login , "uar" ) ;
2497+
2498+ sessionId . Should ( ) . NotBeNullOrWhiteSpace ( ) ;
2499+ refreshToken . Should ( ) . NotBeNullOrWhiteSpace ( ) ;
2500+
2501+ // 2. First refresh using the credentials delivered by login
2502+ using var firstRequest = new HttpRequestMessage (
2503+ HttpMethod . Post , "/auth/refresh" ) ;
2504+
2505+ firstRequest . Headers . TryAddWithoutValidation (
2506+ "X-UAuth-Session" , sessionId ) ;
2507+
2508+ firstRequest . Headers . TryAddWithoutValidation (
2509+ "X-Refresh-Token" , refreshToken ) ;
2510+
2511+ using var first = await client . SendAsync ( firstRequest ) ;
2512+
2513+ first . StatusCode . Should ( ) . Be ( HttpStatusCode . NoContent ) ;
2514+
2515+ var rotatedRefreshToken = ReadCredential ( first , "uar" ) ;
2516+
2517+ rotatedRefreshToken . Should ( ) . NotBeNullOrWhiteSpace ( ) ;
2518+ rotatedRefreshToken . Should ( ) . NotBe ( refreshToken ) ;
2519+
2520+ // Session ID may be reissued depending on the refresh policy.
2521+ var nextSessionId =
2522+ TryReadCredential ( first , "uas" ) ?? sessionId ;
2523+
2524+ // 3. Second refresh using the newly delivered refresh token
2525+ using var secondRequest = new HttpRequestMessage (
2526+ HttpMethod . Post , "/auth/refresh" ) ;
2527+
2528+ secondRequest . Headers . TryAddWithoutValidation (
2529+ "X-UAuth-Session" , nextSessionId ) ;
2530+
2531+ secondRequest . Headers . TryAddWithoutValidation (
2532+ "X-Refresh-Token" , rotatedRefreshToken ) ;
2533+
2534+ using var second = await client . SendAsync ( secondRequest ) ;
2535+
2536+ second . StatusCode . Should ( ) . Be ( HttpStatusCode . NoContent ) ;
2537+ }
2538+
24722539
24732540
24742541 private async Task < string > LoginAsync ( IntegrationTestUser user , string profile = "BlazorServer" )
@@ -2705,4 +2772,25 @@ private static void SetTenant(HttpClient client, string tenant)
27052772 "X-Tenant" ,
27062773 tenant ) ;
27072774 }
2775+
2776+ private static string ReadCredential ( HttpResponseMessage response , string headerName )
2777+ {
2778+ var value = TryReadCredential ( response , headerName ) ;
2779+ value . Should ( ) . NotBeNullOrWhiteSpace ( $ "response must deliver the { headerName } credential") ;
2780+
2781+ return value ! ;
2782+ }
2783+
2784+ private static string ? TryReadCredential ( HttpResponseMessage response , string headerName )
2785+ {
2786+ if ( ! response . Headers . TryGetValues ( headerName , out var values ) )
2787+ return null ;
2788+
2789+ var raw = values . Single ( ) . Trim ( ) ;
2790+
2791+ if ( raw . StartsWith ( "Bearer " , StringComparison . OrdinalIgnoreCase ) )
2792+ raw = raw [ "Bearer " . Length ..] . Trim ( ) ;
2793+
2794+ return string . IsNullOrWhiteSpace ( raw ) ? null : raw ;
2795+ }
27082796}
0 commit comments