14 changed files with 1667 additions and 7 deletions
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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>(); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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…
Reference in new issue