diff --git a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs new file mode 100644 index 0000000..dd4d8c9 --- /dev/null +++ b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs @@ -0,0 +1,268 @@ +//****************************************************************************************************** +// AuthorizationInfoControllerBase.cs - Gbtc +// +// Copyright © 2025, 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/15/2025 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Gemstone.Collections.CollectionExtensions; +using Gemstone.Security.AccessControl; +using Gemstone.Security.AuthenticationProviders; +using Gemstone.Web.Security; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Gemstone.Web.APIController; + +/// +/// Base class for a controller that provides information about claims to client applications. +/// +public abstract partial class AuthorizationInfoControllerBase : ControllerBase +{ + #region [ Members ] + + // Nested Types + + /// + /// Represents an entry in a resource access list. + /// + public class ResourceAccessEntry + { + /// + /// Gets or sets the type of the resource. + /// + public string ResourceType { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the resource. + /// + public string ResourceName { get; set; } = string.Empty; + + /// + /// Gets or sets the level of access needed. + /// + public ResourceAccessType Access { get; set; } + } + + #endregion + + #region [ Methods ] + + /// + /// Gets all the claims associated with the authenticated user. + /// + /// All of the authenticated user's claims. + [HttpGet, Route("user/claims")] + public virtual IActionResult GetAllClaims() + { + var claims = HttpContext.User.Claims + .Select(claim => new { claim.Type, claim.Value }); + + return Ok(claims); + } + + /// + /// Gets the claims of a given type associated with the authenticated user. + /// + /// The type of the claims to be returned + /// The authenticated user's claims of the given type. + [HttpGet, Route("user/claims/{**claimType}")] + public virtual IEnumerable GetClaims(string claimType) + { + return HttpContext.User + .FindAll(claim => claim.Type == claimType) + .Select(claim => claim.Value); + } + + /// + /// Gets the types of claims that the authentication provider associates with its users. + /// + /// Provides injected dependencies + /// Identity of the authentication provider + /// Text used to narrow the list of results + /// The list of claim types used by the authentication provider. + [HttpGet, Route("provider/{providerIdentity}/claimTypes")] + public virtual IActionResult FindClaimTypes(IServiceProvider serviceProvider, string providerIdentity, string? searchText) + { + IAuthenticationProvider? claimsProvider = serviceProvider + .GetKeyedService(providerIdentity); + + if (claimsProvider is null) + return NotFound(); + + Regex? searchPattern = ToSearchPattern(searchText); + + var claimTypes = claimsProvider + .GetClaimTypes() + .Where(type => searchPattern is null || searchPattern.IsMatch(type.Type)) + .Select(type => new { Value = type.Type, Label = type.Alias, LongLabel = type.Description }); + + return Ok(claimTypes); + } + + /// + /// Gets a list of claims that can be assigned to users who authenticate with the provider. + /// + /// Provides injected dependencies + /// Identity of the authentication provider + /// The type of claims to search for + /// Text used to narrow the list of results + /// A list of claims from the authentication provider. + [HttpGet, Route("provider/{providerIdentity}/claims/{**claimType}")] + public virtual IActionResult FindClaims(IServiceProvider serviceProvider, string providerIdentity, string claimType, string? searchText) + { + IAuthenticationProvider? claimsProvider = serviceProvider + .GetKeyedService(providerIdentity); + + if (claimsProvider is null) + return NotFound(); + + if (!isSupported(claimType)) + return BadRequest($"Claim type not supported"); + + var claims = claimsProvider + .FindClaims(claimType, searchText ?? "*") + .Select(claim => new { Label = claim.Description, claim.Value, LongLabel = claim.LongDescription }); + + return Ok(claims); + + bool isSupported(string claimType) => claimsProvider + .GetClaimTypes() + .Any(type => type.Type == claimType); + } + + /// + /// Gets a list of resources available for which permissions can be granted within the application. + /// + /// Provides authorization policies defined within the application + /// Source for endpoint data used to look up controller and action metadata + /// A list of resources within the application. + [HttpGet, Route("resources")] + public virtual async Task GetResources(IAuthorizationPolicyProvider policyProvider, EndpointDataSource endpointDataSource) + { + Dictionary> resourceAccessLookup = []; + + foreach (Endpoint endpoint in endpointDataSource.Endpoints) + { + ControllerActionDescriptor? descriptor = endpoint.Metadata + .GetMetadata(); + + if (descriptor is null) + continue; + + IReadOnlyList authorizeData = endpoint.Metadata.GetOrderedMetadata() ?? []; + IReadOnlyList policies = endpoint.Metadata.GetOrderedMetadata() ?? []; + IReadOnlyList requirementData = endpoint.Metadata.GetOrderedMetadata() ?? []; + AuthorizationPolicy? policy = await AuthorizationPolicy.CombineAsync(policyProvider, authorizeData, policies); + + bool hasControllerAccessRequirement = requirementData + .SelectMany(datum => datum.GetRequirements()) + .Concat(policy?.Requirements ?? []) + .Any(requirement => requirement is ControllerAccessRequirement); + + if (!hasControllerAccessRequirement) + continue; + + ResourceAccessAttribute? accessAttribute = endpoint.Metadata + .GetMetadata(); + + string resourceName = accessAttribute.GetResourceName(descriptor); + IEnumerable accessTypes = ToAccessTypes(endpoint, accessAttribute); + HashSet access = resourceAccessLookup.GetOrAdd(resourceName, _ => []); + access.UnionWith(accessTypes); + } + + var resources = resourceAccessLookup + .OrderBy(kvp => kvp.Key) + .Select(kvp => new + { + Type = "Controller", + Name = kvp.Key, + AccessTypes = kvp.Value.OrderBy(type => type) + }); + + return Ok(resources); + + static IEnumerable ToAccessTypes(Endpoint endpoint, ResourceAccessAttribute? accessAttribute) + { + if (accessAttribute is not null) + return [accessAttribute.Access]; + + HttpMethodMetadata? httpMethodMetadata = endpoint.Metadata + .GetMetadata(); + + IReadOnlyList httpMethods = httpMethodMetadata?.HttpMethods + ?? []; + + return httpMethods + .Select(accessAttribute.GetAccessType) + .Where(type => type is not null) + .Select(type => type.GetValueOrDefault()); + } + } + + /// + /// Checks whether the user has permission to access each of the resources listed in the access list. + /// + /// List of resources the user is attempting to access + /// List of boolean values indicating whether the user has access to each requested resource. + [HttpPost, Route("access")] + public virtual IEnumerable CheckAccess([FromBody] ResourceAccessEntry[] accessList) + { + return accessList.Select(entry => User.HasAccessTo(entry.ResourceType, entry.ResourceName, entry.Access)); + } + + #endregion + + #region [ Static ] + + // Static Methods + + private static Regex? ToSearchPattern(string? searchText) + { + if (searchText is null) + return null; + + Regex conversionPattern = SearchTextConversionPattern(); + + string searchPattern = conversionPattern.Replace(searchText, match => match.Value switch + { + "*" => ".*", + @"\\" => @"\\", + string v when v.StartsWith('\\') => Regex.Escape(v[1..]), + string v => Regex.Escape(v) + }); + + return new(searchPattern); + } + + [GeneratedRegex(@"\\.|\*|[^\\*]+")] + private static partial Regex SearchTextConversionPattern(); + + #endregion +} diff --git a/src/Gemstone.Web/APIController/ClaimsControllerBase.cs b/src/Gemstone.Web/APIController/ClaimsControllerBase.cs deleted file mode 100644 index d7aac93..0000000 --- a/src/Gemstone.Web/APIController/ClaimsControllerBase.cs +++ /dev/null @@ -1,117 +0,0 @@ -//****************************************************************************************************** -// ClaimsControllerBase.cs - Gbtc -// -// Copyright © 2025, 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/15/2025 - Stephen C. Wills -// Generated original version of source code. -// -//****************************************************************************************************** - -using System; -using System.Collections.Generic; -using System.Linq; -using Gemstone.Security.AuthenticationProviders; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; - -namespace Gemstone.Web.APIController; - -/// -/// Base class for a controller that provides information about claims to client applications. -/// -public abstract class ClaimsControllerBase : ControllerBase -{ - /// - /// Gets all the claims associated with the authenticated user. - /// - /// All of the authenticated user's claims. - [HttpGet, Route("claims")] - public IActionResult GetAllClaims() - { - var claims = HttpContext.User.Claims - .Select(claim => new { claim.Type, claim.Value }); - - return Ok(claims); - } - - /// - /// Gets the claims of a given type associated with the authenticated user. - /// - /// The type of the claims to be returned - /// The authenticated user's claims of the given type. - [HttpGet, Route("claims/{**claimType}")] - public IEnumerable GetClaims(string claimType) - { - return HttpContext.User - .FindAll(claim => claim.Type == claimType) - .Select(claim => claim.Value); - } - - /// - /// Gets the types of claims that the authentication provider associates with its users. - /// - /// Provides injected dependencies - /// Identity of the authentication provider - /// The list of claim types used by the authentication provider. - [HttpGet, Route("provider/{providerIdentity}/claimTypes")] - public IActionResult GetClaimTypes(IServiceProvider serviceProvider, string providerIdentity) - { - IAuthenticationClaimsProvider? claimsProvider = serviceProvider - .GetKeyedService(providerIdentity); - - return claimsProvider is not null - ? Ok(claimsProvider.GetClaimTypes()) - : NotFound(); - } - - /// - /// Gets a list of users that can be authorized via the authentication provider. - /// - /// Provides injected dependencies - /// Identity of the authentication provider - /// Text used to narrow the list of results - /// A list of users from the authentication provider. - [HttpGet, Route("provider/{providerIdentity}/users")] - public IActionResult FindUsers(IServiceProvider serviceProvider, string providerIdentity, string? searchText) - { - IAuthenticationClaimsProvider? claimsProvider = serviceProvider - .GetKeyedService(providerIdentity); - - return claimsProvider is not null - ? Ok(claimsProvider.FindUsers(searchText ?? "*")) - : NotFound(); - } - - /// - /// Gets a list of claims that can be assigned to users who authenticate with the provider. - /// - /// Provides injected dependencies - /// Identity of the authentication provider - /// The type of claims to search for - /// Text used to narrow the list of results - /// A list of claims from the authentication provider. - [HttpGet, Route("provider/{providerIdentity}/claims/{**claimType}")] - public IActionResult FindClaims(IServiceProvider serviceProvider, string providerIdentity, string claimType, string? searchText) - { - IAuthenticationClaimsProvider? claimsProvider = serviceProvider - .GetKeyedService(providerIdentity); - - return claimsProvider is not null - ? Ok(claimsProvider.FindClaims(claimType, searchText ?? "*")) - : NotFound(); - } -} diff --git a/src/Gemstone.Web/APIController/ModelController.cs b/src/Gemstone.Web/APIController/ModelController.cs index 542c280..b2e096a 100644 --- a/src/Gemstone.Web/APIController/ModelController.cs +++ b/src/Gemstone.Web/APIController/ModelController.cs @@ -44,33 +44,10 @@ namespace Gemstone.Web.APIController /// /// Creates a new /// - public ModelController() - { - DeleteRoles = typeof(T).GetCustomAttribute()?.Roles ?? ""; - PostRoles = typeof(T).GetCustomAttribute()?.Roles ?? ""; - PatchRoles = typeof(T).GetCustomAttribute()?.Roles ?? ""; - } - + public ModelController() { } + #endregion - #region [ Properties ] - - /// - /// Gets the roles required for PATCH requests. - /// - protected string PatchRoles { get; } - - /// - /// Gets the roles required for POST requests. - /// - protected string PostRoles { get; } - - /// - /// Gets the roles required for DELETE requests. - /// - protected string DeleteRoles { get; } - - #endregion /// /// Updates a record from associated table. @@ -81,9 +58,6 @@ public ModelController() [HttpPatch, Route("")] public virtual async Task Patch([FromBody] T record, CancellationToken cancellationToken) { - if (!PatchAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); await tableOperations.UpdateRecordAsync(record, cancellationToken); @@ -100,9 +74,6 @@ public virtual async Task Patch([FromBody] T record, Cancellation [HttpPost, Route("")] public virtual async Task Post([FromBody]T record, CancellationToken cancellationToken) { - if (!PostAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); await tableOperations.AddNewRecordAsync(record, cancellationToken); @@ -120,9 +91,6 @@ public virtual async Task Post([FromBody]T record, CancellationTo [HttpDelete, Route("")] public virtual async Task Delete([FromBody] T record, CancellationToken cancellationToken) { - if (!DeleteAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); await tableOperations.DeleteRecordAsync(record, cancellationToken); @@ -139,9 +107,6 @@ public virtual async Task Delete([FromBody] T record, Cancellatio [HttpDelete, Route("{id}")] public virtual async Task Delete(string id, CancellationToken cancellationToken) { - if(!DeleteAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); await tableOperations.DeleteRecordWhereAsync($"{PrimaryKeyField} = {{0}}", cancellationToken, id); @@ -149,36 +114,7 @@ public virtual async Task Delete(string id, CancellationToken can return Ok(1); } - #region [ Methods ] - - /// - /// Check if current user is authorized for POST Requests. - /// - /// true if User is authorized for POST requests; otherwise, false. - protected virtual bool PostAuthCheck() - { - return PostRoles == string.Empty || User.IsInRole(PostRoles); - } - - /// - /// Check if current user is authorized for DELETE Requests. - /// - /// true if User is authorized for DELETE requests; otherwise, false. - protected virtual bool DeleteAuthCheck() - { - return DeleteRoles == string.Empty || User.IsInRole(DeleteRoles); - } - - /// - /// Check if current user is authorized for PATCH Requests. - /// - /// true if User is authorized for PATCH requests; otherwise, false. - protected virtual bool PatchAuthCheck() - { - return PatchRoles == string.Empty || User.IsInRole(PatchRoles); - } - - #endregion + #region [ Static ] diff --git a/src/Gemstone.Web/APIController/ReadOnlyModelController.cs b/src/Gemstone.Web/APIController/ReadOnlyModelController.cs index b7fcb70..ba5c6fb 100644 --- a/src/Gemstone.Web/APIController/ReadOnlyModelController.cs +++ b/src/Gemstone.Web/APIController/ReadOnlyModelController.cs @@ -33,6 +33,7 @@ using Gemstone.Configuration; using Gemstone.Data; using Gemstone.Data.Model; +using Gemstone.Security.AccessControl; using Microsoft.AspNetCore.Mvc; namespace Gemstone.Web.APIController @@ -174,9 +175,6 @@ public ReadOnlyModelController() [HttpGet, Route("Open/{filterExpression}/{parameters}/{expiration:double?}")] public Task Open(string? filterExpression, object?[] parameters, double? expiration, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Task.FromResult(Unauthorized()); - ConnectionCache cache = ConnectionCache.Create(expiration ?? 1.0D); cache.Records = cache.Table.QueryRecordsWhereAsync(filterExpression, cancellationToken, parameters).GetAsyncEnumerator(cancellationToken); @@ -199,9 +197,6 @@ public Task Open(string? filterExpression, object?[] parameters, [HttpGet, Route("Next/{token}/{count:int?}")] public async Task Next(string token, int? count, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - if (!ConnectionCache.TryGet(token, out ConnectionCache? cache) || cache is null) return NotFound(); @@ -228,9 +223,6 @@ public async Task Next(string token, int? count, CancellationToke [HttpGet, Route("Close/{token}")] public IActionResult Close(string token) { - if (!GetAuthCheck()) - return Unauthorized(); - return ConnectionCache.Close(token) ? Ok() : NotFound(); } @@ -244,9 +236,6 @@ public IActionResult Close(string token) [HttpGet, Route("{page:min(0)}/{parentID?}")] public virtual async Task Get(string? parentID, int page, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); RecordFilter? filter = null; @@ -277,9 +266,6 @@ public virtual async Task Get(string? parentID, int page, Cancell [HttpGet, Route("{page:min(0)}/{sort}/{ascending:bool}")] public virtual async Task Get(string sort, bool ascending, int page, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); RecordFilter? filter = null; @@ -301,9 +287,6 @@ public virtual async Task Get(string sort, bool ascending, int pa [HttpGet, Route("{page:min(0)}/{parentID}/{sort}/{ascending:bool}")] public virtual async Task Get(string parentID, string sort, bool ascending, int page, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); RecordFilter filter = new() @@ -327,9 +310,6 @@ public virtual async Task Get(string parentID, string sort, bool [HttpGet, Route("One/{id}")] public virtual async Task GetOne(string id, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); T? result = await tableOperations.QueryRecordAsync(new RecordRestriction($"{PrimaryKeyField} = {{0}}", id), cancellationToken).ConfigureAwait(false); @@ -348,11 +328,9 @@ public virtual async Task GetOne(string id, CancellationToken can /// Propagates notification that operations should be canceled. /// An containing or . [HttpPost, Route("Search/{page:min(0)}/{parentID?}")] + [ResourceAccess(ResourceAccessType.Read)] public virtual async Task Search([FromBody] SearchPost postData, int page, string? parentID, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); RecordFilter[] filters = postData.Searches.ToArray(); @@ -380,11 +358,9 @@ public virtual async Task Search([FromBody] SearchPost postDat /// Propagates notification that operations should be canceled. /// A object containing the pagination information or . [HttpPost, Route("PageInfo/{parentID?}")] + [ResourceAccess(ResourceAccessType.Read)] public virtual async Task GetPageInfo(SearchPost postData, string? parentID, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); RecordFilter[] filters = postData.Searches.ToArray(); @@ -419,9 +395,6 @@ public virtual async Task GetPageInfo(SearchPost postData, str [HttpGet, Route("PageInfo/{parentID?}")] public virtual async Task GetPageInfo(string? parentID, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); RecordFilter[] filters = []; @@ -454,9 +427,6 @@ public virtual async Task GetPageInfo(string? parentID, Cancellat [HttpGet, Route("New")] public virtual async Task New(CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); @@ -473,9 +443,6 @@ public virtual async Task New(CancellationToken cancellationToken [HttpGet, Route("Max/{fieldName}")] public virtual async Task GetMaxValue(string fieldName, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - // Validate that the field exists on the model T using reflection PropertyInfo? property = typeof(T).GetProperty(fieldName); if (property is null) @@ -492,15 +459,6 @@ public virtual async Task GetMaxValue(string fieldName, Cancellat return Ok(maxValue ?? 0); } - /// - /// Check if current User is authorized for GET Requests. - /// - /// true if User is authorized for GET requests; otherwise, false. - protected virtual bool GetAuthCheck() - { - return GetRoles == string.Empty || User.IsInRole(GetRoles); - } - /// /// Create the for the controller. /// diff --git a/src/Gemstone.Web/Gemstone.Web.csproj b/src/Gemstone.Web/Gemstone.Web.csproj index a8d1e9b..dd9f46d 100644 --- a/src/Gemstone.Web/Gemstone.Web.csproj +++ b/src/Gemstone.Web/Gemstone.Web.csproj @@ -63,6 +63,7 @@ + diff --git a/src/Gemstone.Web/Hosting/AuthenticationSessionMiddleware.cs b/src/Gemstone.Web/Hosting/AuthenticationSessionMiddleware.cs deleted file mode 100644 index ec712c8..0000000 --- a/src/Gemstone.Web/Hosting/AuthenticationSessionMiddleware.cs +++ /dev/null @@ -1,280 +0,0 @@ -//****************************************************************************************************** -// AuthenticationSessionMiddleware.cs - Gbtc -// -// Copyright © 2025, 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: -// ---------------------------------------------------------------------------------------------------- -// 04/18/2025 - Stephen C. Wills -// Generated original version of source code. -// -//****************************************************************************************************** - -using System; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace Gemstone.Web.Hosting; - -/// -/// Represents configuration options for the . -/// -public class AuthenticationSessionOptions -{ - /// - /// Gets or sets the base path of the application to which the cookie will be scoped. - /// - public string ApplicationBasePath { get; set; } = "/"; - - /// - /// Gets or sets the name of the cookie used for storing the authentication session token. - /// - public string AuthenticationSessionCookie { get; set; } = "x-gemstone-auth"; - - /// - /// Gets or sets the amount of time the server will continue to store - /// the authentication session since the last request made by the user. - /// - public double IdleAuthenticationTokenExpirationMinutes { get; set; } = 15.0D; - - /// - /// Gets or sets the amount of time after the authenticated - /// session is established before the session cookie expires. - /// - public double AuthenticationSessionExpirationHours { get; set; } = 24.0D; - - /// - /// Gets the amount of time the server will continue to store the - /// authentication session since the last request made by the user. - /// - public TimeSpan AuthenticationTokenExpiration => - TimeSpan.FromMinutes(IdleAuthenticationTokenExpirationMinutes); - - /// - /// Gets the amount of time after the authenticated session - /// is established before the session cookie expires. - /// - public DateTimeOffset AuthenticationCookieExpiration => - DateTime.UtcNow.AddHours(AuthenticationSessionExpirationHours); -} - -/// -/// Represents a middleware used to establish an authenticated session between the client and server. -/// -/// Delegate used to invoke the next middleware in the pipeline. -/// Cache used to store session data. -/// Options used to configure how the session is managed. -public class AuthenticationSessionMiddleware(RequestDelegate next, IMemoryCache memoryCache, IOptionsMonitor options) -{ - #region [ Properties ] - - private RequestDelegate Next { get; } = next; - private IMemoryCache MemoryCache { get; } = memoryCache; - private AuthenticationSessionOptions Options => options.CurrentValue; - - #endregion - - #region [ Methods ] - - /// - /// Handles an HTTP request. - /// - /// The object providing context about the request. - /// Task that indicates when the middleware is finished processing the request. - public async Task Invoke(HttpContext httpContext) - { - AuthenticationSessionOptions options = Options; - - if (httpContext.Request.Headers.ContainsKey("Authorization")) - await HandleRequestWithCredentialsAsync(httpContext, options); - else - await HandleRequestAsync(httpContext, options); - } - - // If a request with credentials is authenticated, create a new session and provide a session cookie. - // If a request with credentials is not authenticated, flush session data and expire the session cookie. - private async Task HandleRequestWithCredentialsAsync(HttpContext httpContext, AuthenticationSessionOptions options) - { - await Next(httpContext); - - string? oldAuthenticationToken = ReadAuthenticationCookie(httpContext, options); - - if (oldAuthenticationToken is not null) - FlushSessionPrincipal(oldAuthenticationToken); - - if (httpContext.User.Identity?.IsAuthenticated == true) - HandleAuthenticatedRequest(httpContext, options); - else - ExpireAuthenticationCookie(httpContext, options); - } - - // Request without credentials will attempt to use the session to authenticate the request. - private async Task HandleRequestAsync(HttpContext httpContext, AuthenticationSessionOptions options) - { - string? authenticationToken = ReadAuthenticationCookie(httpContext, options); - - if (authenticationToken is null) - { - await Next(httpContext); - return; - } - - ClaimsPrincipal? sessionPrincipal = FetchSessionPrincipal(authenticationToken); - - if (sessionPrincipal is null) - { - await Next(httpContext); - return; - } - - httpContext.User = sessionPrincipal; - await Next(httpContext); - UpdateSessionPrincipal(authenticationToken, sessionPrincipal, options); - } - - // Establishes a new session with the client. - private void HandleAuthenticatedRequest(HttpContext httpContext, AuthenticationSessionOptions options) - { - string newAuthenticationToken = GenerateNewAuthenticationToken(); - ClaimsPrincipal sessionPrincipal = BuildSessionPrincipal(httpContext.User); - UpdateSessionPrincipal(newAuthenticationToken, sessionPrincipal, options); - UpdateAuthenticationCookie(httpContext, newAuthenticationToken, options); - } - - // Retrieve session data from the server-side cache. - private ClaimsPrincipal? FetchSessionPrincipal(string authenticationToken) - { - return MemoryCache.TryGetValue(authenticationToken, out ClaimsPrincipal? principal) - ? principal : null; - } - - // Updates the server-side cache entry and resets the expiration window. - private void UpdateSessionPrincipal(string authenticationToken, ClaimsPrincipal sessionPrincipal, AuthenticationSessionOptions options) - { - MemoryCache - .CreateEntry(authenticationToken) - .SetValue(sessionPrincipal) - .SetSlidingExpiration(options.AuthenticationTokenExpiration) - .Dispose(); - } - - // Removes session data from the server-side cache. - private void FlushSessionPrincipal(string authenticationToken) - { - MemoryCache.Remove(authenticationToken); - } - - // Prepares the cookie with the session token in the response to send it to the client. - private void UpdateAuthenticationCookie(HttpContext httpContext, string authenticationToken, AuthenticationSessionOptions options) - { - CookieOptions cookieOptions = new() - { - Path = Options.ApplicationBasePath, - HttpOnly = true, - Secure = true, - IsEssential = true, - Expires = options.AuthenticationCookieExpiration - }; - - httpContext.Response.Cookies.Append(options.AuthenticationSessionCookie, authenticationToken, cookieOptions); - } - - #endregion - - #region [ Static ] - - // Static Methods - - // Reads the session token from the cookie provided by the client. - private static string? ReadAuthenticationCookie(HttpContext httpContext, AuthenticationSessionOptions options) - { - return httpContext.Request.Cookies.TryGetValue(options.AuthenticationSessionCookie, out string? token) - ? token : null; - } - - // Generates a token the server can use to look up the session principal. - private static string GenerateNewAuthenticationToken() - { - byte[] tokenData = new byte[128]; - RandomNumberGenerator.Fill(tokenData); - return Convert.ToBase64String(tokenData); - } - - // Copies the claims from the authenticated request's principal into a new - // principal that will persist across requests within the same session. - private static ClaimsPrincipal BuildSessionPrincipal(ClaimsPrincipal httpPrincipal) - { - ClaimsIdentity sessionIdentity = new(httpPrincipal.Identity, httpPrincipal.Claims); - ClaimsPrincipal sessionPrincipal = new(sessionIdentity); - sessionIdentity.AddClaim(new(ClaimTypes.Role, "Session")); - return sessionPrincipal; - } - - // If the client provided a cookie, indicate to the client that the - // cookie is no longer valid by sending an expired cookie to replace it. - private static void ExpireAuthenticationCookie(HttpContext httpContext, AuthenticationSessionOptions options) - { - string authenticationCookie = options.AuthenticationSessionCookie; - - if (httpContext.Request.Cookies.ContainsKey(authenticationCookie)) - httpContext.Response.Cookies.Delete(authenticationCookie); - } - - #endregion -} - -/// -/// Extension methods for setting up the . -/// -public static class AuthenticationSessionMiddlewareExtensions -{ - /// - /// Registers the services required by the . - /// - /// Collection of services used by the app. - /// Collection of services used by the app. - public static IServiceCollection AddAuthenticationCache(this IServiceCollection services) - { - return services.AddMemoryCache(); - } - - /// - /// Registers the services required by the . - /// - /// Collection of services used by the app. - /// The action used to configure the options. - /// Collection of services used by the app. - public static IServiceCollection AddAuthenticationCache(this IServiceCollection services, Action configureOptions) - { - return services - .Configure(configureOptions) - .AddAuthenticationCache(); - } - - /// - /// Registers the in the app pipeline. - /// - /// The app builder with which to register the middleware. - /// The app builder with middleware registered. - public static IApplicationBuilder UseAuthenticationCache(this IApplicationBuilder app) - { - return app.UseMiddleware(); - } -} diff --git a/src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs b/src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs new file mode 100644 index 0000000..d099685 --- /dev/null +++ b/src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs @@ -0,0 +1,57 @@ +//****************************************************************************************************** +// AuthenticationRuntimeMiddleware.cs - Gbtc +// +// Copyright © 2025, 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/23/2025 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Gemstone.Security.AuthenticationProviders; +using Microsoft.AspNetCore.Http; + +namespace Gemstone.Web.Security; + +/// +/// Represents a middleware used to assign claims to authenticated users. +/// +/// Delegate used to invoke the next middleware in the pipeline +/// The authentication runtime used to query assigned claims +/// The identity of the provider that authenticated the user +public class AuthenticationRuntimeMiddleware(RequestDelegate next, IAuthenticationRuntime runtime, string providerIdentity) +{ + private RequestDelegate Next { get; } = next; + private IAuthenticationRuntime Runtime { get; } = runtime; + private string ProviderIdentity { get; } = providerIdentity; + + /// + /// Assigns claims from the authentication runtime to the authenticated user. + /// + /// The context of the HTTP request + public async Task Invoke(HttpContext httpContext) + { + ClaimsPrincipal user = httpContext.User; + ClaimsIdentity newIdentity = new(user.Identity, user.FindAll(ClaimTypes.Name)); + IEnumerable assignedClaims = Runtime.GetAssignedClaims(ProviderIdentity, user); + newIdentity.AddClaims(assignedClaims); + httpContext.User = new(newIdentity); + await Next(httpContext); + } +} diff --git a/src/Gemstone.Web/Security/ControllerAccessHandler.cs b/src/Gemstone.Web/Security/ControllerAccessHandler.cs new file mode 100644 index 0000000..1006838 --- /dev/null +++ b/src/Gemstone.Web/Security/ControllerAccessHandler.cs @@ -0,0 +1,195 @@ +//****************************************************************************************************** +// ControllerAccessHandler.cs - Gbtc +// +// Copyright © 2025, 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/29/2025 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Security.Claims; +using System.Threading.Tasks; +using Gemstone.Security.AccessControl; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; + +namespace Gemstone.Web.Security; + +/// +/// Authorization handler for access to controller actions. +/// +public class ControllerAccessHandler : AuthorizationHandler +{ + #region [ Members ] + + // Nested Types + private enum Permission + { + Allow, + Deny, + Neither + } + + private class ContextWrapper(AuthorizationHandlerContext context, ControllerAccessRequirement requirement, HttpContext httpContext, Endpoint endpoint, ControllerActionDescriptor descriptor) + { + private AuthorizationHandlerContext Context { get; } = context; + private ControllerAccessRequirement Requirement { get; } = requirement; + + public ClaimsPrincipal User { get; } = context.User; + public Endpoint Endpoint { get; } = endpoint; + public ControllerActionDescriptor Descriptor { get; } = descriptor; + public string HttpMethod => httpContext.Request.Method; + + public bool Succeed() + { + Context.Succeed(Requirement); + return true; + } + + public bool Fail(AuthorizationFailureReason reason) + { + Context.Fail(reason); + return true; + } + } + + #endregion + + #region [ Methods ] + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ControllerAccessRequirement requirement) + { + if (context.Resource is not HttpContext httpContext) + return Task.CompletedTask; + + IEndpointFeature? endpointFeature = httpContext.Features.Get(); + Endpoint? endpoint = endpointFeature?.Endpoint; + + if (endpoint is null) + return Task.CompletedTask; + + ControllerActionDescriptor? descriptor = endpoint.Metadata + .GetMetadata(); + + if (descriptor is null) + return Task.CompletedTask; + + ContextWrapper wrapper = new(context, requirement, httpContext, endpoint, descriptor); + + if (HandleResourceActionPermission(wrapper)) + return Task.CompletedTask; + + HandleResourceAccessPermission(wrapper); + return Task.CompletedTask; + } + + private bool HandleResourceActionPermission(ContextWrapper wrapper) + { + IRouteNameMetadata? routeNameMetadata = wrapper.Endpoint.Metadata + .GetMetadata(); + + string? routeName = routeNameMetadata?.RouteName; + + string resource = wrapper.Descriptor.ControllerName; + string action = routeName ?? wrapper.Descriptor.ActionName; + string claimValue = $"Controller {resource} {action}"; + Permission permission = GetResourceActionPermission(wrapper.User, claimValue); + + return + (permission == Permission.Deny && fail()) || + (permission == Permission.Allow && succeed()); + + bool succeed() => + wrapper.Succeed(); + + bool fail() + { + AuthorizationFailureReason reason = ToFailureReason(claimValue); + return wrapper.Fail(reason); + } + } + + private AuthorizationFailureReason ToFailureReason(string claim) + { + return new AuthorizationFailureReason(this, $"{claim} permission denied"); + } + + #endregion + + #region [ Static ] + + // Static Methods + + private static Permission GetResourceActionPermission(ClaimsPrincipal user, string claimValue) + { + string allowClaim = $"Gemstone.ResourceAction.Allow"; + string denyClaim = $"Gemstone.ResourceAction.Deny"; + + if (user.HasClaim(denyClaim, claimValue)) + return Permission.Deny; + + return user.HasClaim(allowClaim, claimValue) + ? Permission.Allow + : Permission.Neither; + } + + private static void HandleResourceAccessPermission(ContextWrapper wrapper) + { + IResourceAccessAttribute? accessAttribute = wrapper.Endpoint.Metadata + .GetMetadata(); + + if (accessAttribute is NoResourceAccessAttribute) + return; + + string resourceName = accessAttribute.GetResourceName(wrapper.Descriptor); + ResourceAccessType? access = accessAttribute.GetAccessType(wrapper.HttpMethod); + + if (access.HasValue && wrapper.User.HasAccessTo("Controller", resourceName, access.GetValueOrDefault())) + wrapper.Succeed(); + } + + #endregion +} + +/// +/// Requirement to be handled by the . +/// +public class ControllerAccessRequirement : IAuthorizationRequirement +{ +} + +/// +/// Defines extension methods for the controller access handler. +/// +public static class ControllerAccessHandlerExtensions +{ + private static ControllerAccessRequirement Requirement { get; } = new(); + + /// + /// Adds the to the policy. + /// + /// The policy builder + /// The policy builder. + public static AuthorizationPolicyBuilder RequireControllerAccess(this AuthorizationPolicyBuilder builder) + { + return builder.AddRequirements(Requirement); + } +} diff --git a/src/Gemstone.Web/Security/DefaultTicketStore.cs b/src/Gemstone.Web/Security/DefaultTicketStore.cs new file mode 100644 index 0000000..b31acdb --- /dev/null +++ b/src/Gemstone.Web/Security/DefaultTicketStore.cs @@ -0,0 +1,107 @@ +//****************************************************************************************************** +// DefaultTicketStore.cs - Gbtc +// +// Copyright © 2025, 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: +// ---------------------------------------------------------------------------------------------------- +// 08/05/2025 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace Gemstone.Web.Security; + +/// +/// Options to configure the memory cache used by the authentication ticket store. +/// +public class SessionCacheOptions +{ + /// + /// Gets or sets the amount of time a ticket should + /// remain valid since the last time it was accessed. + /// + public TimeSpan SlidingExpiration { get; set; } = TimeSpan.FromMinutes(15); +} + +/// +/// Represents an in-memory storage mechanism for authentication tickets. +/// +/// The cache in which tickets will be stored +public class DefaultTicketStore(IMemoryCache memoryCache, IOptionsMonitor optionsMonitor) : ITicketStore +{ + private IMemoryCache MemoryCache { get; } = memoryCache; + private SessionCacheOptions Options { get; } = optionsMonitor.CurrentValue; + + /// + public Task StoreAsync(AuthenticationTicket ticket) + { + string key = GenerateNewSessionToken(); + UpdateEntry(key, ticket); + return Task.FromResult(key); + } + + /// + public Task RenewAsync(string key, AuthenticationTicket ticket) + { + UpdateEntry(key, ticket); + return Task.CompletedTask; + } + + /// + public Task RetrieveAsync(string key) + { + if (!MemoryCache.TryGetValue(key, out AuthenticationTicket? ticket)) + ticket = null; + + return Task.FromResult(ticket); + } + + /// + public Task RemoveAsync(string key) + { + MemoryCache.Remove(key); + return Task.CompletedTask; + } + + private void UpdateEntry(string key, AuthenticationTicket ticket) + { + DateTimeOffset? expiration = ticket.Properties.ExpiresUtc; + MemoryCacheEntryOptions options = new() { SlidingExpiration = Options.SlidingExpiration }; + + if (expiration is not null) + options.AbsoluteExpiration = ticket.Properties.ExpiresUtc; + + MemoryCache + .CreateEntry(key) + .SetOptions(options) + .SetValue(ticket) + .Dispose(); + } + + private static string GenerateNewSessionToken() + { + byte[] tokenData = new byte[128]; + RandomNumberGenerator.Fill(tokenData); + return Convert.ToBase64String(tokenData); + } +} diff --git a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs new file mode 100644 index 0000000..3898fa9 --- /dev/null +++ b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs @@ -0,0 +1,233 @@ +//****************************************************************************************************** +// IAuthenticationWebBuilder.cs - Gbtc +// +// Copyright © 2025, 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/25/2025 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Threading.Tasks; +using Gemstone.Security.AuthenticationProviders; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; + +namespace Gemstone.Web.Security; + +/// +/// Represents a builder for Gemstone authentication web components. +/// +public interface IAuthenticationWebBuilder +{ + /// + /// Adds the default middleware type for the given provider identity to the application's request pipeline. + /// + /// The identity of the authentication provider + /// The authentication web builder. + IAuthenticationWebBuilder UseProvider(string providerIdentity); + + /// + /// Adds the default middleware type for the given provider identity to the application's request pipeline. + /// + /// The identity of the authentication provider + /// The path on which requests will require authentication using this provider + /// The authentication web builder. + IAuthenticationWebBuilder UseProvider(string providerIdentity, string pathMatch); + + /// + /// Adds the default middleware type for the given provider identity to the application's request pipeline. + /// + /// The identity of the authentication provider + /// The path on which requests will require authentication using this provider + /// The authentication scheme to require on the endpoint + /// The authentication web builder. + IAuthenticationWebBuilder UseProvider(string providerIdentity, string pathMatch, string scheme); +} + +/// +/// Extensions for the authentication web builder. +/// +public static class AuthenticationWebBuilderExtensions +{ + private class AuthenticationWebBuilder(IApplicationBuilder app) : IAuthenticationWebBuilder + { + public IAuthenticationWebBuilder UseProvider(string providerIdentity) + { + string endpoint = $"/asi/auth/{providerIdentity}"; + return UseProvider(providerIdentity, endpoint); + } + + public IAuthenticationWebBuilder UseProvider(string providerIdentity, string pathMatch) + { + return UseProvider(providerIdentity, pathMatch, providerIdentity); + } + + public IAuthenticationWebBuilder UseProvider(string providerIdentity, string pathMatch, string scheme) + { + app.Map(pathMatch, branch => branch + .UseRouting() + .UseAuthorization() + .UseMiddleware(providerIdentity) + .UseEndpoints(endpoints => + { + IEndpointConventionBuilder conventions = endpoints.MapGet("/", async context => + { + await context.SignInAsync(context.User); + + string? returnURL = context.Request.Query["redir"]; + context.Response.Redirect(returnURL ?? "/"); + }); + + conventions.RequireAuthorization(policy => policy + .AddAuthenticationSchemes(scheme) + .RequireAuthenticatedUser()); + })); + + return this; + } + } + + private class LogoutMiddleware(RequestDelegate next, IOptionsMonitor cookieOptions) + { + private CookieAuthenticationOptions Options => cookieOptions + .Get(CookieAuthenticationDefaults.AuthenticationScheme); + + public async Task Invoke(HttpContext httpContext) + { + if (!IsLogoutRequest(httpContext.Request)) + { + await next(httpContext); + return; + } + + await httpContext.SignOutAsync(); + + // This handles the redirect to login page + if (!IsAjaxRequest(httpContext.Request)) + await httpContext.ChallengeAsync(); + } + + private bool IsLogoutRequest(HttpRequest request) + { + PathString logoutPath = Options.LogoutPath; + return logoutPath.HasValue && request.Path.StartsWithSegments(logoutPath); + } + + // Taken from Microsoft's reference source for the Cookie authentication implementation + // https://github.com/dotnet/aspnetcore/blob/v9.0.8/src/Security/Authentication/Cookies/src/CookieAuthenticationEvents.cs#L105 + private static bool IsAjaxRequest(HttpRequest request) + { + return string.Equals(request.Query[HeaderNames.XRequestedWith], "XMLHttpRequest", StringComparison.Ordinal) || + string.Equals(request.Headers.XRequestedWith, "XMLHttpRequest", StringComparison.Ordinal); + } + } + + /// + /// Sets up the default configuration for services to support Gemstone authentication. + /// + /// Provides setup information for the runtime. + /// The collection of services + /// Builder for adding additional authentication schemes. + public static AuthenticationBuilder ConfigureGemstoneWebAuthentication(this IServiceCollection services) where T : class, IAuthenticationSetup + { + return services + .AddGemstoneAuthentication() + .ConfigureGemstoneWebDefaults(); + } + + /// + /// Sets up the default configuration for services to support Gemstone authentication. + /// + /// The collection of services + /// Method to configure the runtime + /// Builder for adding additional authentication schemes. + public static AuthenticationBuilder ConfigureGemstoneWebAuthentication(this IServiceCollection services, Action configure) + { + return services + .AddGemstoneAuthentication(configure) + .ConfigureGemstoneWebDefaults(); + } + + /// + /// Automatically configures the request pipeline to support well-known authentication providers. + /// + /// The application builder + /// The application builder. + public static IApplicationBuilder UseGemstoneAuthentication(this IApplicationBuilder app) + { + return app.UseGemstoneAuthentication(configure); + + void configure(IAuthenticationWebBuilder builder) + { + IAuthenticationSetup setup = app.ApplicationServices.GetRequiredService(); + + foreach (string providerIdentity in setup.GetProviderIdentities()) + builder.UseProvider(providerIdentity); + } + } + + /// + /// Configures the request pipeline to support authentication providers using the given configuration method. + /// + /// The application builder + /// The method to configure authentication provider middleware + /// The application builder. + public static IApplicationBuilder UseGemstoneAuthentication(this IApplicationBuilder app, Action configure) + { + AuthenticationWebBuilder builder = new(app.UseAuthentication()); + configure(builder); + return app.UseMiddleware(); + } + + private static AuthenticationBuilder ConfigureGemstoneWebDefaults(this IServiceCollection services) + { + services + .AddMemoryCache() + .TryAddSingleton(); + + services + .AddOptions(CookieAuthenticationDefaults.AuthenticationScheme) + .Configure((options, sessionStore) => + { + options.SessionStore = sessionStore; + + options.ExpireTimeSpan = TimeSpan.FromHours(24); + options.SlidingExpiration = false; + options.LoginPath = "/Login"; + options.LogoutPath = "/asi/logout"; + options.ReturnUrlParameter = "redir"; + + options.Cookie.Name = "x-gemstone-auth"; + options.Cookie.Path = "/"; + options.Cookie.HttpOnly = true; + options.Cookie.IsEssential = true; + }); + + return services + .AddWindowsAuthenticationProvider() + .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddNegotiate("windows", _ => { }) + .AddCookie(); + } +}