From a210c7d96b6dd7faa9ca241290c8669f1e68e414 Mon Sep 17 00:00:00 2001 From: kailash-b Date: Fri, 21 Nov 2025 17:37:01 +0530 Subject: [PATCH] Remove refresh_token from cookie instead of setting to NULL --- .../AuthenticationBuilderExtensions.cs | 3 +- .../Auth0MiddlewareTests.cs | 81 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs b/src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs index 6aa9c0b1..3ebff6a3 100644 --- a/src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs +++ b/src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs @@ -210,7 +210,8 @@ private static async Task RefreshTokenIfNeccesary(CookieValidatePrincipalContext } else { - context.Properties.UpdateTokenValue("refresh_token", null!); + // Remove the refresh token when refresh fails to ensure OnMissingRefreshToken is called on subsequent requests + context.Properties.Items.Remove(".Token.refresh_token"); } context.ShouldRenew = true; diff --git a/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Auth0MiddlewareTests.cs b/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Auth0MiddlewareTests.cs index 073c744a..177906fd 100644 --- a/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Auth0MiddlewareTests.cs +++ b/tests/Auth0.AspNetCore.Authentication.IntegrationTests/Auth0MiddlewareTests.cs @@ -1758,5 +1758,86 @@ public async Task Should_Clear_Cookies_When_Logging_Out_Using_Custom_Cookie_Sche } } } + + [Fact] + public async Task Should_Call_OnMissingRefreshToken_After_Refresh_Fails() + { + var nonce = ""; + var configuration = TestConfiguration.GetConfiguration(); + var domain = configuration["Auth0:Domain"]; + var clientId = configuration["Auth0:ClientId"]; + var onMissingRefreshTokenCalled = false; + + var mockHandler = new OidcMockBuilder() + .MockOpenIdConfig() + .MockJwks() + // Mock initial token with very short expiration (1 second) to trigger refresh on second request + .MockToken(() => JwtUtils.GenerateToken(1, $"https://{domain}/", clientId, null, nonce, DateTime.UtcNow.AddSeconds(20)), (me) => me.HasGrantType("authorization_code"), 1) + // Mock the refresh token endpoint to fail + .MockToken(() => JwtUtils.GenerateToken(1, $"https://{domain}/", clientId, null, null, DateTime.UtcNow.AddSeconds(20)), (me) => me.HasGrantType("refresh_token"), 20, true, HttpStatusCode.BadRequest) + .Build(); + + using (var server = TestServerBuilder.CreateServer(opts => + { + opts.ClientSecret = "123"; + opts.Backchannel = new HttpClient(mockHandler.Object); + }, opts => + { + opts.Audience = "123"; + opts.Events = new Auth0WebAppWithAccessTokenEvents + { + OnMissingRefreshToken = (context) => + { + onMissingRefreshTokenCalled = true; + context.Response.Redirect("http://missing.rt/"); + return Task.CompletedTask; + } + }; + opts.UseRefreshTokens = true; + })) + { + using (var client = server.CreateClient()) + { + var loginResponse = (await client.SendAsync($"{TestServerBuilder.Host}/{TestServerBuilder.Login}")); + var setCookie = Assert.Single(loginResponse.Headers, h => h.Key == "Set-Cookie"); + + var queryParameters = UriUtils.GetQueryParams(loginResponse.Headers.Location); + + // Keep track of the nonce as we need to: + // - Send it to the `/oauth/token` endpoint + // - Include it in the generated ID Token + nonce = queryParameters["nonce"]; + + // Keep track of the state as we need to: + // - Send it to the `/oauth/token` endpoint + var state = queryParameters["state"]; + + var message = new HttpRequestMessage(HttpMethod.Get, $"{TestServerBuilder.Host}/{TestServerBuilder.Callback}?state={state}&nonce={nonce}&code=123"); + + // Pass along the Set-Cookies to ensure `Nonce` and `Correlation` cookies are set. + var callbackResponse = (await client.SendAsync(message, setCookie.Value)); + + // Wait for token to expire (1 second + some buffer) + await Task.Delay(2000); + + // First request after token expires - this will trigger refresh (which will fail), clearing the refresh token + var firstResponse = await client.SendAsync($"{TestServerBuilder.Host}/{TestServerBuilder.Process}", callbackResponse.Headers.GetValues("Set-Cookie")); + var firstContent = JObject.Parse(await firstResponse.Content.ReadAsStringAsync()); + + // Verify refresh token was cleared after failed refresh + firstContent.GetValue("RefreshToken").Value().Should().BeNull(); + onMissingRefreshTokenCalled.Should().BeFalse(); + + // Second request - now OnMissingRefreshToken should be called since refresh token is missing + var secondResponse = await client.SendAsync($"{TestServerBuilder.Host}/{TestServerBuilder.Process}", firstResponse.Headers.GetValues("Set-Cookie")); + + // Verify OnMissingRefreshToken was called + onMissingRefreshTokenCalled.Should().BeTrue(); + secondResponse.Headers.Location.AbsoluteUri.Should().Be("http://missing.rt/"); + + mockHandler.Verify(); + } + } + } } }