diff --git a/README.md b/README.md index 1fb6bf6..9361126 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/Catglobe.Openiddict.Contrib.Client/Catglobe.Openiddict.Contrib.Client.csproj b/src/Catglobe.Openiddict.Contrib.Client/Catglobe.Openiddict.Contrib.Client.csproj index 43b791d..f7d4d52 100644 --- a/src/Catglobe.Openiddict.Contrib.Client/Catglobe.Openiddict.Contrib.Client.csproj +++ b/src/Catglobe.Openiddict.Contrib.Client/Catglobe.Openiddict.Contrib.Client.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/Catglobe.Openiddict.Contrib.Client/ControllerHelpers/AuthorizationCodeHelpers.cs b/src/Catglobe.Openiddict.Contrib.Client/ControllerHelpers/AuthorizationCodeHelpers.cs index 814ed43..4e6d9be 100644 --- a/src/Catglobe.Openiddict.Contrib.Client/ControllerHelpers/AuthorizationCodeHelpers.cs +++ b/src/Catglobe.Openiddict.Contrib.Client/ControllerHelpers/AuthorizationCodeHelpers.cs @@ -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; @@ -22,8 +23,9 @@ public static class AuthorizationCodeHelpers /// The controller /// Parameter from UI where the user will be redirected after the auth is done /// The client you want to authenticate with + /// Any additional claims you want to request. See . /// The result you need to return from the controller - 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. @@ -31,6 +33,8 @@ public static ChallengeResult InitiateAuthorizationCodeLogin(this ControllerBase }; 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); } diff --git a/src/Catglobe.Openiddict.Contrib.Client/MinimalApiHelpers/AuthorizationCodeHelpers.cs b/src/Catglobe.Openiddict.Contrib.Client/MinimalApiHelpers/AuthorizationCodeHelpers.cs index bfaf14c..28ef4eb 100644 --- a/src/Catglobe.Openiddict.Contrib.Client/MinimalApiHelpers/AuthorizationCodeHelpers.cs +++ b/src/Catglobe.Openiddict.Contrib.Client/MinimalApiHelpers/AuthorizationCodeHelpers.cs @@ -21,8 +21,9 @@ public static class AuthorizationCodeHelpers /// The controller /// Parameter from UI where the user will be redirected after the auth is done /// The client you want to authenticate with + /// Any additional claims you want to request. See . /// The result you need to return from the controller - 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. @@ -30,6 +31,8 @@ public static IResult InitiateAuthorizationCodeLogin(this HttpContext httpContex }; 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 { OpenIddictClientAspNetCoreDefaults.AuthenticationScheme }); } diff --git a/src/Catglobe.Openiddict.Contrib.Client/RazorPageHelpers/AuthorizationCodeHelpers.cs b/src/Catglobe.Openiddict.Contrib.Client/RazorPageHelpers/AuthorizationCodeHelpers.cs index 66a5eb2..46b6d20 100644 --- a/src/Catglobe.Openiddict.Contrib.Client/RazorPageHelpers/AuthorizationCodeHelpers.cs +++ b/src/Catglobe.Openiddict.Contrib.Client/RazorPageHelpers/AuthorizationCodeHelpers.cs @@ -23,8 +23,9 @@ public static class AuthorizationCodeHelpers /// The PageModel /// Parameter from UI where the user will be redirected after the auth is done /// The client you want to authenticate with + /// Any additional claims you want to request. See . /// The result you need to return from the PageModel - 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. @@ -32,6 +33,8 @@ public static ChallengeResult InitiateAuthorizationCodeLogin(this PageModel mode }; 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); } diff --git a/src/Catglobe.Openiddict.Contrib.Client/RequestClaimsParameterValue.cs b/src/Catglobe.Openiddict.Contrib.Client/RequestClaimsParameterValue.cs new file mode 100644 index 0000000..9a78af1 --- /dev/null +++ b/src/Catglobe.Openiddict.Contrib.Client/RequestClaimsParameterValue.cs @@ -0,0 +1,129 @@ +#if NET +using System.Text.Json.Nodes; + +namespace Openiddict.Contrib.Client; + +/// +/// Helper class for setting parameters matching https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter. +/// +public class RequestClaimsParameterValue +{ + private readonly Dictionary _userinfo = []; + private readonly Dictionary _claims = []; + + /// + /// Specify a user info claim that should be returned from the authentication request. + /// + /// See . + /// Indicates whether the Claim being requested is an Essential Claim. If the value is true, this indicates that the Claim is an Essential Claim. + /// This class for chaining. + public RequestClaimsParameterValue UserInfoClaim(string claim, bool essential = false) + { + _userinfo.Add(claim, AsNode(essential, null, null)); + return this; + } + + /// + /// Specify a user info claim that should be returned from the authentication request. + /// + /// See . + /// Requests that the Claim be returned with a particular value. + /// Indicates whether the Claim being requested is an Essential Claim. If the value is true, this indicates that the Claim is an Essential Claim. + /// This class for chaining. + public RequestClaimsParameterValue UserInfoClaim(string claim, string value, bool essential = false) + { + _userinfo.Add(claim, AsNode(essential, value, null)); + return this; + } + + /// + /// Specify a user info claim that should be returned from the authentication request. + /// + /// See . + /// Requests that the Claim be returned with one of a set of values, with the values appearing in order of preference. + /// Indicates whether the Claim being requested is an Essential Claim. If the value is true, this indicates that the Claim is an Essential Claim. + /// This class for chaining. + public RequestClaimsParameterValue UserInfoClaim(string claim, IReadOnlyCollection values, bool essential) + { + _userinfo.Add(claim, AsNode(essential, null, values)); + return this; + } + + private static JsonObject? AsNode(bool essential, string? value, IReadOnlyCollection? values) + { + var json = new JsonObject(GetJsonNode(essential, value, values)); + return json.Count == 0 ? default : json; + } + + /// + /// Specify a claim that should be returned from the authentication request. + /// + /// See . + /// Indicates whether the Claim being requested is an Essential Claim. If the value is true, this indicates that the Claim is an Essential Claim. + /// This class for chaining. + public RequestClaimsParameterValue IdTokenClaim(string claim, bool essential = false) + { + _claims.Add(claim, AsNode(essential, null, null)); + return this; + } + + /// + /// Specify a claim that should be returned from the authentication request. + /// + /// See . + /// Requests that the Claim be returned with a particular value. + /// Indicates whether the Claim being requested is an Essential Claim. If the value is true, this indicates that the Claim is an Essential Claim. + /// This class for chaining. + public RequestClaimsParameterValue IdTokenClaim(string claim, string value, bool essential = false) + { + _claims.Add(claim, AsNode(essential, value, null)); + return this; + } + + /// + /// Specify a claim that should be returned from the authentication request. + /// + /// See . + /// Requests that the Claim be returned with one of a set of values, with the values appearing in order of preference. + /// Indicates whether the Claim being requested is an Essential Claim. If the value is true, this indicates that the Claim is an Essential Claim. + /// This class for chaining. + public RequestClaimsParameterValue IdTokenClaim(string claim, IReadOnlyCollection values, bool essential) + { + _claims.Add(claim, AsNode(essential, null, values)); + return this; + } + + /// + /// Convert the current settings to a that can be used in a request. + /// + /// + /// new AuthenticationProperties().SetParameter(Parameters.Claims, new RequestClaimsParameterValue().IdTokenClaim(Claims.AuthenticationTime, true).AsOpenIddictParameter()); + /// + /// + /// + public OpenIddictParameter? AsOpenIddictParameter() + { + if (_userinfo.Count == 0 && _claims.Count == 0) + return null; + var lst = new List>(); + + 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> GetJsonNode(bool essential, string? value, IReadOnlyCollection? 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