Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: claims helper #8

Merged
merged 2 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,21 @@ You also need to copy it from the server given claims to the local claims. E.g.
yourContext.StoreRemoteAuthInSchemeAsync(..., (identity, remote)=>OidcClaimsCultureProviderHelper.CopyClaims(identity, remote))))
```

## Claims helpers

https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter specifies how to request claims from the request.

Notice, you still have to manually check they are present in the response.

```csharp
//in the login controller
var claims = new RequestClaimsParameterValue()
.IdTokenClaim(Claims.AuthenticationTime, true);
this.InitiateAuthorizationCodeLogin(returnUrl, ..., claims.AsOpenIddictParameter());
...
//in the callback controller
this.StoreRemoteAuthInSchemeAsync(..., (principal)=>principal.GetClaim(Claims.AuthenticationTime)...)
```

## Http helpers

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '3.0'))) Or&#xD;&#xA; ('$(TargetFrameworkIdentifier)' == '.NETFramework') Or&#xD;&#xA; ('$(TargetFrameworkIdentifier)' == '.NETStandard') ">
<ItemGroup Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '3.0'))) Or ('$(TargetFrameworkIdentifier)' == '.NETFramework') Or ('$(TargetFrameworkIdentifier)' == '.NETStandard') ">
<PackageReference Include="Microsoft.AspNetCore.Authentication" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using OpenIddict.Client.AspNetCore;
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using static OpenIddict.Abstractions.OpenIddictConstants;

namespace Openiddict.Contrib.Client.ControllerHelpers;

Expand All @@ -22,15 +23,18 @@ public static class AuthorizationCodeHelpers
/// <param name="controller">The controller</param>
/// <param name="returnUrl">Parameter from UI where the user will be redirected after the auth is done</param>
/// <param name="provider">The client you want to authenticate with</param>
/// <param name="claims">Any additional claims you want to request. See <see cref="RequestClaimsParameterValue.AsOpenIddictParameter"></see>.</param>
/// <returns>The result you need to return from the controller</returns>
public static ChallengeResult InitiateAuthorizationCodeLogin(this ControllerBase controller, string returnUrl, string? provider = null)
public static ChallengeResult InitiateAuthorizationCodeLogin(this ControllerBase controller, string returnUrl, string? provider = null, OpenIddictParameter? claims = default)
{
var properties = new AuthenticationProperties {
// Only allow local return URLs to prevent open redirect attacks.
RedirectUri = controller.Url.IsLocalUrl(returnUrl) ? returnUrl : "/",
};
if (!string.IsNullOrEmpty(provider))
properties.Items[OpenIddictClientAspNetCoreConstants.Properties.ProviderName] = provider;
if (claims is {} claim)
properties.Parameters[Parameters.Claims] = claim;
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
return controller.Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@ public static class AuthorizationCodeHelpers
/// <param name="httpContext">The controller</param>
/// <param name="returnUrl">Parameter from UI where the user will be redirected after the auth is done</param>
/// <param name="provider">The client you want to authenticate with</param>
/// <param name="claims">Any additional claims you want to request. See <see cref="RequestClaimsParameterValue.AsOpenIddictParameter"></see>.</param>
/// <returns>The result you need to return from the controller</returns>
public static IResult InitiateAuthorizationCodeLogin(this HttpContext httpContext, string returnUrl, string? provider = null)
public static IResult InitiateAuthorizationCodeLogin(this HttpContext httpContext, string returnUrl, string? provider = null, OpenIddictParameter? claims = default)
{
var properties = new AuthenticationProperties {
// Only allow local return URLs to prevent open redirect attacks.
RedirectUri = returnUrl,
};
if (!string.IsNullOrEmpty(provider))
properties.Items[OpenIddictClientAspNetCoreConstants.Properties.ProviderName] = provider;
if (claims is {} claim)
properties.Parameters[Parameters.Claims] = claim;
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
return Results.Challenge(properties, new List<string> { OpenIddictClientAspNetCoreDefaults.AuthenticationScheme });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@ public static class AuthorizationCodeHelpers
/// <param name="model">The PageModel</param>
/// <param name="returnUrl">Parameter from UI where the user will be redirected after the auth is done</param>
/// <param name="provider">The client you want to authenticate with</param>
/// <param name="claims">Any additional claims you want to request. See <see cref="RequestClaimsParameterValue.AsOpenIddictParameter"></see>.</param>
/// <returns>The result you need to return from the PageModel</returns>
public static ChallengeResult InitiateAuthorizationCodeLogin(this PageModel model, string returnUrl, string? provider = null)
public static ChallengeResult InitiateAuthorizationCodeLogin(this PageModel model, string returnUrl, string? provider = null, OpenIddictParameter? claims = default)
{
var properties = new AuthenticationProperties {
// Only allow local return URLs to prevent open redirect attacks.
RedirectUri = model.Url.IsLocalUrl(returnUrl) ? returnUrl : "/",
};
if (!string.IsNullOrEmpty(provider))
properties.Items[OpenIddictClientAspNetCoreConstants.Properties.ProviderName] = provider;
if (claims is {} claim)
properties.Parameters[Parameters.Claims] = claim;
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
return model.Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
}
Expand Down
129 changes: 129 additions & 0 deletions src/Catglobe.Openiddict.Contrib.Client/RequestClaimsParameterValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#if NET
using System.Text.Json.Nodes;

namespace Openiddict.Contrib.Client;

/// <summary>
/// Helper class for setting parameters matching https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter.
/// </summary>
public class RequestClaimsParameterValue
{
private readonly Dictionary<string, JsonNode?> _userinfo = new();
private readonly Dictionary<string, JsonNode?> _claims = new();

/// <summary>
/// Specify a user info claim that should be returned from the authentication request.
/// </summary>
/// <param name="claim">See <see cref="Claims"/>.</param>
/// <param name="essential">Indicates whether the Claim being requested is an Essential Claim. If the value is true, this indicates that the Claim is an Essential Claim.</param>
/// <returns>This class for chaining.</returns>
public RequestClaimsParameterValue UserInfoClaim(string claim, bool essential = false)
{
_userinfo.Add(claim, AsNode(essential, null, null));
return this;
}

/// <summary>
/// Specify a user info claim that should be returned from the authentication request.
/// </summary>
/// <param name="claim">See <see cref="Claims"/>.</param>
/// <param name="value">Requests that the Claim be returned with a particular value.</param>
/// <param name="essential">Indicates whether the Claim being requested is an Essential Claim. If the value is true, this indicates that the Claim is an Essential Claim.</param>
/// <returns>This class for chaining.</returns>
public RequestClaimsParameterValue UserInfoClaim(string claim, string value, bool essential = false)
{
_userinfo.Add(claim, AsNode(essential, value, null));
return this;
}

/// <summary>
/// Specify a user info claim that should be returned from the authentication request.
/// </summary>
/// <param name="claim">See <see cref="Claims"/>.</param>
/// <param name="values">Requests that the Claim be returned with one of a set of values, with the values appearing in order of preference.</param>
/// <param name="essential">Indicates whether the Claim being requested is an Essential Claim. If the value is true, this indicates that the Claim is an Essential Claim.</param>
/// <returns>This class for chaining.</returns>
public RequestClaimsParameterValue UserInfoClaim(string claim, IReadOnlyCollection<string> values, bool essential)
{
_userinfo.Add(claim, AsNode(essential, null, values));
return this;
}

private static JsonObject? AsNode(bool essential, string? value, IReadOnlyCollection<string?>? values)
{
var json = new JsonObject(GetJsonNode(essential, value, values));
return json.Count == 0 ? default : json;
}

/// <summary>
/// Specify a claim that should be returned from the authentication request.
/// </summary>
/// <param name="claim">See <see cref="Claims"/>.</param>
/// <param name="essential">Indicates whether the Claim being requested is an Essential Claim. If the value is true, this indicates that the Claim is an Essential Claim.</param>
/// <returns>This class for chaining.</returns>
public RequestClaimsParameterValue IdTokenClaim(string claim, bool essential = false)
{
_claims.Add(claim, AsNode(essential, null, null));
return this;
}

/// <summary>
/// Specify a claim that should be returned from the authentication request.
/// </summary>
/// <param name="claim">See <see cref="Claims"/>.</param>
/// <param name="value">Requests that the Claim be returned with a particular value.</param>
/// <param name="essential">Indicates whether the Claim being requested is an Essential Claim. If the value is true, this indicates that the Claim is an Essential Claim.</param>
/// <returns>This class for chaining.</returns>
public RequestClaimsParameterValue IdTokenClaim(string claim, string value, bool essential = false)
{
_claims.Add(claim, AsNode(essential, value, null));
return this;
}

/// <summary>
/// Specify a claim that should be returned from the authentication request.
/// </summary>
/// <param name="claim">See <see cref="Claims"/>.</param>
/// <param name="values">Requests that the Claim be returned with one of a set of values, with the values appearing in order of preference.</param>
/// <param name="essential">Indicates whether the Claim being requested is an Essential Claim. If the value is true, this indicates that the Claim is an Essential Claim.</param>
/// <returns>This class for chaining.</returns>
public RequestClaimsParameterValue IdTokenClaim(string claim, IReadOnlyCollection<string> values, bool essential)
{
_claims.Add(claim, AsNode(essential, null, values));
return this;
}

/// <summary>
/// Convert the current settings to a <see cref="OpenIddictParameter"/> that can be used in a request.
/// <example>
/// <code>
/// new AuthenticationProperties().SetParameter(Parameters.Claims, new RequestClaimsParameterValue().IdTokenClaim(Claims.AuthenticationTime, true).AsOpenIddictParameter());
/// </code></example>
/// </summary>
/// <returns></returns>
public OpenIddictParameter? AsOpenIddictParameter()
{
if (_userinfo.Count == 0 && _claims.Count == 0)
return null;
var lst = new List<KeyValuePair<string, JsonNode?>>();

if (_userinfo.Count != 0)
lst.Add(new("userinfo", new JsonObject(_userinfo)));
if (_claims.Count != 0)
lst.Add(new(Parameters.IdToken, new JsonObject(_claims)));
return new OpenIddictParameter(new JsonObject(lst));
}

private static IEnumerable<KeyValuePair<string, JsonNode?>> GetJsonNode(bool essential, string? value, IReadOnlyCollection<string?>? values)
{
if (essential)
yield return new("essential", true);
if (value is not null)
yield return new("value", value!);
if (values is not null)
yield return new("values", new JsonArray(values.Select(x => (JsonNode?)JsonValue.Create(x)).ToArray()));
}


}
#endif
Loading