From 1c382d90ba28c0a5123d36610ba7dd441314eac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Tue, 12 Sep 2023 09:21:29 +0200 Subject: [PATCH] Introduce application settings and support configuring token lifetimes per client --- .../Startup.cs | 6 + .../Worker.cs | 5 + .../OpenIddictApplicationDescriptor.cs | 5 + .../Managers/IOpenIddictApplicationManager.cs | 11 + .../OpenIddictConstants.cs | 44 ++- .../Stores/IOpenIddictApplicationStore.cs | 21 ++ .../Managers/OpenIddictApplicationManager.cs | 31 ++ .../OpenIddictEntityFrameworkApplication.cs | 6 + ...enIddictEntityFrameworkApplicationStore.cs | 80 +++++ ...penIddictEntityFrameworkCoreApplication.cs | 6 + ...dictEntityFrameworkCoreApplicationStore.cs | 80 +++++ .../OpenIddictMongoDbApplication.cs | 7 + .../OpenIddictMongoDbApplicationStore.cs | 37 +++ .../OpenIddictServerHandlers.cs | 285 ++++++++++++++++-- ...ctServerIntegrationTests.Authentication.cs | 18 ++ ...OpenIddictServerIntegrationTests.Device.cs | 6 + ...enIddictServerIntegrationTests.Exchange.cs | 15 + .../OpenIddictServerIntegrationTests.cs | 12 + 18 files changed, 632 insertions(+), 43 deletions(-) diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs index fbafdd8e..790e9b2a 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Threading.Tasks; using System.Web.Http; using System.Web.Mvc; @@ -237,6 +238,11 @@ namespace OpenIddict.Sandbox.AspNet.Server Permissions.Scopes.Email, Permissions.Scopes.Profile, Permissions.Scopes.Roles + }, + Settings = + { + // Use a shorter access token lifetime for tokens issued to the Postman application. + [Settings.TokenLifetimes.AccessToken] = TimeSpan.FromMinutes(10).ToString("c", CultureInfo.InvariantCulture) } }); } diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs index f0904764..a1667b0a 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs @@ -228,6 +228,11 @@ public class Worker : IHostedService Permissions.Scopes.Email, Permissions.Scopes.Profile, Permissions.Scopes.Roles + }, + Settings = + { + // Use a shorter access token lifetime for tokens issued to the Postman application. + [Settings.TokenLifetimes.AccessToken] = TimeSpan.FromMinutes(10).ToString("c", CultureInfo.InvariantCulture) } }); } diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs index 76391bba..d4e8a31e 100644 --- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs @@ -71,6 +71,11 @@ public class OpenIddictApplicationDescriptor /// public HashSet Requirements { get; } = new(StringComparer.Ordinal); + /// + /// Gets the settings associated with the application. + /// + public Dictionary Settings { get; } = new(StringComparer.Ordinal); + /// /// Gets or sets the client type associated with the application. /// diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs index e167d7d5..125faecd 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs @@ -318,6 +318,17 @@ public interface IOpenIddictApplicationManager /// ValueTask> GetRequirementsAsync(object application, CancellationToken cancellationToken = default); + /// + /// Retrieves the settings associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the settings associated with the application. + /// + ValueTask> GetSettingsAsync(object application, CancellationToken cancellationToken = default); + /// /// Determines whether a given application has the specified application type. /// diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index 44dcb378..32f0c075 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs @@ -457,6 +457,23 @@ public static class OpenIddictConstants public const string Token = "token"; } + public static class Schemes + { + public const string Basic = "Basic"; + public const string Bearer = "Bearer"; + } + + public static class Scopes + { + public const string Address = "address"; + public const string Email = "email"; + public const string OfflineAccess = "offline_access"; + public const string OpenId = "openid"; + public const string Phone = "phone"; + public const string Profile = "profile"; + public const string Roles = "roles"; + } + public static class Separators { public static readonly char[] Ampersand = { '&' }; @@ -471,21 +488,22 @@ public static class OpenIddictConstants public static readonly char[] Space = { ' ' }; } - public static class Schemes + public static class Settings { - public const string Basic = "Basic"; - public const string Bearer = "Bearer"; - } + public static class Prefixes + { + public const string TokenLifetime = "tkn_lft:"; + } - public static class Scopes - { - public const string Address = "address"; - public const string Email = "email"; - public const string OfflineAccess = "offline_access"; - public const string OpenId = "openid"; - public const string Phone = "phone"; - public const string Profile = "profile"; - public const string Roles = "roles"; + public static class TokenLifetimes + { + public const string AccessToken = "tkn_lft:act"; + public const string AuthorizationCode = "tkn_lft:auc"; + public const string DeviceCode = "tkn_lft:dvc"; + public const string IdentityToken = "tkn_lft:idt"; + public const string RefreshToken = "tkn_lft:reft"; + public const string UserCode = "tkn_lft:usrc"; + } } public static class Statuses diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs index 94bbfcd5..b60d35a3 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs @@ -256,6 +256,17 @@ public interface IOpenIddictApplicationStore where TApplication : /// ValueTask> GetRequirementsAsync(TApplication application, CancellationToken cancellationToken); + /// + /// Retrieves the settings associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the settings associated with the application. + /// + ValueTask> GetSettingsAsync(TApplication application, CancellationToken cancellationToken); + /// /// Instantiates a new application. /// @@ -400,6 +411,16 @@ public interface IOpenIddictApplicationStore where TApplication : /// A that can be used to monitor the asynchronous operation. ValueTask SetRequirementsAsync(TApplication application, ImmutableArray requirements, CancellationToken cancellationToken); + /// + /// Sets the settings associated with an application. + /// + /// The application. + /// The settings associated with the application. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask SetSettingsAsync(TApplication application, + ImmutableDictionary settings, CancellationToken cancellationToken); + /// /// Updates an existing application. /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index b883bd51..267f6978 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -770,6 +770,26 @@ public class OpenIddictApplicationManager : IOpenIddictApplication return Store.GetRequirementsAsync(application, cancellationToken); } + /// + /// Retrieves the settings associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the settings associated with the application. + /// + public virtual ValueTask> GetSettingsAsync( + TApplication application, CancellationToken cancellationToken = default) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + return Store.GetSettingsAsync(application, cancellationToken); + } + /// /// Determines whether a given application has the specified application type. /// @@ -971,6 +991,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication await Store.SetRedirectUrisAsync(application, ImmutableArray.CreateRange( descriptor.RedirectUris.Select(uri => uri.OriginalString)), cancellationToken); await Store.SetRequirementsAsync(application, descriptor.Requirements.ToImmutableArray(), cancellationToken); + await Store.SetSettingsAsync(application, descriptor.Settings.ToImmutableDictionary(), cancellationToken); } /// @@ -1054,6 +1075,12 @@ public class OpenIddictApplicationManager : IOpenIddictApplication descriptor.RedirectUris.Add(value); } + + descriptor.Settings.Clear(); + foreach (var pair in await Store.GetSettingsAsync(application, cancellationToken)) + { + descriptor.Settings.Add(pair.Key, pair.Value); + } } /// @@ -1763,6 +1790,10 @@ public class OpenIddictApplicationManager : IOpenIddictApplication ValueTask> IOpenIddictApplicationManager.GetRequirementsAsync(object application, CancellationToken cancellationToken) => GetRequirementsAsync((TApplication) application, cancellationToken); + /// + ValueTask> IOpenIddictApplicationManager.GetSettingsAsync(object application, CancellationToken cancellationToken) + => GetSettingsAsync((TApplication) application, cancellationToken); + /// ValueTask IOpenIddictApplicationManager.HasApplicationTypeAsync(object application, string type, CancellationToken cancellationToken) => HasApplicationTypeAsync((TApplication) application, type, cancellationToken); diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs index 22bf0f04..ecb1ad51 100644 --- a/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictEntityFrameworkApplication.cs @@ -120,6 +120,12 @@ public class OpenIddictEntityFrameworkApplication [StringSyntax(StringSyntaxAttribute.Json)] public virtual string? Requirements { get; set; } + /// + /// Gets or sets the settings serialized as a JSON object. + /// + [StringSyntax(StringSyntaxAttribute.Json)] + public virtual string? Settings { get; set; } + /// /// Gets the list of the tokens associated with this application. /// diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs index d7d62f63..aedee4a6 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkApplicationStore.cs @@ -634,6 +634,47 @@ public class OpenIddictEntityFrameworkApplicationStore + public virtual ValueTask> GetSettingsAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(application.Settings)) + { + return new(ImmutableDictionary.Create()); + } + + // Note: parsing the stringified settings is an expensive operation. + // To mitigate that, the resulting object is stored in the memory cache. + var key = string.Concat("492ea63f-c26f-47ea-bf9b-b0a0c3d02656", "\x1e", application.Settings); + var settings = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + using var document = JsonDocument.Parse(application.Settings); + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var property in document.RootElement.EnumerateObject()) + { + var value = property.Value.GetString(); + if (string.IsNullOrEmpty(value)) + { + continue; + } + + builder[property.Name] = value; + } + + return builder.ToImmutable(); + })!; + + return new(settings); + } + /// public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) { @@ -988,6 +1029,45 @@ public class OpenIddictEntityFrameworkApplicationStore + public virtual ValueTask SetSettingsAsync(TApplication application, + ImmutableDictionary settings, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (settings is not { Count: > 0 }) + { + application.Settings = null; + + return default; + } + + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Indented = false + }); + + writer.WriteStartObject(); + + foreach (var setting in settings) + { + writer.WritePropertyName(setting.Key); + writer.WriteStringValue(setting.Value); + } + + writer.WriteEndObject(); + writer.Flush(); + + application.Settings = Encoding.UTF8.GetString(stream.ToArray()); + + return default; + } + /// public virtual async ValueTask UpdateAsync(TApplication application, CancellationToken cancellationToken) { diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs index 23cae155..70c1233f 100644 --- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictEntityFrameworkCoreApplication.cs @@ -128,6 +128,12 @@ public class OpenIddictEntityFrameworkCoreApplication + /// Gets or sets the settings serialized as a JSON object. + /// + [StringSyntax(StringSyntaxAttribute.Json)] + public virtual string? Settings { get; set; } + /// /// Gets the list of the tokens associated with this application. /// diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs index fb5a92a4..b9cf4138 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs @@ -676,6 +676,47 @@ public class OpenIddictEntityFrameworkCoreApplicationStore + public virtual ValueTask> GetSettingsAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(application.Settings)) + { + return new(ImmutableDictionary.Create()); + } + + // Note: parsing the stringified settings is an expensive operation. + // To mitigate that, the resulting object is stored in the memory cache. + var key = string.Concat("492ea63f-c26f-47ea-bf9b-b0a0c3d02656", "\x1e", application.Settings); + var settings = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + using var document = JsonDocument.Parse(application.Settings); + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var property in document.RootElement.EnumerateObject()) + { + var value = property.Value.GetString(); + if (string.IsNullOrEmpty(value)) + { + continue; + } + + builder[property.Name] = value; + } + + return builder.ToImmutable(); + })!; + + return new(settings); + } + /// public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) { @@ -1029,6 +1070,45 @@ public class OpenIddictEntityFrameworkCoreApplicationStore + public virtual ValueTask SetSettingsAsync(TApplication application, + ImmutableDictionary settings, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (settings is not { Count: > 0 }) + { + application.Settings = null; + + return default; + } + + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Indented = false + }); + + writer.WriteStartObject(); + + foreach (var setting in settings) + { + writer.WritePropertyName(setting.Key); + writer.WriteStringValue(setting.Value); + } + + writer.WriteEndObject(); + writer.Flush(); + + application.Settings = Encoding.UTF8.GetString(stream.ToArray()); + + return default; + } + /// public virtual async ValueTask UpdateAsync(TApplication application, CancellationToken cancellationToken) { diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbApplication.cs b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbApplication.cs index 30912e5f..f80d572c 100644 --- a/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbApplication.cs +++ b/src/OpenIddict.MongoDb.Models/OpenIddictMongoDbApplication.cs @@ -101,4 +101,11 @@ public class OpenIddictMongoDbApplication /// [BsonElement("requirements"), BsonIgnoreIfNull] public virtual IReadOnlyList? Requirements { get; set; } = ImmutableList.Create(); + + /// + /// Gets or sets the settings associated with the current application. + /// + [BsonElement("settings"), BsonIgnoreIfNull] + public virtual IReadOnlyDictionary? Settings { get; set; } + = ImmutableDictionary.Create(); } diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs index 52eca6dd..00eeea43 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbApplicationStore.cs @@ -386,6 +386,22 @@ public class OpenIddictMongoDbApplicationStore : IOpenIddictApplic return new(application.Requirements.ToImmutableArray()); } + /// + public virtual ValueTask> GetSettingsAsync(TApplication application, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (application.Settings is not { Count: > 0 }) + { + return new(ImmutableDictionary.Create()); + } + + return new(application.Settings.ToImmutableDictionary()); + } + /// public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) { @@ -679,6 +695,27 @@ public class OpenIddictMongoDbApplicationStore : IOpenIddictApplic return default; } + /// + public virtual ValueTask SetSettingsAsync(TApplication application, + ImmutableDictionary settings, CancellationToken cancellationToken) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (settings is not { Count: > 0 }) + { + application.Settings = null; + + return default; + } + + application.Settings = settings; + + return default; + } + /// public virtual async ValueTask UpdateAsync(TApplication application, CancellationToken cancellationToken) { diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 6d83ebc6..5f175608 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -7,9 +7,12 @@ using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics; +using System.Globalization; using System.Security.Claims; using System.Text; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using OpenIddict.Extensions; @@ -1868,19 +1871,34 @@ public static partial class OpenIddictServerHandlers /// public sealed class PrepareAccessTokenPrincipal : IOpenIddictServerHandler { + private readonly IOpenIddictApplicationManager? _applicationManager; + + public PrepareAccessTokenPrincipal(IOpenIddictApplicationManager? applicationManager = null) + => _applicationManager = applicationManager; + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() + .UseScopedHandler(static provider => + { + // Note: the application manager is only resolved if the degraded mode was not enabled to ensure + // invalid core configuration exceptions are not thrown even if the managers were registered. + var options = provider.GetRequiredService>().CurrentValue; + + return options.EnableDegradedMode ? + new PrepareAccessTokenPrincipal() : + new PrepareAccessTokenPrincipal(provider.GetService() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016))); + }) .SetOrder(AttachAuthorization.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// - public ValueTask HandleAsync(ProcessSignInContext context) + public async ValueTask HandleAsync(ProcessSignInContext context) { if (context is null) { @@ -1950,7 +1968,31 @@ public static partial class OpenIddictServerHandlers principal.SetCreationDate(DateTimeOffset.UtcNow); - var lifetime = context.Principal.GetAccessTokenLifetime() ?? context.Options.AccessTokenLifetime; + // If a specific token lifetime was attached to the principal, prefer it over any other value. + var lifetime = context.Principal.GetAccessTokenLifetime(); + + // If the client to which the token is returned is known, use the attached setting if available. + if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId)) + { + if (_applicationManager is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0017)); + + var settings = await _applicationManager.GetSettingsAsync(application); + if (settings.TryGetValue(Settings.TokenLifetimes.AccessToken, out string? setting) && + TimeSpan.TryParse(setting, CultureInfo.InvariantCulture, out var value)) + { + lifetime = value; + } + } + + // Otherwise, fall back to the global value. + lifetime ??= context.Options.AccessTokenLifetime; + if (lifetime.HasValue) { principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); @@ -1978,8 +2020,6 @@ public static partial class OpenIddictServerHandlers } context.AccessTokenPrincipal = principal; - - return default; } } @@ -1989,19 +2029,34 @@ public static partial class OpenIddictServerHandlers /// public sealed class PrepareAuthorizationCodePrincipal : IOpenIddictServerHandler { + private readonly IOpenIddictApplicationManager? _applicationManager; + + public PrepareAuthorizationCodePrincipal(IOpenIddictApplicationManager? applicationManager = null) + => _applicationManager = applicationManager; + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() + .UseScopedHandler(static provider => + { + // Note: the application manager is only resolved if the degraded mode was not enabled to ensure + // invalid core configuration exceptions are not thrown even if the managers were registered. + var options = provider.GetRequiredService>().CurrentValue; + + return options.EnableDegradedMode ? + new PrepareAuthorizationCodePrincipal() : + new PrepareAuthorizationCodePrincipal(provider.GetService() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016))); + }) .SetOrder(PrepareAccessTokenPrincipal.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// - public ValueTask HandleAsync(ProcessSignInContext context) + public async ValueTask HandleAsync(ProcessSignInContext context) { if (context is null) { @@ -2037,7 +2092,31 @@ public static partial class OpenIddictServerHandlers principal.SetCreationDate(DateTimeOffset.UtcNow); - var lifetime = context.Principal.GetAuthorizationCodeLifetime() ?? context.Options.AuthorizationCodeLifetime; + // If a specific token lifetime was attached to the principal, prefer it over any other value. + var lifetime = context.Principal.GetAuthorizationCodeLifetime(); + + // If the client to which the token is returned is known, use the attached setting if available. + if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId)) + { + if (_applicationManager is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0017)); + + var settings = await _applicationManager.GetSettingsAsync(application); + if (settings.TryGetValue(Settings.TokenLifetimes.AuthorizationCode, out string? setting) && + TimeSpan.TryParse(setting, CultureInfo.InvariantCulture, out var value)) + { + lifetime = value; + } + } + + // Otherwise, fall back to the global value. + lifetime ??= context.Options.AuthorizationCodeLifetime; + if (lifetime.HasValue) { principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); @@ -2067,8 +2146,6 @@ public static partial class OpenIddictServerHandlers principal.SetClaim(Claims.Private.Nonce, context.Request.Nonce); context.AuthorizationCodePrincipal = principal; - - return default; } } @@ -2078,19 +2155,34 @@ public static partial class OpenIddictServerHandlers /// public sealed class PrepareDeviceCodePrincipal : IOpenIddictServerHandler { + private readonly IOpenIddictApplicationManager? _applicationManager; + + public PrepareDeviceCodePrincipal(IOpenIddictApplicationManager? applicationManager = null) + => _applicationManager = applicationManager; + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() + .UseScopedHandler(static provider => + { + // Note: the application manager is only resolved if the degraded mode was not enabled to ensure + // invalid core configuration exceptions are not thrown even if the managers were registered. + var options = provider.GetRequiredService>().CurrentValue; + + return options.EnableDegradedMode ? + new PrepareDeviceCodePrincipal() : + new PrepareDeviceCodePrincipal(provider.GetService() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016))); + }) .SetOrder(PrepareAuthorizationCodePrincipal.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// - public ValueTask HandleAsync(ProcessSignInContext context) + public async ValueTask HandleAsync(ProcessSignInContext context) { if (context is null) { @@ -2126,7 +2218,31 @@ public static partial class OpenIddictServerHandlers principal.SetCreationDate(DateTimeOffset.UtcNow); - var lifetime = context.Principal.GetDeviceCodeLifetime() ?? context.Options.DeviceCodeLifetime; + // If a specific token lifetime was attached to the principal, prefer it over any other value. + var lifetime = context.Principal.GetDeviceCodeLifetime(); + + // If the client to which the token is returned is known, use the attached setting if available. + if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId)) + { + if (_applicationManager is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0017)); + + var settings = await _applicationManager.GetSettingsAsync(application); + if (settings.TryGetValue(Settings.TokenLifetimes.DeviceCode, out string? setting) && + TimeSpan.TryParse(setting, CultureInfo.InvariantCulture, out var value)) + { + lifetime = value; + } + } + + // Otherwise, fall back to the global value. + lifetime ??= context.Options.DeviceCodeLifetime; + if (lifetime.HasValue) { principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); @@ -2143,8 +2259,6 @@ public static partial class OpenIddictServerHandlers } context.DeviceCodePrincipal = principal; - - return default; } } @@ -2154,19 +2268,34 @@ public static partial class OpenIddictServerHandlers /// public sealed class PrepareRefreshTokenPrincipal : IOpenIddictServerHandler { + private readonly IOpenIddictApplicationManager? _applicationManager; + + public PrepareRefreshTokenPrincipal(IOpenIddictApplicationManager? applicationManager = null) + => _applicationManager = applicationManager; + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() + .UseScopedHandler(static provider => + { + // Note: the application manager is only resolved if the degraded mode was not enabled to ensure + // invalid core configuration exceptions are not thrown even if the managers were registered. + var options = provider.GetRequiredService>().CurrentValue; + + return options.EnableDegradedMode ? + new PrepareRefreshTokenPrincipal() : + new PrepareRefreshTokenPrincipal(provider.GetService() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016))); + }) .SetOrder(PrepareDeviceCodePrincipal.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// - public ValueTask HandleAsync(ProcessSignInContext context) + public async ValueTask HandleAsync(ProcessSignInContext context) { if (context is null) { @@ -2219,7 +2348,31 @@ public static partial class OpenIddictServerHandlers else { - var lifetime = context.Principal.GetRefreshTokenLifetime() ?? context.Options.RefreshTokenLifetime; + // If a specific token lifetime was attached to the principal, prefer it over any other value. + var lifetime = context.Principal.GetRefreshTokenLifetime(); + + // If the client to which the token is returned is known, use the attached setting if available. + if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId)) + { + if (_applicationManager is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0017)); + + var settings = await _applicationManager.GetSettingsAsync(application); + if (settings.TryGetValue(Settings.TokenLifetimes.RefreshToken, out string? setting) && + TimeSpan.TryParse(setting, CultureInfo.InvariantCulture, out var value)) + { + lifetime = value; + } + } + + // Otherwise, fall back to the global value. + lifetime ??= context.Options.RefreshTokenLifetime; + if (lifetime.HasValue) { principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); @@ -2230,8 +2383,6 @@ public static partial class OpenIddictServerHandlers principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri); context.RefreshTokenPrincipal = principal; - - return default; } } @@ -2241,19 +2392,34 @@ public static partial class OpenIddictServerHandlers /// public sealed class PrepareIdentityTokenPrincipal : IOpenIddictServerHandler { + private readonly IOpenIddictApplicationManager? _applicationManager; + + public PrepareIdentityTokenPrincipal(IOpenIddictApplicationManager? applicationManager = null) + => _applicationManager = applicationManager; + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() + .UseScopedHandler(static provider => + { + // Note: the application manager is only resolved if the degraded mode was not enabled to ensure + // invalid core configuration exceptions are not thrown even if the managers were registered. + var options = provider.GetRequiredService>().CurrentValue; + + return options.EnableDegradedMode ? + new PrepareIdentityTokenPrincipal() : + new PrepareIdentityTokenPrincipal(provider.GetService() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016))); + }) .SetOrder(PrepareRefreshTokenPrincipal.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// - public ValueTask HandleAsync(ProcessSignInContext context) + public async ValueTask HandleAsync(ProcessSignInContext context) { if (context is null) { @@ -2316,7 +2482,31 @@ public static partial class OpenIddictServerHandlers principal.SetCreationDate(DateTimeOffset.UtcNow); - var lifetime = context.Principal.GetIdentityTokenLifetime() ?? context.Options.IdentityTokenLifetime; + // If a specific token lifetime was attached to the principal, prefer it over any other value. + var lifetime = context.Principal.GetIdentityTokenLifetime(); + + // If the client to which the token is returned is known, use the attached setting if available. + if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId)) + { + if (_applicationManager is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0017)); + + var settings = await _applicationManager.GetSettingsAsync(application); + if (settings.TryGetValue(Settings.TokenLifetimes.IdentityToken, out string? setting) && + TimeSpan.TryParse(setting, CultureInfo.InvariantCulture, out var value)) + { + lifetime = value; + } + } + + // Otherwise, fall back to the global value. + lifetime ??= context.Options.IdentityTokenLifetime; + if (lifetime.HasValue) { principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); @@ -2345,8 +2535,6 @@ public static partial class OpenIddictServerHandlers }); context.IdentityTokenPrincipal = principal; - - return default; } } @@ -2356,19 +2544,34 @@ public static partial class OpenIddictServerHandlers /// public sealed class PrepareUserCodePrincipal : IOpenIddictServerHandler { + private readonly IOpenIddictApplicationManager? _applicationManager; + + public PrepareUserCodePrincipal(IOpenIddictApplicationManager? applicationManager = null) + => _applicationManager = applicationManager; + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() + .UseScopedHandler(static provider => + { + // Note: the application manager is only resolved if the degraded mode was not enabled to ensure + // invalid core configuration exceptions are not thrown even if the managers were registered. + var options = provider.GetRequiredService>().CurrentValue; + + return options.EnableDegradedMode ? + new PrepareUserCodePrincipal() : + new PrepareUserCodePrincipal(provider.GetService() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016))); + }) .SetOrder(PrepareIdentityTokenPrincipal.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// - public ValueTask HandleAsync(ProcessSignInContext context) + public async ValueTask HandleAsync(ProcessSignInContext context) { if (context is null) { @@ -2404,7 +2607,31 @@ public static partial class OpenIddictServerHandlers principal.SetCreationDate(DateTimeOffset.UtcNow); - var lifetime = context.Principal.GetUserCodeLifetime() ?? context.Options.UserCodeLifetime; + // If a specific token lifetime was attached to the principal, prefer it over any other value. + var lifetime = context.Principal.GetUserCodeLifetime(); + + // If the client to which the token is returned is known, use the attached setting if available. + if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId)) + { + if (_applicationManager is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0017)); + + var settings = await _applicationManager.GetSettingsAsync(application); + if (settings.TryGetValue(Settings.TokenLifetimes.UserCode, out string? setting) && + TimeSpan.TryParse(setting, CultureInfo.InvariantCulture, out var value)) + { + lifetime = value; + } + } + + // Otherwise, fall back to the global value. + lifetime ??= context.Options.UserCodeLifetime; + if (lifetime.HasValue) { principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); @@ -2417,8 +2644,6 @@ public static partial class OpenIddictServerHandlers principal.SetClaim(Claims.ClientId, context.Request.ClientId); context.UserCodePrincipal = principal; - - return default; } } diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs index ca4ddd27..b6d87ba6 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs @@ -852,6 +852,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); })); options.Services.AddSingleton(CreateScopeManager(mock => @@ -1426,6 +1429,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); }); await using var server = await CreateServerAsync(options => @@ -1681,6 +1687,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasRequirementAsync(application, Requirements.Features.ProofKeyForCodeExchange, It.IsAny())) .ReturnsAsync(false); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); }); await using var server = await CreateServerAsync(options => @@ -1741,6 +1750,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasRequirementAsync(application, Requirements.Features.ProofKeyForCodeExchange, It.IsAny())) .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); }); await using var server = await CreateServerAsync(options => @@ -1801,6 +1813,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasRequirementAsync(application, Requirements.Features.ProofKeyForCodeExchange, It.IsAny())) .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); }); await using var server = await CreateServerAsync(options => @@ -1979,6 +1994,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasPermissionAsync(application, Permissions.Endpoints.Authorization, It.IsAny())) .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); }); await using var server = await CreateServerAsync(options => diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs index a756aa65..79978100 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs @@ -242,6 +242,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); })); options.Services.AddSingleton(CreateTokenManager(mock => @@ -315,6 +318,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); })); options.Services.AddSingleton(CreateTokenManager(mock => diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs index 9923ee07..dfc86190 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs @@ -1887,6 +1887,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasRequirementAsync(application, Requirements.Features.ProofKeyForCodeExchange, It.IsAny())) .ReturnsAsync(false); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); }); await using var server = await CreateServerAsync(options => @@ -1955,6 +1958,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasRequirementAsync(application, Requirements.Features.ProofKeyForCodeExchange, It.IsAny())) .ReturnsAsync(false); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); }); await using var server = await CreateServerAsync(options => @@ -2158,6 +2164,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); })); options.SetDeviceEndpointUris(Array.Empty()); @@ -3304,6 +3313,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); })); options.Services.AddSingleton(CreateTokenManager(mock => @@ -3994,6 +4006,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); })); options.Services.AddSingleton(manager); diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index 01e2cb4a..a119ccc8 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -6,6 +6,8 @@ * the license and the contributors participating to this project. */ +using System.Collections.Immutable; +using System.Globalization; using System.Security.Claims; using System.Text.Json; using Microsoft.Extensions.DependencyInjection; @@ -3100,6 +3102,10 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create() + .SetItem(Settings.TokenLifetimes.AccessToken, TimeSpan.FromMinutes(5).ToString("c", CultureInfo.InvariantCulture))); })); options.Services.AddSingleton(manager); @@ -3292,6 +3298,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetIdAsync(application, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); })); options.Services.AddSingleton(CreateTokenManager(mock => @@ -3369,6 +3378,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetIdAsync(application, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) + .ReturnsAsync(ImmutableDictionary.Create()); })); options.Services.AddSingleton(CreateTokenManager(mock =>