Browse Source

Backport the validation middleware to OpenIddict 1.x

pull/670/head
Kévin Chalet 8 years ago
parent
commit
59c3d51ae7
  1. 14
      OpenIddict.sln
  2. 3
      samples/Mvc.Server/Mvc.Server.csproj
  3. 14
      samples/Mvc.Server/Startup.cs
  4. 1
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  5. 287
      src/OpenIddict.Validation/Internal/OpenIddictValidationHandler.cs
  6. 78
      src/OpenIddict.Validation/Internal/OpenIddictValidationHelpers.cs
  7. 52
      src/OpenIddict.Validation/Internal/OpenIddictValidationMiddleware.cs
  8. 25
      src/OpenIddict.Validation/OpenIddict.Validation.csproj
  9. 126
      src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
  10. 124
      src/OpenIddict.Validation/OpenIddictValidationExtensions.cs
  11. 24
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs
  12. 1
      src/OpenIddict/OpenIddict.csproj
  13. 28
      test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj
  14. 897
      test/OpenIddict.Validation.Tests/OpenIddictValidationHandlerTests.cs

14
OpenIddict.sln

@ -54,6 +54,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server.Tests", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Abstractions", "src\OpenIddict.Abstractions\OpenIddict.Abstractions.csproj", "{886A16DA-C9CF-4979-9B38-D06DF8A714B6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Validation.Tests", "test\OpenIddict.Validation.Tests\OpenIddict.Validation.Tests.csproj", "{F470E734-F4B6-4355-AF32-53412B619E41}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Validation", "src\OpenIddict.Validation\OpenIddict.Validation.csproj", "{6AB8F9E7-47F8-4A40-837F-C8753362AF54}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -128,6 +132,14 @@ Global
{886A16DA-C9CF-4979-9B38-D06DF8A714B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{886A16DA-C9CF-4979-9B38-D06DF8A714B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{886A16DA-C9CF-4979-9B38-D06DF8A714B6}.Release|Any CPU.Build.0 = Release|Any CPU
{F470E734-F4B6-4355-AF32-53412B619E41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F470E734-F4B6-4355-AF32-53412B619E41}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F470E734-F4B6-4355-AF32-53412B619E41}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F470E734-F4B6-4355-AF32-53412B619E41}.Release|Any CPU.Build.0 = Release|Any CPU
{6AB8F9E7-47F8-4A40-837F-C8753362AF54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6AB8F9E7-47F8-4A40-837F-C8753362AF54}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6AB8F9E7-47F8-4A40-837F-C8753362AF54}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6AB8F9E7-47F8-4A40-837F-C8753362AF54}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -150,6 +162,8 @@ Global
{21A7F241-CBE7-4F5C-9787-F2C50D135AEA} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{07B02B98-8A68-432D-A932-48E6D52B221A} = {5FC71D6A-A994-4F62-977F-88A7D25379D7}
{886A16DA-C9CF-4979-9B38-D06DF8A714B6} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{F470E734-F4B6-4355-AF32-53412B619E41} = {5FC71D6A-A994-4F62-977F-88A7D25379D7}
{6AB8F9E7-47F8-4A40-837F-C8753362AF54} = {D544447C-D701-46BB-9A5B-C76C612A596B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616}

3
samples/Mvc.Server/Mvc.Server.csproj

@ -23,11 +23,10 @@
<ProjectReference Include="..\..\src\OpenIddict\OpenIddict.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Mvc\OpenIddict.Mvc.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Validation\OpenIddict.Validation.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.Introspection" Version="$(AspNetContribOpenIdExtensionsVersion)" />
<PackageReference Include="AspNet.Security.OAuth.Validation" Version="$(AspNetContribOpenIdExtensionsVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Twitter" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(AspNetCoreVersion)" />

14
samples/Mvc.Server/Startup.cs

@ -65,7 +65,7 @@ namespace Mvc.Server
options.AddEntityFrameworkCoreStores<ApplicationDbContext>();
})
// Register the OpenIddict server handler.
// Register the OpenIddict server services.
.AddServer(options =>
{
// Register the ASP.NET Core MVC binder used by OpenIddict.
@ -112,7 +112,10 @@ namespace Mvc.Server
//
// options.UseJsonWebTokens();
// options.AddEphemeralSigningKey();
});
})
// Register the OpenIddict validation services.
.AddValidation();
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
@ -126,9 +129,10 @@ namespace Mvc.Server
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), branch =>
{
// Add a middleware used to validate access
// tokens and protect the API endpoints.
branch.UseOAuthValidation();
// Note: the OpenIddict validation handler is only compatible with the
// default token format or with reference tokens and cannot be used with
// JWT tokens. For JWT tokens, use the Microsoft JWT bearer handler.
branch.UseOpenIddictValidation();
// If you prefer using JWT, don't forget to disable the automatic
// JWT -> WS-Federation claims mapping used by the JWT middleware:

1
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -79,6 +79,7 @@ namespace OpenIddict.Abstractions
public const string AuthorizationId = ".authorization_id";
public const string ReferenceToken = ".reference_token";
public const string Token = ".token";
public const string TokenId = ".token_id";
}
public static class PropertyTypes

287
src/OpenIddict.Validation/Internal/OpenIddictValidationHandler.cs

@ -0,0 +1,287 @@
/*
* 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.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OAuth.Validation;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions;
using OpenIddict.Core;
namespace OpenIddict.Validation
{
[EditorBrowsable(EditorBrowsableState.Never)]
public class OpenIddictValidationHandler : OAuthValidationHandler { }
[EditorBrowsable(EditorBrowsableState.Never)]
public class OpenIddictValidationHandler<TToken> : OpenIddictValidationHandler where TToken : class
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var context = new RetrieveTokenContext(Context, Options);
await Options.Events.RetrieveToken(context);
if (context.HandledResponse)
{
// If no ticket has been provided, return a failed result to
// indicate that authentication was rejected by application code.
if (context.Ticket == null)
{
return AuthenticateResult.Fail("Authentication was stopped by application code.");
}
return AuthenticateResult.Success(context.Ticket);
}
else if (context.Skipped)
{
Logger.LogInformation("Authentication was skipped by application code.");
return AuthenticateResult.Skip();
}
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.Skip();
}
// 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))
{
Logger.LogDebug("Authentication was skipped because an incompatible " +
"scheme was used in the 'Authorization' header.");
return AuthenticateResult.Skip();
}
// Extract the token from the authorization header.
token = header.Substring(OAuthValidationConstants.Schemes.Bearer.Length + 1).Trim();
if (string.IsNullOrEmpty(token))
{
Logger.LogDebug("Authentication was skipped because the bearer token " +
"was missing from the 'Authorization' header.");
return AuthenticateResult.Skip();
}
}
// 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 AuthenticateResult.Fail("Authentication failed because the access token was invalid.");
}
// 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, Options, ticket);
await Options.Events.ValidateToken(notification);
if (notification.HandledResponse)
{
// If no ticket has been provided, return a failed result to
// indicate that authentication was rejected by application code.
if (notification.Ticket == null)
{
return AuthenticateResult.Fail("Authentication was stopped by application code.");
}
return AuthenticateResult.Success(notification.Ticket);
}
else if (notification.Skipped)
{
Logger.LogInformation("Authentication was skipped by application code.");
return AuthenticateResult.Skip();
}
// Allow the application code to replace the ticket
// reference from the ValidateToken event.
ticket = notification.Ticket;
if (ticket == null)
{
return AuthenticateResult.Fail("Authentication was stopped by application code.");
}
return AuthenticateResult.Success(ticket);
}
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)
{
var manager = Context.RequestServices.GetService<OpenIddictTokenManager<TToken>>();
if (manager == null)
{
throw new InvalidOperationException("The token manager was not correctly registered.");
}
// 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, Options, ticket);
await Options.Events.CreateTicket(notification);
if (notification.HandledResponse)
{
// If no ticket has been provided, return a failed result to
// indicate that authentication was rejected by application code.
if (notification.Ticket == null)
{
return AuthenticateResult.Skip();
}
return AuthenticateResult.Success(notification.Ticket);
}
else if (notification.Skipped)
{
return AuthenticateResult.Skip();
}
return AuthenticateResult.Success(notification.Ticket);
}
}
}

78
src/OpenIddict.Validation/Internal/OpenIddictValidationHelpers.cs

@ -0,0 +1,78 @@
/*
* 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 JetBrains.Annotations;
using Microsoft.AspNetCore.Http.Authentication;
namespace OpenIddict.Validation
{
/// <summary>
/// Defines a set of commonly used helpers.
/// </summary>
internal static class OpenIddictValidationHelpers
{
/// <summary>
/// Gets a given property from the authentication properties.
/// </summary>
/// <param name="properties">The authentication properties.</param>
/// <param name="property">The specific property to look for.</param>
/// <returns>The value corresponding to the property, or <c>null</c> if the property cannot be found.</returns>
public static string GetProperty([NotNull] this AuthenticationProperties properties, [NotNull] string property)
{
if (properties == null)
{
throw new ArgumentNullException(nameof(properties));
}
if (string.IsNullOrEmpty(property))
{
throw new ArgumentException("The property name cannot be null or empty.", nameof(property));
}
if (!properties.Items.TryGetValue(property, out string value))
{
return null;
}
return value;
}
/// <summary>
/// Sets the specified property in the authentication properties.
/// </summary>
/// <param name="properties">The authentication properties.</param>
/// <param name="property">The property name.</param>
/// <param name="value">The property value.</param>
/// <returns>The <see cref="AuthenticationProperties"/> so that multiple calls can be chained.</returns>
public static AuthenticationProperties SetProperty(
[NotNull] this AuthenticationProperties properties,
[NotNull] string property, [CanBeNull] string value)
{
if (properties == null)
{
throw new ArgumentNullException(nameof(properties));
}
if (string.IsNullOrEmpty(property))
{
throw new ArgumentException("The property name cannot be null or empty.", nameof(property));
}
if (string.IsNullOrEmpty(value))
{
properties.Items.Remove(property);
}
else
{
properties.Items[property] = value;
}
return properties;
}
}
}

52
src/OpenIddict.Validation/Internal/OpenIddictValidationMiddleware.cs

@ -0,0 +1,52 @@
/*
* 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.ComponentModel;
using System.Text.Encodings.Web;
using AspNet.Security.OAuth.Validation;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace OpenIddict.Validation
{
[EditorBrowsable(EditorBrowsableState.Never)]
public class OpenIddictValidationMiddleware : OAuthValidationMiddleware
{
public OpenIddictValidationMiddleware(
[NotNull] RequestDelegate next,
[NotNull] IOptions<OpenIddictValidationOptions> options,
[NotNull] ILoggerFactory loggerFactory,
[NotNull] UrlEncoder encoder,
[NotNull] IDataProtectionProvider dataProtectionProvider)
: base(next, options, loggerFactory, encoder, dataProtectionProvider)
{
}
protected override AuthenticationHandler<OAuthValidationOptions> CreateHandler()
=> new OpenIddictValidationHandler();
}
[EditorBrowsable(EditorBrowsableState.Never)]
public class OpenIddictValidationMiddleware<TToken> : OpenIddictValidationMiddleware where TToken : class
{
public OpenIddictValidationMiddleware(
[NotNull] RequestDelegate next,
[NotNull] IOptions<OpenIddictValidationOptions> options,
[NotNull] ILoggerFactory loggerFactory,
[NotNull] UrlEncoder encoder,
[NotNull] IDataProtectionProvider dataProtectionProvider)
: base(next, options, loggerFactory, encoder, dataProtectionProvider)
{
}
protected override AuthenticationHandler<OAuthValidationOptions> CreateHandler()
=> new OpenIddictValidationHandler<TToken>();
}
}

25
src/OpenIddict.Validation/OpenIddict.Validation.csproj

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\packages.props" />
<PropertyGroup>
<TargetFrameworks>net451;netstandard1.3</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<Description>OpenIddict token validation middleware for ASP.NET Core.</Description>
<Authors>Kévin Chalet;Chino Chang</Authors>
<PackageTags>aspnetcore;authentication;jwt;openidconnect;openiddict;security</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenIddict.Core\OpenIddict.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.Validation" Version="$(AspNetContribOpenIdExtensionsVersion)" />
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="$(AspNetCoreVersion)" />
</ItemGroup>
</Project>

126
src/OpenIddict.Validation/OpenIddictValidationBuilder.cs

@ -0,0 +1,126 @@
/*
* 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.Linq;
using JetBrains.Annotations;
using Microsoft.AspNetCore.DataProtection;
using OpenIddict.Validation;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Exposes the necessary methods required to configure the OpenIddict validation services.
/// </summary>
public class OpenIddictValidationBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictValidationBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictValidationBuilder([NotNull] IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
Services = services;
}
/// <summary>
/// Gets the services collection.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public IServiceCollection Services { get; }
/// <summary>
/// Amends the default OpenIddict validation configuration.
/// </summary>
/// <param name="configuration">The delegate used to configure the OpenIddict options.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder Configure([NotNull] Action<OpenIddictValidationOptions> configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
/// <summary>
/// Registers the specified values as valid audiences. Setting the audiences is recommended
/// when the authorization server issues access tokens for multiple distinct resource servers.
/// </summary>
/// <param name="audiences">The audiences valid for this resource server.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder AddAudiences([NotNull] params string[] audiences)
{
if (audiences == null)
{
throw new ArgumentNullException(nameof(audiences));
}
if (audiences.Any(audience => string.IsNullOrEmpty(audience)))
{
throw new ArgumentException("Audiences cannot be null or empty.", nameof(audiences));
}
return Configure(options => options.Audiences.UnionWith(audiences));
}
/// <summary>
/// Configures OpenIddict not to return the authentication error
/// details as part of the standard WWW-Authenticate response header.
/// </summary>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder RemoveErrorDetails()
=> Configure(options => options.IncludeErrorDetails = false);
/// <summary>
/// Sets the realm, which is used to compute the WWW-Authenticate response header.
/// </summary>
/// <param name="realm">The realm.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder SetRealm([NotNull] string realm)
{
if (string.IsNullOrEmpty(realm))
{
throw new ArgumentException("The realm cannot be null or empty.", nameof(realm));
}
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.
/// </summary>
/// <param name="provider">The data protection provider used to create token protectors.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder UseDataProtectionProvider([NotNull] IDataProtectionProvider provider)
{
if (provider == null)
{
throw new ArgumentNullException(nameof(provider));
}
return Configure(options => options.DataProtectionProvider = provider);
}
}
}

124
src/OpenIddict.Validation/OpenIddictValidationExtensions.cs

@ -0,0 +1,124 @@
/*
* 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.Text;
using AspNet.Security.OAuth.Validation;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Options;
using OpenIddict.Core;
using OpenIddict.Validation;
namespace Microsoft.Extensions.DependencyInjection
{
public static class OpenIddictValidationExtensions
{
/// <summary>
/// Registers the OpenIddict token validation services in the DI container.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public static OpenIddictValidationBuilder AddValidation([NotNull] this OpenIddictBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.Services.AddAuthentication();
return new OpenIddictValidationBuilder(builder.Services);
}
/// <summary>
/// Registers the OpenIddict token validation services in the DI container.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <param name="configuration">The configuration delegate used to configure the validation services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictBuilder"/>.</returns>
public static OpenIddictBuilder AddValidation(
[NotNull] this OpenIddictBuilder builder,
[NotNull] Action<OpenIddictValidationBuilder> configuration)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(builder.AddValidation());
return builder;
}
/// <summary>
/// Registers the OpenIddict validation middleware in the ASP.NET Core pipeline.
/// </summary>
/// <param name="app">The application builder used to register middleware instances.</param>
/// <returns>The <see cref="IApplicationBuilder"/>.</returns>
public static IApplicationBuilder UseOpenIddictValidation([NotNull] this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
var configuration = app.ApplicationServices.GetRequiredService<IOptions<OpenIddictCoreOptions>>().Value;
var options = app.ApplicationServices.GetRequiredService<IOptions<OpenIddictValidationOptions>>().Value;
if (options.Events == null)
{
options.Events = new OAuthValidationEvents();
}
if (options.DataProtectionProvider == null)
{
options.DataProtectionProvider = app.ApplicationServices.GetDataProtectionProvider();
}
if (options.UseReferenceTokens && options.AccessTokenFormat == null)
{
var protector = options.DataProtectionProvider.CreateProtector(
"OpenIdConnectServerHandler",
nameof(options.AccessTokenFormat),
nameof(options.UseReferenceTokens), "ASOS");
options.AccessTokenFormat = new TicketDataFormat(protector);
}
if (options.TokenType == null)
{
options.TokenType = configuration.DefaultTokenType;
}
if (options.UseReferenceTokens)
{
if (options.TokenType == null)
{
throw new InvalidOperationException(new StringBuilder()
.AppendLine("The entity types must be configured for the token validation services to work correctly.")
.Append("To configure the entities, use either 'services.AddOpenIddict().AddCore().UseDefaultModels()' ")
.Append("or 'services.AddOpenIddict().AddCore().UseCustomModels()'.")
.ToString());
}
return app.UseMiddleware(typeof(OpenIddictValidationMiddleware<>)
.MakeGenericType(options.TokenType), new OptionsWrapper<OpenIddictValidationOptions>(options));
}
return app.UseMiddleware<OpenIddictValidationMiddleware>(new OptionsWrapper<OpenIddictValidationOptions>(options));
}
}
}

24
src/OpenIddict.Validation/OpenIddictValidationOptions.cs

@ -0,0 +1,24 @@
/*
* 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;
namespace OpenIddict.Validation
{
public class OpenIddictValidationOptions : OAuthValidationOptions
{
/// <summary>
/// Gets or sets the type corresponding to the Token entity.
/// </summary>
public Type TokenType { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether reference tokens are used.
/// </summary>
public bool UseReferenceTokens { get; set; }
}
}

1
src/OpenIddict/OpenIddict.csproj

@ -15,6 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\OpenIddict.Models\OpenIddict.Models.csproj" />
<ProjectReference Include="..\OpenIddict.Server\OpenIddict.Server.csproj" />
<ProjectReference Include="..\OpenIddict.Validation\OpenIddict.Validation.csproj" />
</ItemGroup>
<ItemGroup>

28
test/OpenIddict.Validation.Tests/OpenIddict.Validation.Tests.csproj

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\tests.props" />
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict\OpenIddict.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
<PackageReference Include="Moq" Version="$(MoqVersion)" />
<PackageReference Include="xunit" Version="$(XunitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitVersion)" />
</ItemGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
</Project>

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

@ -0,0 +1,897 @@
/*
* 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.Http.Authentication;
using Microsoft.AspNetCore.Http.Features.Authentication;
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 OpenIddict.Models;
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_SkipToNextMiddlewareFromReceiveTokenCausesInvalidAuthentication()
{
// Arrange
var server = CreateResourceServer(builder =>
{
builder.Configure(options => options.Events.OnRetrieveToken = context =>
{
context.SkipToNextMiddleware();
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_NullTicketAndHandleResponseFromReceiveTokenCauseInvalidAuthentication()
{
// Arrange
var server = CreateResourceServer(builder =>
{
builder.Configure(options => options.Events.OnRetrieveToken = context =>
{
context.Ticket = null;
context.HandleResponse();
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_ReplacedTicketAndHandleResponseFromReceiveTokenCauseSuccessfulAuthentication()
{
// Arrange
var server = CreateResourceServer(builder =>
{
builder.Configure(options => options.Events.OnRetrieveToken = context =>
{
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam"));
context.Ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
context.HandleResponse();
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_SkipToNextMiddlewareFromValidateTokenCausesInvalidAuthentication()
{
// Arrange
var server = CreateResourceServer(builder =>
{
builder.Configure(options => options.Events.OnValidateToken = context =>
{
context.SkipToNextMiddleware();
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_NullTicketAndHandleResponseFromValidateTokenCauseInvalidAuthentication()
{
// Arrange
var server = CreateResourceServer(builder =>
{
builder.Configure(options => options.Events.OnValidateToken = context =>
{
context.Ticket = null;
context.HandleResponse();
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_ReplacedTicketAndHandleResponseFromValidateTokenCauseSuccessfulAuthentication()
{
// Arrange
var server = CreateResourceServer(builder =>
{
builder.Configure(options => options.Events.OnValidateToken = context =>
{
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Contoso"));
context.Ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
context.HandleResponse();
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 HandleAuthenticateAsync_UpdatedTicketFromValidateTokenCausesSuccessfulAuthentication()
{
// Arrange
var server = CreateResourceServer(builder =>
{
builder.Configure(options => options.Events.OnValidateToken = context =>
{
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Contoso"));
context.Ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
context.HandleResponse();
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(context.Error, "custom_error");
Assert.Equal(context.ErrorDescription, "custom_error_description");
Assert.Equal(context.ErrorUri, "custom_error_uri");
Assert.Equal(context.Realm, "custom_realm");
Assert.Equal(context.Scope, "custom_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"));
}
[Fact]
public async Task HandleUnauthorizedAsync_ApplyChallenge_AllowsSkippingToNextMiddleware()
{
// Arrange
var server = CreateResourceServer(builder =>
{
builder.Configure(options => options.Events.OnApplyChallenge = context =>
{
context.SkipToNextMiddleware();
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.Empty(await response.Content.ReadAsStringAsync());
}
[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(OAuthValidationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam"));
var properties = new AuthenticationProperties();
return new AuthenticationTicket(new ClaimsPrincipal(identity),
properties, OAuthValidationDefaults.AuthenticationScheme);
});
format.Setup(mock => mock.Unprotect(It.Is<string>(token => token == "valid-token-with-scopes")))
.Returns(delegate
{
var identity = new ClaimsIdentity(OAuthValidationDefaults.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, OAuthValidationDefaults.AuthenticationScheme);
});
format.Setup(mock => mock.Unprotect(It.Is<string>(token => token == "valid-token-with-single-audience")))
.Returns(delegate
{
var identity = new ClaimsIdentity(OAuthValidationDefaults.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, OAuthValidationDefaults.AuthenticationScheme);
});
format.Setup(mock => mock.Unprotect(It.Is<string>(token => token == "valid-token-with-multiple-audiences")))
.Returns(delegate
{
var identity = new ClaimsIdentity(OAuthValidationDefaults.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, OAuthValidationDefaults.AuthenticationScheme);
});
format.Setup(mock => mock.Unprotect(It.Is<string>(token => token == "expired-token")))
.Returns(delegate
{
var identity = new ClaimsIdentity(OAuthValidationDefaults.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, OAuthValidationDefaults.AuthenticationScheme);
});
var builder = new WebHostBuilder();
builder.UseEnvironment("Testing");
builder.ConfigureLogging(options => options.AddDebug());
builder.ConfigureServices(services =>
{
services.AddOpenIddict()
.AddCore(options =>
{
options.UseDefaultModels();
// 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.UseOpenIddictValidation();
app.Map("/ticket", map => map.Run(async context =>
{
var ticket = new AuthenticateContext(OAuthValidationDefaults.AuthenticationScheme);
await context.Authentication.AuthenticateAsync(ticket);
if (!ticket.Accepted || ticket.Principal == null || ticket.Properties == null)
{
await context.Authentication.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 ticket.Principal.Claims
select new { claim.Type, claim.Value },
Properties = from property in ticket.Properties
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.Authentication.ChallengeAsync(OAuthValidationDefaults.AuthenticationScheme, properties);
}));
app.Run(context =>
{
if (!context.User.Identities.Any(identity => identity.IsAuthenticated))
{
return context.Authentication.ChallengeAsync();
}
var identifier = context.User.FindFirst(OAuthValidationConstants.Claims.Subject).Value;
return context.Response.WriteAsync(identifier);
});
});
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<IOptions<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<IOptions<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<IOptions<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<IOptions<OpenIddictCoreOptions>>());
configuration?.Invoke(manager);
return manager.Object;
}
}
}
Loading…
Cancel
Save