diff --git a/src/Duende.AccessTokenManagement.OpenIdConnect/UserTokenEndpointService.cs b/src/Duende.AccessTokenManagement.OpenIdConnect/UserTokenEndpointService.cs index 0681da6..dda6a8c 100755 --- a/src/Duende.AccessTokenManagement.OpenIdConnect/UserTokenEndpointService.cs +++ b/src/Duende.AccessTokenManagement.OpenIdConnect/UserTokenEndpointService.cs @@ -145,7 +145,7 @@ public async Task RefreshAccessTokenAsync( token.Expiration = response.ExpiresIn == 0 ? DateTimeOffset.MaxValue : DateTimeOffset.UtcNow.AddSeconds(response.ExpiresIn); - token.RefreshToken = response.RefreshToken; + token.RefreshToken = response.RefreshToken ?? userToken.RefreshToken; token.Scope = response.Scope; } diff --git a/test/Tests/UserTokenManagementTests.cs b/test/Tests/UserTokenManagementTests.cs index 971e237..42751e4 100644 --- a/test/Tests/UserTokenManagementTests.cs +++ b/test/Tests/UserTokenManagementTests.cs @@ -12,7 +12,7 @@ public class UserTokenManagementTests : IntegrationTestBase { public UserTokenManagementTests() : base("web") { } - + [Fact] public async Task Anonymous_user_should_return_user_token_error() { @@ -21,7 +21,7 @@ public async Task Anonymous_user_should_return_user_token_error() token!.IsError.ShouldBeTrue(); } - + [Fact] public async Task Anonymous_user_should_return_client_token() { @@ -31,7 +31,7 @@ public async Task Anonymous_user_should_return_client_token() token!.AccessToken.ShouldNotBeNull(); token.AccessTokenType.ShouldBe("Bearer"); token.Expiration.ShouldNotBe(DateTimeOffset.MaxValue); - + token.IsError.ShouldBeFalse(); } @@ -40,7 +40,7 @@ public async Task Standard_initial_token_response_should_return_expected_values( { var mockHttp = new MockHttpMessageHandler(); AppHost.IdentityServerHttpHandler = mockHttp; - + var initialTokenResponse = new { id_token = IdentityServerHost.CreateIdToken("1", "web"), @@ -49,12 +49,12 @@ public async Task Standard_initial_token_response_should_return_expected_values( expires_in = 3600, refresh_token = "initial_refresh_token", }; - + // response for re-deeming code mockHttp.When("/connect/token") .WithFormData("grant_type", "authorization_code") .Respond("application/json", JsonSerializer.Serialize(initialTokenResponse)); - + await AppHost.InitializeAsync(); await AppHost.LoginAsync("alice"); @@ -68,7 +68,7 @@ public async Task Standard_initial_token_response_should_return_expected_values( token.AccessTokenType.ShouldBe("token_type"); token.RefreshToken.ShouldBe("initial_refresh_token"); token.Expiration.ShouldNotBe(DateTimeOffset.MaxValue); - + // 2nd request should not trigger a token request response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token")); token = await response.Content.ReadFromJsonAsync(); @@ -80,13 +80,13 @@ public async Task Standard_initial_token_response_should_return_expected_values( token.RefreshToken.ShouldBe("initial_refresh_token"); token.Expiration.ShouldNotBe(DateTimeOffset.MaxValue); } - + [Fact] public async Task Missing_expires_in_should_result_in_long_lived_token() { var mockHttp = new MockHttpMessageHandler(); AppHost.IdentityServerHttpHandler = mockHttp; - + var initialTokenResponse = new { id_token = IdentityServerHost.CreateIdToken("1", "web"), @@ -94,12 +94,12 @@ public async Task Missing_expires_in_should_result_in_long_lived_token() token_type = "token_type", refresh_token = "initial_refresh_token", }; - + // response for re-deeming code mockHttp.When("/connect/token") .WithFormData("grant_type", "authorization_code") .Respond("application/json", JsonSerializer.Serialize(initialTokenResponse)); - + await AppHost.InitializeAsync(); await AppHost.LoginAsync("alice"); @@ -113,13 +113,13 @@ public async Task Missing_expires_in_should_result_in_long_lived_token() token.RefreshToken.ShouldBe("initial_refresh_token"); token.Expiration.ShouldBe(DateTimeOffset.MaxValue); } - + [Fact] public async Task Missing_initial_refresh_token_response_should_return_access_token() { var mockHttp = new MockHttpMessageHandler(); AppHost.IdentityServerHttpHandler = mockHttp; - + var initialTokenResponse = new { id_token = IdentityServerHost.CreateIdToken("1", "web"), @@ -127,12 +127,12 @@ public async Task Missing_initial_refresh_token_response_should_return_access_to token_type = "token_type", expires_in = 3600 }; - + // response for re-deeming code mockHttp.When("/connect/token") .WithFormData("grant_type", "authorization_code") .Respond("application/json", JsonSerializer.Serialize(initialTokenResponse)); - + await AppHost.InitializeAsync(); await AppHost.LoginAsync("alice"); @@ -146,13 +146,13 @@ public async Task Missing_initial_refresh_token_response_should_return_access_to token.RefreshToken.ShouldBeNull(); token.Expiration.ShouldNotBe(DateTimeOffset.MaxValue); } - + [Fact] public async Task Missing_initial_refresh_token_and_expired_access_token_should_return_initial_access_token() { var mockHttp = new MockHttpMessageHandler(); AppHost.IdentityServerHttpHandler = mockHttp; - + var initialTokenResponse = new { id_token = IdentityServerHost.CreateIdToken("1", "web"), @@ -160,12 +160,12 @@ public async Task Missing_initial_refresh_token_and_expired_access_token_should_ token_type = "token_type", expires_in = 10 }; - + // response for re-deeming code mockHttp.When("/connect/token") .WithFormData("grant_type", "authorization_code") .Respond("application/json", JsonSerializer.Serialize(initialTokenResponse)); - + await AppHost.InitializeAsync(); await AppHost.LoginAsync("alice"); @@ -179,13 +179,13 @@ public async Task Missing_initial_refresh_token_and_expired_access_token_should_ token.RefreshToken.ShouldBeNull(); token.Expiration.ShouldNotBe(DateTimeOffset.MaxValue); } - + [Fact] public async Task Short_token_lifetime_should_trigger_refresh() { var mockHttp = new MockHttpMessageHandler(); AppHost.IdentityServerHttpHandler = mockHttp; - + // short token lifetime should trigger refresh on 1st use var initialTokenResponse = new { @@ -195,12 +195,12 @@ public async Task Short_token_lifetime_should_trigger_refresh() expires_in = 10, refresh_token = "initial_refresh_token", }; - + // response for re-deeming code mockHttp.When("/connect/token") .WithFormData("grant_type", "authorization_code") .Respond("application/json", JsonSerializer.Serialize(initialTokenResponse)); - + // short token lifetime should trigger refresh on 1st use var refreshTokenResponse = new { @@ -209,13 +209,13 @@ public async Task Short_token_lifetime_should_trigger_refresh() expires_in = 10, refresh_token = "refreshed1_refresh_token", }; - + // response for refresh 1 mockHttp.When("/connect/token") .WithFormData("grant_type", "refresh_token") .WithFormData("refresh_token", "initial_refresh_token") .Respond("application/json", JsonSerializer.Serialize(refreshTokenResponse)); - + // short token lifetime should trigger refresh on 2nd use var refreshTokenResponse2 = new { @@ -224,18 +224,18 @@ public async Task Short_token_lifetime_should_trigger_refresh() expires_in = 3600, refresh_token = "refreshed2_refresh_token", }; - + // response for refresh 1 mockHttp.When("/connect/token") .WithFormData("grant_type", "refresh_token") .WithFormData("refresh_token", "refreshed1_refresh_token") .Respond("application/json", JsonSerializer.Serialize(refreshTokenResponse2)); - - + + // setup host await AppHost.InitializeAsync(); await AppHost.LoginAsync("alice"); - + // first request should trigger refresh var response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token")); var token = await response.Content.ReadFromJsonAsync(); @@ -246,7 +246,7 @@ public async Task Short_token_lifetime_should_trigger_refresh() token.AccessTokenType.ShouldBe("token_type1"); token.RefreshToken.ShouldBe("refreshed1_refresh_token"); token.Expiration.ShouldNotBe(DateTimeOffset.MaxValue); - + // second request should trigger refresh response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token")); token = await response.Content.ReadFromJsonAsync(); @@ -257,7 +257,7 @@ public async Task Short_token_lifetime_should_trigger_refresh() token.AccessTokenType.ShouldBe("token_type2"); token.RefreshToken.ShouldBe("refreshed2_refresh_token"); token.Expiration.ShouldNotBe(DateTimeOffset.MaxValue); - + // third request should not trigger refresh response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token")); token = await response.Content.ReadFromJsonAsync(); @@ -275,7 +275,7 @@ public async Task Resources_get_distinct_tokens() { var mockHttp = new MockHttpMessageHandler(); AppHost.IdentityServerHttpHandler = mockHttp; - + // no resource specified var initialTokenResponse = new { @@ -288,7 +288,7 @@ public async Task Resources_get_distinct_tokens() mockHttp.When("/connect/token") .WithFormData("grant_type", "authorization_code") .Respond("application/json", JsonSerializer.Serialize(initialTokenResponse)); - + // resource 1 specified var resource1TokenResponse = new { @@ -301,7 +301,7 @@ public async Task Resources_get_distinct_tokens() .WithFormData("grant_type", "refresh_token") .WithFormData("resource", "urn:api1") .Respond("application/json", JsonSerializer.Serialize(resource1TokenResponse)); - + // resource 2 specified var resource2TokenResponse = new { @@ -314,11 +314,11 @@ public async Task Resources_get_distinct_tokens() .WithFormData("grant_type", "refresh_token") .WithFormData("resource", "urn:api2") .Respond("application/json", JsonSerializer.Serialize(resource2TokenResponse)); - + // setup host await AppHost.InitializeAsync(); await AppHost.LoginAsync("alice"); - + // first request - no resource var response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token")); var token = await response.Content.ReadFromJsonAsync(); @@ -328,7 +328,7 @@ public async Task Resources_get_distinct_tokens() token.AccessToken.ShouldBe("access_token_without_resource"); token.RefreshToken.ShouldBe("initial_refresh_token"); token.Expiration.ShouldNotBe(DateTimeOffset.MaxValue); - + // second request - with resource api1 response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token_with_resource/urn:api1")); token = await response.Content.ReadFromJsonAsync(); @@ -346,7 +346,55 @@ public async Task Resources_get_distinct_tokens() token.ShouldNotBeNull(); token.IsError.ShouldBeFalse(); token.AccessToken.ShouldBe("urn:api2_access_token"); - token.RefreshToken.ShouldBe("initial_refresh_token"); + token.RefreshToken.ShouldBe("initial_refresh_token"); token.Expiration.ShouldNotBe(DateTimeOffset.MaxValue); } -} + + [Fact] + public async Task Refresh_responses_without_refresh_token_use_old_refresh_token() + { + var mockHttp = new MockHttpMessageHandler(); + AppHost.IdentityServerHttpHandler = mockHttp; + + // short token lifetime should trigger refresh on 1st use + var initialTokenResponse = new + { + id_token = IdentityServerHost.CreateIdToken("1", "web"), + access_token = "initial_access_token", + token_type = "token_type", + expires_in = 10, + refresh_token = "initial_refresh_token", + }; + + // response for re-deeming code + mockHttp.When("/connect/token") + .WithFormData("grant_type", "authorization_code") + .Respond("application/json", JsonSerializer.Serialize(initialTokenResponse)); + + // note lack of refresh_token + var refreshTokenResponse = new + { + access_token = "refreshed1_access_token", + token_type = "token_type1", + expires_in = 3600, + }; + + // response for refresh + mockHttp.When("/connect/token") + .WithFormData("grant_type", "refresh_token") + .WithFormData("refresh_token", "initial_refresh_token") + .Respond("application/json", JsonSerializer.Serialize(refreshTokenResponse)); + + // setup host + await AppHost.InitializeAsync(); + await AppHost.LoginAsync("alice"); + + // first request should trigger refresh + var response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token")); + var token = await response.Content.ReadFromJsonAsync(); + + token.ShouldNotBeNull(); + token.IsError.ShouldBeFalse(); + token.RefreshToken.ShouldBe("initial_refresh_token"); + } +} \ No newline at end of file