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