diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs index 749c0650..88821b2a 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs @@ -504,14 +504,14 @@ namespace OpenIddict.Server { // Note: the request cancellation token is deliberately not used here to ensure the caller // cannot prevent this operation from being executed by resetting the TCP connection. - var date = options.SystemClock.UtcNow + lifetime; + var date = options.SystemClock.UtcNow + lifetime.Value; await _tokenManager.ExtendAsync(token, date); _logger.LogInformation("The expiration date of the refresh token '{Identifier}' " + "was automatically updated: {Date}.", identifier, date); } - else + else if (await _tokenManager.GetExpirationDateAsync(token) != null) { // Note: the request cancellation token is deliberately not used here to ensure the caller // cannot prevent this operation from being executed by resetting the TCP connection. diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index abdd1823..9b12a209 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -701,29 +701,32 @@ namespace Microsoft.Extensions.DependencyInjection /// a new access token by making a grant_type=refresh_token token request /// or a prompt=none authorization request, depending on the selected flow. /// Using long-lived access tokens or tokens that never expire is not recommended. + /// While discouraged, null can be specified to issue tokens that never expire. /// /// The access token lifetime. /// The . - public OpenIddictServerBuilder SetAccessTokenLifetime(TimeSpan lifetime) + public OpenIddictServerBuilder SetAccessTokenLifetime([CanBeNull] TimeSpan? lifetime) => Configure(options => options.AccessTokenLifetime = lifetime); /// /// Sets the authorization code lifetime, after which client applications /// are unable to send a grant_type=authorization_code token request. /// Using short-lived authorization codes is strongly recommended. + /// While discouraged, null can be specified to issue codes that never expire. /// /// The authorization code lifetime. /// The . - public OpenIddictServerBuilder SetAuthorizationCodeLifetime(TimeSpan lifetime) + public OpenIddictServerBuilder SetAuthorizationCodeLifetime([CanBeNull] TimeSpan? lifetime) => Configure(options => options.AuthorizationCodeLifetime = lifetime); /// /// Sets the identity token lifetime, after which client /// applications should refuse processing identity tokens. + /// While discouraged, null can be specified to issue tokens that never expire. /// /// The identity token lifetime. /// The . - public OpenIddictServerBuilder SetIdentityTokenLifetime(TimeSpan lifetime) + public OpenIddictServerBuilder SetIdentityTokenLifetime([CanBeNull] TimeSpan? lifetime) => Configure(options => options.IdentityTokenLifetime = lifetime); /// @@ -731,10 +734,11 @@ namespace Microsoft.Extensions.DependencyInjection /// a new authorization from the user. When sliding expiration is enabled, /// a new refresh token is always issued to the client application, /// which prolongs the validity period of the refresh token. + /// While discouraged, null can be specified to issue tokens that never expire. /// /// The refresh token lifetime. /// The . - public OpenIddictServerBuilder SetRefreshTokenLifetime(TimeSpan lifetime) + public OpenIddictServerBuilder SetRefreshTokenLifetime([CanBeNull] TimeSpan? lifetime) => Configure(options => options.RefreshTokenLifetime = lifetime); /// diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs index 4e432688..3654d496 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs @@ -1005,7 +1005,7 @@ namespace OpenIddict.Server.Tests new AuthenticationProperties(), OpenIddictServerDefaults.AuthenticationScheme); - ticket.SetProperty(OpenIddictConstants.Properties.InternalTokenId, "3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetProperty(OpenIddictConstants.Properties.InternalTokenId, "60FFF7EA-F98E-437B-937E-5073CC313103"); ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); @@ -1063,6 +1063,138 @@ namespace OpenIddict.Server.Tests It.IsAny()), Times.Never()); } + [Fact] + public async Task ProcessSigninResponse_DoesNotUpdateExpirationDateWhenAlreadyNull() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIddictServerDefaults.AuthenticationScheme); + + ticket.SetProperty(OpenIddictConstants.Properties.InternalTokenId, "60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) + .Returns(new ValueTask(result: null)); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + options.RefreshTokenLifetime = null; + options.RefreshTokenFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, null, It.IsAny()), Times.Never()); + } + + [Fact] + public async Task ProcessSigninResponse_SetsExpirationDateToNullWhenLifetimeIsNull() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIddictServerDefaults.AuthenticationScheme); + + ticket.SetProperty(OpenIddictConstants.Properties.InternalTokenId, "60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) + .Returns(new ValueTask(DateTimeOffset.Now + TimeSpan.FromDays(1))); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + options.RefreshTokenLifetime = null; + options.RefreshTokenFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, null, It.IsAny()), Times.Once()); + } + [Fact] public async Task ProcessSigninResponse_IgnoresErrorWhenExtendingLifetimeOfExistingTokenFailed() { diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs index f30dbbbf..0d417a4a 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs +++ b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs @@ -601,6 +601,22 @@ namespace OpenIddict.Server.Tests Assert.Equal(TimeSpan.FromMinutes(42), options.AccessTokenLifetime); } + [Fact] + public void SetAccessTokenLifetime_AccessTokenLifetimeCanBeSetToNull() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetAccessTokenLifetime(null); + + var options = GetOptions(services); + + // Assert + Assert.Null(options.AccessTokenLifetime); + } + [Fact] public void SetAuthorizationCodeLifetime_DefaultAuthorizationCodeLifetimeIsReplaced() { @@ -617,6 +633,22 @@ namespace OpenIddict.Server.Tests Assert.Equal(TimeSpan.FromMinutes(42), options.AuthorizationCodeLifetime); } + [Fact] + public void SetAuthorizationCodeLifetime_AuthorizationCodeLifetimeCanBeSetToNull() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetAuthorizationCodeLifetime(null); + + var options = GetOptions(services); + + // Assert + Assert.Null(options.AuthorizationCodeLifetime); + } + [Fact] public void SetIdentityTokenLifetime_DefaultIdentityTokenLifetimeIsReplaced() { @@ -633,6 +665,22 @@ namespace OpenIddict.Server.Tests Assert.Equal(TimeSpan.FromMinutes(42), options.IdentityTokenLifetime); } + [Fact] + public void SetIdentityTokenLifetime_IdentityTokenLifetimeCanBeSetToNull() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetIdentityTokenLifetime(null); + + var options = GetOptions(services); + + // Assert + Assert.Null(options.IdentityTokenLifetime); + } + [Fact] public void SetRefreshTokenLifetime_DefaultRefreshTokenLifetimeIsReplaced() { @@ -649,6 +697,22 @@ namespace OpenIddict.Server.Tests Assert.Equal(TimeSpan.FromMinutes(42), options.RefreshTokenLifetime); } + [Fact] + public void SetRefreshTokenLifetime_RefreshTokenLifetimeCanBeSetToNull() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetRefreshTokenLifetime(null); + + var options = GetOptions(services); + + // Assert + Assert.Null(options.RefreshTokenLifetime); + } + [Fact] public void SetIssuer_AddressIsReplaced() {