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";
+ }
+}