22 changed files with 1919 additions and 44 deletions
@ -0,0 +1,26 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net461;net472;net48;netcoreapp3.1;net6.0</TargetFrameworks> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Client.AspNetCore\OpenIddict.Client.AspNetCore.csproj" /> |
|||
<ProjectReference Include="..\OpenIddict.Client.IntegrationTests\OpenIddict.Client.IntegrationTests.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.AspNetCore.TestHost" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> |
|||
<Reference Include="System.Net.Http" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Using Include="OpenIddict.Abstractions" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,31 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net461;net472;net48;netcoreapp3.1;net6.0</TargetFrameworks> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Client\OpenIddict.Client.csproj" /> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Core\OpenIddict.Core.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="AngleSharp" /> |
|||
<PackageReference Include="MartinCostello.Logging.XUnit" /> |
|||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> |
|||
<PackageReference Include="Moq" /> |
|||
<PackageReference Include="System.Linq.Async" /> |
|||
<PackageReference Include="System.Net.Http.Json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> |
|||
<Reference Include="System.Net.Http" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Using Include="OpenIddict.Abstractions" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,26 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net461;net472;net48</TargetFrameworks> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Client.Owin\OpenIddict.Client.Owin.csproj" /> |
|||
<ProjectReference Include="..\OpenIddict.Client.IntegrationTests\OpenIddict.Client.IntegrationTests.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.Owin.Testing" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> |
|||
<Reference Include="System.Net.Http" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Using Include="OpenIddict.Abstractions" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,26 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net461;net472;net48;netcoreapp3.1;net6.0</TargetFrameworks> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Validation.AspNetCore\OpenIddict.Validation.AspNetCore.csproj" /> |
|||
<ProjectReference Include="..\OpenIddict.Validation.IntegrationTests\OpenIddict.Validation.IntegrationTests.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.AspNetCore.TestHost" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> |
|||
<Reference Include="System.Net.Http" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Using Include="OpenIddict.Abstractions" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,65 @@ |
|||
/* |
|||
* 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.Diagnostics.CodeAnalysis; |
|||
using Microsoft.AspNetCore.TestHost; |
|||
using OpenIddict.Validation.IntegrationTests; |
|||
|
|||
#if SUPPORTS_GENERIC_HOST
|
|||
using Microsoft.Extensions.Hosting; |
|||
#endif
|
|||
|
|||
namespace OpenIddict.Validation.AspNetCore.IntegrationTests; |
|||
|
|||
/// <summary>
|
|||
/// Represents a test host used by the validation integration tests.
|
|||
/// </summary>
|
|||
public class OpenIddictValidationAspNetCoreIntegrationTestServer : OpenIddictValidationIntegrationTestServer |
|||
{ |
|||
#if SUPPORTS_GENERIC_HOST
|
|||
public OpenIddictValidationAspNetCoreIntegrationTestServer(IHost host) |
|||
{ |
|||
Host = host; |
|||
Server = host.GetTestServer(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the generic host used by this instance.
|
|||
/// </summary>
|
|||
public IHost Host { get; } |
|||
#else
|
|||
public OpenIddictValidationAspNetCoreIntegrationTestServer(TestServer server) |
|||
=> Server = server; |
|||
#endif
|
|||
|
|||
/// <summary>
|
|||
/// Gets the ASP.NET Core test server used by this instance.
|
|||
/// </summary>
|
|||
public TestServer Server { get; } |
|||
|
|||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", |
|||
Justification = "The caller is responsible for disposing the test client.")] |
|||
public override ValueTask<OpenIddictValidationIntegrationTestClient> CreateClientAsync() |
|||
=> new(new OpenIddictValidationIntegrationTestClient(Server.CreateClient())); |
|||
|
|||
public override |
|||
#if SUPPORTS_GENERIC_HOST
|
|||
async |
|||
#endif
|
|||
ValueTask DisposeAsync() |
|||
{ |
|||
// Dispose of the underlying test server.
|
|||
Server.Dispose(); |
|||
|
|||
#if SUPPORTS_GENERIC_HOST
|
|||
// Stop and dispose of the underlying generic host.
|
|||
await Host.StopAsync(); |
|||
Host.Dispose(); |
|||
#else
|
|||
return default; |
|||
#endif
|
|||
} |
|||
} |
|||
@ -0,0 +1,234 @@ |
|||
/* |
|||
* 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.Diagnostics.CodeAnalysis; |
|||
using System.Security.Claims; |
|||
using System.Text.Json; |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.AspNetCore.Builder; |
|||
using Microsoft.AspNetCore.Hosting; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.AspNetCore.TestHost; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Microsoft.Extensions.Logging; |
|||
using OpenIddict.Validation.IntegrationTests; |
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
using static OpenIddict.Validation.OpenIddictValidationEvents; |
|||
using static OpenIddict.Validation.OpenIddictValidationHandlers.Protection; |
|||
|
|||
#if SUPPORTS_JSON_NODES
|
|||
using System.Text.Json.Nodes; |
|||
#endif
|
|||
|
|||
namespace OpenIddict.Validation.AspNetCore.IntegrationTests; |
|||
|
|||
public partial class OpenIddictValidationAspNetCoreIntegrationTests : OpenIddictValidationIntegrationTests |
|||
{ |
|||
public OpenIddictValidationAspNetCoreIntegrationTests(ITestOutputHelper outputHelper) |
|||
: base(outputHelper) |
|||
{ |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ProcessAuthentication_CreationDateIsMappedToIssuedUtc() |
|||
{ |
|||
// Arrange
|
|||
await using var server = await CreateServerAsync(options => |
|||
{ |
|||
options.AddEventHandler<ValidateTokenContext>(builder => |
|||
{ |
|||
builder.UseInlineHandler(context => |
|||
{ |
|||
Assert.Equal("access_token", context.Token); |
|||
Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); |
|||
|
|||
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) |
|||
.SetTokenType(TokenTypeHints.AccessToken) |
|||
.SetClaim(Claims.Subject, "Bob le Magnifique") |
|||
.SetCreationDate(new DateTimeOffset(2020, 01, 01, 00, 00, 00, TimeSpan.Zero)); |
|||
|
|||
return default; |
|||
}); |
|||
|
|||
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); |
|||
}); |
|||
}); |
|||
|
|||
await using var client = await server.CreateClientAsync(); |
|||
|
|||
// Act
|
|||
var response = await client.GetAsync("/authenticate/properties", new OpenIddictRequest |
|||
{ |
|||
AccessToken = "access_token" |
|||
}); |
|||
|
|||
// Assert
|
|||
var properties = new AuthenticationProperties(response.GetParameters() |
|||
.ToDictionary(parameter => parameter.Key, parameter => (string?) parameter.Value)); |
|||
|
|||
Assert.Equal(new DateTimeOffset(2020, 01, 01, 00, 00, 00, TimeSpan.Zero), properties.IssuedUtc); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ProcessAuthentication_ExpirationDateIsMappedToIssuedUtc() |
|||
{ |
|||
// Arrange
|
|||
await using var server = await CreateServerAsync(options => |
|||
{ |
|||
options.AddEventHandler<ValidateTokenContext>(builder => |
|||
{ |
|||
builder.UseInlineHandler(context => |
|||
{ |
|||
Assert.Equal("access_token", context.Token); |
|||
Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); |
|||
|
|||
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) |
|||
.SetTokenType(TokenTypeHints.AccessToken) |
|||
.SetExpirationDate(new DateTimeOffset(2120, 01, 01, 00, 00, 00, TimeSpan.Zero)); |
|||
|
|||
return default; |
|||
}); |
|||
|
|||
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); |
|||
}); |
|||
}); |
|||
|
|||
await using var client = await server.CreateClientAsync(); |
|||
|
|||
// Act
|
|||
var response = await client.GetAsync("/authenticate/properties", new OpenIddictRequest |
|||
{ |
|||
AccessToken = "access_token" |
|||
}); |
|||
|
|||
// Assert
|
|||
var properties = new AuthenticationProperties(response.GetParameters() |
|||
.ToDictionary(parameter => parameter.Key, parameter => (string?) parameter.Value)); |
|||
|
|||
Assert.Equal(new DateTimeOffset(2120, 01, 01, 00, 00, 00, TimeSpan.Zero), properties.ExpiresUtc); |
|||
} |
|||
|
|||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", |
|||
Justification = "The caller is responsible for disposing the test server.")] |
|||
protected override |
|||
#if SUPPORTS_GENERIC_HOST
|
|||
async |
|||
#endif
|
|||
ValueTask<OpenIddictValidationIntegrationTestServer> CreateServerAsync(Action<OpenIddictValidationBuilder>? configuration = null) |
|||
{ |
|||
#if SUPPORTS_GENERIC_HOST
|
|||
var builder = new HostBuilder(); |
|||
#else
|
|||
var builder = new WebHostBuilder(); |
|||
#endif
|
|||
builder.UseEnvironment("Testing"); |
|||
|
|||
builder.ConfigureLogging(options => options.AddXUnit(OutputHelper)); |
|||
|
|||
builder.ConfigureServices(ConfigureServices); |
|||
builder.ConfigureServices(services => |
|||
{ |
|||
services.AddOpenIddict() |
|||
.AddValidation(options => |
|||
{ |
|||
options.UseAspNetCore(); |
|||
|
|||
configuration?.Invoke(options); |
|||
}); |
|||
}); |
|||
|
|||
#if SUPPORTS_GENERIC_HOST
|
|||
builder.ConfigureWebHost(options => |
|||
{ |
|||
options.UseTestServer(); |
|||
options.Configure(ConfigurePipeline); |
|||
}); |
|||
#else
|
|||
builder.Configure(ConfigurePipeline); |
|||
#endif
|
|||
|
|||
#if SUPPORTS_GENERIC_HOST
|
|||
var host = await builder.StartAsync(); |
|||
|
|||
return new OpenIddictValidationAspNetCoreIntegrationTestServer(host); |
|||
#else
|
|||
var server = new TestServer(builder); |
|||
|
|||
return new(new OpenIddictValidationAspNetCoreIntegrationTestServer(server)); |
|||
#endif
|
|||
|
|||
void ConfigurePipeline(IApplicationBuilder app) |
|||
{ |
|||
app.Use(next => async context => |
|||
{ |
|||
await next(context); |
|||
|
|||
var feature = context.Features.Get<OpenIddictValidationAspNetCoreFeature>(); |
|||
var response = feature?.Transaction?.GetProperty<object>("custom_response"); |
|||
if (response is not null) |
|||
{ |
|||
context.Response.ContentType = "application/json"; |
|||
await context.Response.WriteAsync(JsonSerializer.Serialize(response)); |
|||
} |
|||
}); |
|||
|
|||
app.UseAuthentication(); |
|||
|
|||
app.Use(next => async context => |
|||
{ |
|||
if (context.Request.Path == "/authenticate") |
|||
{ |
|||
var result = await context.AuthenticateAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); |
|||
if (result?.Principal is null) |
|||
{ |
|||
await context.ChallengeAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); |
|||
return; |
|||
} |
|||
|
|||
var claims = result.Principal.Claims.GroupBy(claim => claim.Type) |
|||
.Select(group => new KeyValuePair<string, string?[]?>( |
|||
group.Key, group.Select(claim => claim.Value).ToArray())); |
|||
|
|||
context.Response.ContentType = "application/json"; |
|||
await context.Response.WriteAsync(JsonSerializer.Serialize(new OpenIddictResponse(claims))); |
|||
return; |
|||
} |
|||
|
|||
else if (context.Request.Path == "/authenticate/properties") |
|||
{ |
|||
var result = await context.AuthenticateAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); |
|||
if (result?.Properties is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
context.Response.ContentType = "application/json"; |
|||
await context.Response.WriteAsync(JsonSerializer.Serialize(new OpenIddictResponse(result.Properties.Items))); |
|||
return; |
|||
} |
|||
|
|||
else if (context.Request.Path == "/challenge") |
|||
{ |
|||
await context.ChallengeAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); |
|||
return; |
|||
} |
|||
|
|||
await next(context); |
|||
}); |
|||
|
|||
app.Run(context => |
|||
{ |
|||
context.Response.ContentType = "application/json"; |
|||
return context.Response.WriteAsync(JsonSerializer.Serialize(new |
|||
{ |
|||
name = "Bob le Magnifique" |
|||
})); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,35 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net461;net472;net48;netcoreapp3.1;net6.0</TargetFrameworks> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<EmbeddedResource Include="Certificate.cer" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Core\OpenIddict.Core.csproj" /> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Validation\OpenIddict.Validation.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="AngleSharp" /> |
|||
<PackageReference Include="MartinCostello.Logging.XUnit" /> |
|||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> |
|||
<PackageReference Include="Moq" /> |
|||
<PackageReference Include="System.Linq.Async" /> |
|||
<PackageReference Include="System.Net.Http.Json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> |
|||
<Reference Include="System.Net.Http" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Using Include="OpenIddict.Abstractions" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,509 @@ |
|||
/* |
|||
* 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.Net.Http.Json; |
|||
using System.Text; |
|||
using System.Text.Encodings.Web; |
|||
using AngleSharp.Html.Parser; |
|||
using Microsoft.Extensions.Primitives; |
|||
|
|||
namespace OpenIddict.Validation.IntegrationTests; |
|||
|
|||
/// <summary>
|
|||
/// Exposes methods that allow sending OpenID Connect
|
|||
/// requests and extracting the corresponding responses.
|
|||
/// </summary>
|
|||
public class OpenIddictValidationIntegrationTestClient : IAsyncDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the OpenID Connect client.
|
|||
/// </summary>
|
|||
public OpenIddictValidationIntegrationTestClient() |
|||
: this(new HttpClient()) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the OpenID Connect client.
|
|||
/// </summary>
|
|||
/// <param name="client">The HTTP client used to communicate with the OpenID Connect server.</param>
|
|||
public OpenIddictValidationIntegrationTestClient(HttpClient client) |
|||
: this(client, new HtmlParser()) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the OpenID Connect client.
|
|||
/// </summary>
|
|||
/// <param name="client">The HTTP client used to communicate with the OpenID Connect server.</param>
|
|||
/// <param name="parser">The HTML parser used to parse the responses returned by the OpenID Connect server.</param>
|
|||
public OpenIddictValidationIntegrationTestClient(HttpClient client, HtmlParser parser) |
|||
{ |
|||
HttpClient = client ?? throw new ArgumentNullException(nameof(client)); |
|||
HtmlParser = parser ?? throw new ArgumentNullException(nameof(parser)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the underlying HTTP client used to
|
|||
/// communicate with the OpenID Connect server.
|
|||
/// </summary>
|
|||
public HttpClient HttpClient { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the underlying HTML parser used to parse the
|
|||
/// responses returned by the OpenID Connect server.
|
|||
/// </summary>
|
|||
public HtmlParser HtmlParser { get; } |
|||
|
|||
/// <summary>
|
|||
/// Sends an empty OpenID Connect request to the given endpoint using GET
|
|||
/// and converts the returned response to an OpenID Connect response.
|
|||
/// </summary>
|
|||
/// <param name="uri">The endpoint to which the request is sent.</param>
|
|||
/// <returns>The OpenID Connect response returned by the server.</returns>
|
|||
public Task<OpenIddictResponse> GetAsync(string uri) |
|||
=> GetAsync(uri, new OpenIddictRequest()); |
|||
|
|||
/// <summary>
|
|||
/// Sends an empty OpenID Connect request to the given endpoint using GET
|
|||
/// and converts the returned response to an OpenID Connect response.
|
|||
/// </summary>
|
|||
/// <param name="uri">The endpoint to which the request is sent.</param>
|
|||
/// <returns>The OpenID Connect response returned by the server.</returns>
|
|||
public Task<OpenIddictResponse> GetAsync(Uri uri) |
|||
=> GetAsync(uri, new OpenIddictRequest()); |
|||
|
|||
/// <summary>
|
|||
/// Sends a generic OpenID Connect request to the given endpoint using GET
|
|||
/// and converts the returned response to an OpenID Connect response.
|
|||
/// </summary>
|
|||
/// <param name="uri">The endpoint to which the request is sent.</param>
|
|||
/// <param name="request">The OpenID Connect request to send.</param>
|
|||
/// <returns>The OpenID Connect response returned by the server.</returns>
|
|||
public Task<OpenIddictResponse> GetAsync(string uri, OpenIddictRequest request) |
|||
{ |
|||
if (request is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(request)); |
|||
} |
|||
|
|||
if (string.IsNullOrEmpty(uri)) |
|||
{ |
|||
throw new ArgumentException("The URL cannot be null or empty.", nameof(uri)); |
|||
} |
|||
|
|||
return GetAsync(new Uri(uri, UriKind.RelativeOrAbsolute), request); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sends a generic OpenID Connect request to the given endpoint using GET
|
|||
/// and converts the returned response to an OpenID Connect response.
|
|||
/// </summary>
|
|||
/// <param name="uri">The endpoint to which the request is sent.</param>
|
|||
/// <param name="request">The OpenID Connect request to send.</param>
|
|||
/// <returns>The OpenID Connect response returned by the server.</returns>
|
|||
public Task<OpenIddictResponse> GetAsync(Uri uri, OpenIddictRequest request) |
|||
=> SendAsync(HttpMethod.Get, uri, request); |
|||
|
|||
/// <summary>
|
|||
/// Sends a generic OpenID Connect request to the given endpoint using POST
|
|||
/// and converts the returned response to an OpenID Connect response.
|
|||
/// </summary>
|
|||
/// <param name="uri">The endpoint to which the request is sent.</param>
|
|||
/// <param name="request">The OpenID Connect request to send.</param>
|
|||
/// <returns>The OpenID Connect response returned by the server.</returns>
|
|||
public Task<OpenIddictResponse> PostAsync(string uri, OpenIddictRequest request) |
|||
{ |
|||
if (request is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(request)); |
|||
} |
|||
|
|||
if (string.IsNullOrEmpty(uri)) |
|||
{ |
|||
throw new ArgumentException("The URL cannot be null or empty.", nameof(uri)); |
|||
} |
|||
|
|||
return PostAsync(new Uri(uri, UriKind.RelativeOrAbsolute), request); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sends a generic OpenID Connect request to the given endpoint using POST
|
|||
/// and converts the returned response to an OpenID Connect response.
|
|||
/// </summary>
|
|||
/// <param name="uri">The endpoint to which the request is sent.</param>
|
|||
/// <param name="request">The OpenID Connect request to send.</param>
|
|||
/// <returns>The OpenID Connect response returned by the server.</returns>
|
|||
public Task<OpenIddictResponse> PostAsync(Uri uri, OpenIddictRequest request) |
|||
=> SendAsync(HttpMethod.Post, uri, request); |
|||
|
|||
/// <summary>
|
|||
/// Sends a generic OpenID Connect request to the given endpoint and
|
|||
/// converts the returned response to an OpenID Connect response.
|
|||
/// </summary>
|
|||
/// <param name="method">The HTTP method used to send the OpenID Connect request.</param>
|
|||
/// <param name="uri">The endpoint to which the request is sent.</param>
|
|||
/// <param name="request">The OpenID Connect request to send.</param>
|
|||
/// <returns>The OpenID Connect response returned by the server.</returns>
|
|||
public Task<OpenIddictResponse> SendAsync(string method, string uri, OpenIddictRequest request) |
|||
{ |
|||
if (request is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(request)); |
|||
} |
|||
|
|||
if (string.IsNullOrEmpty(method)) |
|||
{ |
|||
throw new ArgumentException("The HTTP method cannot be null or empty.", nameof(method)); |
|||
} |
|||
|
|||
if (string.IsNullOrEmpty(uri)) |
|||
{ |
|||
throw new ArgumentException("The URL cannot be null or empty.", nameof(uri)); |
|||
} |
|||
|
|||
return SendAsync(new HttpMethod(method), uri, request); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sends a generic OpenID Connect request to the given endpoint and
|
|||
/// converts the returned response to an OpenID Connect response.
|
|||
/// </summary>
|
|||
/// <param name="method">The HTTP method used to send the OpenID Connect request.</param>
|
|||
/// <param name="uri">The endpoint to which the request is sent.</param>
|
|||
/// <param name="request">The OpenID Connect request to send.</param>
|
|||
/// <returns>The OpenID Connect response returned by the server.</returns>
|
|||
public Task<OpenIddictResponse> SendAsync(HttpMethod method, string uri, OpenIddictRequest request) |
|||
{ |
|||
if (method is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(method)); |
|||
} |
|||
|
|||
if (request is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(request)); |
|||
} |
|||
|
|||
if (string.IsNullOrEmpty(uri)) |
|||
{ |
|||
throw new ArgumentException("The URL cannot be null or empty.", nameof(uri)); |
|||
} |
|||
|
|||
return SendAsync(method, new Uri(uri, UriKind.RelativeOrAbsolute), request); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sends a generic OpenID Connect request to the given endpoint and
|
|||
/// converts the returned response to an OpenID Connect response.
|
|||
/// </summary>
|
|||
/// <param name="method">The HTTP method used to send the OpenID Connect request.</param>
|
|||
/// <param name="uri">The endpoint to which the request is sent.</param>
|
|||
/// <param name="request">The OpenID Connect request to send.</param>
|
|||
/// <returns>The OpenID Connect response returned by the server.</returns>
|
|||
public virtual async Task<OpenIddictResponse> SendAsync(HttpMethod method, Uri uri, OpenIddictRequest request) |
|||
{ |
|||
if (method is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(method)); |
|||
} |
|||
|
|||
if (uri is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(uri)); |
|||
} |
|||
|
|||
if (request is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(request)); |
|||
} |
|||
|
|||
if (HttpClient.BaseAddress is null && !uri.IsAbsoluteUri) |
|||
{ |
|||
throw new ArgumentException("The address cannot be a relative URI when no base address " + |
|||
"is associated with the HTTP client.", nameof(uri)); |
|||
} |
|||
|
|||
using var message = CreateRequestMessage(request, method, uri); |
|||
using var response = await HttpClient.SendAsync(message); |
|||
|
|||
return await GetResponseAsync(response); |
|||
} |
|||
|
|||
private HttpRequestMessage CreateRequestMessage(OpenIddictRequest request, HttpMethod method, Uri uri) |
|||
{ |
|||
// Note: a dictionary is deliberately not used here to allow multiple parameters with the
|
|||
// same name to be specified. While initially not allowed by the core OAuth2 specification,
|
|||
// this is required for derived drafts like the OAuth2 token exchange specification.
|
|||
var parameters = new List<KeyValuePair<string?, string?>>(); |
|||
|
|||
foreach (var parameter in request.GetParameters()) |
|||
{ |
|||
// If the parameter is null or empty, send an empty value.
|
|||
if (OpenIddictParameter.IsNullOrEmpty(parameter.Value)) |
|||
{ |
|||
parameters.Add(new KeyValuePair<string?, string?>(parameter.Key, string.Empty)); |
|||
|
|||
continue; |
|||
} |
|||
|
|||
var values = (string?[]?) parameter.Value; |
|||
if (values is not { Length: > 0 }) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
foreach (var value in values) |
|||
{ |
|||
parameters.Add(new KeyValuePair<string?, string?>(parameter.Key, value)); |
|||
} |
|||
} |
|||
|
|||
if (method == HttpMethod.Get && parameters.Count != 0) |
|||
{ |
|||
var builder = new StringBuilder(); |
|||
|
|||
foreach (var parameter in parameters) |
|||
{ |
|||
if (string.IsNullOrEmpty(parameter.Key)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (builder.Length != 0) |
|||
{ |
|||
builder.Append('&'); |
|||
} |
|||
|
|||
builder.Append(UrlEncoder.Default.Encode(parameter.Key)); |
|||
|
|||
if (!string.IsNullOrEmpty(parameter.Value)) |
|||
{ |
|||
builder.Append('='); |
|||
builder.Append(UrlEncoder.Default.Encode(parameter.Value)); |
|||
} |
|||
} |
|||
|
|||
if (!uri.IsAbsoluteUri) |
|||
{ |
|||
uri = new Uri(HttpClient.BaseAddress!, uri); |
|||
} |
|||
|
|||
uri = new UriBuilder(uri) { Query = builder.ToString() }.Uri; |
|||
} |
|||
|
|||
var message = new HttpRequestMessage(method, uri); |
|||
|
|||
if (method != HttpMethod.Get) |
|||
{ |
|||
message.Content = new FormUrlEncodedContent(parameters); |
|||
} |
|||
|
|||
return message; |
|||
} |
|||
|
|||
private async Task<OpenIddictResponse> GetResponseAsync(HttpResponseMessage message) |
|||
{ |
|||
if (message.Headers.WwwAuthenticate.Count is not 0) |
|||
{ |
|||
var parameters = new Dictionary<string, StringValues>(message.Headers.WwwAuthenticate.Count); |
|||
|
|||
foreach (var header in message.Headers.WwwAuthenticate) |
|||
{ |
|||
if (string.IsNullOrEmpty(header.Parameter)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
// Note: while initially not allowed by the core OAuth 2.0 specification, multiple
|
|||
// parameters with the same name are used by derived drafts like the OAuth 2.0
|
|||
// token exchange specification. For consistency, multiple parameters with the
|
|||
// same name are also supported when returned as part of WWW-Authentication headers.
|
|||
|
|||
foreach (var parameter in header.Parameter.Split(Separators.Comma, StringSplitOptions.RemoveEmptyEntries)) |
|||
{ |
|||
var values = parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries); |
|||
if (values.Length is not 2) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var (name, value) = ( |
|||
values[0]?.Trim(Separators.Space[0]), |
|||
values[1]?.Trim(Separators.Space[0], Separators.DoubleQuote[0])); |
|||
|
|||
if (string.IsNullOrEmpty(name)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
parameters[name] = parameters.ContainsKey(name) ? |
|||
StringValues.Concat(parameters[name], value?.Replace("\\\"", "\"")) : |
|||
new StringValues(value?.Replace("\\\"", "\"")); |
|||
} |
|||
} |
|||
|
|||
return new OpenIddictResponse(parameters); |
|||
} |
|||
|
|||
else if (message.Headers.Location is not null) |
|||
{ |
|||
var payload = message.Headers.Location.Fragment; |
|||
if (string.IsNullOrEmpty(payload)) |
|||
{ |
|||
payload = message.Headers.Location.Query; |
|||
} |
|||
|
|||
if (string.IsNullOrEmpty(payload)) |
|||
{ |
|||
return new OpenIddictResponse(); |
|||
} |
|||
|
|||
static string? UnescapeDataString(string value) |
|||
{ |
|||
if (string.IsNullOrEmpty(value)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
return Uri.UnescapeDataString(value.Replace("+", "%20")); |
|||
} |
|||
|
|||
// Note: a dictionary is deliberately not used here to allow multiple parameters with the
|
|||
// same name to be retrieved. While initially not allowed by the core OAuth2 specification,
|
|||
// this is required for derived drafts like the OAuth2 token exchange specification.
|
|||
var parameters = new List<KeyValuePair<string, string?>>(); |
|||
|
|||
foreach (var element in new StringTokenizer(payload, Separators.Ampersand)) |
|||
{ |
|||
var segment = element; |
|||
if (segment.Length == 0) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
// Always skip the first char (# or ?).
|
|||
if (segment.Offset == 0) |
|||
{ |
|||
segment = segment.Subsegment(1, segment.Length - 1); |
|||
} |
|||
|
|||
var index = segment.IndexOf('='); |
|||
if (index == -1) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var name = UnescapeDataString(segment.Substring(0, index)); |
|||
if (string.IsNullOrEmpty(name)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var value = UnescapeDataString(segment.Substring(index + 1, segment.Length - (index + 1))); |
|||
|
|||
parameters.Add(new KeyValuePair<string, string?>(name, value)); |
|||
} |
|||
|
|||
return new OpenIddictResponse( |
|||
from parameter in parameters |
|||
group parameter by parameter.Key into grouping |
|||
let values = grouping.Select(parameter => parameter.Value) |
|||
select new KeyValuePair<string, StringValues>(grouping.Key, values.ToArray())); |
|||
} |
|||
|
|||
else if (string.Equals(message.Content?.Headers?.ContentType?.MediaType, "application/json", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
return (await message.Content!.ReadFromJsonAsync<OpenIddictResponse>())!; |
|||
} |
|||
|
|||
else if (string.Equals(message.Content?.Headers?.ContentType?.MediaType, "text/html", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
// Note: this test client is only used with OpenIddict's ASP.NET Core or OWIN hosts,
|
|||
// that always return their HTTP responses encoded using UTF-8. As such, the stream
|
|||
// returned by ReadAsStreamAsync() is always assumed to contain UTF-8 encoded payloads.
|
|||
using var stream = await message.Content!.ReadAsStreamAsync(); |
|||
|
|||
using var document = await HtmlParser.ParseDocumentAsync(stream); |
|||
if (document.Body is null) |
|||
{ |
|||
return new OpenIddictResponse(); |
|||
} |
|||
|
|||
// Note: a dictionary is deliberately not used here to allow multiple parameters with the
|
|||
// same name to be retrieved. While initially not allowed by the core OAuth2 specification,
|
|||
// this is required for derived drafts like the OAuth2 token exchange specification.
|
|||
var parameters = new List<KeyValuePair<string, string?>>(); |
|||
|
|||
foreach (var element in document.Body.GetElementsByTagName("input")) |
|||
{ |
|||
var name = element.GetAttribute("name"); |
|||
if (string.IsNullOrEmpty(name)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var value = element.GetAttribute("value"); |
|||
|
|||
parameters.Add(new KeyValuePair<string, string?>(name, value)); |
|||
} |
|||
|
|||
return new OpenIddictResponse( |
|||
from parameter in parameters |
|||
group parameter by parameter.Key into grouping |
|||
let values = grouping.Select(parameter => parameter.Value) |
|||
select new KeyValuePair<string, StringValues>(grouping.Key, values.ToArray())); |
|||
} |
|||
|
|||
else if (string.Equals(message.Content?.Headers?.ContentType?.MediaType, "text/plain", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
// Note: this test client is only used with OpenIddict's ASP.NET Core or OWIN hosts,
|
|||
// that always return their HTTP responses encoded using UTF-8. As such, the stream
|
|||
// returned by ReadAsStreamAsync() is always assumed to contain UTF-8 encoded payloads.
|
|||
using var stream = await message.Content!.ReadAsStreamAsync(); |
|||
using var reader = new StreamReader(stream); |
|||
|
|||
// Note: a dictionary is deliberately not used here to allow multiple parameters with the
|
|||
// same name to be retrieved. While initially not allowed by the core OAuth2 specification,
|
|||
// this is required for derived drafts like the OAuth2 token exchange specification.
|
|||
var parameters = new List<KeyValuePair<string, string>>(); |
|||
|
|||
for (var line = await reader.ReadLineAsync(); line is not null; line = await reader.ReadLineAsync()) |
|||
{ |
|||
var index = line.IndexOf(':'); |
|||
if (index == -1) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var name = line.Substring(0, index); |
|||
if (string.IsNullOrEmpty(name)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var value = line.Substring(index + 1); |
|||
|
|||
parameters.Add(new KeyValuePair<string, string>(name, value)); |
|||
} |
|||
|
|||
return new OpenIddictResponse( |
|||
from parameter in parameters |
|||
group parameter by parameter.Key into grouping |
|||
let values = grouping.Select(parameter => parameter.Value) |
|||
select new KeyValuePair<string, StringValues>(grouping.Key, values.ToArray())); |
|||
} |
|||
|
|||
return new OpenIddictResponse(); |
|||
} |
|||
|
|||
public ValueTask DisposeAsync() |
|||
{ |
|||
HttpClient.Dispose(); |
|||
|
|||
return default; |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
/* |
|||
* 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. |
|||
*/ |
|||
|
|||
namespace OpenIddict.Validation.IntegrationTests; |
|||
|
|||
/// <summary>
|
|||
/// Represents a test host used by the validation integration tests.
|
|||
/// </summary>
|
|||
public abstract class OpenIddictValidationIntegrationTestServer : IAsyncDisposable |
|||
{ |
|||
public abstract ValueTask<OpenIddictValidationIntegrationTestClient> CreateClientAsync(); |
|||
|
|||
public virtual ValueTask DisposeAsync() => default; |
|||
} |
|||
@ -0,0 +1,325 @@ |
|||
|
|||
|
|||
/* |
|||
* 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.Reflection; |
|||
using System.Security.Claims; |
|||
using System.Security.Cryptography.X509Certificates; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using Microsoft.IdentityModel.Protocols; |
|||
using Microsoft.IdentityModel.Tokens; |
|||
using Moq; |
|||
using OpenIddict.Core; |
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
using static OpenIddict.Validation.OpenIddictValidationEvents; |
|||
using static OpenIddict.Validation.OpenIddictValidationHandlers; |
|||
using static OpenIddict.Validation.OpenIddictValidationHandlers.Protection; |
|||
|
|||
namespace OpenIddict.Validation.IntegrationTests; |
|||
|
|||
public abstract partial class OpenIddictValidationIntegrationTests |
|||
{ |
|||
protected OpenIddictValidationIntegrationTests(ITestOutputHelper outputHelper) |
|||
{ |
|||
OutputHelper = outputHelper; |
|||
} |
|||
|
|||
protected ITestOutputHelper OutputHelper { get; } |
|||
|
|||
[Fact] |
|||
public async Task ProcessAuthentication_InvalidIssuerThrowsAnException() |
|||
{ |
|||
// Arrange
|
|||
await using var server = await CreateServerAsync(options => options.Configure(options => |
|||
{ |
|||
options.ConfigurationManager = new StaticConfigurationManager<OpenIddictConfiguration>(new() |
|||
{ |
|||
Issuer = new Uri("https://fabrikam.com/") |
|||
}); |
|||
})); |
|||
|
|||
await using var client = await server.CreateClientAsync(); |
|||
|
|||
// Act and assert
|
|||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(delegate |
|||
{ |
|||
return client.PostAsync("/authenticate", new OpenIddictRequest()); |
|||
}); |
|||
|
|||
Assert.Equal(SR.GetResourceString(SR.ID0307), exception.Message); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ProcessAuthentication_EvalutesCorrectValidatedTokens() |
|||
{ |
|||
// Arrange
|
|||
await using var server = await CreateServerAsync(options => |
|||
{ |
|||
options.AddEventHandler<ProcessAuthenticationContext>(builder => |
|||
{ |
|||
builder.UseInlineHandler(context => |
|||
{ |
|||
// Assert
|
|||
Assert.True(context.ExtractAccessToken); |
|||
Assert.True(context.RequireAccessToken); |
|||
Assert.True(context.ValidateAccessToken); |
|||
|
|||
return default; |
|||
}); |
|||
|
|||
builder.SetOrder(EvaluateValidatedTokens.Descriptor.Order + 1); |
|||
}); |
|||
}); |
|||
|
|||
await using var client = await server.CreateClientAsync(); |
|||
|
|||
// Act
|
|||
var response = await client.PostAsync("/authenticate", new OpenIddictRequest()); |
|||
|
|||
// Assert
|
|||
Assert.Equal(0, response.Count); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ProcessAuthentication_RejectsDemandWhenAccessTokenIsMissing() |
|||
{ |
|||
// Arrange
|
|||
await using var server = await CreateServerAsync(options => |
|||
{ |
|||
options.AddEventHandler<ProcessAuthenticationContext>(builder => |
|||
{ |
|||
builder.UseInlineHandler(context => |
|||
{ |
|||
// Assert
|
|||
Assert.True(context.IsRejected); |
|||
Assert.Equal(Errors.MissingToken, context.Error); |
|||
Assert.Equal(SR.GetResourceString(SR.ID2000), context.ErrorDescription); |
|||
|
|||
return default; |
|||
}); |
|||
|
|||
builder.SetOrder(ValidateRequiredTokens.Descriptor.Order + 1); |
|||
}); |
|||
}); |
|||
|
|||
await using var client = await server.CreateClientAsync(); |
|||
|
|||
// Act
|
|||
var response = await client.PostAsync("/authenticate", new OpenIddictRequest()); |
|||
|
|||
// Assert
|
|||
Assert.Equal(0, response.Count); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ProcessAuthentication_RejectsDemandWhenAccessTokenIsInvalid() |
|||
{ |
|||
// Arrange
|
|||
await using var server = await CreateServerAsync(); |
|||
await using var client = await server.CreateClientAsync(); |
|||
|
|||
// Act
|
|||
var response = await client.PostAsync("/authenticate", new OpenIddictRequest |
|||
{ |
|||
AccessToken = "SlAV32hkKG" |
|||
}); |
|||
|
|||
// Assert
|
|||
Assert.Equal(Errors.InvalidToken, response.Error); |
|||
Assert.Equal(SR.GetResourceString(SR.ID2004), response.ErrorDescription); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ProcessAuthentication_ReturnsExpectedIdentityWhenAccessTokenIsValid() |
|||
{ |
|||
// Arrange
|
|||
await using var server = await CreateServerAsync(options => |
|||
{ |
|||
options.AddEventHandler<ValidateTokenContext>(builder => |
|||
{ |
|||
builder.UseInlineHandler(context => |
|||
{ |
|||
Assert.Equal("access_token", context.Token); |
|||
Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); |
|||
|
|||
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) |
|||
.SetTokenType(TokenTypeHints.AccessToken) |
|||
.SetClaim(Claims.Subject, "Bob le Magnifique"); |
|||
|
|||
return default; |
|||
}); |
|||
|
|||
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); |
|||
}); |
|||
}); |
|||
|
|||
await using var client = await server.CreateClientAsync(); |
|||
|
|||
// Act
|
|||
var response = await client.GetAsync("/authenticate", new OpenIddictRequest |
|||
{ |
|||
AccessToken = "access_token" |
|||
}); |
|||
|
|||
// Assert
|
|||
Assert.Equal("Bob le Magnifique", (string?) response[Claims.Subject]); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ProcessChallenge_ReturnsDefaultErrorWhenNoneIsSpecified() |
|||
{ |
|||
// Arrange
|
|||
await using var server = await CreateServerAsync(); |
|||
await using var client = await server.CreateClientAsync(); |
|||
|
|||
// Act
|
|||
var response = await client.PostAsync("/challenge", new OpenIddictRequest()); |
|||
|
|||
// Assert
|
|||
Assert.Equal(Errors.InsufficientAccess, response.Error); |
|||
Assert.Equal(SR.GetResourceString(SR.ID2095), response.ErrorDescription); |
|||
Assert.Equal(SR.FormatID8000(SR.ID2095), response.ErrorUri); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("custom_error", null, null)] |
|||
[InlineData("custom_error", "custom_description", null)] |
|||
[InlineData("custom_error", "custom_description", "custom_uri")] |
|||
[InlineData(null, "custom_description", null)] |
|||
[InlineData(null, "custom_description", "custom_uri")] |
|||
[InlineData(null, null, "custom_uri")] |
|||
[InlineData(null, null, null)] |
|||
public async Task ProcessChallenge_AllowsRejectingRequest(string error, string description, string uri) |
|||
{ |
|||
// Arrange
|
|||
await using var server = await CreateServerAsync(options => |
|||
{ |
|||
options.AddEventHandler<ProcessChallengeContext>(builder => |
|||
builder.UseInlineHandler(context => |
|||
{ |
|||
context.Reject(error, description, uri); |
|||
|
|||
return default; |
|||
})); |
|||
}); |
|||
|
|||
await using var client = await server.CreateClientAsync(); |
|||
|
|||
// Act
|
|||
var response = await client.PostAsync("/challenge", new OpenIddictRequest()); |
|||
|
|||
// Assert
|
|||
Assert.Equal(error ?? Errors.InvalidRequest, response.Error); |
|||
Assert.Equal(description, response.ErrorDescription); |
|||
Assert.Equal(uri, response.ErrorUri); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ProcessChallenge_AllowsHandlingResponse() |
|||
{ |
|||
// Arrange
|
|||
await using var server = await CreateServerAsync(options => |
|||
{ |
|||
options.AddEventHandler<ProcessChallengeContext>(builder => |
|||
builder.UseInlineHandler(context => |
|||
{ |
|||
context.Transaction.SetProperty("custom_response", new |
|||
{ |
|||
name = "Bob le Bricoleur" |
|||
}); |
|||
|
|||
context.HandleRequest(); |
|||
|
|||
return default; |
|||
})); |
|||
}); |
|||
|
|||
await using var client = await server.CreateClientAsync(); |
|||
|
|||
// Act
|
|||
var response = await client.PostAsync("/challenge", new OpenIddictRequest()); |
|||
|
|||
// Assert
|
|||
Assert.Equal("Bob le Bricoleur", (string?) response["name"]); |
|||
} |
|||
|
|||
protected virtual void ConfigureServices(IServiceCollection services) |
|||
{ |
|||
services.AddOpenIddict() |
|||
.AddCore(options => |
|||
{ |
|||
options.SetDefaultAuthorizationEntity<OpenIddictAuthorization>() |
|||
.SetDefaultTokenEntity<OpenIddictToken>(); |
|||
|
|||
options.Services.AddSingleton(CreateAuthorizationManager()) |
|||
.AddSingleton(CreateTokenManager()); |
|||
}) |
|||
|
|||
.AddValidation(options => |
|||
{ |
|||
options.SetIssuer(new Uri("https://contoso.com/")); |
|||
|
|||
options.SetConfiguration(new OpenIddictConfiguration |
|||
{ |
|||
SigningKeys = |
|||
{ |
|||
new X509SecurityKey(GetSigningCertificate( |
|||
assembly: typeof(OpenIddictValidationIntegrationTests).Assembly, |
|||
resource: "OpenIddict.Validation.IntegrationTests.Certificate.cer", |
|||
password: null)) |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
static X509Certificate2 GetSigningCertificate(Assembly assembly, string resource, string? password) |
|||
{ |
|||
using var stream = assembly.GetManifestResourceStream(resource) ?? |
|||
throw new InvalidOperationException(SR.GetResourceString(SR.ID0064)); |
|||
|
|||
using var buffer = new MemoryStream(); |
|||
stream.CopyTo(buffer); |
|||
|
|||
return new X509Certificate2(buffer.ToArray(), password, X509KeyStorageFlags.MachineKeySet); |
|||
} |
|||
} |
|||
|
|||
protected abstract ValueTask<OpenIddictValidationIntegrationTestServer> CreateServerAsync( |
|||
Action<OpenIddictValidationBuilder>? configuration = null); |
|||
|
|||
protected OpenIddictAuthorizationManager<OpenIddictAuthorization> CreateAuthorizationManager( |
|||
Action<Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>>? configuration = null) |
|||
{ |
|||
var manager = new Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>( |
|||
Mock.Of<IOpenIddictAuthorizationCache<OpenIddictAuthorization>>(), |
|||
OutputHelper.ToLogger<OpenIddictAuthorizationManager<OpenIddictAuthorization>>(), |
|||
Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(), |
|||
Mock.Of<IOpenIddictAuthorizationStoreResolver>()); |
|||
|
|||
configuration?.Invoke(manager); |
|||
|
|||
return manager.Object; |
|||
} |
|||
|
|||
protected OpenIddictTokenManager<OpenIddictToken> CreateTokenManager( |
|||
Action<Mock<OpenIddictTokenManager<OpenIddictToken>>>? configuration = null) |
|||
{ |
|||
var manager = new Mock<OpenIddictTokenManager<OpenIddictToken>>( |
|||
Mock.Of<IOpenIddictTokenCache<OpenIddictToken>>(), |
|||
OutputHelper.ToLogger<OpenIddictTokenManager<OpenIddictToken>>(), |
|||
Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(), |
|||
Mock.Of<IOpenIddictTokenStoreResolver>()); |
|||
|
|||
configuration?.Invoke(manager); |
|||
|
|||
return manager.Object; |
|||
} |
|||
|
|||
public class OpenIddictAuthorization { } |
|||
public class OpenIddictToken { } |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net461;net472;net48</TargetFrameworks> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\OpenIddict.Validation.Owin\OpenIddict.Validation.Owin.csproj" /> |
|||
<ProjectReference Include="..\OpenIddict.Validation.IntegrationTests\OpenIddict.Validation.IntegrationTests.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.Owin.Testing" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> |
|||
<Reference Include="System.Net.Http" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Using Include="OpenIddict.Abstractions" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,38 @@ |
|||
/* |
|||
* 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.Diagnostics.CodeAnalysis; |
|||
using Microsoft.Owin.Testing; |
|||
using OpenIddict.Validation.IntegrationTests; |
|||
|
|||
namespace OpenIddict.Validation.Owin.IntegrationTests; |
|||
|
|||
/// <summary>
|
|||
/// Represents a test host used by the validation integration tests.
|
|||
/// </summary>
|
|||
public class OpenIddictValidationOwinIntegrationTestValidation : OpenIddictValidationIntegrationTestServer |
|||
{ |
|||
public OpenIddictValidationOwinIntegrationTestValidation(TestServer server) |
|||
=> Server = server; |
|||
|
|||
/// <summary>
|
|||
/// Gets the ASP.NET Core test server used by this instance.
|
|||
/// </summary>
|
|||
public TestServer Server { get; } |
|||
|
|||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", |
|||
Justification = "The caller is responsible for disposing the test client.")] |
|||
public override ValueTask<OpenIddictValidationIntegrationTestClient> CreateClientAsync() |
|||
=> new(new OpenIddictValidationIntegrationTestClient(Server.HttpClient)); |
|||
|
|||
public override ValueTask DisposeAsync() |
|||
{ |
|||
// Dispose of the underlying test server.
|
|||
Server.Dispose(); |
|||
|
|||
return default; |
|||
} |
|||
} |
|||
@ -0,0 +1,217 @@ |
|||
/* |
|||
* 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.Diagnostics.CodeAnalysis; |
|||
using System.Security.Claims; |
|||
using System.Text.Json; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Owin; |
|||
using Microsoft.Owin.Security; |
|||
using Microsoft.Owin.Testing; |
|||
using OpenIddict.Validation.IntegrationTests; |
|||
using Owin; |
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
using static OpenIddict.Validation.OpenIddictValidationEvents; |
|||
using static OpenIddict.Validation.OpenIddictValidationHandlers.Protection; |
|||
|
|||
namespace OpenIddict.Validation.Owin.IntegrationTests; |
|||
|
|||
public partial class OpenIddictValidationOwinIntegrationTests : OpenIddictValidationIntegrationTests |
|||
{ |
|||
public OpenIddictValidationOwinIntegrationTests(ITestOutputHelper outputHelper) |
|||
: base(outputHelper) |
|||
{ |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ProcessAuthentication_CreationDateIsMappedToIssuedUtc() |
|||
{ |
|||
// Arrange
|
|||
await using var server = await CreateServerAsync(options => |
|||
{ |
|||
options.AddEventHandler<ValidateTokenContext>(builder => |
|||
{ |
|||
builder.UseInlineHandler(context => |
|||
{ |
|||
Assert.Equal("access_token", context.Token); |
|||
Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); |
|||
|
|||
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) |
|||
.SetTokenType(TokenTypeHints.AccessToken) |
|||
.SetClaim(Claims.Subject, "Bob le Magnifique") |
|||
.SetCreationDate(new DateTimeOffset(2020, 01, 01, 00, 00, 00, TimeSpan.Zero)); |
|||
|
|||
return default; |
|||
}); |
|||
|
|||
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); |
|||
}); |
|||
}); |
|||
|
|||
await using var client = await server.CreateClientAsync(); |
|||
|
|||
// Act
|
|||
var response = await client.GetAsync("/authenticate/properties", new OpenIddictRequest |
|||
{ |
|||
AccessToken = "access_token" |
|||
}); |
|||
|
|||
// Assert
|
|||
var properties = new AuthenticationProperties(response.GetParameters() |
|||
.ToDictionary(parameter => parameter.Key, parameter => (string?) parameter.Value)); |
|||
|
|||
Assert.Equal(new DateTimeOffset(2020, 01, 01, 00, 00, 00, TimeSpan.Zero), properties.IssuedUtc); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ProcessAuthentication_ExpirationDateIsMappedToIssuedUtc() |
|||
{ |
|||
// Arrange
|
|||
await using var server = await CreateServerAsync(options => |
|||
{ |
|||
options.AddEventHandler<ValidateTokenContext>(builder => |
|||
{ |
|||
builder.UseInlineHandler(context => |
|||
{ |
|||
Assert.Equal("access_token", context.Token); |
|||
Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); |
|||
|
|||
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) |
|||
.SetTokenType(TokenTypeHints.AccessToken) |
|||
.SetExpirationDate(new DateTimeOffset(2120, 01, 01, 00, 00, 00, TimeSpan.Zero)); |
|||
|
|||
return default; |
|||
}); |
|||
|
|||
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); |
|||
}); |
|||
}); |
|||
|
|||
await using var client = await server.CreateClientAsync(); |
|||
|
|||
// Act
|
|||
var response = await client.GetAsync("/authenticate/properties", new OpenIddictRequest |
|||
{ |
|||
AccessToken = "access_token" |
|||
}); |
|||
|
|||
// Assert
|
|||
var properties = new AuthenticationProperties(response.GetParameters() |
|||
.ToDictionary(parameter => parameter.Key, parameter => (string?) parameter.Value)); |
|||
|
|||
Assert.Equal(new DateTimeOffset(2120, 01, 01, 00, 00, 00, TimeSpan.Zero), properties.ExpiresUtc); |
|||
} |
|||
|
|||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", |
|||
Justification = "The caller is responsible for disposing the test Validation.")] |
|||
protected override ValueTask<OpenIddictValidationIntegrationTestServer> CreateServerAsync(Action<OpenIddictValidationBuilder>? configuration = null) |
|||
{ |
|||
var services = new ServiceCollection(); |
|||
ConfigureServices(services); |
|||
|
|||
services.AddLogging(options => options.AddXUnit(OutputHelper)); |
|||
|
|||
services.AddOpenIddict() |
|||
.AddValidation(options => |
|||
{ |
|||
options.UseOwin(); |
|||
|
|||
configuration?.Invoke(options); |
|||
}); |
|||
|
|||
var provider = services.BuildServiceProvider(); |
|||
|
|||
var server = TestServer.Create(app => |
|||
{ |
|||
app.Use(async (context, next) => |
|||
{ |
|||
using var scope = provider.CreateScope(); |
|||
|
|||
context.Set(typeof(IServiceProvider).FullName, scope.ServiceProvider); |
|||
|
|||
try |
|||
{ |
|||
await next(); |
|||
} |
|||
|
|||
finally |
|||
{ |
|||
context.Environment.Remove(typeof(IServiceProvider).FullName); |
|||
} |
|||
}); |
|||
|
|||
app.Use(async (context, next) => |
|||
{ |
|||
await next(); |
|||
|
|||
var transaction = context.Get<OpenIddictValidationTransaction>(typeof(OpenIddictValidationTransaction).FullName); |
|||
var response = transaction?.GetProperty<object>("custom_response"); |
|||
if (response is not null) |
|||
{ |
|||
context.Response.ContentType = "application/json"; |
|||
await context.Response.WriteAsync(JsonSerializer.Serialize(response)); |
|||
} |
|||
}); |
|||
|
|||
app.UseOpenIddictValidation(); |
|||
|
|||
app.Use(async (context, next) => |
|||
{ |
|||
if (context.Request.Path == new PathString("/authenticate")) |
|||
{ |
|||
var result = await context.Authentication.AuthenticateAsync(OpenIddictValidationOwinDefaults.AuthenticationType); |
|||
if (result?.Identity is null) |
|||
{ |
|||
context.Authentication.Challenge(OpenIddictValidationOwinDefaults.AuthenticationType); |
|||
return; |
|||
} |
|||
|
|||
var claims = result.Identity.Claims.GroupBy(claim => claim.Type) |
|||
.Select(group => new KeyValuePair<string, string?[]?>( |
|||
group.Key, group.Select(claim => claim.Value).ToArray())); |
|||
|
|||
context.Response.ContentType = "application/json"; |
|||
await context.Response.WriteAsync(JsonSerializer.Serialize(new OpenIddictResponse(claims))); |
|||
return; |
|||
} |
|||
|
|||
else if (context.Request.Path == new PathString("/authenticate/properties")) |
|||
{ |
|||
var result = await context.Authentication.AuthenticateAsync(OpenIddictValidationOwinDefaults.AuthenticationType); |
|||
if (result?.Properties is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
context.Response.ContentType = "application/json"; |
|||
await context.Response.WriteAsync(JsonSerializer.Serialize(new OpenIddictResponse(result.Properties.Dictionary))); |
|||
return; |
|||
} |
|||
|
|||
else if (context.Request.Path == new PathString("/challenge")) |
|||
{ |
|||
context.Authentication.Challenge(OpenIddictValidationOwinDefaults.AuthenticationType); |
|||
return; |
|||
} |
|||
|
|||
await next(); |
|||
}); |
|||
|
|||
app.Run(context => |
|||
{ |
|||
context.Response.ContentType = "application/json"; |
|||
return context.Response.WriteAsync(JsonSerializer.Serialize(new |
|||
{ |
|||
name = "Bob le Magnifique" |
|||
})); |
|||
}); |
|||
}); |
|||
|
|||
return new(new OpenIddictValidationOwinIntegrationTestValidation(server)); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue