/* * 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; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Security.Claims; using JetBrains.Annotations; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using static OpenIddict.Abstractions.OpenIddictConstants; namespace OpenIddict.Abstractions { /// /// Provides extension methods to make /// and easier to work with. /// public static class OpenIddictExtensions { /// /// Gets all the parameters associated with the specified message as a flattened collection: /// array parameters are automatically converted to multiple parameters and parameters that /// can't be converted to string instances are ignored and excluded from the returned collection. /// This extension is primarily intended to be used by components that need to represent /// an OpenID Connect message as a query string or as a list of key/value pairs in a HTTP form. /// /// The instance. /// The parameters, as a flattened collection. public static ImmutableList> GetFlattenedParameters([NotNull] this OpenIddictMessage message) { if (message == null) { throw new ArgumentNullException(nameof(message)); } var parameters = ImmutableList.CreateBuilder>(); foreach (var parameter in message.GetParameters()) { var values = (string[]) parameter.Value; if (values == null) { continue; } foreach (var value in values) { parameters.Add(new KeyValuePair(parameter.Key, value)); } } return parameters.ToImmutable(); } /// /// Extracts the authentication context class values from an . /// /// The instance. public static ImmutableHashSet GetAcrValues([NotNull] this OpenIddictRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.IsNullOrEmpty(request.AcrValues)) { return ImmutableHashSet.Create(StringComparer.Ordinal); } return ImmutableHashSet.CreateRange(StringComparer.Ordinal, GetValues(request.AcrValues, Separators.Space)); } /// /// Extracts the scopes from an . /// /// The instance. public static ImmutableHashSet GetScopes([NotNull] this OpenIddictRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.IsNullOrEmpty(request.Scope)) { return ImmutableHashSet.Create(StringComparer.Ordinal); } return ImmutableHashSet.CreateRange(StringComparer.Ordinal, GetValues(request.Scope, Separators.Space)); } /// /// Determines whether the requested authentication context class values contain the specified item. /// /// The instance. /// The component to look for in the parameter. public static bool HasAcrValue([NotNull] this OpenIddictRequest request, [NotNull] string value) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.IsNullOrEmpty(value)) { throw new ArgumentException("The value cannot be null or empty.", nameof(value)); } if (string.IsNullOrEmpty(request.AcrValues)) { return false; } return HasValue(request.AcrValues, value, Separators.Space); } /// /// Determines whether the requested prompt contains the specified value. /// /// The instance. /// The component to look for in the parameter. public static bool HasPrompt([NotNull] this OpenIddictRequest request, [NotNull] string prompt) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.IsNullOrEmpty(prompt)) { throw new ArgumentException("The prompt cannot be null or empty.", nameof(prompt)); } if (string.IsNullOrEmpty(request.Prompt)) { return false; } return HasValue(request.Prompt, prompt, Separators.Space); } /// /// Determines whether the requested response type contains the specified value. /// /// The instance. /// The component to look for in the parameter. public static bool HasResponseType([NotNull] this OpenIddictRequest request, [NotNull] string type) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The response type cannot be null or empty.", nameof(type)); } if (string.IsNullOrEmpty(request.ResponseType)) { return false; } return HasValue(request.ResponseType, type, Separators.Space); } /// /// Determines whether the requested scope contains the specified value. /// /// The instance. /// The component to look for in the parameter. public static bool HasScope([NotNull] this OpenIddictRequest request, [NotNull] string scope) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.IsNullOrEmpty(scope)) { throw new ArgumentException("The scope cannot be null or empty.", nameof(scope)); } if (string.IsNullOrEmpty(request.Scope)) { return false; } return HasValue(request.Scope, scope, Separators.Space); } /// /// Determines whether the "response_type" parameter corresponds to the "none" response type. /// See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#none for more information. /// /// The instance. /// true if the request is a response_type=none request, false otherwise. public static bool IsNoneFlow([NotNull] this OpenIddictRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.IsNullOrEmpty(request.ResponseType)) { return false; } var segment = Trim(new StringSegment(request.ResponseType), Separators.Space); if (segment.Length == 0) { return false; } return segment.Equals(ResponseTypes.None, StringComparison.Ordinal); } /// /// Determines whether the "response_type" parameter corresponds to the authorization code flow. /// See http://tools.ietf.org/html/rfc6749#section-4.1.1 for more information. /// /// The instance. /// true if the request is a code flow request, false otherwise. public static bool IsAuthorizationCodeFlow([NotNull] this OpenIddictRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.IsNullOrEmpty(request.ResponseType)) { return false; } var segment = Trim(new StringSegment(request.ResponseType), Separators.Space); if (segment.Length == 0) { return false; } return segment.Equals(ResponseTypes.Code, StringComparison.Ordinal); } /// /// Determines whether the "response_type" parameter corresponds to the implicit flow. /// See http://tools.ietf.org/html/rfc6749#section-4.2.1 and /// http://openid.net/specs/openid-connect-core-1_0.html for more information /// /// The instance. /// true if the request is an implicit flow request, false otherwise. public static bool IsImplicitFlow([NotNull] this OpenIddictRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.IsNullOrEmpty(request.ResponseType)) { return false; } var flags = /* none: */ 0x00; foreach (var element in new StringTokenizer(request.ResponseType, Separators.Space)) { var segment = Trim(element, Separators.Space); if (segment.Length == 0) { continue; } if (segment.Equals(ResponseTypes.IdToken, StringComparison.Ordinal)) { flags |= /* id_token: */ 0x01; continue; } // Note: though the OIDC core specs does not include the OAuth 2.0-inherited response_type=token, // it is considered as a valid response_type for the implicit flow for backward compatibility. else if (segment.Equals(ResponseTypes.Token, StringComparison.Ordinal)) { flags |= /* token */ 0x02; continue; } // Always return false if the response_type item // is not a valid component for the implicit flow. return false; } // Return true if the response_type parameter contains "id_token" or "token". return (flags & /* id_token: */ 0x01) == 0x01 || (flags & /* token: */ 0x02) == 0x02; } /// /// Determines whether the "response_type" parameter corresponds to the hybrid flow. /// See http://tools.ietf.org/html/rfc6749#section-4.2.1 and /// http://openid.net/specs/openid-connect-core-1_0.html for more information. /// /// The instance. /// true if the request is an hybrid flow request, false otherwise. public static bool IsHybridFlow([NotNull] this OpenIddictRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.IsNullOrEmpty(request.ResponseType)) { return false; } var flags = /* none */ 0x00; foreach (var element in new StringTokenizer(request.ResponseType, Separators.Space)) { var segment = Trim(element, Separators.Space); if (segment.Length == 0) { continue; } if (segment.Equals(ResponseTypes.Code, StringComparison.Ordinal)) { flags |= /* code: */ 0x01; continue; } else if (segment.Equals(ResponseTypes.IdToken, StringComparison.Ordinal)) { flags |= /* id_token: */ 0x02; continue; } else if (segment.Equals(ResponseTypes.Token, StringComparison.Ordinal)) { flags |= /* token: */ 0x04; continue; } // Always return false if the response_type item // is not a valid component for the hybrid flow. return false; } // Return false if the response_type parameter doesn't contain "code". if ((flags & /* code: */ 0x01) != 0x01) { return false; } // Return true if the response_type parameter contains "id_token" or "token". return (flags & /* id_token: */ 0x02) == 0x02 || (flags & /* token: */ 0x04) == 0x04; } /// /// Determines whether the "response_mode" parameter corresponds to the fragment response mode. /// See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html for more information. /// /// The instance. /// /// true if the request specified the fragment response mode or if /// it's the default value for the requested flow, false otherwise. /// public static bool IsFragmentResponseMode([NotNull] this OpenIddictRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.Equals(request.ResponseMode, ResponseModes.Fragment, StringComparison.Ordinal)) { return true; } // Don't guess the response_mode value // if an explicit value has been provided. if (!string.IsNullOrEmpty(request.ResponseMode)) { return false; } // Both the implicit and the hybrid flows // use response_mode=fragment by default. return request.IsImplicitFlow() || request.IsHybridFlow(); } /// /// Determines whether the "response_mode" parameter corresponds to the query response mode. /// See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html for more information. /// /// The instance. /// /// true if the request specified the query response mode or if /// it's the default value for the requested flow, false otherwise. /// public static bool IsQueryResponseMode([NotNull] this OpenIddictRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.Equals(request.ResponseMode, ResponseModes.Query, StringComparison.Ordinal)) { return true; } // Don't guess the response_mode value // if an explicit value has been provided. if (!string.IsNullOrEmpty(request.ResponseMode)) { return false; } // Code flow and "response_type=none" use response_mode=query by default. return request.IsAuthorizationCodeFlow() || request.IsNoneFlow(); } /// /// Determines whether the "response_mode" parameter corresponds to the form post response mode. /// See http://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html for more information. /// /// The instance. /// /// true if the request specified the form post response mode or if /// it's the default value for the requested flow, false otherwise. /// public static bool IsFormPostResponseMode([NotNull] this OpenIddictRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } return string.Equals(request.ResponseMode, ResponseModes.FormPost, StringComparison.Ordinal); } /// /// Determines whether the "grant_type" parameter corresponds to the authorization code grant. /// See http://tools.ietf.org/html/rfc6749#section-4.1.3 for more information. /// /// The instance. /// true if the request is a code grant request, false otherwise. public static bool IsAuthorizationCodeGrantType([NotNull] this OpenIddictRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } return string.Equals(request.GrantType, GrantTypes.AuthorizationCode, StringComparison.Ordinal); } /// /// Determines whether the "grant_type" parameter corresponds to the client credentials grant. /// See http://tools.ietf.org/html/rfc6749#section-4.4.2 for more information. /// /// The instance. /// true if the request is a client credentials grant request, false otherwise. public static bool IsClientCredentialsGrantType([NotNull] this OpenIddictRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } return string.Equals(request.GrantType, GrantTypes.ClientCredentials, StringComparison.Ordinal); } /// /// Determines whether the "grant_type" parameter corresponds to the password grant. /// See http://tools.ietf.org/html/rfc6749#section-4.3.2 for more information. /// /// The instance. /// true if the request is a password grant request, false otherwise. public static bool IsPasswordGrantType([NotNull] this OpenIddictRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } return string.Equals(request.GrantType, GrantTypes.Password, StringComparison.Ordinal); } /// /// Determines whether the "grant_type" parameter corresponds to the refresh token grant. /// See http://tools.ietf.org/html/rfc6749#section-6 for more information. /// /// The instance. /// true if the request is a refresh token grant request, false otherwise. public static bool IsRefreshTokenGrantType([NotNull] this OpenIddictRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } return string.Equals(request.GrantType, GrantTypes.RefreshToken, StringComparison.Ordinal); } /// /// Gets the destinations associated with a claim. /// /// The instance. /// The destinations associated with the claim. public static ImmutableHashSet GetDestinations([NotNull] this Claim claim) { if (claim == null) { throw new ArgumentNullException(nameof(claim)); } claim.Properties.TryGetValue(Properties.Destinations, out string destinations); if (string.IsNullOrEmpty(destinations)) { return ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase); } return ImmutableHashSet.CreateRange(StringComparer.OrdinalIgnoreCase, JArray.Parse(destinations).Values()); } /// /// Determines whether the given claim contains the required destination. /// /// The instance. /// The required destination. public static bool HasDestination([NotNull] this Claim claim, [NotNull] string destination) { if (claim == null) { throw new ArgumentNullException(nameof(claim)); } if (string.IsNullOrEmpty(destination)) { throw new ArgumentException("The destination cannot be null or empty.", nameof(destination)); } return GetDestinations(claim).Contains(destination, StringComparer.OrdinalIgnoreCase); } /// /// Adds specific destinations to a claim. /// /// The instance. /// The destinations. public static Claim SetDestinations([NotNull] this Claim claim, IEnumerable destinations) { if (claim == null) { throw new ArgumentNullException(nameof(claim)); } if (destinations == null || !destinations.Any()) { claim.Properties.Remove(Properties.Destinations); return claim; } if (destinations.Any(destination => string.IsNullOrEmpty(destination))) { throw new ArgumentException("Destinations cannot be null or empty.", nameof(destinations)); } claim.Properties[Properties.Destinations] = new JArray(destinations.Distinct(StringComparer.OrdinalIgnoreCase)).ToString(Formatting.None); return claim; } /// /// Adds specific destinations to a claim. /// /// The instance. /// The destinations. public static Claim SetDestinations([NotNull] this Claim claim, params string[] destinations) // Note: guarding the destinations parameter against null values // is not necessary as AsEnumerable() doesn't throw on null values. => claim.SetDestinations(destinations.AsEnumerable()); /// /// Clones an identity by filtering its claims and the claims of its actor, recursively. /// /// The instance to filter. /// /// The delegate filtering the claims: return true /// to accept the claim, false to remove it. /// public static ClaimsIdentity Clone( [NotNull] this ClaimsIdentity identity, [NotNull] Func filter) { if (identity == null) { throw new ArgumentNullException(nameof(identity)); } if (filter == null) { throw new ArgumentNullException(nameof(filter)); } var clone = identity.Clone(); // Note: make sure to call ToList() to avoid modifying // the initial collection iterated by ClaimsIdentity.Claims. foreach (var claim in clone.Claims.ToList()) { if (!filter(claim)) { clone.RemoveClaim(claim); } } if (clone.Actor != null) { clone.Actor = clone.Actor.Clone(filter); } return clone; } /// /// Clones a principal by filtering its identities. /// /// The instance to filter. /// /// The delegate filtering the claims: return true /// to accept the claim, false to remove it. /// public static ClaimsPrincipal Clone( [NotNull] this ClaimsPrincipal principal, [NotNull] Func filter) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (filter == null) { throw new ArgumentNullException(nameof(filter)); } var clone = new ClaimsPrincipal(); foreach (var identity in principal.Identities) { clone.AddIdentity(identity.Clone(filter)); } return clone; } /// /// Adds a claim to a given identity. /// /// The identity. /// The type associated with the claim. /// The value associated with the claim. public static ClaimsIdentity AddClaim( [NotNull] this ClaimsIdentity identity, [NotNull] string type, [NotNull] string value) { if (identity == null) { throw new ArgumentNullException(nameof(identity)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); } if (string.IsNullOrEmpty(value)) { throw new ArgumentException("The claim value cannot be null or empty.", nameof(value)); } identity.AddClaim(new Claim(type, value)); return identity; } /// /// Adds a claim to a given identity and specify one or more destinations. /// /// The identity. /// The type associated with the claim. /// The value associated with the claim. /// The destinations associated with the claim. public static ClaimsIdentity AddClaim( [NotNull] this ClaimsIdentity identity, [NotNull] string type, [NotNull] string value, [NotNull] IEnumerable destinations) { if (identity == null) { throw new ArgumentNullException(nameof(identity)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); } if (string.IsNullOrEmpty(value)) { throw new ArgumentException("The claim value cannot be null or empty.", nameof(value)); } if (destinations == null) { throw new ArgumentNullException(nameof(destinations)); } identity.AddClaim(new Claim(type, value).SetDestinations(destinations)); return identity; } /// /// Adds a claim to a given identity and specify one or more destinations. /// /// The identity. /// The type associated with the claim. /// The value associated with the claim. /// The destinations associated with the claim. public static ClaimsIdentity AddClaim( [NotNull] this ClaimsIdentity identity, [NotNull] string type, [NotNull] string value, [NotNull] params string[] destinations) // Note: guarding the destinations parameter against null values // is not necessary as AsEnumerable() doesn't throw on null values. => identity.AddClaim(type, value, destinations.AsEnumerable()); /// /// Gets the claim value corresponding to the given type. /// /// The identity. /// The type associated with the claim. /// The claim value. public static string GetClaim([NotNull] this ClaimsIdentity identity, [NotNull] string type) { if (identity == null) { throw new ArgumentNullException(nameof(identity)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); } return identity.FindFirst(type)?.Value; } /// /// Gets the claim value corresponding to the given type. /// /// The principal. /// The type associated with the claim. /// The claim value. public static string GetClaim([NotNull] this ClaimsPrincipal principal, [NotNull] string type) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); } return principal.FindFirst(type)?.Value; } /// /// Gets the claim values corresponding to the given type. /// /// The identity. /// The type associated with the claims. /// The claim values. public static ImmutableHashSet GetClaims([NotNull] this ClaimsIdentity identity, [NotNull] string type) { if (identity == null) { throw new ArgumentNullException(nameof(identity)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); } return ImmutableHashSet.CreateRange(StringComparer.Ordinal, identity.FindAll(type).Select(claim => claim.Value)); } /// /// Gets the claim values corresponding to the given type. /// /// The principal. /// The type associated with the claims. /// The claim values. public static ImmutableHashSet GetClaims([NotNull] this ClaimsPrincipal principal, [NotNull] string type) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); } return ImmutableHashSet.CreateRange(StringComparer.Ordinal, principal.FindAll(type).Select(claim => claim.Value)); } /// /// Removes all the claims corresponding to the given type. /// /// The identity. /// The type associated with the claims. /// The claims identity. public static ClaimsIdentity RemoveClaims([NotNull] this ClaimsIdentity identity, [NotNull] string type) { if (identity == null) { throw new ArgumentNullException(nameof(identity)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); } foreach (var claim in identity.FindAll(type).ToList()) { identity.RemoveClaim(claim); } return identity; } /// /// Removes all the claims corresponding to the given type. /// /// The principal. /// The type associated with the claims. /// The claims identity. public static ClaimsPrincipal RemoveClaims([NotNull] this ClaimsPrincipal principal, [NotNull] string type) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); } foreach (var identity in principal.Identities) { foreach (var claim in identity.FindAll(type).ToList()) { identity.RemoveClaim(claim); } } return principal; } /// /// Sets the claim value corresponding to the given type. /// /// The identity. /// The type associated with the claims. /// The claim value. /// The claims identity. public static ClaimsIdentity SetClaims( [NotNull] this ClaimsIdentity identity, [NotNull] string type, [CanBeNull] string value) { if (identity == null) { throw new ArgumentNullException(nameof(identity)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); } identity.RemoveClaims(type); if (!string.IsNullOrEmpty(value)) { identity.AddClaim(type, value); } return identity; } /// /// Sets the claim value corresponding to the given type. /// /// The principal. /// The type associated with the claims. /// The claim value. /// The claims identity. public static ClaimsPrincipal SetClaim( [NotNull] this ClaimsPrincipal principal, [NotNull] string type, [CanBeNull] string value) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); } principal.RemoveClaims(type); if (!string.IsNullOrEmpty(value)) { ((ClaimsIdentity) principal.Identity).AddClaim(type, value); } return principal; } /// /// Sets the claim values corresponding to the given type. /// /// The identity. /// The type associated with the claims. /// The claim values. /// The claims identity. public static ClaimsIdentity SetClaims([NotNull] this ClaimsIdentity identity, [NotNull] string type, [NotNull] IEnumerable values) { if (identity == null) { throw new ArgumentNullException(nameof(identity)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); } identity.RemoveClaims(type); foreach (var value in values) { identity.AddClaim(type, value); } return identity; } /// /// Sets the claim values corresponding to the given type. /// /// The principal. /// The type associated with the claims. /// The claim values. /// The claims identity. public static ClaimsPrincipal SetClaims([NotNull] this ClaimsPrincipal principal, [NotNull] string type, [NotNull] IEnumerable values) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); } principal.RemoveClaims(type); foreach (var value in values) { ((ClaimsIdentity) principal.Identity).AddClaim(type, value); } return principal; } /// /// Gets the creation date stored in the claims principal. /// /// The claims principal. /// The creation date or null if the claim cannot be found. public static DateTimeOffset? GetCreationDate([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } var claim = principal.FindFirst(Claims.IssuedAt); if (claim == null) { return null; } if (!long.TryParse(claim.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) { return null; } return DateTimeOffset.FromUnixTimeSeconds(value); } /// /// Gets the expiration date stored in the claims principal. /// /// The claims principal. /// The expiration date or null if the claim cannot be found. public static DateTimeOffset? GetExpirationDate([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } var claim = principal.FindFirst(Claims.ExpiresAt); if (claim == null) { return null; } if (!long.TryParse(claim.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) { return null; } return DateTimeOffset.FromUnixTimeSeconds(value); } /// /// Gets the audiences list stored in the claims principal. /// /// The claims principal. /// The audiences list or an empty set if the claims cannot be found. public static ImmutableHashSet GetAudiences([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return ImmutableHashSet.CreateRange(StringComparer.Ordinal, principal.GetClaims(Claims.Audience)); } /// /// Gets the presenters list stored in the claims principal. /// /// The claims principal. /// The presenters list or an empty set if the claims cannot be found. public static ImmutableHashSet GetPresenters([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return ImmutableHashSet.CreateRange(StringComparer.Ordinal, principal.GetClaims(Claims.AuthorizedParty)); } /// /// Gets the resources list stored in the claims principal. /// /// The claims principal. /// The resources list or an empty set if the claims cannot be found. public static ImmutableHashSet GetResources([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return ImmutableHashSet.CreateRange(StringComparer.Ordinal, principal.GetClaims(Claims.Private.Resource)); } /// /// Gets the scopes list stored in the claims principal. /// /// The claims principal. /// The scopes list or an empty set if the claim cannot be found. public static ImmutableHashSet GetScopes([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } // Note: scopes are deliberately formatted as a single space-separated // string to respect the usual representation of the standard scope claim. // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-02. var value = principal.GetClaim(Claims.Scope); if (string.IsNullOrEmpty(value)) { return ImmutableHashSet.Create(StringComparer.Ordinal); } return ImmutableHashSet.CreateRange(StringComparer.Ordinal, GetValues(value, Separators.Space)); } /// /// Gets the access token lifetime associated with the claims principal. /// /// The claims principal. /// The access token lifetime or null if the claim cannot be found. public static TimeSpan? GetAccessTokenLifetime([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } var value = principal.GetClaim(Claims.Private.AccessTokenLifetime); if (string.IsNullOrEmpty(value)) { return null; } if (double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out double result)) { return TimeSpan.FromSeconds(result); } return null; } /// /// Gets the authorization code lifetime associated with the claims principal. /// /// The claims principal. /// The authorization code lifetime or null if the claim cannot be found. public static TimeSpan? GetAuthorizationCodeLifetime([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } var value = principal.GetClaim(Claims.Private.AuthorizationCodeLifetime); if (string.IsNullOrEmpty(value)) { return null; } if (double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out double result)) { return TimeSpan.FromSeconds(result); } return null; } /// /// Gets the identity token lifetime associated with the claims principal. /// /// The claims principal. /// The identity token lifetime or null if the claim cannot be found. public static TimeSpan? GetIdentityTokenLifetime([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } var value = principal.GetClaim(Claims.Private.IdentityTokenLifetime); if (string.IsNullOrEmpty(value)) { return null; } if (double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out double result)) { return TimeSpan.FromSeconds(result); } return null; } /// /// Gets the refresh token lifetime associated with the claims principal. /// /// The claims principal. /// The refresh token lifetime or null if the claim cannot be found. public static TimeSpan? GetRefreshTokenLifetime([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } var value = principal.GetClaim(Claims.Private.RefreshTokenLifetime); if (string.IsNullOrEmpty(value)) { return null; } if (double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out double result)) { return TimeSpan.FromSeconds(result); } return null; } /// /// Gets the unique identifier associated with the claims principal. /// /// The claims principal. /// The unique identifier or null if the claim cannot be found. public static string GetTokenId([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal.GetClaim(Claims.JwtId); } /// /// Gets the token usage associated with the claims principal. /// /// The claims principal. /// The token usage or null if the claim cannot be found. public static string GetTokenUsage([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal.GetClaim(Claims.Private.TokenUsage); } /// /// Gets a boolean value indicating whether the /// claims principal corresponds to an access token. /// /// The claims principal. /// true if the principal corresponds to an access token. public static bool IsAccessToken([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return string.Equals(principal.GetTokenUsage(), TokenUsages.AccessToken, StringComparison.OrdinalIgnoreCase); } /// /// Gets a boolean value indicating whether the /// claims principal corresponds to an access token. /// /// The claims principal. /// true if the principal corresponds to an authorization code. public static bool IsAuthorizationCode([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return string.Equals(principal.GetTokenUsage(), TokenUsages.AuthorizationCode, StringComparison.OrdinalIgnoreCase); } /// /// Gets a boolean value indicating whether the /// claims principal corresponds to an identity token. /// /// The claims principal. /// true if the principal corresponds to an identity token. public static bool IsIdentityToken([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return string.Equals(principal.GetTokenUsage(), TokenUsages.IdToken, StringComparison.OrdinalIgnoreCase); } /// /// Gets a boolean value indicating whether the /// claims principal corresponds to a refresh token. /// /// The claims principal. /// true if the principal corresponds to a refresh token. public static bool IsRefreshToken([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return string.Equals(principal.GetTokenUsage(), TokenUsages.RefreshToken, StringComparison.OrdinalIgnoreCase); } /// /// Determines whether the claims principal contains at least one audience. /// /// The claims principal. /// true if the principal contains at least one audience. public static bool HasAudience([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal.FindAll(Claims.Audience).Any(); } /// /// Determines whether the claims principal contains the given audience. /// /// The claims principal. /// The audience. /// true if the principal contains the given audience. public static bool HasAudience([NotNull] this ClaimsPrincipal principal, [NotNull] string audience) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (string.IsNullOrEmpty(audience)) { throw new ArgumentException("The audience cannot be null or empty.", nameof(audience)); } return principal.GetAudiences().Contains(audience); } /// /// Determines whether the claims principal contains at least one presenter. /// /// The claims principal. /// true if the principal contains at least one presenter. public static bool HasPresenter([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal.FindAll(Claims.AuthorizedParty).Any(); } /// /// Determines whether the claims principal contains the given presenter. /// /// The claims principal. /// The presenter. /// true if the principal contains the given presenter. public static bool HasPresenter([NotNull] this ClaimsPrincipal principal, [NotNull] string presenter) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (string.IsNullOrEmpty(presenter)) { throw new ArgumentException("The presenter cannot be null or empty.", nameof(presenter)); } return principal.GetPresenters().Contains(presenter); } /// /// Determines whether the claims principal contains at least one resource. /// /// The claims principal. /// true if the principal contains at least one resource. public static bool HasResource([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal.FindAll(Claims.Private.Resource).Any(); } /// /// Determines whether the claims principal contains the given resource. /// /// The claims principal. /// The resource. /// true if the principal contains the given resource. public static bool HasResource([NotNull] this ClaimsPrincipal principal, [NotNull] string resource) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (string.IsNullOrEmpty(resource)) { throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); } return principal.GetResources().Contains(resource); } /// /// Determines whether the claims principal contains at least one scope. /// /// The claims principal. /// true if the principal contains at least one scope. public static bool HasScope([NotNull] this ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal.FindAll(Claims.Scope).Any(); } /// /// Determines whether the claims principal contains the given scope. /// /// The claims principal. /// The scope. /// true if the principal contains the given scope. public static bool HasScope([NotNull] this ClaimsPrincipal principal, [NotNull] string scope) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (string.IsNullOrEmpty(scope)) { throw new ArgumentException("The scope cannot be null or empty.", nameof(scope)); } return principal.GetScopes().Contains(scope); } /// /// Sets the creation date in the claims principal. /// /// The claims principal. /// The creation date /// The claims principal. public static ClaimsPrincipal SetCreationDate([NotNull] this ClaimsPrincipal principal, [CanBeNull] DateTimeOffset? date) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } principal.RemoveClaims(Claims.IssuedAt); if (date.HasValue) { var claim = new Claim(Claims.IssuedAt, date?.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64); ((ClaimsIdentity) principal.Identity).AddClaim(claim); } return principal; } /// /// Sets the expiration date in the claims principal. /// /// The claims principal. /// The expiration date /// The claims principal. public static ClaimsPrincipal SetExpirationDate([NotNull] this ClaimsPrincipal principal, [CanBeNull] DateTimeOffset? date) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } principal.RemoveClaims(Claims.ExpiresAt); if (date.HasValue) { var claim = new Claim(Claims.ExpiresAt, date?.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64); ((ClaimsIdentity) principal.Identity).AddClaim(claim); } return principal; } /// /// Sets the audiences list in the claims principal. /// Note: this method automatically excludes duplicate audiences. /// /// The claims principal. /// The audiences to store. /// The claims principal. public static ClaimsPrincipal SetAudiences( [NotNull] this ClaimsPrincipal principal, [CanBeNull] IEnumerable audiences) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal.SetClaims(Claims.Audience, audiences.Distinct(StringComparer.Ordinal)); } /// /// Sets the audiences list in the claims principal. /// Note: this method automatically excludes duplicate audiences. /// /// The claims principal. /// The audiences to store. /// The claims principal. public static ClaimsPrincipal SetAudiences( [NotNull] this ClaimsPrincipal principal, [CanBeNull] params string[] audiences) // Note: guarding the audiences parameter against null values // is not necessary as AsEnumerable() doesn't throw on null values. => principal.SetAudiences(audiences.AsEnumerable()); /// /// Sets the presenters list in the claims principal. /// Note: this method automatically excludes duplicate presenters. /// /// The claims principal. /// The presenters to store. /// The claims principal. public static ClaimsPrincipal SetPresenters( [NotNull] this ClaimsPrincipal principal, [CanBeNull] IEnumerable presenters) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal.SetClaims(Claims.AuthorizedParty, presenters.Distinct(StringComparer.Ordinal)); } /// /// Sets the presenters list in the claims principal. /// Note: this method automatically excludes duplicate presenters. /// /// The claims principal. /// The presenters to store. /// The claims principal. public static ClaimsPrincipal SetPresenters( [NotNull] this ClaimsPrincipal principal, [CanBeNull] params string[] presenters) // Note: guarding the presenters parameter against null values // is not necessary as AsEnumerable() doesn't throw on null values. => principal.SetPresenters(presenters.AsEnumerable()); /// /// Sets the resources list in the claims principal. /// Note: this method automatically excludes duplicate resources. /// /// The claims principal. /// The resources to store. /// The claims principal. public static ClaimsPrincipal SetResources( [NotNull] this ClaimsPrincipal principal, [CanBeNull] IEnumerable resources) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal.SetClaims(Claims.Private.Resource, resources.Distinct(StringComparer.Ordinal)); } /// /// Sets the resources list in the claims principal. /// Note: this method automatically excludes duplicate resources. /// /// The claims principal. /// The resources to store. /// The claims principal. public static ClaimsPrincipal SetResources( [NotNull] this ClaimsPrincipal principal, [CanBeNull] params string[] resources) // Note: guarding the resources parameter against null values // is not necessary as AsEnumerable() doesn't throw on null values. => principal.SetResources(resources.AsEnumerable()); /// /// Sets the scopes list in the claims principal. /// Note: this method automatically excludes duplicate scopes. /// /// The claims principal. /// The scopes to store. /// The claims principal. public static ClaimsPrincipal SetScopes( [NotNull] this ClaimsPrincipal principal, [CanBeNull] IEnumerable scopes) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (scopes == null) { return principal.RemoveClaims(Claims.Scope); } // Note: scopes are deliberately formatted as a single space-separated // string to respect the usual representation of the standard scope claim. // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-02. return principal.SetClaim(Claims.Scope, string.Join(" ", scopes.Distinct(StringComparer.Ordinal))); } /// /// Sets the scopes list in the claims principal. /// Note: this method automatically excludes duplicate scopes. /// /// The claims principal. /// The scopes to store. /// The claims principal. public static ClaimsPrincipal SetScopes( [NotNull] this ClaimsPrincipal principal, [CanBeNull] params string[] scopes) // Note: guarding the scopes parameter against null values // is not necessary as AsEnumerable() doesn't throw on null values. => principal.SetScopes(scopes.AsEnumerable()); /// /// Sets the access token lifetime associated with the claims principal. /// /// The claims principal. /// The access token lifetime to store. /// The claims principal. public static ClaimsPrincipal SetAccessTokenLifetime([NotNull] this ClaimsPrincipal principal, TimeSpan? lifetime) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal.SetClaim(Claims.Private.AccessTokenLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture)); } /// /// Sets the authorization code lifetime associated with the claims principal. /// /// The claims principal. /// The authorization code lifetime to store. /// The claims principal. public static ClaimsPrincipal SetAuthorizationCodeLifetime([NotNull] this ClaimsPrincipal principal, TimeSpan? lifetime) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal.SetClaim(Claims.Private.AuthorizationCodeLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture)); } /// /// Sets the identity token lifetime associated with the claims principal. /// /// The claims principal. /// The identity token lifetime to store. /// The claims principal. public static ClaimsPrincipal SetIdentityTokenLifetime([NotNull] this ClaimsPrincipal principal, TimeSpan? lifetime) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal.SetClaim(Claims.Private.IdentityTokenLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture)); } /// /// Sets the refresh token lifetime associated with the claims principal. /// /// The claims principal. /// The refresh token lifetime to store. /// The claims principal. public static ClaimsPrincipal SetRefreshTokenLifetime([NotNull] this ClaimsPrincipal principal, TimeSpan? lifetime) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal.SetClaim(Claims.Private.RefreshTokenLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture)); } /// /// Sets the unique identifier associated with the claims principal. /// /// The claims principal. /// The unique identifier to store. /// The claims principal. public static ClaimsPrincipal SetTokenId([NotNull] this ClaimsPrincipal principal, string identifier) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal.SetClaim(Claims.JwtId, identifier); } private static IEnumerable GetValues(string source, char[] separators) { Debug.Assert(!string.IsNullOrEmpty(source), "The source string shouldn't be null or empty."); Debug.Assert(separators?.Length != 0, "The separators collection shouldn't be null or empty."); foreach (var element in new StringTokenizer(source, separators)) { var segment = Trim(element, separators); if (segment.Length == 0) { continue; } yield return segment.Value; } yield break; } private static bool HasValue(string source, string value, char[] separators) { Debug.Assert(!string.IsNullOrEmpty(source), "The source string shouldn't be null or empty."); Debug.Assert(!string.IsNullOrEmpty(value), "The value string shouldn't be null or empty."); Debug.Assert(separators?.Length != 0, "The separators collection shouldn't be null or empty."); foreach (var element in new StringTokenizer(source, separators)) { var segment = Trim(element, separators); if (segment.Length == 0) { continue; } if (segment.Equals(value, StringComparison.Ordinal)) { return true; } } return false; } private static StringSegment TrimStart(StringSegment segment, char[] separators) { Debug.Assert(separators?.Length != 0, "The separators collection shouldn't be null or empty."); var index = segment.Offset; while (index < segment.Offset + segment.Length) { if (!IsSeparator(segment.Buffer[index], separators)) { break; } index++; } return new StringSegment(segment.Buffer, index, segment.Offset + segment.Length - index); } private static StringSegment TrimEnd(StringSegment segment, char[] separators) { Debug.Assert(separators?.Length != 0, "The separators collection shouldn't be null or empty."); var index = segment.Offset + segment.Length - 1; while (index >= segment.Offset) { if (!IsSeparator(segment.Buffer[index], separators)) { break; } index--; } return new StringSegment(segment.Buffer, segment.Offset, index - segment.Offset + 1); } private static StringSegment Trim(StringSegment segment, char[] separators) { Debug.Assert(separators?.Length != 0, "The separators collection shouldn't be null or empty."); return TrimEnd(TrimStart(segment, separators), separators); } private static bool IsSeparator(char character, char[] separators) { Debug.Assert(separators?.Length != 0, "The separators collection shouldn't be null or empty."); for (var index = 0; index < separators.Length; index++) { if (character == separators[index]) { return true; } } return false; } } }