Browse Source

Migrate to the latest aspnet-contrib packages

pull/616/head
Kévin Chalet 8 years ago
parent
commit
91d5592d55
  1. 4
      build/dependencies.props
  2. 25
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs
  3. 25
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs
  4. 134
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  5. 95
      src/OpenIddict.Validation/Internal/OpenIddictValidationEvents.cs
  6. 254
      src/OpenIddict.Validation/Internal/OpenIddictValidationHandler.cs
  7. 12
      src/OpenIddict.Validation/Internal/OpenIddictValidationInitializer.cs
  8. 67
      src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
  9. 1
      src/OpenIddict.Validation/OpenIddictValidationExtensions.cs
  10. 27
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs
  11. 32
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Authentication.cs
  12. 26
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs
  13. 69
      test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs
  14. 338
      test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationEventsTests.cs
  15. 110
      test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationInitializerTests.cs
  16. 1
      test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj
  17. 190
      test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs
  18. 837
      test/OpenIddict.Validation.Tests/OpenIddictValidationHandlerTests.cs

4
build/dependencies.props

@ -1,8 +1,8 @@
<Project>
<PropertyGroup Label="Package Versions">
<AspNetContribOpenIdExtensionsVersion>2.0.0-rc2-final</AspNetContribOpenIdExtensionsVersion>
<AspNetContribOpenIdServerVersion>2.0.0-rc2-final</AspNetContribOpenIdServerVersion>
<AspNetContribOpenIdExtensionsVersion>2.0.0-rc3-0307</AspNetContribOpenIdExtensionsVersion>
<AspNetContribOpenIdServerVersion>2.0.0-rc3-1371</AspNetContribOpenIdServerVersion>
<AspNetCoreVersion>2.0.0</AspNetCoreVersion>
<CoreFxVersion>4.4.0</CoreFxVersion>
<CryptoHelperVersion>3.0.2</CryptoHelperVersion>

25
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<string>(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;

25
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<string>(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;

134
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -489,73 +489,6 @@ namespace Microsoft.Extensions.DependencyInjection
return Configure(options => options.UserinfoEndpointPath = path);
}
/// <summary>
/// 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.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder RequireClientIdentification()
=> Configure(options => options.RequireClientIdentification = true);
/// <summary>
/// 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.
/// </summary>
/// <param name="lifetime">The access token lifetime.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetAccessTokenLifetime(TimeSpan lifetime)
=> Configure(options => options.AccessTokenLifetime = lifetime);
/// <summary>
/// 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.
/// </summary>
/// <param name="lifetime">The authorization code lifetime.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetAuthorizationCodeLifetime(TimeSpan lifetime)
=> Configure(options => options.AuthorizationCodeLifetime = lifetime);
/// <summary>
/// Sets the identity token lifetime, after which client
/// applications should refuse processing identity tokens.
/// </summary>
/// <param name="lifetime">The identity token lifetime.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetIdentityTokenLifetime(TimeSpan lifetime)
=> Configure(options => options.IdentityTokenLifetime = lifetime);
/// <summary>
/// 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.
/// </summary>
/// <param name="lifetime">The refresh token lifetime.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetRefreshTokenLifetime(TimeSpan lifetime)
=> Configure(options => options.RefreshTokenLifetime = lifetime);
/// <summary>
/// Sets the issuer address, which is used as the base address
/// for the endpoint URIs returned from the discovery endpoint.
/// </summary>
/// <param name="address">The issuer address.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetIssuer([NotNull] Uri address)
{
if (address == null)
{
throw new ArgumentNullException(nameof(address));
}
return Configure(options => options.Issuer = address);
}
/// <summary>
/// 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));
}
/// <summary>
/// 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.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder RequireClientIdentification()
=> Configure(options => options.RequireClientIdentification = true);
/// <summary>
/// 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.
/// </summary>
/// <param name="lifetime">The access token lifetime.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetAccessTokenLifetime(TimeSpan lifetime)
=> Configure(options => options.AccessTokenLifetime = lifetime);
/// <summary>
/// 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.
/// </summary>
/// <param name="lifetime">The authorization code lifetime.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetAuthorizationCodeLifetime(TimeSpan lifetime)
=> Configure(options => options.AuthorizationCodeLifetime = lifetime);
/// <summary>
/// Sets the identity token lifetime, after which client
/// applications should refuse processing identity tokens.
/// </summary>
/// <param name="lifetime">The identity token lifetime.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetIdentityTokenLifetime(TimeSpan lifetime)
=> Configure(options => options.IdentityTokenLifetime = lifetime);
/// <summary>
/// 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.
/// </summary>
/// <param name="lifetime">The refresh token lifetime.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetRefreshTokenLifetime(TimeSpan lifetime)
=> Configure(options => options.RefreshTokenLifetime = lifetime);
/// <summary>
/// Sets the issuer address, which is used as the base address
/// for the endpoint URIs returned from the discovery endpoint.
/// </summary>
/// <param name="address">The issuer address.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetIssuer([NotNull] Uri address)
{
if (address == null)
{
throw new ArgumentNullException(nameof(address));
}
return Configure(options => options.Issuer = address);
}
/// <summary>
/// Configures OpenIddict to use a specific data protection provider
/// instead of relying on the default instance provided by the DI container.

95
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
{
/// <summary>
/// Provides the logic necessary to extract, validate and handle OAuth2 requests.
/// </summary>
[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<IOpenIddictTokenManager>();
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;
}
}
}

254
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<AuthenticateResult> 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<string>())
{
if (Options.Audiences.Contains(audience))
{
return true;
}
}
return false;
}
private async Task<AuthenticateResult> 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<IOpenIddictTokenManager>();
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<string>())
{
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;
}

12
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)

67
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));
}
/// <summary>
/// Registers application-specific OAuth2 validation events that are automatically
/// invoked for each request handled by the OpenIddict validation handler.
/// </summary>
/// <param name="events">The custom <see cref="OAuthValidationEvents"/> service.</param>
/// <returns>The <see cref="OAuthValidationEvents"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictValidationBuilder RegisterEvents([NotNull] OAuthValidationEvents events)
{
if (events == null)
{
throw new ArgumentNullException(nameof(events));
}
return Configure(options => options.ApplicationEvents = events);
}
/// <summary>
/// Registers application-specific OAuth2 validation events that are automatically
/// invoked for each request handled by the OpenIddict validation handler.
/// </summary>
/// <typeparam name="TEvents">The type of the custom <see cref="OAuthValidationEvents"/> service.</typeparam>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictValidationBuilder RegisterEvents<TEvents>() where TEvents : OAuthValidationEvents
=> RegisterEvents(typeof(TEvents));
/// <summary>
/// Registers application-specific OAuth2 validation events that are automatically
/// invoked for each request handled by the OpenIddict validation handler.
/// </summary>
/// <param name="type">The type of the custom <see cref="OAuthValidationEvents"/> service.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
[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);
}
/// <summary>
/// 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);
}
/// <summary>
/// Configures the OpenIddict validation handler to use reference tokens.
/// </summary>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder UseReferenceTokens()
=> Configure(options => options.UseReferenceTokens = true);
/// <summary>
/// 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);
}
/// <summary>
/// Configures the OpenIddict validation handler to use reference tokens.
/// </summary>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder UseReferenceTokens()
=> Configure(options => options.UseReferenceTokens = true);
}
}

1
src/OpenIddict.Validation/OpenIddictValidationExtensions.cs

@ -31,6 +31,7 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.AddAuthentication();
builder.Services.TryAddScoped<OpenIddictValidationEvents>();
builder.Services.TryAddScoped<OpenIddictValidationHandler>();
// Note: TryAddEnumerable() is used here to ensure the initializer is only registered once.

27
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
{
/// <summary>
/// Provides various settings needed to configure the OpenIddict validation handler.
/// </summary>
public class OpenIddictValidationOptions : OAuthValidationOptions
{
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationOptions"/> class.
/// </summary>
public OpenIddictValidationOptions()
{
Events = null;
EventsType = typeof(OpenIddictValidationEvents);
}
/// <summary>
/// Gets or sets the user-provided <see cref="OAuthValidationEvents"/> that the OpenIddict
/// validation handler invokes to enable developer control over the entire authentication process.
/// </summary>
public OAuthValidationEvents ApplicationEvents { get; set; }
/// <summary>
/// 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.
/// </summary>
public Type ApplicationEventsType { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether reference tokens are used.
/// </summary>

32
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<ImmutableArray<string>>(scopes => scopes.Length == 1 && scopes[0] == "unregistered_scope"),
It.IsAny<CancellationToken>()))
.ReturnsAsync(ImmutableArray.Create<OpenIddictScope>());
}));
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<CancellationToken>()))
.ReturnsAsync(new OpenIddictScope());
instance.Setup(mock => mock.FindByNamesAsync(
It.Is<ImmutableArray<string>>(scopes => scopes.Length == 1 && scopes[0] == "scope_registered_in_database"),
It.IsAny<CancellationToken>()))
.ReturnsAsync(ImmutableArray.Create(scope));
instance.Setup(mock => mock.GetNameAsync(scope, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("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<CancellationToken>()))
OpenIddictConstants.Permissions.Prefixes.Scope + "scope_registered_in_database", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Prefixes.Scope + "scope_registered_in_options", It.IsAny<CancellationToken>()))
.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

26
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<ImmutableArray<string>>(scopes => scopes.Length == 1 && scopes[0] == "unregistered_scope"),
It.IsAny<CancellationToken>()))
.ReturnsAsync(ImmutableArray.Create<OpenIddictScope>());
}));
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<CancellationToken>()))
.ReturnsAsync(new OpenIddictScope());
instance.Setup(mock => mock.FindByNamesAsync(
It.Is<ImmutableArray<string>>(scopes => scopes.Length == 1 && scopes[0] == "scope_registered_in_database"),
It.IsAny<CancellationToken>()))
.ReturnsAsync(ImmutableArray.Create(scope));
instance.Setup(mock => mock.GetNameAsync(scope, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("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

69
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<ArgumentException>(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<IOptionsMonitor<OpenIddictServerOptions>>();
return options.Get(OpenIddictServerDefaults.AuthenticationScheme);
}
public class OpenIddictApplication { }
public class OpenIddictAuthorization { }
public class OpenIddictScope { }
public class OpenIddictToken { }
}
}

338
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<InvalidOperationException>(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<CancellationToken>()))
.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<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(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<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task DecryptToken_ReturnsFailedResultForInvalidReferenceTokenPayload()
{
// Arrange
var token = new OpenIddictToken();
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
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<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("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<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()), 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<ISecureDataFormat<AuthenticationTicket>>();
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<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("valid-reference-token-payload"));
instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<DateTimeOffset?>(new DateTimeOffset(2018, 01, 01, 00, 00, 00, TimeSpan.Zero)));
instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<DateTimeOffset?>(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<JArray>("Properties")
select new
{
Name = property.Value<string>("Name"),
Value = property.Value<string>("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<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetCreationDateAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetExpirationDateAsync(token, It.IsAny<CancellationToken>()), Times.Once());
format.Verify(mock => mock.Unprotect("valid-reference-token-payload"), Times.Once());
}
private static TestServer CreateResourceServer(Action<OpenIddictValidationBuilder> configuration = null)
{
var builder = new WebHostBuilder();
builder.UseEnvironment("Testing");
builder.ConfigureLogging(options => options.AddDebug());
builder.ConfigureServices(services =>
{
services.AddOpenIddict()
.AddCore(options =>
{
options.SetDefaultTokenEntity<OpenIddictToken>();
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<OpenIddictToken> CreateTokenManager(
Action<Mock<OpenIddictTokenManager<OpenIddictToken>>> configuration = null)
{
var manager = new Mock<OpenIddictTokenManager<OpenIddictToken>>(
Mock.Of<IOpenIddictTokenStoreResolver>(),
Mock.Of<ILogger<OpenIddictTokenManager<OpenIddictToken>>>(),
Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>());
configuration?.Invoke(manager);
return manager.Object;
}
public class OpenIddictToken { }
}
}

110
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<InvalidOperationException>(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<InvalidOperationException>(delegate
{
return client.GetAsync("/");
});
// Assert
Assert.Equal("Application events must inherit from OAuthValidationEvents.", exception.Message);
}
private static TestServer CreateAuthorizationServer(Action<OpenIddictValidationBuilder> 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<OpenIddictApplication>()
.SetDefaultAuthorizationEntity<OpenIddictAuthorization>()
.SetDefaultScopeEntity<OpenIddictScope>()
.SetDefaultTokenEntity<OpenIddictToken>();
})
.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 { }
}
}

1
test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj

@ -12,6 +12,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AspNet.Security.OpenIdConnect.Client" Version="$(AspNetContribOpenIdServerVersion)" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(AspNetCoreVersion)" />

190
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<ArgumentException>(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<EphemeralDataProtectionProvider>(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<IOptionsMonitor<OpenIddictValidationOptions>>();
return options.Get(OpenIddictValidationDefaults.AuthenticationScheme);
}
}
}

837
test/OpenIddict.Validation.Tests/OpenIddictValidationHandlerTests.cs

@ -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<JArray>("Claims")
select new
{
Type = claim.Value<string>(nameof(Claim.Type)),
Value = claim.Value<string>(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<JArray>("Properties")
select new
{
Name = claim.Value<string>("Name"),
Value = claim.Value<string>("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<OpenIddictValidationBuilder> configuration = null)
{
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>(MockBehavior.Strict);
format.Setup(mock => mock.Unprotect(It.Is<string>(token => token == "invalid-token")))
.Returns(value: null);
format.Setup(mock => mock.Unprotect(It.Is<string>(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<string>(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<string>(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<string, string>
{
[OAuthValidationConstants.Properties.Audiences] = @"[""http://www.contoso.com/""]"
});
return new AuthenticationTicket(new ClaimsPrincipal(identity),
properties, OpenIddictValidationDefaults.AuthenticationScheme);
});
format.Setup(mock => mock.Unprotect(It.Is<string>(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<string, string>
{
[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<string>(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<OpenIddictApplication>()
.SetDefaultAuthorizationEntity<OpenIddictAuthorization>()
.SetDefaultScopeEntity<OpenIddictScope>()
.SetDefaultTokenEntity<OpenIddictToken>();
// 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<string, string>
{
[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<OpenIddictApplication> CreateApplicationManager(
Action<Mock<OpenIddictApplicationManager<OpenIddictApplication>>> configuration = null)
{
var manager = new Mock<OpenIddictApplicationManager<OpenIddictApplication>>(
Mock.Of<IOpenIddictApplicationStoreResolver>(),
Mock.Of<ILogger<OpenIddictApplicationManager<OpenIddictApplication>>>(),
Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>());
configuration?.Invoke(manager);
return manager.Object;
}
private static OpenIddictAuthorizationManager<OpenIddictAuthorization> CreateAuthorizationManager(
Action<Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>> configuration = null)
{
var manager = new Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>(
Mock.Of<IOpenIddictAuthorizationStoreResolver>(),
Mock.Of<ILogger<OpenIddictAuthorizationManager<OpenIddictAuthorization>>>(),
Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>());
configuration?.Invoke(manager);
return manager.Object;
}
private static OpenIddictScopeManager<OpenIddictScope> CreateScopeManager(
Action<Mock<OpenIddictScopeManager<OpenIddictScope>>> configuration = null)
{
var manager = new Mock<OpenIddictScopeManager<OpenIddictScope>>(
Mock.Of<IOpenIddictScopeStoreResolver>(),
Mock.Of<ILogger<OpenIddictScopeManager<OpenIddictScope>>>(),
Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>());
configuration?.Invoke(manager);
return manager.Object;
}
private static OpenIddictTokenManager<OpenIddictToken> CreateTokenManager(
Action<Mock<OpenIddictTokenManager<OpenIddictToken>>> configuration = null)
{
var manager = new Mock<OpenIddictTokenManager<OpenIddictToken>>(
Mock.Of<IOpenIddictTokenStoreResolver>(),
Mock.Of<ILogger<OpenIddictTokenManager<OpenIddictToken>>>(),
Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>());
configuration?.Invoke(manager);
return manager.Object;
}
public class OpenIddictApplication { }
public class OpenIddictAuthorization { }
public class OpenIddictScope { }
public class OpenIddictToken { }
}
}
Loading…
Cancel
Save