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()
{