Browse Source

Allow returning custom parameters from the OWIN client/server hosts

pull/1457/head
Kévin Chalet 4 years ago
parent
commit
791da55480
  1. 8
      src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs
  2. 40
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs
  3. 8
      src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
  4. 236
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
  5. 4
      test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs
  6. 151
      test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs

8
src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs

@ -35,6 +35,14 @@ public static class OpenIddictClientOwinConstants
public const string UserinfoTokenPrincipal = ".userinfo_token_principal";
}
public static class PropertyTypes
{
public const string Boolean = "#boolean";
public const string Integer = "#integer";
public const string Json = "#json";
public const string String = "#string";
}
public static class Tokens
{
public const string AuthorizationCode = "authorization_code";

40
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs

@ -7,8 +7,10 @@
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
@ -454,6 +456,44 @@ public static partial class OpenIddictClientOwinHandlers
context.TargetLinkUri = properties.RedirectUri;
}
// Note: unlike ASP.NET Core, Owin's AuthenticationProperties doesn't offer a strongly-typed
// dictionary that allows flowing parameters while preserving their original types. To allow
// returning custom parameters, the OWIN host allows using AuthenticationProperties.Dictionary
// but requires suffixing the properties that are meant to be used as parameters using a special
// suffix that indicates that the property is public and determines its actual representation.
foreach (var property in properties.Dictionary)
{
var (name, value) = property.Key switch
{
// If the property ends with #string, represent it as a string parameter.
string key when key.EndsWith(PropertyTypes.String, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.String.Length),
Value: new OpenIddictParameter(property.Value)),
// If the property ends with #boolean, return it as a boolean parameter.
string key when key.EndsWith(PropertyTypes.Boolean, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Boolean.Length),
Value: new OpenIddictParameter(bool.Parse(property.Value))),
// If the property ends with #integer, return it as an integer parameter.
string key when key.EndsWith(PropertyTypes.Integer, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Integer.Length),
Value: new OpenIddictParameter(long.Parse(property.Value, CultureInfo.InvariantCulture))),
// If the property ends with #json, return it as a JSON parameter.
string key when key.EndsWith(PropertyTypes.Json, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Json.Length),
Value: new OpenIddictParameter(JsonSerializer.Deserialize<JsonElement>(property.Value))),
_ => default
};
if (!string.IsNullOrEmpty(name))
{
context.Parameters[name] = value;
}
}
return default;
}
}

8
src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs

@ -51,6 +51,14 @@ public static class OpenIddictServerOwinConstants
public const string UserCodePrincipal = ".user_code_principal";
}
public static class PropertyTypes
{
public const string Boolean = "#boolean";
public const string Integer = "#integer";
public const string Json = "#json";
public const string String = "#string";
}
public static class Tokens
{
public const string AccessToken = "access_token";

236
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs

@ -7,6 +7,7 @@
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
@ -32,7 +33,18 @@ public static partial class OpenIddictServerOwinHandlers
/*
* Challenge processing:
*/
AttachHostChallengeError.Descriptor)
AttachHostChallengeError.Descriptor,
ResolveHostChallengeParameters.Descriptor,
/*
* Sign-in processing:
*/
ResolveHostSignInParameters.Descriptor,
/*
* Sign-out processing:
*/
ResolveHostSignOutParameters.Descriptor)
.AddRange(Authentication.DefaultHandlers)
.AddRange(Device.DefaultHandlers)
.AddRange(Discovery.DefaultHandlers)
@ -304,6 +316,228 @@ public static partial class OpenIddictServerOwinHandlers
}
}
/// <summary>
/// Contains the logic responsible for resolving the additional sign-in parameters stored in the
/// OWIN authentication properties specified by the application that triggered the sign-in operation.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class ResolveHostChallengeParameters : IOpenIddictServerHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ResolveHostChallengeParameters>()
.SetOrder(AttachChallengeParameters.Descriptor.Order - 500)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName!);
if (properties is null)
{
return default;
}
// Note: unlike ASP.NET Core, Owin's AuthenticationProperties doesn't offer a strongly-typed
// dictionary that allows flowing parameters while preserving their original types. To allow
// returning custom parameters, the OWIN host allows using AuthenticationProperties.Dictionary
// but requires suffixing the properties that are meant to be used as parameters using a special
// suffix that indicates that the property is public and determines its actual representation.
foreach (var property in properties.Dictionary)
{
var (name, value) = property.Key switch
{
// If the property ends with #string, represent it as a string parameter.
string key when key.EndsWith(PropertyTypes.String, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.String.Length),
Value: new OpenIddictParameter(property.Value)),
// If the property ends with #boolean, return it as a boolean parameter.
string key when key.EndsWith(PropertyTypes.Boolean, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Boolean.Length),
Value: new OpenIddictParameter(bool.Parse(property.Value))),
// If the property ends with #integer, return it as an integer parameter.
string key when key.EndsWith(PropertyTypes.Integer, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Integer.Length),
Value: new OpenIddictParameter(long.Parse(property.Value, CultureInfo.InvariantCulture))),
// If the property ends with #json, return it as a JSON parameter.
string key when key.EndsWith(PropertyTypes.Json, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Json.Length),
Value: new OpenIddictParameter(JsonSerializer.Deserialize<JsonElement>(property.Value))),
_ => default
};
if (!string.IsNullOrEmpty(name))
{
context.Parameters[name] = value;
}
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for resolving the additional sign-in parameters stored in the
/// OWIN authentication properties specified by the application that triggered the sign-in operation.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class ResolveHostSignInParameters : IOpenIddictServerHandler<ProcessSignInContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ResolveHostSignInParameters>()
.SetOrder(AttachSignInParameters.Descriptor.Order - 500)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignInContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName!);
if (properties is null)
{
return default;
}
// Note: unlike ASP.NET Core, Owin's AuthenticationProperties doesn't offer a strongly-typed
// dictionary that allows flowing parameters while preserving their original types. To allow
// returning custom parameters, the OWIN host allows using AuthenticationProperties.Dictionary
// but requires suffixing the properties that are meant to be used as parameters using a special
// suffix that indicates that the property is public and determines its actual representation.
foreach (var property in properties.Dictionary)
{
var (name, value) = property.Key switch
{
// If the property ends with #string, represent it as a string parameter.
string key when key.EndsWith(PropertyTypes.String, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.String.Length),
Value: new OpenIddictParameter(property.Value)),
// If the property ends with #boolean, return it as a boolean parameter.
string key when key.EndsWith(PropertyTypes.Boolean, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Boolean.Length),
Value: new OpenIddictParameter(bool.Parse(property.Value))),
// If the property ends with #integer, return it as an integer parameter.
string key when key.EndsWith(PropertyTypes.Integer, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Integer.Length),
Value: new OpenIddictParameter(long.Parse(property.Value, CultureInfo.InvariantCulture))),
// If the property ends with #json, return it as a JSON parameter.
string key when key.EndsWith(PropertyTypes.Json, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Json.Length),
Value: new OpenIddictParameter(JsonSerializer.Deserialize<JsonElement>(property.Value))),
_ => default
};
if (!string.IsNullOrEmpty(name))
{
context.Parameters[name] = value;
}
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for resolving the additional sign-out parameters stored in the
/// OWIN authentication properties specified by the application that triggered the sign-out operation.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class ResolveHostSignOutParameters : IOpenIddictServerHandler<ProcessSignOutContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ResolveHostSignOutParameters>()
.SetOrder(AttachSignOutParameters.Descriptor.Order - 500)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName!);
if (properties is null)
{
return default;
}
// Note: unlike ASP.NET Core, Owin's AuthenticationProperties doesn't offer a strongly-typed
// dictionary that allows flowing parameters while preserving their original types. To allow
// returning custom parameters, the OWIN host allows using AuthenticationProperties.Dictionary
// but requires suffixing the properties that are meant to be used as parameters using a special
// suffix that indicates that the property is public and determines its actual representation.
foreach (var property in properties.Dictionary)
{
var (name, value) = property.Key switch
{
// If the property ends with #string, represent it as a string parameter.
string key when key.EndsWith(PropertyTypes.String, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.String.Length),
Value: new OpenIddictParameter(property.Value)),
// If the property ends with #boolean, return it as a boolean parameter.
string key when key.EndsWith(PropertyTypes.Boolean, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Boolean.Length),
Value: new OpenIddictParameter(bool.Parse(property.Value))),
// If the property ends with #integer, return it as an integer parameter.
string key when key.EndsWith(PropertyTypes.Integer, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Integer.Length),
Value: new OpenIddictParameter(long.Parse(property.Value, CultureInfo.InvariantCulture))),
// If the property ends with #json, return it as a JSON parameter.
string key when key.EndsWith(PropertyTypes.Json, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Json.Length),
Value: new OpenIddictParameter(JsonSerializer.Deserialize<JsonElement>(property.Value))),
_ => default
};
if (!string.IsNullOrEmpty(name))
{
context.Parameters[name] = value;
}
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting OpenID Connect requests from GET HTTP requests.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.

4
test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs

@ -171,6 +171,10 @@ public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServ
Assert.Equal(JsonValueKind.Number, ((JsonElement) response["integer_parameter"]).ValueKind);
Assert.Equal("Bob l'Eponge", (string?) response["string_parameter"]);
Assert.Equal(JsonValueKind.String, ((JsonElement) response["string_parameter"]).ValueKind);
Assert.Equal(new[] { "Contoso", "Fabrikam" }, (string[]?) response["array_parameter"]);
Assert.Equal(JsonValueKind.Array, ((JsonElement) response["array_parameter"]).ValueKind);
Assert.Equal("value", (string?) response["object_parameter"]?["parameter"]);
Assert.Equal(JsonValueKind.Object, ((JsonElement) response["object_parameter"]).ValueKind);
#if SUPPORTS_JSON_NODES
Assert.Equal(new[] { "Contoso", "Fabrikam" }, (string[]?) response["node_array_parameter"]);

151
test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs

@ -130,6 +130,45 @@ public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerInte
Assert.Equal(new DateTimeOffset(2120, 01, 01, 00, 00, 00, TimeSpan.Zero), properties.ExpiresUtc);
}
[Fact]
public async Task ProcessChallenge_ReturnsParametersFromAuthenticationProperties()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.SetTokenEndpointUris("/challenge/custom");
options.AddEventHandler<HandleTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/challenge/custom", new OpenIddictRequest
{
GrantType = GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w"
});
// Assert
Assert.True((bool) response["boolean_parameter"]);
Assert.Equal(JsonValueKind.True, ((JsonElement) response["boolean_parameter"]).ValueKind);
Assert.Equal(42, (long) response["integer_parameter"]);
Assert.Equal(JsonValueKind.Number, ((JsonElement) response["integer_parameter"]).ValueKind);
Assert.Equal("Bob l'Eponge", (string?) response["string_parameter"]);
Assert.Equal(JsonValueKind.String, ((JsonElement) response["string_parameter"]).ValueKind);
Assert.Equal(new[] { "Contoso", "Fabrikam" }, (string[]?) response["json_parameter"]);
Assert.Equal(JsonValueKind.Array, ((JsonElement) response["json_parameter"]).ValueKind);
}
[Fact]
public async Task ProcessChallenge_ReturnsErrorFromAuthenticationProperties()
{
@ -680,6 +719,78 @@ public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerInte
Assert.Equal("Bob le Magnifique", (string?) response["name"]);
}
[Fact]
public async Task ProcessSignIn_ReturnsParametersFromAuthenticationProperties()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.SetTokenEndpointUris("/signin/custom");
options.AddEventHandler<HandleTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/signin/custom", new OpenIddictRequest
{
GrantType = GrantTypes.Password,
Username = "johndoe",
Password = "A3ddj3w"
});
// Assert
Assert.True((bool) response["boolean_parameter"]);
Assert.Equal(JsonValueKind.True, ((JsonElement) response["boolean_parameter"]).ValueKind);
Assert.Equal(42, (long) response["integer_parameter"]);
Assert.Equal(JsonValueKind.Number, ((JsonElement) response["integer_parameter"]).ValueKind);
Assert.Equal("Bob l'Eponge", (string?) response["string_parameter"]);
Assert.Equal(JsonValueKind.String, ((JsonElement) response["string_parameter"]).ValueKind);
Assert.Equal(new[] { "Contoso", "Fabrikam" }, (string[]?) response["json_parameter"]);
Assert.Equal(JsonValueKind.Array, ((JsonElement) response["json_parameter"]).ValueKind);
}
[Fact]
public async Task ProcessSignOut_ReturnsParametersFromAuthenticationProperties()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.SetLogoutEndpointUris("/signout/custom");
options.AddEventHandler<HandleLogoutRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/signout/custom", new OpenIddictRequest
{
PostLogoutRedirectUri = "http://www.fabrikam.com/path",
State = "af0ifjsldkj"
});
// Assert
Assert.True((bool) response["boolean_parameter"]);
Assert.Equal(42, (long) response["integer_parameter"]);
Assert.Equal("Bob l'Eponge", (string?) response["string_parameter"]);
}
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope",
Justification = "The caller is responsible for disposing the test server.")]
protected override ValueTask<OpenIddictServerIntegrationTestServer> CreateServerAsync(Action<OpenIddictServerBuilder>? configuration = null)
@ -746,12 +857,45 @@ public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerInte
return;
}
else if (context.Request.Path == new PathString("/signin/custom"))
{
var identity = new ClaimsIdentity(OpenIddictServerOwinDefaults.AuthenticationType);
identity.AddClaim(Claims.Subject, "Bob le Bricoleur");
var principal = new ClaimsPrincipal(identity);
var properties = new AuthenticationProperties(new Dictionary<string, string?>
{
["boolean_parameter#boolean"] = "true",
["integer_parameter#integer"] = "42",
["string_parameter#string"] = "Bob l'Eponge",
["json_parameter#json"] = @"[""Contoso"",""Fabrikam""]"
});
context.Authentication.SignIn(properties, identity);
return;
}
else if (context.Request.Path == new PathString("/signout"))
{
context.Authentication.SignOut(OpenIddictServerOwinDefaults.AuthenticationType);
return;
}
else if (context.Request.Path == new PathString("/signout/custom"))
{
var properties = new AuthenticationProperties(new Dictionary<string, string?>
{
["boolean_parameter#boolean"] = "true",
["integer_parameter#integer"] = "42",
["string_parameter#string"] = "Bob l'Eponge"
});
context.Authentication.SignOut(properties, OpenIddictServerOwinDefaults.AuthenticationType);
return;
}
else if (context.Request.Path == new PathString("/challenge"))
{
context.Authentication.Challenge(OpenIddictServerOwinDefaults.AuthenticationType);
@ -764,7 +908,12 @@ public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerInte
{
[OpenIddictServerOwinConstants.Properties.Error] = "custom_error",
[OpenIddictServerOwinConstants.Properties.ErrorDescription] = "custom_error_description",
[OpenIddictServerOwinConstants.Properties.ErrorUri] = "custom_error_uri"
[OpenIddictServerOwinConstants.Properties.ErrorUri] = "custom_error_uri",
["boolean_parameter#boolean"] = "true",
["integer_parameter#integer"] = "42",
["string_parameter#string"] = "Bob l'Eponge",
["json_parameter#json"] = @"[""Contoso"",""Fabrikam""]"
});
context.Authentication.Challenge(properties, OpenIddictServerOwinDefaults.AuthenticationType);

Loading…
Cancel
Save