Skip to content

Commit

Permalink
feat: claims helper
Browse files Browse the repository at this point in the history
  • Loading branch information
davhdavh committed Nov 13, 2023
1 parent ee21c6c commit 0f650a2
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 4 deletions.
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 = [];

Check failure on line 11 in src/Catglobe.Openiddict.Contrib.Client/RequestClaimsParameterValue.cs

View workflow job for this annotation

GitHub Actions / build-windows-latest

Invalid expression term '['

Check failure on line 11 in src/Catglobe.Openiddict.Contrib.Client/RequestClaimsParameterValue.cs

View workflow job for this annotation

GitHub Actions / build-windows-latest

Syntax error; value expected

Check failure on line 11 in src/Catglobe.Openiddict.Contrib.Client/RequestClaimsParameterValue.cs

View workflow job for this annotation

GitHub Actions / build-windows-latest

Invalid expression term '['

Check failure on line 11 in src/Catglobe.Openiddict.Contrib.Client/RequestClaimsParameterValue.cs

View workflow job for this annotation

GitHub Actions / build-windows-latest

Syntax error; value expected

Check failure on line 11 in src/Catglobe.Openiddict.Contrib.Client/RequestClaimsParameterValue.cs

View workflow job for this annotation

GitHub Actions / build-windows-latest

Invalid expression term '['

Check failure on line 11 in src/Catglobe.Openiddict.Contrib.Client/RequestClaimsParameterValue.cs

View workflow job for this annotation

GitHub Actions / build-windows-latest

Syntax error; value expected
private readonly Dictionary<string, JsonNode?> _claims = [];

Check failure on line 12 in src/Catglobe.Openiddict.Contrib.Client/RequestClaimsParameterValue.cs

View workflow job for this annotation

GitHub Actions / build-windows-latest

Invalid expression term '['

Check failure on line 12 in src/Catglobe.Openiddict.Contrib.Client/RequestClaimsParameterValue.cs

View workflow job for this annotation

GitHub Actions / build-windows-latest

Syntax error; value expected

Check failure on line 12 in src/Catglobe.Openiddict.Contrib.Client/RequestClaimsParameterValue.cs

View workflow job for this annotation

GitHub Actions / build-windows-latest

Invalid expression term '['

Check failure on line 12 in src/Catglobe.Openiddict.Contrib.Client/RequestClaimsParameterValue.cs

View workflow job for this annotation

GitHub Actions / build-windows-latest

Syntax error; value expected

/// <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

0 comments on commit 0f650a2

Please sign in to comment.