// Copyright (c) Umbraco. // See LICENSE for more details. using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Security.Claims; using System.Security.Principal; using Umbraco.Cms.Core; namespace Umbraco.Extensions; public static class ClaimsIdentityExtensions { /// /// Returns the required claim types for a back office identity /// /// /// This does not include the role claim type or allowed apps type since that is a collection and in theory could be /// empty /// public static IEnumerable RequiredBackOfficeClaimTypes => new[] { ClaimTypes.NameIdentifier, // id ClaimTypes.Name, // username ClaimTypes.GivenName, // Constants.Security.StartContentNodeIdClaimType, These seem to be able to be null... // Constants.Security.StartMediaNodeIdClaimType, ClaimTypes.Locality, Constants.Security.SecurityStampClaimType, }; public static T? GetUserId(this IIdentity identity) { var strId = identity.GetUserId(); Attempt converted = strId.TryConvertTo(); return converted.Result ?? default; } /// /// Returns the user id from the of either the claim type /// or "sub" /// /// /// /// The string value of the user id if found otherwise null /// public static string? GetUserId(this IIdentity identity) { if (identity == null) { throw new ArgumentNullException(nameof(identity)); } string? userId = null; if (identity is ClaimsIdentity claimsIdentity) { userId = claimsIdentity.FindFirstValue(ClaimTypes.NameIdentifier) ?? claimsIdentity.FindFirstValue("sub"); } return userId; } /// /// Returns the user name from the of either the claim type or /// "preferred_username" /// /// /// /// The string value of the user name if found otherwise null /// public static string? GetUserName(this IIdentity identity) { if (identity == null) { throw new ArgumentNullException(nameof(identity)); } string? username = null; if (identity is ClaimsIdentity claimsIdentity) { username = claimsIdentity.FindFirstValue(ClaimTypes.Name) ?? claimsIdentity.FindFirstValue("preferred_username"); } return username; } public static string? GetEmail(this IIdentity identity) { if (identity == null) { throw new ArgumentNullException(nameof(identity)); } string? email = null; if (identity is ClaimsIdentity claimsIdentity) { email = claimsIdentity.FindFirstValue(ClaimTypes.Email); } return email; } /// /// Returns the first claim value found in the for the given claimType /// /// /// /// /// The string value of the claim if found otherwise null /// public static string? FindFirstValue(this ClaimsIdentity identity, string claimType) { if (identity == null) { throw new ArgumentNullException(nameof(identity)); } return identity.FindFirst(claimType)?.Value; } /// /// Verify that a ClaimsIdentity has all the required claim types /// /// /// Verified identity wrapped in a ClaimsIdentity with BackOfficeAuthentication type /// True if ClaimsIdentity public static bool VerifyBackOfficeIdentity( this ClaimsIdentity identity, [MaybeNullWhen(false)] out ClaimsIdentity verifiedIdentity) { if (identity is null) { verifiedIdentity = null; return false; } // Validate that all required claims exist foreach (var claimType in RequiredBackOfficeClaimTypes) { if (identity.HasClaim(x => x.Type == claimType) == false || identity.HasClaim(x => x.Type == claimType && x.Value.IsNullOrWhiteSpace())) { verifiedIdentity = null; return false; } } verifiedIdentity = identity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType ? identity : new ClaimsIdentity(identity.Claims, Constants.Security.BackOfficeAuthenticationType); return true; } /// /// Add the required claims to be a BackOffice ClaimsIdentity /// /// this /// The users Id /// Username /// Real name /// Start content nodes /// Start media nodes /// The locality of the user /// Security stamp /// Allowed apps /// Roles public static void AddRequiredClaims(this ClaimsIdentity identity, string userId, string username, string realName, IEnumerable? startContentNodes, IEnumerable? startMediaNodes, string culture, string securityStamp, IEnumerable allowedApps, IEnumerable roles) { // This is the id that 'identity' uses to check for the user id if (identity.HasClaim(x => x.Type == ClaimTypes.NameIdentifier) == false) { identity.AddClaim(new Claim( ClaimTypes.NameIdentifier, userId, ClaimValueTypes.String, Constants.Security.BackOfficeAuthenticationType, Constants.Security.BackOfficeAuthenticationType, identity)); } if (identity.HasClaim(x => x.Type == ClaimTypes.Name) == false) { identity.AddClaim(new Claim( ClaimTypes.Name, username, ClaimValueTypes.String, Constants.Security.BackOfficeAuthenticationType, Constants.Security.BackOfficeAuthenticationType, identity)); } if (identity.HasClaim(x => x.Type == ClaimTypes.GivenName) == false) { identity.AddClaim(new Claim( ClaimTypes.GivenName, realName, ClaimValueTypes.String, Constants.Security.BackOfficeAuthenticationType, Constants.Security.BackOfficeAuthenticationType, identity)); } if (identity.HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false && startContentNodes != null) { foreach (var startContentNode in startContentNodes) { identity.AddClaim(new Claim( Constants.Security.StartContentNodeIdClaimType, startContentNode.ToInvariantString(), ClaimValueTypes.Integer32, Constants.Security.BackOfficeAuthenticationType, Constants.Security.BackOfficeAuthenticationType, identity)); } } if (identity.HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false && startMediaNodes != null) { foreach (var startMediaNode in startMediaNodes) { identity.AddClaim(new Claim( Constants.Security.StartMediaNodeIdClaimType, startMediaNode.ToInvariantString(), ClaimValueTypes.Integer32, Constants.Security.BackOfficeAuthenticationType, Constants.Security.BackOfficeAuthenticationType, identity)); } } if (identity.HasClaim(x => x.Type == ClaimTypes.Locality) == false) { identity.AddClaim(new Claim( ClaimTypes.Locality, culture, ClaimValueTypes.String, Constants.Security.BackOfficeAuthenticationType, Constants.Security.BackOfficeAuthenticationType, identity)); } // The security stamp claim is also required if (identity.HasClaim(x => x.Type == Constants.Security.SecurityStampClaimType) == false) { identity.AddClaim(new Claim( Constants.Security.SecurityStampClaimType, securityStamp, ClaimValueTypes.String, Constants.Security.BackOfficeAuthenticationType, Constants.Security.BackOfficeAuthenticationType, identity)); } // Add each app as a separate claim if (identity.HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false && allowedApps != null) { foreach (var application in allowedApps) { identity.AddClaim(new Claim( Constants.Security.AllowedApplicationsClaimType, application, ClaimValueTypes.String, Constants.Security.BackOfficeAuthenticationType, Constants.Security.BackOfficeAuthenticationType, identity)); } } // Claims are added by the ClaimsIdentityFactory because our UserStore supports roles, however this identity might // not be made with that factory if it was created with a different ticket so perform the check if (identity.HasClaim(x => x.Type == ClaimsIdentity.DefaultRoleClaimType) == false && roles != null) { // Manually add them foreach (var roleName in roles) { identity.AddClaim(new Claim( identity.RoleClaimType, roleName, ClaimValueTypes.String, Constants.Security.BackOfficeAuthenticationType, Constants.Security.BackOfficeAuthenticationType, identity)); } } } /// /// Get the start content nodes from a ClaimsIdentity /// /// /// Array of start content nodes public static int[] GetStartContentNodes(this ClaimsIdentity identity) => identity.FindAll(x => x.Type == Constants.Security.StartContentNodeIdClaimType) .Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : default) .Where(x => x != default).ToArray(); /// /// Get the start media nodes from a ClaimsIdentity /// /// /// Array of start media nodes public static int[] GetStartMediaNodes(this ClaimsIdentity identity) => identity.FindAll(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) .Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : default) .Where(x => x != default).ToArray(); /// /// Get the allowed applications from a ClaimsIdentity /// /// /// public static string[] GetAllowedApplications(this ClaimsIdentity identity) => identity .FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToArray(); /// /// Get the user ID from a ClaimsIdentity /// /// /// User ID as integer public static int? GetId(this ClaimsIdentity identity) { var firstValue = identity.FindFirstValue(ClaimTypes.NameIdentifier); if (firstValue is not null) { return int.Parse(firstValue, CultureInfo.InvariantCulture); } return null; } /// /// Get the real name belonging to the user from a ClaimsIdentity /// /// /// Real name of the user public static string? GetRealName(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.GivenName); /// /// Get the username of the user from a ClaimsIdentity /// /// /// Username of the user public static string? GetUsername(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.Name); /// /// Get the culture string from a ClaimsIdentity /// /// /// Culture string public static string? GetCultureString(this ClaimsIdentity identity) => identity.FindFirstValue(ClaimTypes.Locality); /// /// Get the security stamp from a ClaimsIdentity /// /// /// Security stamp public static string? GetSecurityStamp(this ClaimsIdentity identity) => identity.FindFirstValue(Constants.Security.SecurityStampClaimType); /// /// Get the roles assigned to a user from a ClaimsIdentity /// /// /// Array of roles public static string[] GetRoles(this ClaimsIdentity identity) => identity .FindAll(x => x.Type == ClaimsIdentity.DefaultRoleClaimType).Select(role => role.Value).ToArray(); /// /// Adds or updates and existing claim. /// public static void AddOrUpdateClaim(this ClaimsIdentity identity, Claim? claim) { if (identity == null) { throw new ArgumentNullException(nameof(identity)); } if (claim is not null) { Claim? existingClaim = identity.Claims.FirstOrDefault(x => x.Type == claim.Type); identity.TryRemoveClaim(existingClaim); identity.AddClaim(claim); } } }