Skip to content

Commit 5f73c46

Browse files
authored
Remove refresh_token from cookie instead of setting to NULL (#193)
2 parents 623bd02 + a210c7d commit 5f73c46

File tree

2 files changed

+83
-1
lines changed

2 files changed

+83
-1
lines changed

src/Auth0.AspNetCore.Authentication/AuthenticationBuilderExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,8 @@ private static async Task RefreshTokenIfNeccesary(CookieValidatePrincipalContext
210210
}
211211
else
212212
{
213-
context.Properties.UpdateTokenValue("refresh_token", null!);
213+
// Remove the refresh token when refresh fails to ensure OnMissingRefreshToken is called on subsequent requests
214+
context.Properties.Items.Remove(".Token.refresh_token");
214215
}
215216

216217
context.ShouldRenew = true;

tests/Auth0.AspNetCore.Authentication.IntegrationTests/Auth0MiddlewareTests.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)