diff --git a/src/Gemstone.Security/AccessControl/ResourceAccessType.cs b/src/Gemstone.Security/AccessControl/ResourceAccessType.cs index baeca622..b92fd2aa 100644 --- a/src/Gemstone.Security/AccessControl/ResourceAccessType.cs +++ b/src/Gemstone.Security/AccessControl/ResourceAccessType.cs @@ -23,6 +23,7 @@ using System; using System.Security.Claims; +using Gemstone.Security.AuthenticationProviders; namespace Gemstone.Security.AccessControl; @@ -94,25 +95,26 @@ public static bool HasAccessTo(this ClaimsPrincipal user, string resourceType, s { ThrowIfNotValid(access); - const string AllowClaim = "Gemstone.ResourceAccess.Allow"; - const string DenyClaim = "Gemstone.ResourceAccess.Deny"; - const string BaseClaim = "Gemstone.ResourceAccess.Default"; - if (access == ResourceAccessType.None) return false; string claimValue = $"{resourceType} {resourceName} {access}"; bool IsDenied() => - user.HasClaim(DenyClaim, claimValue); + user.HasClaim(GemstoneClaimTypes.DenyClaim, claimValue); bool IsAllowed() => - user.HasClaim(AllowClaim, claimValue) || - user.HasClaim(BaseClaim, $"{access}"); + user.HasClaim(GemstoneClaimTypes.AllowClaim, claimValue) || + (!user.IsAPIUser() && user.HasClaim(GemstoneClaimTypes.BaseClaim, $"{access}")); return !IsDenied() && IsAllowed(); } + private static bool IsAPIUser(this ClaimsPrincipal user) + { + return user.Identity?.AuthenticationType == APIAuthenticationHandler.AuthenticationType; + } + private static void ThrowIfNotValid(ResourceAccessType access) { switch (access) diff --git a/src/Gemstone.Security/AuthenticationProviders/APIAuthenticationHandler.cs b/src/Gemstone.Security/AuthenticationProviders/APIAuthenticationHandler.cs new file mode 100644 index 00000000..8ded5475 --- /dev/null +++ b/src/Gemstone.Security/AuthenticationProviders/APIAuthenticationHandler.cs @@ -0,0 +1,185 @@ +//****************************************************************************************************** +// APIAuthenticationHandler.cs - Gbtc +// +// Copyright © 2026, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 07/09/2026 - C. Lackner +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Gemstone.Security.AuthenticationProviders; + +/// +/// Options for the class. +/// +public class APIAuthenticationOptions : AuthenticationSchemeOptions +{ + /// + /// Function that parses and validates an API token. + /// + public Func? ValidateToken { get; set; } +} + +/// +/// Represents metadata associated with an API token. +/// +public class APIToken +{ + /// + /// Gets or sets the name of the API user. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the time at which the token expires. + /// + public DateTime Expiration { get; set; } + + /// + /// Gets or sets the list of claims assigned to the API user. + /// + public Claim[] Claims { get; set; } = []; +} + +/// +/// Represents an authentication handler for API users. +/// +public class APIAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) + : AuthenticationHandler(options, logger, encoder) +{ + /// + /// Authentication type used for API authentication. + /// + public const string AuthenticationType = "APIAuthentication"; + + private const string HttpAuthenticationScheme = "Bearer"; + + private string AuthorizationHeader => Request.Headers.Authorization.ToString(); + + /// + /// Parses the Authorization header and API token. + /// + /// The result of authentication. + protected override Task HandleAuthenticateAsync() + { + AuthenticateResult result = Authenticate(); + return Task.FromResult(result); + } + + /// + /// Returns a 401 Unauthorized response with the WWW-Authenticate header. + /// + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + Response.Headers.WWWAuthenticate = HttpAuthenticationScheme; + return base.HandleChallengeAsync(properties); + } + + private AuthenticateResult Authenticate() + { + string prefix = $"{HttpAuthenticationScheme} "; + + if (!AuthorizationHeader.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + return AuthenticateResult.NoResult(); + + string token = AuthorizationHeader[prefix.Length..].Trim(); + APIToken? resolvedToken; + + try + { + resolvedToken = Options.ValidateToken?.Invoke(token); + } + catch (Exception ex) + { + return AuthenticateResult.Fail(ex); + } + + if (resolvedToken is null) + return AuthenticateResult.NoResult(); + + if (resolvedToken.Expiration < DateTime.UtcNow) + return AuthenticateResult.Fail("Token expired"); + + ClaimsIdentity identity = new(AuthenticationType); + identity.AddClaim(new(ClaimTypes.Name, resolvedToken.Name, Options.ClaimsIssuer)); + identity.AddClaims(resolvedToken.Claims); + + ClaimsPrincipal principal = new(identity); + AuthenticationTicket ticket = new(principal, Scheme.Name); + return AuthenticateResult.Success(ticket); + } +} + +/// +/// Extension methods for . +/// +public static class APIAuthenticationHandlerExtensions +{ + /// + /// Adds the API authentication handler to the application. + /// + /// The builder used to configure authentication + /// The builder used to configure authentication. + public static AuthenticationBuilder AddAPIAuthentication(this AuthenticationBuilder builder) + { + return builder.AddAPIAuthentication(options => { }); + } + + /// + /// Adds the API authentication handler to the application. + /// + /// The builder used to configure authentication + /// Action to configure the + /// The builder used to configure authentication. + public static AuthenticationBuilder AddAPIAuthentication(this AuthenticationBuilder builder, Action configureOptions) + { + return builder.AddAPIAuthentication("api", configureOptions); + } + + /// + /// Adds the API authentication handler to the application. + /// + /// The builder used to configure authentication + /// The name of the scheme + /// Action to configure the + /// The builder used to configure authentication. + public static AuthenticationBuilder AddAPIAuthentication(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions) + { + return builder.AddAPIAuthentication(authenticationScheme, null, configureOptions); + } + + /// + /// Adds the API authentication handler to the application. + /// + /// The builder used to configure authentication + /// The name of the scheme + /// The display name of the scheme + /// Action to configure the + /// The builder used to configure authentication. + public static AuthenticationBuilder AddAPIAuthentication(this AuthenticationBuilder builder, string authenticationScheme, string? displayName, Action configureOptions) + { + return builder.AddScheme(authenticationScheme, displayName, configureOptions); + } +} diff --git a/src/Gemstone.Security/AuthenticationProviders/IAuthenticationBuilder.cs b/src/Gemstone.Security/AuthenticationProviders/IAuthenticationBuilder.cs index 669af0dd..41367416 100644 --- a/src/Gemstone.Security/AuthenticationProviders/IAuthenticationBuilder.cs +++ b/src/Gemstone.Security/AuthenticationProviders/IAuthenticationBuilder.cs @@ -96,9 +96,6 @@ public IEnumerable GetProviderIdentities() public IEnumerable GetAssignedClaims(string providerIdentity, ClaimsPrincipal principal) { - const string ProviderIdentityClaim = "Gemstone.ProviderIdentity"; - const string UserIdentityClaim = "Gemstone.UserIdentity"; - IAuthenticationProvider? provider = ProviderLookup(providerIdentity); if (provider is null) @@ -106,13 +103,16 @@ public IEnumerable GetAssignedClaims(string providerIdentity, ClaimsPrinc string userIdentity = provider.GetIdentity(principal); - IEnumerable providerClaims = Setup + IEnumerable providerClaims = principal.Claims + .Append(new(GemstoneClaimTypes.AllUsers, string.Empty)); + + IEnumerable assignedClaims = Setup .GetProviderClaims(providerIdentity) - .Join(principal.Claims, ToKey, ToKey, (providerClaim, _) => providerClaim.Assigned) - .Prepend(new(UserIdentityClaim, userIdentity)) - .Prepend(new(ProviderIdentityClaim, providerIdentity)); + .Join(providerClaims, ToKey, ToKey, (mapping, _) => mapping.Assigned) + .Prepend(new(GemstoneClaimTypes.UserIdentity, userIdentity)) + .Prepend(new(GemstoneClaimTypes.ProviderIdentity, providerIdentity)); - return providerClaims; + return assignedClaims; } private static (string, string) ToKey((Claim Match, Claim) tuple) diff --git a/src/Gemstone.Security/AuthenticationProviders/OAuthAuthenticationProvider.cs b/src/Gemstone.Security/AuthenticationProviders/OAuthAuthenticationProvider.cs index b5266466..0484811d 100644 --- a/src/Gemstone.Security/AuthenticationProviders/OAuthAuthenticationProvider.cs +++ b/src/Gemstone.Security/AuthenticationProviders/OAuthAuthenticationProvider.cs @@ -122,12 +122,14 @@ public OAuthAuthenticationProvider() /// public string GetIdentity(ClaimsPrincipal principal) { - if (ClaimTypes.Length == 1) + if (ClaimTypes is null) ClaimTypes = principal .Claims .Select(claim => claim.Type) + .Prepend(GemstoneClaimTypes.UserIdentity) + .Prepend(GemstoneClaimTypes.AllUsers) .Distinct() - .Select(type => new ClaimType(type)).Prepend(new ClaimType("Gemstone.AllUsers")).ToArray(); + .Select(type => new ClaimType(type)).ToArray(); string? identity = principal .FindFirst(Options.UserIdClaim ?? "sub")? @@ -140,7 +142,7 @@ public string GetIdentity(ClaimsPrincipal principal) /// public IEnumerable GetClaimTypes() { - return ClaimTypes; + return ClaimTypes ?? [new ClaimType(GemstoneClaimTypes.AllUsers), new(GemstoneClaimTypes.UserIdentity)]; } /// @@ -154,11 +156,11 @@ public IEnumerable FindClaims(string claimType, string searchTex #region [ Static ] // Static Properties - private static ClaimType[] ClaimTypes + private static ClaimType[]? ClaimTypes { get; set; - } = [new ClaimType("Gemstone.AllUsers")]; + } = null; // Static Methods @@ -256,7 +258,7 @@ public static IServiceCollection AddOAuthAuthenticationProvider(this IServiceCol /// The collection of services. public static IServiceCollection AddOAuthAuthenticationProvider(this IServiceCollection services, string identity, Action configure) { - return services.AddKeyedTransient(identity, (_, _) => + return services.AddKeyedSingleton(identity, (_, _) => { OAuthAuthenticationProviderOptions options = new(); configure(options); diff --git a/src/Gemstone.Security/GemstoneClaimTypes.cs b/src/Gemstone.Security/GemstoneClaimTypes.cs new file mode 100644 index 00000000..fa53a0f4 --- /dev/null +++ b/src/Gemstone.Security/GemstoneClaimTypes.cs @@ -0,0 +1,62 @@ +//****************************************************************************************************** +// GemstoneClaimTypes.cs - Gbtc +// +// Copyright © 2026, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 10/16/2019 - J. Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** + +namespace Gemstone.Security +{ + /// + /// The claim types used by the namespace. + /// + public static class GemstoneClaimTypes + { + /// + /// Assigned claim that holds the unique identifier for the User. + /// + public const string UserIdentity = "Gemstone.UserIdentity"; + + /// + /// Assigned claim that holds the unique identifier for the Authentication Provider. + /// + public const string ProviderIdentity = "Gemstone.ProviderIdentity"; + + /// + /// Implicit claim that masquerades as a provider claim and applies + /// to any user principal regardless of what claims they have. + /// + public const string AllUsers = "Gemstone.AllUsers"; + + /// + /// Assigned claim that allows a user to access a resource. + /// + public const string AllowClaim = "Gemstone.ResourceAccess.Allow"; + + /// + /// Assigned claim that denies a user access to a resource. + /// + public const string DenyClaim = "Gemstone.ResourceAccess.Deny"; + + /// + /// Assigned claim that allows access to the value as the default access level. + /// + public const string BaseClaim = "Gemstone.ResourceAccess.Default"; + } +}