From 675037fe6d02f2162cf7b2bf9437d10a4a289821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 3 Jul 2020 17:39:43 +0200 Subject: [PATCH] Revamp the access token extraction logic used in the validation stack --- .../OpenIddictServerHandlers.cs | 1 + .../OpenIddictValidationAspNetCoreHandlers.cs | 134 +++++++++++++---- .../OpenIddictValidationOwinHandlers.cs | 135 ++++++++++++++---- .../OpenIddictValidationHandlers.cs | 25 ++-- .../OpenIddictValidationService.cs | 70 +++++++++ 5 files changed, 299 insertions(+), 66 deletions(-) diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 55a321a3..7373d134 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -446,6 +446,7 @@ namespace OpenIddict.Server var parameters = context.Options.TokenValidationParameters.Clone(); parameters.ValidIssuer ??= context.Issuer?.AbsoluteUri; + parameters.ValidateIssuer = !string.IsNullOrEmpty(parameters.ValidIssuer); parameters.ValidTypes = context.TokenType switch { // If no specific token type is expected, accept all token types at this stage. diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs index bc7b2a2a..f265b568 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs @@ -9,7 +9,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.IO; -using System.Linq; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; @@ -20,8 +19,8 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlerFilters; using static OpenIddict.Validation.OpenIddictValidationEvents; @@ -37,8 +36,13 @@ namespace OpenIddict.Validation.AspNetCore * Request top-level processing: */ InferIssuerFromHost.Descriptor, - ExtractGetOrPostRequest.Descriptor, - ExtractAccessToken.Descriptor, + + /* + * Authentication processing: + */ + ExtractAccessTokenFromAuthorizationHeader.Descriptor, + ExtractAccessTokenFromBodyForm.Descriptor, + ExtractAccessTokenFromQueryString.Descriptor, /* * Challenge processing: @@ -132,19 +136,19 @@ namespace OpenIddict.Validation.AspNetCore } /// - /// Contains the logic responsible of extracting OpenID Connect requests from GET or POST HTTP requests. + /// Contains the logic responsible of extracting the access token from the standard HTTP Authorization header. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// - public class ExtractGetOrPostRequest : IOpenIddictValidationHandler + public class ExtractAccessTokenFromAuthorizationHeader : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() + = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() - .SetOrder(InferIssuerFromHost.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(int.MinValue + 50_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -155,13 +159,19 @@ namespace OpenIddict.Validation.AspNetCore /// /// A that can be used to monitor the asynchronous operation. /// - public async ValueTask HandleAsync([NotNull] ProcessRequestContext context) + public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } + // If a token was already resolved, don't overwrite it. + if (!string.IsNullOrEmpty(context.Token)) + { + return default; + } + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, // this may indicate that the request was incorrectly processed by another server stack. var request = context.Transaction.GetHttpRequest(); @@ -170,38 +180,99 @@ namespace OpenIddict.Validation.AspNetCore throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); } - if (HttpMethods.IsGet(request.Method)) + // Resolve the access token from the standard Authorization header. + // See https://tools.ietf.org/html/rfc6750#section-2.1 for more information. + string header = request.Headers[HeaderNames.Authorization]; + if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + context.Token = header.Substring("Bearer ".Length); + context.TokenType = TokenTypeHints.AccessToken; + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the access token from the standard access_token form parameter. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ExtractAccessTokenFromBodyForm : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ExtractAccessTokenFromAuthorizationHeader.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a token was already resolved, don't overwrite it. + if (!string.IsNullOrEmpty(context.Token)) { - context.Request = new OpenIddictRequest(request.Query); + return; } - else if (HttpMethods.IsPost(request.Method) && !string.IsNullOrEmpty(request.ContentType) && - request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (string.IsNullOrEmpty(request.ContentType) || + !request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { - context.Request = new OpenIddictRequest(await request.ReadFormAsync(request.HttpContext.RequestAborted)); + return; } - else + // Resolve the access token from the standard access_token form parameter. + // See https://tools.ietf.org/html/rfc6750#section-2.2 for more information. + var form = await request.ReadFormAsync(request.HttpContext.RequestAborted); + if (form.TryGetValue(Parameters.AccessToken, out StringValues token)) { - context.Request = new OpenIddictRequest(); + context.Token = token; + context.TokenType = TokenTypeHints.AccessToken; + + return; } } } /// - /// Contains the logic responsible of extracting an access token from the standard HTTP Authorization header. + /// Contains the logic responsible of extracting the access token from the standard access_token query parameter. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// - public class ExtractAccessToken : IOpenIddictValidationHandler + public class ExtractAccessTokenFromQueryString : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() + = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() - .SetOrder(ExtractGetOrPostRequest.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(ExtractAccessTokenFromBodyForm.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -212,13 +283,19 @@ namespace OpenIddict.Validation.AspNetCore /// /// A that can be used to monitor the asynchronous operation. /// - public ValueTask HandleAsync([NotNull] ProcessRequestContext context) + public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } + // If a token was already resolved, don't overwrite it. + if (!string.IsNullOrEmpty(context.Token)) + { + return default; + } + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, // this may indicate that the request was incorrectly processed by another server stack. var request = context.Transaction.GetHttpRequest(); @@ -227,15 +304,16 @@ namespace OpenIddict.Validation.AspNetCore throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); } - string header = request.Headers[HeaderNames.Authorization]; - if (string.IsNullOrEmpty(header) || !header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + // Resolve the access token from the standard access_token query parameter. + // See https://tools.ietf.org/html/rfc6750#section-2.3 for more information. + if (request.Query.TryGetValue(Parameters.AccessToken, out StringValues token)) { + context.Token = token; + context.TokenType = TokenTypeHints.AccessToken; + return default; } - // Attach the access token to the request message. - context.Request.AccessToken = header.Substring("Bearer ".Length); - return default; } } diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs index a1738790..d237f9fe 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs @@ -17,7 +17,6 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Owin.Security; -using OpenIddict.Abstractions; using Owin; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Validation.OpenIddictValidationEvents; @@ -34,8 +33,13 @@ namespace OpenIddict.Validation.Owin * Request top-level processing: */ InferIssuerFromHost.Descriptor, - ExtractGetOrPostRequest.Descriptor, - ExtractAccessToken.Descriptor, + + /* + * Authentication processing: + */ + ExtractAccessTokenFromAuthorizationHeader.Descriptor, + ExtractAccessTokenFromBodyForm.Descriptor, + ExtractAccessTokenFromQueryString.Descriptor, /* * Challenge processing: @@ -129,19 +133,19 @@ namespace OpenIddict.Validation.Owin } /// - /// Contains the logic responsible of extracting OpenID Connect requests from GET or POST HTTP requests. + /// Contains the logic responsible of extracting the access token from the standard HTTP Authorization header. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. /// - public class ExtractGetOrPostRequest : IOpenIddictValidationHandler + public class ExtractAccessTokenFromAuthorizationHeader : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() + = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() - .SetOrder(InferIssuerFromHost.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(int.MinValue + 50_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -152,13 +156,19 @@ namespace OpenIddict.Validation.Owin /// /// A that can be used to monitor the asynchronous operation. /// - public async ValueTask HandleAsync([NotNull] ProcessRequestContext context) + public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } + // If a token was already resolved, don't overwrite it. + if (!string.IsNullOrEmpty(context.Token)) + { + return default; + } + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, // this may indicate that the request was incorrectly processed by another server stack. var request = context.Transaction.GetOwinRequest(); @@ -167,39 +177,100 @@ namespace OpenIddict.Validation.Owin throw new InvalidOperationException("The OWIN request cannot be resolved."); } - if (string.Equals(request.Method, "GET", StringComparison.OrdinalIgnoreCase)) + // Resolve the access token from the standard Authorization header. + // See https://tools.ietf.org/html/rfc6750#section-2.1 for more information. + string header = request.Headers["Authorization"]; + if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + context.Token = header.Substring("Bearer ".Length); + context.TokenType = TokenTypeHints.AccessToken; + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the access token from the standard access_token form parameter. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ExtractAccessTokenFromBodyForm : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ExtractAccessTokenFromAuthorizationHeader.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a token was already resolved, don't overwrite it. + if (!string.IsNullOrEmpty(context.Token)) { - context.Request = new OpenIddictRequest(request.Query); + return; } - else if (string.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) && - !string.IsNullOrEmpty(request.ContentType) && - request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + if (string.IsNullOrEmpty(request.ContentType) || + !request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { - context.Request = new OpenIddictRequest(await request.ReadFormAsync()); + return; } - else + // Resolve the access token from the standard access_token form parameter. + // See https://tools.ietf.org/html/rfc6750#section-2.2 for more information. + var form = await request.ReadFormAsync(); + string token = form[Parameters.AccessToken]; + if (!string.IsNullOrEmpty(token)) { - context.Request = new OpenIddictRequest(); + context.Token = token; + context.TokenType = TokenTypeHints.AccessToken; + + return; } } } /// - /// Contains the logic responsible of extracting an access token from the standard HTTP Authorization header. + /// Contains the logic responsible of extracting the access token from the standard access_token query parameter. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. /// - public class ExtractAccessToken : IOpenIddictValidationHandler + public class ExtractAccessTokenFromQueryString : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() + = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() - .SetOrder(ExtractGetOrPostRequest.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(ExtractAccessTokenFromBodyForm.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -210,13 +281,19 @@ namespace OpenIddict.Validation.Owin /// /// A that can be used to monitor the asynchronous operation. /// - public ValueTask HandleAsync([NotNull] ProcessRequestContext context) + public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } + // If a token was already resolved, don't overwrite it. + if (!string.IsNullOrEmpty(context.Token)) + { + return default; + } + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, // this may indicate that the request was incorrectly processed by another server stack. var request = context.Transaction.GetOwinRequest(); @@ -225,15 +302,17 @@ namespace OpenIddict.Validation.Owin throw new InvalidOperationException("The OWIN request cannot be resolved."); } - string header = request.Headers["Authorization"]; - if (string.IsNullOrEmpty(header) || !header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + // Resolve the access token from the standard access_token query parameter. + // See https://tools.ietf.org/html/rfc6750#section-2.3 for more information. + string token = request.Query[Parameters.AccessToken]; + if (!string.IsNullOrEmpty(token)) { + context.Token = token; + context.TokenType = TokenTypeHints.AccessToken; + return default; } - // Attach the access token to the request message. - context.Request.AccessToken = header.Substring("Bearer ".Length); - return default; } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index a62610eb..6d031a2f 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -30,7 +30,7 @@ namespace OpenIddict.Validation /* * Authentication processing: */ - ValidateAccessTokenParameter.Descriptor, + ValidateToken.Descriptor, ValidateReferenceTokenIdentifier.Descriptor, ValidateIdentityModelToken.Descriptor, IntrospectToken.Descriptor, @@ -52,16 +52,16 @@ namespace OpenIddict.Validation .AddRange(Introspection.DefaultHandlers); /// - /// Contains the logic responsible of validating the access token resolved from the current request. + /// Contains the logic responsible of ensuring a token was correctly resolved from the context. /// - public class ValidateAccessTokenParameter : IOpenIddictValidationHandler + public class ValidateToken : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -80,18 +80,22 @@ namespace OpenIddict.Validation throw new ArgumentNullException(nameof(context)); } - if (string.IsNullOrEmpty(context.Request.AccessToken)) + // Note: unlike the equivalent event in the server stack, authentication can be triggered for + // arbitrary requests (typically, API endpoints that are not owned by the validation stack). + // As such, the token is not directly resolved from the request, that may be null at this stage. + // Instead, the token is expected to be populated by one or multiple handlers provided by the host. + // + // Note: this event can also be triggered by the validation service to validate an arbitrary token. + + if (string.IsNullOrEmpty(context.Token)) { context.Reject( error: Errors.MissingToken, - description: "The access token is missing."); + description: "The security token is missing."); return default; } - context.Token = context.Request.AccessToken; - context.TokenType = TokenTypeHints.AccessToken; - return default; } } @@ -121,7 +125,7 @@ namespace OpenIddict.Validation .AddFilter() .AddFilter() .UseScopedHandler() - .SetOrder(ValidateAccessTokenParameter.Descriptor.Order + 1_000) + .SetOrder(ValidateToken.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -223,6 +227,7 @@ namespace OpenIddict.Validation // OpenID Connect server configuration (that can be static or retrieved using discovery). var parameters = context.Options.TokenValidationParameters.Clone(); parameters.ValidIssuer ??= configuration.Issuer ?? context.Issuer?.AbsoluteUri; + parameters.ValidateIssuer = !string.IsNullOrEmpty(parameters.ValidIssuer); // Combine the signing keys registered statically in the token validation parameters // with the signing keys resolved from the OpenID Connect server configuration. diff --git a/src/OpenIddict.Validation/OpenIddictValidationService.cs b/src/OpenIddict.Validation/OpenIddictValidationService.cs index e019a133..b75cff1b 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationService.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationService.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Validation.OpenIddictValidationEvents; namespace OpenIddict.Validation @@ -568,5 +569,74 @@ namespace OpenIddict.Validation } } } + + /// + /// Validates the specified access token and returns the principal extracted from the token. + /// + /// The access token to validate. + /// The that can be used to abort the operation. + /// The principal containing the claims extracted from the token. + public async ValueTask ValidateAccessTokenAsync( + [NotNull] string token, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(token)) + { + throw new ArgumentException("The access token cannot be null or empty.", nameof(token)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Note: this service is registered as a singleton service. As such, it cannot + // directly depend on scoped services like the validation provider. To work around + // this limitation, a scope is manually created for each method to this service. + var scope = _provider.CreateScope(); + + // Note: a try/finally block is deliberately used here to ensure the service scope + // can be disposed of asynchronously if it implements IAsyncDisposable. + try + { + var dispatcher = scope.ServiceProvider.GetRequiredService(); + var factory = scope.ServiceProvider.GetRequiredService(); + var transaction = await factory.CreateTransactionAsync(); + + var context = new ProcessAuthenticationContext(transaction) + { + Token = token, + TokenType = TokenTypeHints.AccessToken + }; + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) + { + var message = new StringBuilder() + .AppendLine("An error occurred while validating the access token.") + .AppendFormat("Error: {0}", context.Error ?? "(not available)") + .AppendLine() + .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") + .AppendLine() + .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") + .ToString(); + + throw new OpenIddictExceptions.GenericException(message, + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Principal; + } + + finally + { + if (scope is IAsyncDisposable disposable) + { + await disposable.DisposeAsync(); + } + + else + { + scope.Dispose(); + } + } + } } }