From 91d5592d55d78b2cf7c00ec76379fc1a7d9caa92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Wed, 30 May 2018 18:16:51 +0200 Subject: [PATCH] Migrate to the latest aspnet-contrib packages --- build/dependencies.props | 4 +- ...OpenIddictServerProvider.Authentication.cs | 25 +- .../OpenIddictServerProvider.Exchange.cs | 25 +- .../OpenIddictServerBuilder.cs | 134 +-- .../Internal/OpenIddictValidationEvents.cs | 95 ++ .../Internal/OpenIddictValidationHandler.cs | 254 +----- .../OpenIddictValidationInitializer.cs | 12 +- .../OpenIddictValidationBuilder.cs | 67 +- .../OpenIddictValidationExtensions.cs | 1 + .../OpenIddictValidationOptions.cs | 27 + ...ddictServerProviderTests.Authentication.cs | 32 +- .../OpenIddictServerProviderTests.Exchange.cs | 26 +- .../OpenIddictServerBuilderTests.cs | 69 +- .../OpenIddictValidationEventsTests.cs | 338 +++++++ .../OpenIddictValidationInitializerTests.cs | 110 +++ .../OpenIddict.Validation.Tests.csproj | 1 + .../OpenIddictValidationBuilderTests.cs | 190 ++++ .../OpenIddictValidationHandlerTests.cs | 837 ------------------ 18 files changed, 1068 insertions(+), 1179 deletions(-) create mode 100644 src/OpenIddict.Validation/Internal/OpenIddictValidationEvents.cs create mode 100644 test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationEventsTests.cs create mode 100644 test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationInitializerTests.cs create mode 100644 test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs delete mode 100755 test/OpenIddict.Validation.Tests/OpenIddictValidationHandlerTests.cs diff --git a/build/dependencies.props b/build/dependencies.props index 98ef196b..1d29b305 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,8 +1,8 @@ - 2.0.0-rc2-final - 2.0.0-rc2-final + 2.0.0-rc3-0307 + 2.0.0-rc3-1371 2.0.0 4.4.0 3.0.2 diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs index f64b0062..2caaa150 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs @@ -5,6 +5,8 @@ */ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; @@ -178,16 +180,27 @@ namespace OpenIddict.Server } // Validates scopes, unless scope validation was explicitly disabled. - foreach (var scope in context.Request.GetScopes()) + if (options.EnableScopeValidation) { - if (options.EnableScopeValidation && !options.Scopes.Contains(scope) && - await _scopeManager.FindByNameAsync(scope) == null) + var scopes = new HashSet(context.Request.GetScopes(), StringComparer.Ordinal); + scopes.ExceptWith(options.Scopes); + + // If all the specified scopes are registered in the options, avoid making a database lookup. + if (scopes.Count != 0) + { + foreach (var scope in await _scopeManager.FindByNamesAsync(scopes.ToImmutableArray())) + { + scopes.Remove(await _scopeManager.GetNameAsync(scope)); + } + } + + // If at least one scope was not recognized, return an error. + if (scopes.Count != 0) { - _logger.LogError("The authorization request was rejected because an " + - "unregistered scope was specified: {Scope}.", scope); + _logger.LogError("The authentication request was rejected because invalid scopes were specified: {Scopes}.", scopes); context.Reject( - error: OpenIdConnectConstants.Errors.InvalidRequest, + error: OpenIdConnectConstants.Errors.InvalidScope, description: "The specified 'scope' parameter is not valid."); return; diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs index d6874e2b..9e716257 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs @@ -5,6 +5,8 @@ */ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; @@ -76,16 +78,27 @@ namespace OpenIddict.Server } // Validates scopes, unless scope validation was explicitly disabled. - foreach (var scope in context.Request.GetScopes()) + if (options.EnableScopeValidation) { - if (options.EnableScopeValidation && !options.Scopes.Contains(scope) && - await _scopeManager.FindByNameAsync(scope) == null) + var scopes = new HashSet(context.Request.GetScopes(), StringComparer.Ordinal); + scopes.ExceptWith(options.Scopes); + + // If all the specified scopes are registered in the options, avoid making a database lookup. + if (scopes.Count != 0) + { + foreach (var scope in await _scopeManager.FindByNamesAsync(scopes.ToImmutableArray())) + { + scopes.Remove(await _scopeManager.GetNameAsync(scope)); + } + } + + // If at least one scope was not recognized, return an error. + if (scopes.Count != 0) { - _logger.LogError("The token request was rejected because an " + - "unregistered scope was specified: {Scope}.", scope); + _logger.LogError("The token request was rejected because invalid scopes were specified: {Scopes}.", scopes); context.Reject( - error: OpenIdConnectConstants.Errors.InvalidRequest, + error: OpenIdConnectConstants.Errors.InvalidScope, description: "The specified 'scope' parameter is not valid."); return; diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index 2ad99d38..86ad1ba9 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -489,73 +489,6 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.UserinfoEndpointPath = path); } - /// - /// Makes client identification mandatory so that token and revocation - /// requests that don't specify a client_id are automatically rejected. - /// Note: enabling this option doesn't prevent public clients from using - /// the token and revocation endpoints, but specifying a client_id is required. - /// - /// The . - public OpenIddictServerBuilder RequireClientIdentification() - => Configure(options => options.RequireClientIdentification = true); - - /// - /// Sets the access token lifetime, after which client applications must retrieve - /// 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. - /// - /// The access token lifetime. - /// The . - public OpenIddictServerBuilder SetAccessTokenLifetime(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. - /// - /// The authorization code lifetime. - /// The . - public OpenIddictServerBuilder SetAuthorizationCodeLifetime(TimeSpan lifetime) - => Configure(options => options.AuthorizationCodeLifetime = lifetime); - - /// - /// Sets the identity token lifetime, after which client - /// applications should refuse processing identity tokens. - /// - /// The identity token lifetime. - /// The . - public OpenIddictServerBuilder SetIdentityTokenLifetime(TimeSpan lifetime) - => Configure(options => options.IdentityTokenLifetime = lifetime); - - /// - /// Sets the refresh token lifetime, after which client applications must get - /// 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. - /// - /// The refresh token lifetime. - /// The . - public OpenIddictServerBuilder SetRefreshTokenLifetime(TimeSpan lifetime) - => Configure(options => options.RefreshTokenLifetime = lifetime); - - /// - /// Sets the issuer address, which is used as the base address - /// for the endpoint URIs returned from the discovery endpoint. - /// - /// The issuer address. - /// The . - public OpenIddictServerBuilder SetIssuer([NotNull] Uri address) - { - if (address == null) - { - throw new ArgumentNullException(nameof(address)); - } - - return Configure(options => options.Issuer = address); - } - /// /// Registers the specified claims as supported claims so /// they can be returned as part of the discovery document. @@ -654,6 +587,73 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.Scopes.UnionWith(scopes)); } + /// + /// Makes client identification mandatory so that token and revocation + /// requests that don't specify a client_id are automatically rejected. + /// Note: enabling this option doesn't prevent public clients from using + /// the token and revocation endpoints, but specifying a client_id is required. + /// + /// The . + public OpenIddictServerBuilder RequireClientIdentification() + => Configure(options => options.RequireClientIdentification = true); + + /// + /// Sets the access token lifetime, after which client applications must retrieve + /// 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. + /// + /// The access token lifetime. + /// The . + public OpenIddictServerBuilder SetAccessTokenLifetime(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. + /// + /// The authorization code lifetime. + /// The . + public OpenIddictServerBuilder SetAuthorizationCodeLifetime(TimeSpan lifetime) + => Configure(options => options.AuthorizationCodeLifetime = lifetime); + + /// + /// Sets the identity token lifetime, after which client + /// applications should refuse processing identity tokens. + /// + /// The identity token lifetime. + /// The . + public OpenIddictServerBuilder SetIdentityTokenLifetime(TimeSpan lifetime) + => Configure(options => options.IdentityTokenLifetime = lifetime); + + /// + /// Sets the refresh token lifetime, after which client applications must get + /// 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. + /// + /// The refresh token lifetime. + /// The . + public OpenIddictServerBuilder SetRefreshTokenLifetime(TimeSpan lifetime) + => Configure(options => options.RefreshTokenLifetime = lifetime); + + /// + /// Sets the issuer address, which is used as the base address + /// for the endpoint URIs returned from the discovery endpoint. + /// + /// The issuer address. + /// The . + public OpenIddictServerBuilder SetIssuer([NotNull] Uri address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + return Configure(options => options.Issuer = address); + } + /// /// Configures OpenIddict to use a specific data protection provider /// instead of relying on the default instance provided by the DI container. diff --git a/src/OpenIddict.Validation/Internal/OpenIddictValidationEvents.cs b/src/OpenIddict.Validation/Internal/OpenIddictValidationEvents.cs new file mode 100644 index 00000000..7190ec13 --- /dev/null +++ b/src/OpenIddict.Validation/Internal/OpenIddictValidationEvents.cs @@ -0,0 +1,95 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using System.Text; +using System.Threading.Tasks; +using AspNet.Security.OAuth.Validation; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using OpenIddict.Abstractions; + +namespace OpenIddict.Validation +{ + /// + /// Provides the logic necessary to extract, validate and handle OAuth2 requests. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class OpenIddictValidationEvents : OAuthValidationEvents + { + public override async Task DecryptToken([NotNull] DecryptTokenContext context) + { + var options = (OpenIddictValidationOptions) context.Options; + if (options.UseReferenceTokens) + { + // Note: the token manager is deliberately not injected using constructor injection + // to allow using the validation handler without having to register the core services. + var manager = context.HttpContext.RequestServices.GetService(); + if (manager == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling reference tokens support.") + .Append("To register the OpenIddict core services, use 'services.AddOpenIddict().AddCore()'.") + .ToString()); + } + + // Retrieve the token entry from the database. If it + // cannot be found, assume the token is not valid. + var token = await manager.FindByReferenceIdAsync(context.Token); + if (token == null) + { + context.Fail("Authentication failed because the access token cannot be found in the database."); + + return; + } + + // Extract the encrypted payload from the token. If it's null or empty, + // assume the token is not a reference token and consider it as invalid. + var payload = await manager.GetPayloadAsync(token); + if (string.IsNullOrEmpty(payload)) + { + context.Fail("Authentication failed because the access token is not a reference token."); + + return; + } + + var ticket = context.DataFormat.Unprotect(payload); + if (ticket == null) + { + context.Fail("Authentication failed because the reference token cannot be decrypted. " + + "This may indicate that the token entry is corrupted or tampered."); + + return; + } + + // Dynamically set the creation and expiration dates. + ticket.Properties.IssuedUtc = await manager.GetCreationDateAsync(token); + ticket.Properties.ExpiresUtc = await manager.GetExpirationDateAsync(token); + + // Restore the token and authorization identifiers attached with the database entry. + ticket.Properties.SetProperty(OpenIddictConstants.Properties.TokenId, await manager.GetIdAsync(token)); + ticket.Properties.SetProperty(OpenIddictConstants.Properties.AuthorizationId, + await manager.GetAuthorizationIdAsync(token)); + + context.Principal = ticket.Principal; + context.Properties = ticket.Properties; + context.Success(); + } + + await base.DecryptToken(context); + } + + public void Import([NotNull] OAuthValidationEvents events) + { + OnApplyChallenge = events.ApplyChallenge; + OnCreateTicket = events.CreateTicket; + OnDecryptToken = events.DecryptToken; + OnRetrieveToken = events.RetrieveToken; + OnValidateToken = events.ValidateToken; + } + } +} diff --git a/src/OpenIddict.Validation/Internal/OpenIddictValidationHandler.cs b/src/OpenIddict.Validation/Internal/OpenIddictValidationHandler.cs index 792caefb..a14f3cd4 100644 --- a/src/OpenIddict.Validation/Internal/OpenIddictValidationHandler.cs +++ b/src/OpenIddict.Validation/Internal/OpenIddictValidationHandler.cs @@ -6,19 +6,14 @@ using System; using System.ComponentModel; -using System.Security.Claims; using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; using AspNet.Security.OAuth.Validation; using JetBrains.Annotations; using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; -using Newtonsoft.Json.Linq; -using OpenIddict.Abstractions; namespace OpenIddict.Validation { @@ -34,251 +29,34 @@ namespace OpenIddict.Validation { } - protected override async Task HandleAuthenticateAsync() + protected override async Task InitializeEventsAsync() { - if (!Options.UseReferenceTokens) - { - return await base.HandleAuthenticateAsync(); - } - - var context = new RetrieveTokenContext(Context, Scheme, Options); - await Events.RetrieveToken(context); + await base.InitializeEventsAsync(); - if (context.Result != null) + // If an application provider instance or type was specified, import the application provider events. + if (Options.ApplicationEvents != null || Options.ApplicationEventsType != null) { - Logger.LogInformation("The default authentication handling was skipped from user code."); - - return context.Result; - } - - var token = context.Token; - - if (string.IsNullOrEmpty(token)) - { - // Try to retrieve the access token from the authorization header. - string header = Request.Headers[HeaderNames.Authorization]; - if (string.IsNullOrEmpty(header)) - { - Logger.LogDebug("Authentication was skipped because no bearer token was received."); - - return AuthenticateResult.NoResult(); - } - - // Ensure that the authorization header contains the mandatory "Bearer" scheme. - // See https://tools.ietf.org/html/rfc6750#section-2.1 - if (!header.StartsWith(OAuthValidationConstants.Schemes.Bearer + ' ', StringComparison.OrdinalIgnoreCase)) + // Resolve the user provider from the options or from the services container. + var events = Options.ApplicationEvents; + if (events == null) { - Logger.LogDebug("Authentication was skipped because an incompatible " + - "scheme was used in the 'Authorization' header."); - - return AuthenticateResult.NoResult(); + events = Context.RequestServices.GetService(Options.ApplicationEventsType) as OAuthValidationEvents; } - // Extract the token from the authorization header. - token = header.Substring(OAuthValidationConstants.Schemes.Bearer.Length + 1).Trim(); - - if (string.IsNullOrEmpty(token)) + if (events == null) { - Logger.LogDebug("Authentication was skipped because the bearer token " + - "was missing from the 'Authorization' header."); - - return AuthenticateResult.NoResult(); + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The application events cannot be resolved from the dependency injection container. ") + .Append("Make sure they are correctly registered in 'ConfigureServices(IServiceCollection services)'.") + .ToString()); } - } - - // Try to unprotect the token and return an error - // if the ticket can't be decrypted or validated. - var result = await CreateTicketAsync(token); - if (!result.Succeeded) - { - Context.Features.Set(new OAuthValidationFeature - { - Error = new OAuthValidationError - { - Error = OAuthValidationConstants.Errors.InvalidToken, - ErrorDescription = "The access token is not valid." - } - }); - - return result; - } - - // Ensure that the authentication ticket is still valid. - var ticket = result.Ticket; - if (ticket.Properties.ExpiresUtc.HasValue && - ticket.Properties.ExpiresUtc.Value < Options.SystemClock.UtcNow) - { - Context.Features.Set(new OAuthValidationFeature - { - Error = new OAuthValidationError - { - Error = OAuthValidationConstants.Errors.InvalidToken, - ErrorDescription = "The access token is no longer valid." - } - }); - - return AuthenticateResult.Fail("Authentication failed because the access token was expired."); - } - - // Ensure that the access token was issued - // to be used with this resource server. - if (!ValidateAudience(ticket)) - { - Context.Features.Set(new OAuthValidationFeature - { - Error = new OAuthValidationError - { - Error = OAuthValidationConstants.Errors.InvalidToken, - ErrorDescription = "The access token is not valid for this resource server." - } - }); - - return AuthenticateResult.Fail("Authentication failed because the access token " + - "was not valid for this resource server."); - } - - var notification = new ValidateTokenContext(Context, Scheme, Options, ticket); - await Events.ValidateToken(notification); - - if (notification.Result != null) - { - Logger.LogInformation("The default authentication handling was skipped from user code."); - - return notification.Result; - } - // Optimization: avoid allocating a new AuthenticationTicket - // if the principal/properties instances were not replaced. - if (ReferenceEquals(notification.Principal, ticket.Principal) && - ReferenceEquals(notification.Properties, ticket.Properties)) - { - return AuthenticateResult.Success(ticket); + // Update the main events to invoke the user provider's event handlers. + Events.Import(events); } - - return AuthenticateResult.Success(new AuthenticationTicket( - notification.Principal, notification.Properties, Scheme.Name)); - } - - private bool ValidateAudience(AuthenticationTicket ticket) - { - // If no explicit audience has been configured, - // skip the default audience validation. - if (Options.Audiences.Count == 0) - { - return true; - } - - // Extract the audiences from the authentication ticket. - var audiences = ticket.Properties.GetProperty(OAuthValidationConstants.Properties.Audiences); - if (string.IsNullOrEmpty(audiences)) - { - return false; - } - - // Ensure that the authentication ticket contains one of the registered audiences. - foreach (var audience in JArray.Parse(audiences).Values()) - { - if (Options.Audiences.Contains(audience)) - { - return true; - } - } - - return false; - } - - private async Task CreateTicketAsync(string payload) - { - // Note: the token manager is deliberately not injected using constructor injection - // to allow using the validation handler without having to register the core services. - var manager = Context.RequestServices.GetService(); - if (manager == null) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The core services must be registered when enabling reference tokens support.") - .Append("To register the OpenIddict core services, use 'services.AddOpenIddict().AddCore()'.") - .ToString()); - } - - // Retrieve the token entry from the database. If it - // cannot be found, assume the token is not valid. - var token = await manager.FindByReferenceIdAsync(payload); - if (token == null) - { - return AuthenticateResult.Fail("Authentication failed because the access token cannot be found in the database."); - } - - // Extract the encrypted payload from the token. If it's null or empty, - // assume the token is not a reference token and consider it as invalid. - var ciphertext = await manager.GetPayloadAsync(token); - if (string.IsNullOrEmpty(ciphertext)) - { - return AuthenticateResult.Fail("Authentication failed because the access token is not a reference token."); - } - - var ticket = Options.AccessTokenFormat.Unprotect(ciphertext); - if (ticket == null) - { - return AuthenticateResult.Fail( - "Authentication failed because the reference token cannot be decrypted. " + - "This may indicate that the token entry is corrupted or tampered."); - } - - // Dynamically set the creation and expiration dates. - ticket.Properties.IssuedUtc = await manager.GetCreationDateAsync(token); - ticket.Properties.ExpiresUtc = await manager.GetExpirationDateAsync(token); - - // Restore the token and authorization identifiers attached with the database entry. - ticket.Properties.SetProperty(OpenIddictConstants.Properties.TokenId, await manager.GetIdAsync(token)); - ticket.Properties.SetProperty(OpenIddictConstants.Properties.AuthorizationId, - await manager.GetAuthorizationIdAsync(token)); - - if (Options.SaveToken) - { - // Store the access token in the authentication ticket. - ticket.Properties.StoreTokens(new[] - { - new AuthenticationToken { Name = OAuthValidationConstants.Properties.Token, Value = payload } - }); - } - - // Resolve the primary identity associated with the principal. - var identity = (ClaimsIdentity) ticket.Principal.Identity; - - // Copy the scopes extracted from the authentication ticket to the - // ClaimsIdentity to make them easier to retrieve from application code. - var scopes = ticket.Properties.GetProperty(OAuthValidationConstants.Properties.Scopes); - if (!string.IsNullOrEmpty(scopes)) - { - foreach (var scope in JArray.Parse(scopes).Values()) - { - identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Scope, scope)); - } - } - - var notification = new CreateTicketContext(Context, Scheme, Options, ticket); - await Events.CreateTicket(notification); - - if (notification.Result != null) - { - Logger.LogInformation("The default authentication handling was skipped from user code."); - - return notification.Result; - } - - // Optimization: avoid allocating a new AuthenticationTicket - // if the principal/properties instances were not replaced. - if (ReferenceEquals(notification.Principal, ticket.Principal) && - ReferenceEquals(notification.Properties, ticket.Properties)) - { - return AuthenticateResult.Success(ticket); - } - - return AuthenticateResult.Success(new AuthenticationTicket( - notification.Principal, notification.Properties, Scheme.Name)); } - private new OAuthValidationEvents Events => (OAuthValidationEvents) base.Events; + private new OpenIddictValidationEvents Events => (OpenIddictValidationEvents) base.Events; private new OpenIddictValidationOptions Options => (OpenIddictValidationOptions) base.Options; } diff --git a/src/OpenIddict.Validation/Internal/OpenIddictValidationInitializer.cs b/src/OpenIddict.Validation/Internal/OpenIddictValidationInitializer.cs index 8c4d618c..1da101f6 100644 --- a/src/OpenIddict.Validation/Internal/OpenIddictValidationInitializer.cs +++ b/src/OpenIddict.Validation/Internal/OpenIddictValidationInitializer.cs @@ -49,9 +49,17 @@ namespace OpenIddict.Validation throw new ArgumentException("The options instance name cannot be null or empty.", nameof(name)); } - if (options.Events == null) + if (options.ApplicationEventsType != null) { - options.Events = new OAuthValidationEvents(); + if (options.ApplicationEvents != null) + { + throw new InvalidOperationException("Application events cannot be registered when a type is specified."); + } + + if (!typeof(OAuthValidationEvents).IsAssignableFrom(options.ApplicationEventsType)) + { + throw new InvalidOperationException("Application events must inherit from OAuthValidationEvents."); + } } if (options.DataProtectionProvider == null) diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs index 30444bad..c29a7f7c 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs @@ -7,8 +7,10 @@ using System; using System.ComponentModel; using System.Linq; +using AspNet.Security.OAuth.Validation; using JetBrains.Annotations; using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.DependencyInjection.Extensions; using OpenIddict.Validation; namespace Microsoft.Extensions.DependencyInjection @@ -77,6 +79,57 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.Audiences.UnionWith(audiences)); } + /// + /// Registers application-specific OAuth2 validation events that are automatically + /// invoked for each request handled by the OpenIddict validation handler. + /// + /// The custom service. + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictValidationBuilder RegisterEvents([NotNull] OAuthValidationEvents events) + { + if (events == null) + { + throw new ArgumentNullException(nameof(events)); + } + + return Configure(options => options.ApplicationEvents = events); + } + + /// + /// Registers application-specific OAuth2 validation events that are automatically + /// invoked for each request handled by the OpenIddict validation handler. + /// + /// The type of the custom service. + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictValidationBuilder RegisterEvents() where TEvents : OAuthValidationEvents + => RegisterEvents(typeof(TEvents)); + + /// + /// Registers application-specific OAuth2 validation events that are automatically + /// invoked for each request handled by the OpenIddict validation handler. + /// + /// The type of the custom service. + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictValidationBuilder RegisterEvents([NotNull] Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (!typeof(OAuthValidationEvents).IsAssignableFrom(type)) + { + throw new ArgumentException("The specified type is invalid.", nameof(type)); + } + + Services.TryAddScoped(type); + + return Configure(options => options.ApplicationEventsType = type); + } + /// /// Configures OpenIddict not to return the authentication error /// details as part of the standard WWW-Authenticate response header. @@ -100,13 +153,6 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.Realm = realm); } - /// - /// Configures the OpenIddict validation handler to use reference tokens. - /// - /// The . - public OpenIddictValidationBuilder UseReferenceTokens() - => Configure(options => options.UseReferenceTokens = true); - /// /// Configures OpenIddict to use a specific data protection provider /// instead of relying on the default instance provided by the DI container. @@ -122,5 +168,12 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.DataProtectionProvider = provider); } + + /// + /// Configures the OpenIddict validation handler to use reference tokens. + /// + /// The . + public OpenIddictValidationBuilder UseReferenceTokens() + => Configure(options => options.UseReferenceTokens = true); } } \ No newline at end of file diff --git a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs index 6c1d9647..b983bbf1 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs @@ -31,6 +31,7 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.AddAuthentication(); + builder.Services.TryAddScoped(); builder.Services.TryAddScoped(); // Note: TryAddEnumerable() is used here to ensure the initializer is only registered once. diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index 3409cd77..43bccdf7 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -4,12 +4,39 @@ * the license and the contributors participating to this project. */ +using System; using AspNet.Security.OAuth.Validation; namespace OpenIddict.Validation { + /// + /// Provides various settings needed to configure the OpenIddict validation handler. + /// public class OpenIddictValidationOptions : OAuthValidationOptions { + /// + /// Creates a new instance of the class. + /// + public OpenIddictValidationOptions() + { + Events = null; + EventsType = typeof(OpenIddictValidationEvents); + } + + /// + /// Gets or sets the user-provided that the OpenIddict + /// validation handler invokes to enable developer control over the entire authentication process. + /// + public OAuthValidationEvents ApplicationEvents { get; set; } + + /// + /// Gets or sets the user-provided provider type that the OpenIddict validation handler + /// instantiates to enable developer control over the entire authentication process. When this + /// property is set, the provider is resolved from the services container. If the provider is not + /// guaranteed to be thread-safe, registering it as a scoped dependency is strongly recommended. + /// + public Type ApplicationEventsType { get; set; } + /// /// Gets or sets a boolean indicating whether reference tokens are used. /// diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Authentication.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Authentication.cs index 1599a740..adef3266 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Authentication.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Authentication.cs @@ -4,6 +4,7 @@ * the license and the contributors participating to this project. */ +using System.Collections.Immutable; using System.IO; using System.Security.Cryptography; using System.Threading; @@ -197,6 +198,14 @@ namespace OpenIddict.Server.Tests // Arrange var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateScopeManager(instance => + { + instance.Setup(mock => mock.FindByNamesAsync( + It.Is>(scopes => scopes.Length == 1 && scopes[0] == "unregistered_scope"), + It.IsAny())) + .ReturnsAsync(ImmutableArray.Create()); + })); + builder.EnableScopeValidation(); }); @@ -212,7 +221,7 @@ namespace OpenIddict.Server.Tests }); // Assert - Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal(OpenIdConnectConstants.Errors.InvalidScope, response.Error); Assert.Equal("The specified 'scope' parameter is not valid.", response.ErrorDescription); } @@ -275,10 +284,17 @@ namespace OpenIddict.Server.Tests public async Task ValidateAuthorizationRequest_RequestIsValidatedWhenRegisteredScopeIsSpecified() { // Arrange + var scope = new OpenIddictScope(); + var manager = CreateScopeManager(instance => { - instance.Setup(mock => mock.FindByNameAsync("registered_scope", It.IsAny())) - .ReturnsAsync(new OpenIddictScope()); + instance.Setup(mock => mock.FindByNamesAsync( + It.Is>(scopes => scopes.Length == 1 && scopes[0] == "scope_registered_in_database"), + It.IsAny())) + .ReturnsAsync(ImmutableArray.Create(scope)); + + instance.Setup(mock => mock.GetNameAsync(scope, It.IsAny())) + .Returns(new ValueTask("scope_registered_in_database")); }); var server = CreateAuthorizationServer(builder => @@ -305,10 +321,16 @@ namespace OpenIddict.Server.Tests .ReturnsAsync(true); instance.Setup(mock => mock.HasPermissionAsync(application, - OpenIddictConstants.Permissions.Prefixes.Scope + "registered_scope", It.IsAny())) + OpenIddictConstants.Permissions.Prefixes.Scope + "scope_registered_in_database", It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Prefixes.Scope + "scope_registered_in_options", It.IsAny())) .ReturnsAsync(true); })); + builder.RegisterScopes("scope_registered_in_options"); + builder.EnableScopeValidation(); builder.Services.AddSingleton(manager); }); @@ -322,7 +344,7 @@ namespace OpenIddict.Server.Tests Nonce = "n-0S6_WzA2Mj", RedirectUri = "http://www.fabrikam.com/path", ResponseType = OpenIdConnectConstants.ResponseTypes.Token, - Scope = "registered_scope" + Scope = "scope_registered_in_database scope_registered_in_options" }); // Assert diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs index 6258422f..db08b3fa 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs @@ -108,6 +108,14 @@ namespace OpenIddict.Server.Tests // Arrange var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateScopeManager(instance => + { + instance.Setup(mock => mock.FindByNamesAsync( + It.Is>(scopes => scopes.Length == 1 && scopes[0] == "unregistered_scope"), + It.IsAny())) + .ReturnsAsync(ImmutableArray.Create()); + })); + builder.EnableScopeValidation(); }); @@ -123,7 +131,7 @@ namespace OpenIddict.Server.Tests }); // Assert - Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal(OpenIdConnectConstants.Errors.InvalidScope, response.Error); Assert.Equal("The specified 'scope' parameter is not valid.", response.ErrorDescription); } @@ -159,14 +167,24 @@ namespace OpenIddict.Server.Tests public async Task ValidateTokenRequest_RequestIsValidatedWhenRegisteredScopeIsSpecified() { // Arrange + var scope = new OpenIddictScope(); + var manager = CreateScopeManager(instance => { - instance.Setup(mock => mock.FindByNameAsync("registered_scope", It.IsAny())) - .ReturnsAsync(new OpenIddictScope()); + instance.Setup(mock => mock.FindByNamesAsync( + It.Is>(scopes => scopes.Length == 1 && scopes[0] == "scope_registered_in_database"), + It.IsAny())) + .ReturnsAsync(ImmutableArray.Create(scope)); + + instance.Setup(mock => mock.GetNameAsync(scope, It.IsAny())) + .Returns(new ValueTask("scope_registered_in_database")); }); var server = CreateAuthorizationServer(builder => { + builder.EnableScopeValidation(); + builder.RegisterScopes("scope_registered_in_options"); + builder.Services.AddSingleton(manager); }); @@ -178,7 +196,7 @@ namespace OpenIddict.Server.Tests GrantType = OpenIdConnectConstants.GrantTypes.Password, Username = "johndoe", Password = "A3ddj3w", - Scope = "registered_scope" + Scope = "scope_registered_in_database scope_registered_in_options" }); // Assert diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs index 242a0dd0..760d6c24 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs +++ b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs @@ -8,6 +8,7 @@ using System; using System.IdentityModel.Tokens.Jwt; using System.Reflection; using AspNet.Security.OpenIdConnect.Primitives; +using AspNet.Security.OpenIdConnect.Server; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -583,6 +584,69 @@ namespace OpenIddict.Server.Tests Assert.Equal(new Uri("http://www.fabrikam.com/"), options.Issuer); } + [Fact] + public void RegisterProvider_ProviderIsAttached() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.RegisterProvider(new OpenIdConnectServerProvider()); + + var options = GetOptions(services); + + // Assert + Assert.NotNull(options.ApplicationProvider); + } + + [Fact] + public void RegisterProvider_ThrowsAnExceptionForInvalidProviderType() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(delegate + { + return builder.RegisterProvider(typeof(object)); + }); + + Assert.Equal("type", exception.ParamName); + Assert.StartsWith("The specified type is invalid.", exception.Message); + } + + [Fact] + public void RegisterProvider_ProviderTypeIsAttached() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.RegisterProvider(typeof(OpenIdConnectServerProvider)); + + var options = GetOptions(services); + + // Assert + Assert.Equal(typeof(OpenIdConnectServerProvider), options.ApplicationProviderType); + } + + [Fact] + public void RegisterProvider_ProviderIsRegistered() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.RegisterProvider(typeof(OpenIdConnectServerProvider)); + + // Assert + Assert.Contains(services, service => service.ServiceType == typeof(OpenIdConnectServerProvider)); + } + [Fact] public void RegisterClaims_ClaimsAreAdded() { @@ -677,10 +741,5 @@ namespace OpenIddict.Server.Tests var options = provider.GetRequiredService>(); return options.Get(OpenIddictServerDefaults.AuthenticationScheme); } - - public class OpenIddictApplication { } - public class OpenIddictAuthorization { } - public class OpenIddictScope { } - public class OpenIddictToken { } } } diff --git a/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationEventsTests.cs b/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationEventsTests.cs new file mode 100644 index 00000000..41d83e7e --- /dev/null +++ b/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationEventsTests.cs @@ -0,0 +1,338 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AspNet.Security.OAuth.Validation; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using OpenIddict.Core; +using Xunit; + +namespace OpenIddict.Validation.Tests +{ + public class OpenIddictValidationEventsTests + { + [Fact] + public async Task DecryptToken_ThrowsAnExceptionWhenTokenManagerIsNotRegistered() + { + // Arrange + var server = CreateResourceServer(builder => + { + builder.Services.RemoveAll(typeof(IOpenIddictTokenManager)); + }); + + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "/"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-reference-token-id"); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.SendAsync(request); + }); + + Assert.Equal(new StringBuilder() + .AppendLine("The core services must be registered when enabling reference tokens support.") + .Append("To register the OpenIddict core services, use 'services.AddOpenIddict().AddCore()'.") + .ToString(), exception.Message); + } + + [Fact] + public async Task DecryptToken_ReturnsFailedResultForUnknownReferenceToken() + { + // Arrange + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByReferenceIdAsync("invalid-reference-token-id", It.IsAny())) + .ReturnsAsync(value: null); + }); + + var server = CreateResourceServer(builder => + { + builder.Services.AddSingleton(manager); + }); + + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "/"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "invalid-reference-token-id"); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("invalid-reference-token-id", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task DecryptToken_ReturnsFailedResultForNonReferenceToken() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) + .Returns(new ValueTask(result: null)); + }); + + var server = CreateResourceServer(builder => + { + builder.Services.AddSingleton(manager); + }); + + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "/"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-reference-token-id"); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task DecryptToken_ReturnsFailedResultForInvalidReferenceTokenPayload() + { + // Arrange + var token = new OpenIddictToken(); + + var format = new Mock>(); + format.Setup(mock => mock.Unprotect("invalid-reference-token-payload")) + .Returns(value: null); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) + .Returns(new ValueTask("invalid-reference-token-payload")); + }); + + var server = CreateResourceServer(builder => + { + builder.Services.AddSingleton(manager); + builder.Configure(options => options.AccessTokenFormat = format.Object); + }); + + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "/"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-reference-token-id"); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny()), Times.Once()); + format.Verify(mock => mock.Unprotect("invalid-reference-token-payload"), Times.Once()); + } + + [Fact] + public async Task DecryptToken_ReturnsValidResultForValidReferenceToken() + { + // Arrange + var token = new OpenIddictToken(); + + var format = new Mock>(); + format.Setup(mock => mock.Unprotect("valid-reference-token-payload")) + .Returns(delegate + { + var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); + identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); + + return new AuthenticationTicket( + new ClaimsPrincipal(identity), + OpenIddictValidationDefaults.AuthenticationScheme); + }); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) + .Returns(new ValueTask("valid-reference-token-payload")); + + instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) + .Returns(new ValueTask(new DateTimeOffset(2018, 01, 01, 00, 00, 00, TimeSpan.Zero))); + + instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) + .Returns(new ValueTask(new DateTimeOffset(2918, 01, 01, 00, 00, 00, TimeSpan.Zero))); + }); + + var server = CreateResourceServer(builder => + { + builder.Services.AddSingleton(manager); + builder.Configure(options => options.AccessTokenFormat = format.Object); + }); + + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "/ticket"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-reference-token-id"); + + // Act + var response = await client.SendAsync(request); + + var ticket = JObject.Parse(await response.Content.ReadAsStringAsync()); + var properties = (from property in ticket.Value("Properties") + select new + { + Name = property.Value("Name"), + Value = property.Value("Value") + }).ToDictionary(property => property.Name, property => property.Value); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + Assert.Equal( + new DateTimeOffset(2018, 01, 01, 00, 00, 00, TimeSpan.Zero), + DateTimeOffset.Parse(properties[".issued"], CultureInfo.InvariantCulture)); + Assert.Equal( + new DateTimeOffset(2918, 01, 01, 00, 00, 00, TimeSpan.Zero), + DateTimeOffset.Parse(properties[".expires"], CultureInfo.InvariantCulture)); + + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetCreationDateAsync(token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetExpirationDateAsync(token, It.IsAny()), Times.Once()); + format.Verify(mock => mock.Unprotect("valid-reference-token-payload"), Times.Once()); + } + + private static TestServer CreateResourceServer(Action configuration = null) + { + var builder = new WebHostBuilder(); + builder.UseEnvironment("Testing"); + + builder.ConfigureLogging(options => options.AddDebug()); + + builder.ConfigureServices(services => + { + services.AddOpenIddict() + .AddCore(options => + { + options.SetDefaultTokenEntity(); + options.Services.AddSingleton(CreateTokenManager()); + }) + + .AddValidation(options => + { + options.UseReferenceTokens(); + + // Note: overriding the default data protection provider is not necessary for the tests to pass, + // but is useful to ensure unnecessary keys are not persisted in testing environments, which also + // helps make the unit tests run faster, as no registry or disk access is required in this case. + options.UseDataProtectionProvider(new EphemeralDataProtectionProvider()); + + // Run the configuration delegate + // registered by the unit tests. + configuration?.Invoke(options); + }); + }); + + builder.Configure(app => + { + app.Map("/ticket", map => map.Run(async context => + { + var result = await context.AuthenticateAsync(OpenIddictValidationDefaults.AuthenticationScheme); + if (result.Principal == null) + { + await context.ChallengeAsync(OpenIddictValidationDefaults.AuthenticationScheme); + + return; + } + + context.Response.ContentType = "application/json"; + + // Return the authentication ticket as a JSON object. + await context.Response.WriteAsync(JsonConvert.SerializeObject(new + { + Claims = from claim in result.Principal.Claims + select new { claim.Type, claim.Value }, + + Properties = from property in result.Properties.Items + select new { Name = property.Key, property.Value } + })); + })); + + app.Run(async context => + { + var result = await context.AuthenticateAsync(OpenIddictValidationDefaults.AuthenticationScheme); + if (result.Principal == null) + { + await context.ChallengeAsync(OpenIddictValidationDefaults.AuthenticationScheme); + + return; + } + + var subject = result.Principal.FindFirst(OAuthValidationConstants.Claims.Subject)?.Value; + if (string.IsNullOrEmpty(subject)) + { + await context.ChallengeAsync(OpenIddictValidationDefaults.AuthenticationScheme); + + return; + } + + await context.Response.WriteAsync(subject); + }); + }); + + return new TestServer(builder); + } + + private static OpenIddictTokenManager CreateTokenManager( + Action>> configuration = null) + { + var manager = new Mock>( + Mock.Of(), + Mock.Of>>(), + Mock.Of>()); + + configuration?.Invoke(manager); + + return manager.Object; + } + + public class OpenIddictToken { } + } +} diff --git a/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationInitializerTests.cs b/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationInitializerTests.cs new file mode 100644 index 00000000..44b815d1 --- /dev/null +++ b/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationInitializerTests.cs @@ -0,0 +1,110 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Threading.Tasks; +using AspNet.Security.OAuth.Validation; +using AspNet.Security.OpenIdConnect.Client; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace OpenIddict.Validation.Tests +{ + public class OpenIddictValidationInitializerTests + { + [Fact] + public async Task PostConfigure_ThrowsAnExceptionWhenApplicationEventsTypeAndInstanceAreProvided() + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.Configure(options => + { + options.ApplicationEvents = new OAuthValidationEvents(); + options.ApplicationEventsType = typeof(OAuthValidationEvents); + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + // Assert + Assert.Equal("Application events cannot be registered when a type is specified.", exception.Message); + } + + [Fact] + public async Task PostConfigure_ThrowsAnExceptionForInvalidApplicationEventsType() + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.Configure(options => options.ApplicationEventsType = typeof(object)); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + // Assert + Assert.Equal("Application events must inherit from OAuthValidationEvents.", exception.Message); + } + + private static TestServer CreateAuthorizationServer(Action configuration = null) + { + var builder = new WebHostBuilder(); + + builder.UseEnvironment("Testing"); + + builder.ConfigureLogging(options => options.AddDebug()); + + builder.ConfigureServices(services => + { + services.AddAuthentication(); + services.AddOptions(); + services.AddDistributedMemoryCache(); + + services.AddOpenIddict() + .AddCore(options => + { + options.SetDefaultApplicationEntity() + .SetDefaultAuthorizationEntity() + .SetDefaultScopeEntity() + .SetDefaultTokenEntity(); + }) + + .AddValidation(options => configuration?.Invoke(options)); + }); + + builder.Configure(app => + { + app.UseAuthentication(); + + app.Run(context => context.ChallengeAsync(OpenIddictValidationDefaults.AuthenticationScheme)); + }); + + return new TestServer(builder); + } + + public class OpenIddictApplication { } + public class OpenIddictAuthorization { } + public class OpenIddictScope { } + public class OpenIddictToken { } + } +} diff --git a/test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj b/test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj index 0ccaf453..34e21003 100755 --- a/test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj +++ b/test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs b/test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs new file mode 100644 index 00000000..d1750617 --- /dev/null +++ b/test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs @@ -0,0 +1,190 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using AspNet.Security.OAuth.Validation; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace OpenIddict.Validation.Tests +{ + public class OpenIddictValidationBuilderTests + { + [Fact] + public void Configure_OptionsAreCorrectlyAmended() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.Configure(configuration => configuration.ClaimsIssuer = "custom_issuer"); + + var options = GetOptions(services); + + // Assert + Assert.Equal("custom_issuer", options.ClaimsIssuer); + } + + [Fact] + public void AddAudiences_AudiencesAreAdded() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.AddAudiences("Fabrikam", "Contoso"); + + var options = GetOptions(services); + + // Assert + Assert.Equal(new[] { "Fabrikam", "Contoso" }, options.Audiences); + } + + [Fact] + public void RegisterEvents_EventsAreAttached() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.RegisterEvents(new OAuthValidationEvents()); + + var options = GetOptions(services); + + // Assert + Assert.NotNull(options.ApplicationEvents); + } + + [Fact] + public void RegisterEvents_ThrowsAnExceptionForInvalidEventsType() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(delegate + { + return builder.RegisterEvents(typeof(object)); + }); + + Assert.Equal("type", exception.ParamName); + Assert.StartsWith("The specified type is invalid.", exception.Message); + } + + [Fact] + public void RegisterEvents_EventsTypeIsAttached() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.RegisterEvents(typeof(OAuthValidationEvents)); + + var options = GetOptions(services); + + // Assert + Assert.Equal(typeof(OAuthValidationEvents), options.ApplicationEventsType); + } + + [Fact] + public void RegisterEvents_EventsAreRegistered() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.RegisterEvents(typeof(OAuthValidationEvents)); + + // Assert + Assert.Contains(services, service => service.ServiceType == typeof(OAuthValidationEvents)); + } + + [Fact] + public void RemoveErrorDetails_IncludeErrorDetailsIsSetToFalse() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.RemoveErrorDetails(); + + var options = GetOptions(services); + + // Assert + Assert.False(options.IncludeErrorDetails); + } + + [Fact] + public void SetRealm_RealmIsReplaced() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetRealm("custom_realm"); + + var options = GetOptions(services); + + // Assert + Assert.Equal("custom_realm", options.Realm); + } + + [Fact] + public void UseDataProtectionProvider_DefaultProviderIsReplaced() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.UseDataProtectionProvider(new EphemeralDataProtectionProvider()); + + var options = GetOptions(services); + + // Assert + Assert.IsType(options.DataProtectionProvider); + } + + [Fact] + public void UseReferenceTokens_ReferenceTokensAreEnabled() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.UseReferenceTokens(); + + var options = GetOptions(services); + + // Assert + Assert.True(options.UseReferenceTokens); + } + + private static IServiceCollection CreateServices() + => new ServiceCollection().AddOptions(); + + private static OpenIddictValidationBuilder CreateBuilder(IServiceCollection services) + => new OpenIddictValidationBuilder(services); + + private static OpenIddictValidationOptions GetOptions(IServiceCollection services) + { + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>(); + return options.Get(OpenIddictValidationDefaults.AuthenticationScheme); + } + } +} diff --git a/test/OpenIddict.Validation.Tests/OpenIddictValidationHandlerTests.cs b/test/OpenIddict.Validation.Tests/OpenIddictValidationHandlerTests.cs deleted file mode 100755 index b327802f..00000000 --- a/test/OpenIddict.Validation.Tests/OpenIddictValidationHandlerTests.cs +++ /dev/null @@ -1,837 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Security.Claims; -using System.Threading.Tasks; -using AspNet.Security.OAuth.Validation; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Moq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OpenIddict.Abstractions; -using OpenIddict.Core; -using Xunit; - -namespace OpenIddict.Validation.Tests -{ - public class OpenIddictValidationHandlerTests - { - [Fact] - public async Task HandleAuthenticateAsync_InvalidTokenCausesInvalidAuthentication() - { - // Arrange - var server = CreateResourceServer(); - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "invalid-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [Fact] - public async Task HandleAuthenticateAsync_ValidTokenAllowsSuccessfulAuthentication() - { - // Arrange - var server = CreateResourceServer(); - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task HandleAuthenticateAsync_MissingAudienceCausesInvalidAuthentication() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.AddAudiences("http://www.fabrikam.com/"); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [Fact] - public async Task HandleAuthenticateAsync_InvalidAudienceCausesInvalidAuthentication() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.AddAudiences("http://www.fabrikam.com/"); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token-with-single-audience"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [Fact] - public async Task HandleAuthenticateAsync_ValidAudienceAllowsSuccessfulAuthentication() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.AddAudiences("http://www.fabrikam.com/"); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token-with-multiple-audiences"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task HandleAuthenticateAsync_AnyMatchingAudienceCausesSuccessfulAuthentication() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.AddAudiences("http://www.contoso.com/"); - builder.AddAudiences("http://www.fabrikam.com/"); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token-with-single-audience"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task HandleAuthenticateAsync_MultipleMatchingAudienceCausesSuccessfulAuthentication() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.AddAudiences("http://www.contoso.com/"); - builder.AddAudiences("http://www.fabrikam.com/"); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token-with-multiple-audiences"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task HandleAuthenticateAsync_ExpiredTicketCausesInvalidAuthentication() - { - // Arrange - var server = CreateResourceServer(); - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "expired-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [Fact] - public async Task HandleAuthenticateAsync_AuthenticationTicketContainsRequiredClaims() - { - // Arrange - var server = CreateResourceServer(); - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/ticket"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token-with-scopes"); - - // Act - var response = await client.SendAsync(request); - - var ticket = JObject.Parse(await response.Content.ReadAsStringAsync()); - var claims = from claim in ticket.Value("Claims") - select new - { - Type = claim.Value(nameof(Claim.Type)), - Value = claim.Value(nameof(Claim.Value)) - }; - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - Assert.Contains(claims, claim => claim.Type == OAuthValidationConstants.Claims.Subject && - claim.Value == "Fabrikam"); - - Assert.Contains(claims, claim => claim.Type == OAuthValidationConstants.Claims.Scope && - claim.Value == "C54A8F5E-0387-43F4-BA43-FD4B50DC190D"); - - Assert.Contains(claims, claim => claim.Type == OAuthValidationConstants.Claims.Scope && - claim.Value == "5C57E3BD-9EFB-4224-9AB8-C8C5E009FFD7"); - } - - [Fact] - public async Task HandleAuthenticateAsync_AuthenticationTicketContainsRequiredProperties() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.Configure(options => options.SaveToken = true); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/ticket"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); - - // Act - var response = await client.SendAsync(request); - - var ticket = JObject.Parse(await response.Content.ReadAsStringAsync()); - var properties = from claim in ticket.Value("Properties") - select new - { - Name = claim.Value("Name"), - Value = claim.Value("Value") - }; - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - Assert.Contains(properties, property => property.Name == ".Token.access_token" && - property.Value == "valid-token"); - } - - [Fact] - public async Task HandleAuthenticateAsync_InvalidReplacedTokenCausesInvalidAuthentication() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.Configure(options => options.Events.OnRetrieveToken = context => - { - context.Token = "invalid-token"; - - return Task.FromResult(0); - }); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [Fact] - public async Task HandleAuthenticateAsync_ValidReplacedTokenCausesSuccessfulAuthentication() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.Configure(options => options.Events.OnRetrieveToken = context => - { - context.Token = "valid-token"; - - return Task.FromResult(0); - }); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "invalid-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task HandleAuthenticateAsync_FailFromReceiveTokenCausesInvalidAuthentication() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.Configure(options => options.Events.OnRetrieveToken = context => - { - context.Fail(new Exception()); - - return Task.FromResult(0); - }); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [Fact] - public async Task HandleAuthenticateAsync_NoResultFromReceiveTokenCauseInvalidAuthentication() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.Configure(options => options.Events.OnRetrieveToken = context => - { - context.NoResult(); - - return Task.FromResult(0); - }); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [Fact] - public async Task HandleAuthenticateAsync_SuccessFromReceiveTokenCauseSuccessfulAuthentication() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.Configure(options => options.Events.OnRetrieveToken = context => - { - var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); - identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); - - context.Principal = new ClaimsPrincipal(identity); - context.Success(); - - return Task.FromResult(0); - }); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "invalid-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task HandleAuthenticateAsync_FailFromValidateTokenCausesInvalidAuthentication() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.Configure(options => options.Events.OnValidateToken = context => - { - context.Fail(new Exception()); - - return Task.FromResult(0); - }); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [Fact] - public async Task HandleAuthenticateAsync_NoResultFromValidateTokenCauseInvalidAuthentication() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.Configure(options => options.Events.OnValidateToken = context => - { - context.NoResult(); - - return Task.FromResult(0); - }); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - - [Fact] - public async Task HandleAuthenticateAsync_SuccessFromValidateTokenCauseSuccessfulAuthentication() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.Configure(options => options.Events.OnValidateToken = context => - { - var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); - identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Contoso")); - - context.Principal = new ClaimsPrincipal(identity); - context.Success(); - - return Task.FromResult(0); - }); - }); - - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("Contoso", await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task HandleUnauthorizedAsync_ErrorDetailsAreResolvedFromChallengeContext() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.RemoveErrorDetails(); - builder.SetRealm("global_realm"); - - builder.Configure(options => options.Events.OnApplyChallenge = context => - { - // Assert - Assert.Equal("custom_error", context.Error); - Assert.Equal("custom_error_description", context.ErrorDescription); - Assert.Equal("custom_error_uri", context.ErrorUri); - Assert.Equal("custom_realm", context.Realm); - Assert.Equal("custom_scope", context.Scope); - - return Task.FromResult(0); - }); - }); - - var client = server.CreateClient(); - - // Act - var response = await client.GetAsync("/challenge"); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - Assert.Equal(@"Bearer realm=""custom_realm"", error=""custom_error"", error_description=""custom_error_description"", " + - @"error_uri=""custom_error_uri"", scope=""custom_scope""", response.Headers.WwwAuthenticate.ToString()); - } - - [Theory] - [InlineData("invalid-token", OAuthValidationConstants.Errors.InvalidToken, "The access token is not valid.")] - [InlineData("expired-token", OAuthValidationConstants.Errors.InvalidToken, "The access token is no longer valid.")] - public async Task HandleUnauthorizedAsync_ErrorDetailsAreInferredFromAuthenticationFailure( - string token, string error, string description) - { - // Arrange - var server = CreateResourceServer(); - var client = server.CreateClient(); - - var request = new HttpRequestMessage(HttpMethod.Get, "/"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - Assert.Equal($@"Bearer error=""{error}"", error_description=""{description}""", - response.Headers.WwwAuthenticate.ToString()); - } - - [Fact] - public async Task HandleUnauthorizedAsync_ApplyChallenge_AllowsHandlingResponse() - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.Configure(options => options.Events.OnApplyChallenge = context => - { - context.HandleResponse(); - context.HttpContext.Response.Headers["X-Custom-Authentication-Header"] = "Bearer"; - - return Task.FromResult(0); - }); - }); - - var client = server.CreateClient(); - - // Act - var response = await client.GetAsync("/challenge"); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Empty(response.Headers.WwwAuthenticate); - Assert.Equal(new[] { "Bearer" }, response.Headers.GetValues("X-Custom-Authentication-Header")); - } - - [Theory] - [InlineData(null, null, null, null, null, "Bearer")] - [InlineData("custom_error", null, null, null, null, @"Bearer error=""custom_error""")] - [InlineData(null, "custom_error_description", null, null, null, @"Bearer error_description=""custom_error_description""")] - [InlineData(null, null, "custom_error_uri", null, null, @"Bearer error_uri=""custom_error_uri""")] - [InlineData(null, null, null, "custom_realm", null, @"Bearer realm=""custom_realm""")] - [InlineData(null, null, null, null, "custom_scope", @"Bearer scope=""custom_scope""")] - [InlineData("custom_error", "custom_error_description", "custom_error_uri", "custom_realm", "custom_scope", - @"Bearer realm=""custom_realm"", error=""custom_error"", " + - @"error_description=""custom_error_description"", " + - @"error_uri=""custom_error_uri"", scope=""custom_scope""")] - public async Task HandleUnauthorizedAsync_ReturnsExpectedWwwAuthenticateHeader( - string error, string description, string uri, string realm, string scope, string header) - { - // Arrange - var server = CreateResourceServer(builder => - { - builder.Configure(options => options.Events.OnApplyChallenge = context => - { - context.Error = error; - context.ErrorDescription = description; - context.ErrorUri = uri; - context.Realm = realm; - context.Scope = scope; - - return Task.FromResult(0); - }); - }); - - var client = server.CreateClient(); - - // Act - var response = await client.GetAsync("/challenge"); - - // Assert - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - Assert.Equal(header, response.Headers.WwwAuthenticate.ToString()); - } - - private static TestServer CreateResourceServer(Action configuration = null) - { - var format = new Mock>(MockBehavior.Strict); - - format.Setup(mock => mock.Unprotect(It.Is(token => token == "invalid-token"))) - .Returns(value: null); - - format.Setup(mock => mock.Unprotect(It.Is(token => token == "valid-token"))) - .Returns(delegate - { - var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); - identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); - - var properties = new AuthenticationProperties(); - - return new AuthenticationTicket(new ClaimsPrincipal(identity), - properties, OpenIddictValidationDefaults.AuthenticationScheme); - }); - - format.Setup(mock => mock.Unprotect(It.Is(token => token == "valid-token-with-scopes"))) - .Returns(delegate - { - var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); - identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); - - var properties = new AuthenticationProperties(); - properties.Items[OAuthValidationConstants.Properties.Scopes] = - @"[""C54A8F5E-0387-43F4-BA43-FD4B50DC190D"",""5C57E3BD-9EFB-4224-9AB8-C8C5E009FFD7""]"; - - return new AuthenticationTicket(new ClaimsPrincipal(identity), - properties, OpenIddictValidationDefaults.AuthenticationScheme); - }); - - format.Setup(mock => mock.Unprotect(It.Is(token => token == "valid-token-with-single-audience"))) - .Returns(delegate - { - var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); - identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); - - var properties = new AuthenticationProperties(new Dictionary - { - [OAuthValidationConstants.Properties.Audiences] = @"[""http://www.contoso.com/""]" - }); - - return new AuthenticationTicket(new ClaimsPrincipal(identity), - properties, OpenIddictValidationDefaults.AuthenticationScheme); - }); - - format.Setup(mock => mock.Unprotect(It.Is(token => token == "valid-token-with-multiple-audiences"))) - .Returns(delegate - { - var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); - identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); - - var properties = new AuthenticationProperties(new Dictionary - { - [OAuthValidationConstants.Properties.Audiences] = @"[""http://www.contoso.com/"",""http://www.fabrikam.com/""]" - }); - - return new AuthenticationTicket(new ClaimsPrincipal(identity), - properties, OpenIddictValidationDefaults.AuthenticationScheme); - }); - - format.Setup(mock => mock.Unprotect(It.Is(token => token == "expired-token"))) - .Returns(delegate - { - var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); - identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); - - var properties = new AuthenticationProperties(); - properties.ExpiresUtc = DateTimeOffset.UtcNow - TimeSpan.FromDays(1); - - return new AuthenticationTicket(new ClaimsPrincipal(identity), - properties, OpenIddictValidationDefaults.AuthenticationScheme); - }); - - var builder = new WebHostBuilder(); - builder.UseEnvironment("Testing"); - - builder.ConfigureLogging(options => options.AddDebug()); - - builder.ConfigureServices(services => - { - services.AddOpenIddict() - .AddCore(options => - { - options.SetDefaultApplicationEntity() - .SetDefaultAuthorizationEntity() - .SetDefaultScopeEntity() - .SetDefaultTokenEntity(); - - // Replace the default OpenIddict managers. - options.Services.AddSingleton(CreateApplicationManager()); - options.Services.AddSingleton(CreateAuthorizationManager()); - options.Services.AddSingleton(CreateScopeManager()); - options.Services.AddSingleton(CreateTokenManager()); - }) - - .AddValidation(options => - { - options.Configure(settings => settings.AccessTokenFormat = format.Object); - - // Note: overriding the default data protection provider is not necessary for the tests to pass, - // but is useful to ensure unnecessary keys are not persisted in testing environments, which also - // helps make the unit tests run faster, as no registry or disk access is required in this case. - options.UseDataProtectionProvider(new EphemeralDataProtectionProvider()); - - // Run the configuration delegate - // registered by the unit tests. - configuration?.Invoke(options); - }); - }); - - builder.Configure(app => - { - app.Map("/ticket", map => map.Run(async context => - { - var result = await context.AuthenticateAsync(OpenIddictValidationDefaults.AuthenticationScheme); - if (result.Principal == null) - { - await context.ChallengeAsync(); - - return; - } - - context.Response.ContentType = "application/json"; - - // Return the authentication ticket as a JSON object. - await context.Response.WriteAsync(JsonConvert.SerializeObject(new - { - Claims = from claim in result.Principal.Claims - select new { claim.Type, claim.Value }, - - Properties = from property in result.Properties.Items - select new { Name = property.Key, property.Value } - })); - })); - - app.Map("/challenge", map => map.Run(context => - { - var properties = new AuthenticationProperties(new Dictionary - { - [OAuthValidationConstants.Properties.Error] = "custom_error", - [OAuthValidationConstants.Properties.ErrorDescription] = "custom_error_description", - [OAuthValidationConstants.Properties.ErrorUri] = "custom_error_uri", - [OAuthValidationConstants.Properties.Realm] = "custom_realm", - [OAuthValidationConstants.Properties.Scope] = "custom_scope", - }); - - return context.ChallengeAsync(OpenIddictValidationDefaults.AuthenticationScheme, properties); - })); - - app.Run(async context => - { - var result = await context.AuthenticateAsync(OpenIddictValidationDefaults.AuthenticationScheme); - if (result.Principal == null) - { - await context.ChallengeAsync(OpenIddictValidationDefaults.AuthenticationScheme); - - return; - } - - var subject = result.Principal.FindFirst(OAuthValidationConstants.Claims.Subject)?.Value; - if (string.IsNullOrEmpty(subject)) - { - await context.ChallengeAsync(OpenIddictValidationDefaults.AuthenticationScheme); - - return; - } - - await context.Response.WriteAsync(subject); - }); - }); - - return new TestServer(builder); - } - - private static OpenIddictApplicationManager CreateApplicationManager( - Action>> configuration = null) - { - var manager = new Mock>( - Mock.Of(), - Mock.Of>>(), - Mock.Of>()); - - configuration?.Invoke(manager); - - return manager.Object; - } - - private static OpenIddictAuthorizationManager CreateAuthorizationManager( - Action>> configuration = null) - { - var manager = new Mock>( - Mock.Of(), - Mock.Of>>(), - Mock.Of>()); - - configuration?.Invoke(manager); - - return manager.Object; - } - - private static OpenIddictScopeManager CreateScopeManager( - Action>> configuration = null) - { - var manager = new Mock>( - Mock.Of(), - Mock.Of>>(), - Mock.Of>()); - - configuration?.Invoke(manager); - - return manager.Object; - } - - private static OpenIddictTokenManager CreateTokenManager( - Action>> configuration = null) - { - var manager = new Mock>( - Mock.Of(), - Mock.Of>>(), - Mock.Of>()); - - configuration?.Invoke(manager); - - return manager.Object; - } - - public class OpenIddictApplication { } - public class OpenIddictAuthorization { } - public class OpenIddictScope { } - public class OpenIddictToken { } - } -}