From fda3c0b9cbb7c9f72ea598e997ae40530a99051b Mon Sep 17 00:00:00 2001 From: "David G. Moore, Jr." Date: Thu, 1 Feb 2024 17:51:10 -0500 Subject: [PATCH] Need to pull request --- .gitmodules | 3 + src/AzureAdB2C/.vscode/launch.json | 35 +++ src/AzureAdB2C/.vscode/tasks.json | 57 ++++ src/AzureAdB2C/Api/AppRolesGeneratorApi.cs | 32 ++ src/AzureAdB2C/Api/ClaimsGeneratorApi.cs | 44 +++ src/AzureAdB2C/Api/ClaimsValidatorApi.cs | 39 +++ .../Api/Dgmjr.AzureAdB2C.Api.csproj | 29 ++ src/AzureAdB2C/Api/Dgmjr.AzureAdB2C.Api.sln | 84 ++++++ src/AzureAdB2C/Api/LICENSE.md | 35 +++ src/AzureAdB2C/Api/icon.png | Bin 0 -> 26997 bytes src/AzureAdB2C/Services/ClaimsGenerator.cs | 25 ++ src/AzureAdB2C/Services/ClaimsValidator.cs | 43 +++ .../Services/Dgmjr.AzureAdB2C.Services.csproj | 29 ++ .../Services/Dgmjr.AzureAdB2C.Services.sln | 70 +++++ .../Services/Graph/AppRolesService.cs | 167 +++++++++++ src/AzureAdB2C/Services/LICENSE.md | 35 +++ src/AzureAdB2C/Services/Models/ApiRequest.cs | 22 ++ src/AzureAdB2C/Services/Models/ApiResponse.cs | 58 ++++ .../JsonApiContinueResponseConverter.cs | 27 ++ src/AzureAdB2C/Services/UserHydrator.cs | 23 ++ src/AzureAdB2C/Services/icon.png | Bin 0 -> 26997 bytes src/AzureAdB2C/api-connector-samples | 1 + .../Abstractions/ConfigurationOrder.cs | 1 + .../AutoConfiguratorConfiguration.cs | 25 +- ...oConfigureIApplicationBuilderExtensions.cs | 2 +- ...figureIApplicationHostBuilderExtensions.cs | 4 +- src/Configuration/LICENSE.md | 26 +- src/DownstreamApis/LICENSE.md | 28 +- src/Http/Extensions/LICENSE.md | 28 +- src/Http/Headers/LICENSE.md | 28 +- src/Http/Http/LICENSE.md | 28 +- src/Http/Mime/LICENSE.md | 28 +- src/Http/ResponseCodes/LICENSE.md | 26 +- src/Http/Services/.vscode/settings.json | 5 + .../HttpServicesExtensions.AddHttpServices.cs | 21 +- .../HttpServicesExtensions.UseHttpServices.cs | 17 +- .../HttpServicesOptionsAutoConfigurator.cs | 18 +- src/Http/Services/LICENSE.md | 26 +- src/Http/Services/LoggerExtensions.cs | 48 +++ src/MicrosoftGraph/.vscode/settings.json | 5 + .../Configuration/UserAppRolesConfigurator.cs | 95 ++++++ .../Constants/MsGraphConstants.cs | 9 + src/MicrosoftGraph/Constants/ODataUris.cs | 10 + src/MicrosoftGraph/Constants/Scopes.cs | 12 + .../Controllers/DirectoryObjectsController.cs | 1 + .../Controllers/MeController.cs | 1 + .../Controllers/UsersController.cs | 1 + src/MicrosoftGraph/Dgmjr.Graph.csproj | 1 + src/MicrosoftGraph/Dgmjr.Graph.sln | 26 +- .../Extensions/LoggingExtensions.cs | 60 +++- ...crosoftGraphServiceCollectionExtensions.cs | 48 +-- src/MicrosoftGraph/LICENSE.md | 26 +- .../Models/ExtensionProperty.cs | 8 +- src/MicrosoftGraph/Services/UsersService.cs | 1 + .../ClientSecretTokenProvider.cs | 22 ++ .../TokenAcquisitionTokenProvider.cs | 2 + src/Mvc/.vscode/settings.json | 5 + src/Mvc/Dgmjr.AspNetCore.Mvc.csproj | 1 + .../IHostApplicationBuilderMvcExtensions.cs | 32 +- src/Mvc/LoggerExtensions.cs | 48 +++ src/Mvc/MvcAutoConfigurator.cs | 13 +- src/Mvc/MvcOptions.cs | 280 ++++++++++++------ src/Mvc/ServiceNames.cs | 15 + src/Mvc/TypeNameAndAssemblyConfigurator.cs | 48 +++ src/Payloads/LICENSE.md | 28 +- .../BasicAuthenticationHandler.cs | 18 ++ .../BasicAuthenticationHandlerWithUsers.cs | 74 +++++ ...AuthenticationWithUsersAutoConfigurator.cs | 26 ++ .../BasicAuthenticationWithUsersDefaults.cs | 6 + .../BasicAuthenticationWithUsersOptions.cs | 31 ++ ...mjr.AspNetCore.Authentication.Basic.csproj | 25 ++ .../Dgmjr.AspNetCore.Authentication.Basic.sln | 42 +++ src/Security/Authentication.Basic/LICENSE.md | 35 +++ src/Security/Authentication.Basic/icon.png | Bin 0 -> 26997 bytes .../Authentication/AddApiAuthentication.cs | 61 ---- .../Authentication/AllowAnonymousAttribute.cs | 16 - .../ApiAuthenticationExtensions.cs | 224 -------------- .../Authentication/ApiAuthorizeAttribute.cs | 47 --- .../Authentication/AuthenticationHandler.cs | 15 + .../Authentication/AuthenticationResult.cs | 64 ---- ...orizationHeaderParameterOperationFilter.cs | 51 ---- .../Basic/BasicApiAuthHandler.cs | 150 ---------- src/Security/Authentication/Constants.cs | 73 ----- .../Dgmjr.AspNetCore.Authentication.csproj | 50 +--- .../Dgmjr.AspNetCore.Authentication.props | 6 - .../Dgmjr.AspNetCore.Authentication.sln | 30 +- .../Dgmjr.AspNetCore.Authentication.targets | 6 - .../GeneratedAuthenticationSchemeOptions.cs | 23 -- .../Generated/GeneratedCustomAuthHandler.cs | 257 ---------------- .../Handlers/ApiAuthHandlerBase.cs | 44 --- .../Handlers/ApiMultiAuthenticationHandler.cs | 35 --- .../Handlers/IAuthenticationHandler.cs | 16 - .../Authentication/Handlers/JwtAuthHandler.cs | 164 ---------- .../Authentication/Handlers/JwtExtensions.cs | 96 ------ .../Handlers/SharedSecretAuthHandler.cs | 154 ---------- .../Authentication/IBasicApiAuthMiddleware.cs | 23 -- .../Authentication/Jwt/botmessage.json | 29 -- .../Authentication/LoggerExtensions.cs | 86 ------ .../Middlewares/AuthMidewareBase.cs | 97 ------ .../Middlewares/BasicApiAuthMiddleware.cs | 130 -------- .../Old/ApiAuthorizationServerProvider.cs | 132 --------- .../Options/ApiAuthenticationOptions.cs | 238 --------------- .../BasicAuthenticationSchemeOptions.cs | 63 ---- .../Options/IAuthenticationSchemeOptions.cs | 37 --- .../IBasicAuthenticationSchemeOptions.cs | 18 -- .../Options/IJwtConfigurationOptions.cs | 45 --- .../ISharedSecretAuthenticationOptions.cs | 22 -- .../Options/JwtConfigurationOptions.cs | 96 ------ .../SharedSecretAuthenticationOptions.cs | 96 ------ src/Security/Authentication/README.md | 27 -- .../Schemes/ApiMultiAuthenticationScheme.cs | 27 -- .../Schemes/BasicAuthenticationScheme.cs | 29 -- .../Schemes/IAuthenticationScheme.cs | 21 -- .../Schemes/JwtAuthenticationScheme.cs | 29 -- .../Schemes/SharedSecretAuthencationScheme.cs | 29 -- src/Security/Security/icon.png | Bin 0 -> 26997 bytes src/Swagger/LICENSE.md | 26 +- src/TagHelpers/.vscode/settings.json | 5 +- .../Bootstrap/ActiveAnchorTagHelper.cs | 52 ++-- .../Bootstrap/PageElements/Footer.cs | 9 +- src/TagHelpers/LICENSE.md | 4 +- 121 files changed, 2054 insertions(+), 3170 deletions(-) create mode 100644 src/AzureAdB2C/.vscode/launch.json create mode 100644 src/AzureAdB2C/.vscode/tasks.json create mode 100644 src/AzureAdB2C/Api/AppRolesGeneratorApi.cs create mode 100644 src/AzureAdB2C/Api/ClaimsGeneratorApi.cs create mode 100644 src/AzureAdB2C/Api/ClaimsValidatorApi.cs create mode 100644 src/AzureAdB2C/Api/Dgmjr.AzureAdB2C.Api.csproj create mode 100644 src/AzureAdB2C/Api/Dgmjr.AzureAdB2C.Api.sln create mode 100644 src/AzureAdB2C/Api/LICENSE.md create mode 100644 src/AzureAdB2C/Api/icon.png create mode 100644 src/AzureAdB2C/Services/ClaimsGenerator.cs create mode 100644 src/AzureAdB2C/Services/ClaimsValidator.cs create mode 100644 src/AzureAdB2C/Services/Dgmjr.AzureAdB2C.Services.csproj create mode 100644 src/AzureAdB2C/Services/Dgmjr.AzureAdB2C.Services.sln create mode 100644 src/AzureAdB2C/Services/Graph/AppRolesService.cs create mode 100644 src/AzureAdB2C/Services/LICENSE.md create mode 100644 src/AzureAdB2C/Services/Models/ApiRequest.cs create mode 100644 src/AzureAdB2C/Services/Models/ApiResponse.cs create mode 100644 src/AzureAdB2C/Services/Models/JsonApiContinueResponseConverter.cs create mode 100644 src/AzureAdB2C/Services/UserHydrator.cs create mode 100644 src/AzureAdB2C/Services/icon.png create mode 160000 src/AzureAdB2C/api-connector-samples create mode 100644 src/Http/Services/.vscode/settings.json create mode 100644 src/Http/Services/LoggerExtensions.cs create mode 100644 src/MicrosoftGraph/.vscode/settings.json create mode 100644 src/MicrosoftGraph/Configuration/UserAppRolesConfigurator.cs create mode 100644 src/MicrosoftGraph/Constants/ODataUris.cs create mode 100644 src/MicrosoftGraph/TokenProviders/ClientSecretTokenProvider.cs create mode 100644 src/Mvc/.vscode/settings.json create mode 100644 src/Mvc/LoggerExtensions.cs create mode 100644 src/Mvc/ServiceNames.cs create mode 100644 src/Mvc/TypeNameAndAssemblyConfigurator.cs create mode 100644 src/Security/Authentication.Basic/BasicAuthenticationHandler.cs create mode 100644 src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationHandlerWithUsers.cs create mode 100644 src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationWithUsersAutoConfigurator.cs create mode 100644 src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationWithUsersDefaults.cs create mode 100644 src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationWithUsersOptions.cs create mode 100644 src/Security/Authentication.Basic/Dgmjr.AspNetCore.Authentication.Basic.csproj create mode 100644 src/Security/Authentication.Basic/Dgmjr.AspNetCore.Authentication.Basic.sln create mode 100644 src/Security/Authentication.Basic/LICENSE.md create mode 100644 src/Security/Authentication.Basic/icon.png delete mode 100644 src/Security/Authentication/AddApiAuthentication.cs delete mode 100644 src/Security/Authentication/AllowAnonymousAttribute.cs delete mode 100644 src/Security/Authentication/ApiAuthenticationExtensions.cs delete mode 100644 src/Security/Authentication/ApiAuthorizeAttribute.cs create mode 100644 src/Security/Authentication/AuthenticationHandler.cs delete mode 100644 src/Security/Authentication/AuthenticationResult.cs delete mode 100644 src/Security/Authentication/AuthorizationHeaderParameterOperationFilter.cs delete mode 100644 src/Security/Authentication/Basic/BasicApiAuthHandler.cs delete mode 100644 src/Security/Authentication/Constants.cs delete mode 100644 src/Security/Authentication/Dgmjr.AspNetCore.Authentication.props delete mode 100644 src/Security/Authentication/Dgmjr.AspNetCore.Authentication.targets delete mode 100644 src/Security/Authentication/Generated/GeneratedAuthenticationSchemeOptions.cs delete mode 100644 src/Security/Authentication/Generated/GeneratedCustomAuthHandler.cs delete mode 100644 src/Security/Authentication/Handlers/ApiAuthHandlerBase.cs delete mode 100644 src/Security/Authentication/Handlers/ApiMultiAuthenticationHandler.cs delete mode 100644 src/Security/Authentication/Handlers/IAuthenticationHandler.cs delete mode 100644 src/Security/Authentication/Handlers/JwtAuthHandler.cs delete mode 100644 src/Security/Authentication/Handlers/JwtExtensions.cs delete mode 100644 src/Security/Authentication/Handlers/SharedSecretAuthHandler.cs delete mode 100644 src/Security/Authentication/IBasicApiAuthMiddleware.cs delete mode 100644 src/Security/Authentication/Jwt/botmessage.json delete mode 100644 src/Security/Authentication/LoggerExtensions.cs delete mode 100644 src/Security/Authentication/Middlewares/AuthMidewareBase.cs delete mode 100644 src/Security/Authentication/Middlewares/BasicApiAuthMiddleware.cs delete mode 100644 src/Security/Authentication/Old/ApiAuthorizationServerProvider.cs delete mode 100644 src/Security/Authentication/Options/ApiAuthenticationOptions.cs delete mode 100644 src/Security/Authentication/Options/BasicAuthenticationSchemeOptions.cs delete mode 100644 src/Security/Authentication/Options/IAuthenticationSchemeOptions.cs delete mode 100644 src/Security/Authentication/Options/IBasicAuthenticationSchemeOptions.cs delete mode 100644 src/Security/Authentication/Options/IJwtConfigurationOptions.cs delete mode 100644 src/Security/Authentication/Options/ISharedSecretAuthenticationOptions.cs delete mode 100644 src/Security/Authentication/Options/JwtConfigurationOptions.cs delete mode 100644 src/Security/Authentication/Options/SharedSecretAuthenticationOptions.cs delete mode 100644 src/Security/Authentication/README.md delete mode 100644 src/Security/Authentication/Schemes/ApiMultiAuthenticationScheme.cs delete mode 100644 src/Security/Authentication/Schemes/BasicAuthenticationScheme.cs delete mode 100644 src/Security/Authentication/Schemes/IAuthenticationScheme.cs delete mode 100644 src/Security/Authentication/Schemes/JwtAuthenticationScheme.cs delete mode 100644 src/Security/Authentication/Schemes/SharedSecretAuthencationScheme.cs create mode 100644 src/Security/Security/icon.png diff --git a/.gitmodules b/.gitmodules index 6cb6181c..0618efd3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "src/Http/CorrelationId"] path = src/Http/CorrelationId url = https://github.com/dgmjr-io/CorrelationId.git +[submodule "src/AzureAdB2C/api-connector-samples"] + path = src/AzureAdB2C/api-connector-samples + url = https://github.com/azure-ad-b2c/api-connector-samples.git diff --git a/src/AzureAdB2C/.vscode/launch.json b/src/AzureAdB2C/.vscode/launch.json new file mode 100644 index 00000000..448ee2b7 --- /dev/null +++ b/src/AzureAdB2C/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md. + "name": ".NET Core Launch (AzureADB2C Claims Populator/Validator)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build AzureADB2C Claims Populator/Validator", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/Testing/bin/Local/net8.0/Dgmjr.AzureAdB2C.Testing.dll", + "args": [], + "cwd": "${workspaceFolder}/Testing", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} diff --git a/src/AzureAdB2C/.vscode/tasks.json b/src/AzureAdB2C/.vscode/tasks.json new file mode 100644 index 00000000..5413421c --- /dev/null +++ b/src/AzureAdB2C/.vscode/tasks.json @@ -0,0 +1,57 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build AzureADB2C Claims Populator/Validator", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Testing/Dgmjr.AzureAdB2C.Testing.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign", + "-c:Local", + "-p:BuildFromSource=false" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "Launch ngrok (Dgmjr.AzureAdB2C.Testing)", + "command": "ngrok", + "type": "process", + "args": [ + "tunnel", + "--label", + "edge=edghts_2YVlve6yoIHeHG4dzj3cV1Ojnqn", + "https://localhost:7162" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/Testing/Dgmjr.AzureAdB2C.Testing.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign", + "-c:Local", + "-p:BuildFromSource=false" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/Testing/Dgmjr.AzureAdB2C.Testing.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/src/AzureAdB2C/Api/AppRolesGeneratorApi.cs b/src/AzureAdB2C/Api/AppRolesGeneratorApi.cs new file mode 100644 index 00000000..4def083b --- /dev/null +++ b/src/AzureAdB2C/Api/AppRolesGeneratorApi.cs @@ -0,0 +1,32 @@ +namespace Dgmjr.AzureAdB2C.Api; +using Microsoft.AspNetCore.Authorization; +using Dgmjr.AzureAdB2C.Services.Graph; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Dgmjr.AzureAdB2C.Models; + +public class AppRolesGeneratorApi(ILogger logger, IAppRolesService appRolesService) : ApiControllerBase(logger) +{ + [Authorize] + [HttpPost] + [Route("api/approles/generate")] + [Consumes("application/json")] + public async Task GenerateAsync([FromBody] ApiRequest request, CancellationToken cancellationToken = default) + { + // var request = Deserialize(requestJson); + logger.LogDebug("request: {request}", request); + var result = await appRolesService.GenerateClaimsAsync(request, cancellationToken); + + return result.StatusCode switch + { + StatusCodes.Status200OK => Ok(result), + StatusCodes.Status400BadRequest => BadRequest(result), + StatusCodes.Status401Unauthorized => Unauthorized(result), + StatusCodes.Status403Forbidden => Forbid((result as ApiBlockResponse)!.UserMessage), + StatusCodes.Status404NotFound => NotFound(result), + StatusCodes.Status409Conflict => Conflict(result), + StatusCodes.Status500InternalServerError => Problem((result as ApiErrorResponse)!.DeveloperMessage, statusCode: result.StatusCode), + _ => StatusCode(result.StatusCode ?? StatusCodes.Status200OK, result), + }; + } +} diff --git a/src/AzureAdB2C/Api/ClaimsGeneratorApi.cs b/src/AzureAdB2C/Api/ClaimsGeneratorApi.cs new file mode 100644 index 00000000..4f67a89c --- /dev/null +++ b/src/AzureAdB2C/Api/ClaimsGeneratorApi.cs @@ -0,0 +1,44 @@ +namespace Dgmjr.AzureAdB2C.Api; + +using Dgmjr.AspNetCore.Controllers; +using Dgmjr.AzureAdB2C.Models; +using Dgmjr.AzureAdB2C.Services; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +public abstract class ClaimsGeneratorApi( + ILogger logger, + IClaimsGenerator claimsGenerator, + IUserHydrator userHydrator +) : ApiControllerBase(logger) +{ + public virtual IClaimsGenerator ClaimsGenerator => claimsGenerator; + public virtual IUserHydrator UserHydrator => userHydrator; + + [HttpPost] + [Route("api/claims/validate")] + public async Task ValidateAsync( + [FromBody] ApiRequest request, + CancellationToken cancellationToken = default + ) + { + var result = await ClaimsGenerator.GenerateClaimsAsync( + request, + cancellationToken + ); + + return result.StatusCode switch + { + StatusCodes.Status200OK => Ok(result), + StatusCodes.Status400BadRequest => BadRequest(result), + StatusCodes.Status401Unauthorized => Unauthorized(result), + StatusCodes.Status403Forbidden => Forbid((result as ApiBlockResponse)!.UserMessage), + StatusCodes.Status404NotFound => NotFound(result), + StatusCodes.Status409Conflict => Conflict(result), + StatusCodes.Status500InternalServerError => Problem((result as ApiErrorResponse)!.DeveloperMessage, statusCode: result.StatusCode), + _ => StatusCode(result.StatusCode ?? StatusCodes.Status200OK, result), + }; + } +} diff --git a/src/AzureAdB2C/Api/ClaimsValidatorApi.cs b/src/AzureAdB2C/Api/ClaimsValidatorApi.cs new file mode 100644 index 00000000..c549d927 --- /dev/null +++ b/src/AzureAdB2C/Api/ClaimsValidatorApi.cs @@ -0,0 +1,39 @@ +namespace Dgmjr.AzureAdB2C.Api; + +using Dgmjr.AspNetCore.Controllers; +using Dgmjr.AzureAdB2C.Models; +using Dgmjr.AzureAdB2C.Services; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + + +public abstract class ClaimsValidatorApi( + ILogger logger, + IClaimsValidator claimsValidator +) : ApiControllerBase(logger) +{ + public virtual IClaimsValidator ClaimsValidator => claimsValidator; + + [HttpPost] + [Route("api/claims/generate")] + public async Task GenerateAsync( + [FromBody] ApiRequest request, + CancellationToken cancellationToken = default + ) + { + var result = await ClaimsValidator.ValidateAsync(request, cancellationToken); + + if(IsNullOrWhiteSpace(result.ErrorMessage)) + { + var response = new ApiContinueResponse { Version = ClaimsValidator.Version }; + return Ok(response); + } + else + { + var response = new ApiValidationErrorResponse(result.ErrorMessage) { Version = ClaimsValidator.Version }; + return BadRequest(response); + } + } +} diff --git a/src/AzureAdB2C/Api/Dgmjr.AzureAdB2C.Api.csproj b/src/AzureAdB2C/Api/Dgmjr.AzureAdB2C.Api.csproj new file mode 100644 index 00000000..0e80fc67 --- /dev/null +++ b/src/AzureAdB2C/Api/Dgmjr.AzureAdB2C.Api.csproj @@ -0,0 +1,29 @@ + + + + + net6.0;net8.0 + + + + + + + + + + + + + + diff --git a/src/AzureAdB2C/Api/Dgmjr.AzureAdB2C.Api.sln b/src/AzureAdB2C/Api/Dgmjr.AzureAdB2C.Api.sln new file mode 100644 index 00000000..49352a4a --- /dev/null +++ b/src/AzureAdB2C/Api/Dgmjr.AzureAdB2C.Api.sln @@ -0,0 +1,84 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B283EBC2-E01F-412D-9339-FD56EF114549}" + ProjectSection(SolutionItems) = preProject + ..\..\..\Directory.Build.props = ..\..\..\Directory.Build.props + ..\..\..\..\..\Directory.Build.targets = ..\..\..\..\..\Directory.Build.targets + ..\..\..\..\..\global.json = ..\..\..\..\..\global.json + ..\..\..\..\..\Packages\Versions.Local.props = ..\..\..\..\..\Packages\Versions.Local.props + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dgmjr.Web.DownstreamApis", "..\..\DownstreamApis\Dgmjr.Web.DownstreamApis.csproj", "{0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dgmjr.Graph", "..\..\MicrosoftGraph\Dgmjr.Graph.csproj", "{FD458274-EBF4-4E98-9E7C-5D93DA648AEA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dgmjr.AzureAdB2C.Services", "..\Services\Dgmjr.AzureAdB2C.Services.csproj", "{32AD2745-C108-4DC1-8681-44F5318E4BF0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dgmjr.AzureAdB2C.Api", "Dgmjr.AzureAdB2C.Api.csproj", "{AA8E3890-CD65-4B80-AD09-C3A1ED9D7523}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Local|Any CPU = Local|Any CPU + Debug|Any CPU = Debug|Any CPU + Testing|Any CPU = Testing|Any CPU + Staging|Any CPU = Staging|Any CPU + Production|Any CPU = Production|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Local|Any CPU.ActiveCfg = Local|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Local|Any CPU.Build.0 = Local|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Testing|Any CPU.ActiveCfg = Testing|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Testing|Any CPU.Build.0 = Testing|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Staging|Any CPU.ActiveCfg = Staging|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Staging|Any CPU.Build.0 = Staging|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Production|Any CPU.ActiveCfg = Local|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Production|Any CPU.Build.0 = Local|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Release|Any CPU.Build.0 = Release|Any CPU + {FD458274-EBF4-4E98-9E7C-5D93DA648AEA}.Local|Any CPU.ActiveCfg = Local|Any CPU + {FD458274-EBF4-4E98-9E7C-5D93DA648AEA}.Local|Any CPU.Build.0 = Local|Any CPU + {FD458274-EBF4-4E98-9E7C-5D93DA648AEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD458274-EBF4-4E98-9E7C-5D93DA648AEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD458274-EBF4-4E98-9E7C-5D93DA648AEA}.Testing|Any CPU.ActiveCfg = Testing|Any CPU + {FD458274-EBF4-4E98-9E7C-5D93DA648AEA}.Testing|Any CPU.Build.0 = Testing|Any CPU + {FD458274-EBF4-4E98-9E7C-5D93DA648AEA}.Staging|Any CPU.ActiveCfg = Staging|Any CPU + {FD458274-EBF4-4E98-9E7C-5D93DA648AEA}.Staging|Any CPU.Build.0 = Staging|Any CPU + {FD458274-EBF4-4E98-9E7C-5D93DA648AEA}.Production|Any CPU.ActiveCfg = Local|Any CPU + {FD458274-EBF4-4E98-9E7C-5D93DA648AEA}.Production|Any CPU.Build.0 = Local|Any CPU + {FD458274-EBF4-4E98-9E7C-5D93DA648AEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD458274-EBF4-4E98-9E7C-5D93DA648AEA}.Release|Any CPU.Build.0 = Release|Any CPU + {32AD2745-C108-4DC1-8681-44F5318E4BF0}.Local|Any CPU.ActiveCfg = Local|Any CPU + {32AD2745-C108-4DC1-8681-44F5318E4BF0}.Local|Any CPU.Build.0 = Local|Any CPU + {32AD2745-C108-4DC1-8681-44F5318E4BF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32AD2745-C108-4DC1-8681-44F5318E4BF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32AD2745-C108-4DC1-8681-44F5318E4BF0}.Testing|Any CPU.ActiveCfg = Testing|Any CPU + {32AD2745-C108-4DC1-8681-44F5318E4BF0}.Testing|Any CPU.Build.0 = Testing|Any CPU + {32AD2745-C108-4DC1-8681-44F5318E4BF0}.Staging|Any CPU.ActiveCfg = Staging|Any CPU + {32AD2745-C108-4DC1-8681-44F5318E4BF0}.Staging|Any CPU.Build.0 = Staging|Any CPU + {32AD2745-C108-4DC1-8681-44F5318E4BF0}.Production|Any CPU.ActiveCfg = Local|Any CPU + {32AD2745-C108-4DC1-8681-44F5318E4BF0}.Production|Any CPU.Build.0 = Local|Any CPU + {32AD2745-C108-4DC1-8681-44F5318E4BF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32AD2745-C108-4DC1-8681-44F5318E4BF0}.Release|Any CPU.Build.0 = Release|Any CPU + {AA8E3890-CD65-4B80-AD09-C3A1ED9D7523}.Local|Any CPU.ActiveCfg = Local|Any CPU + {AA8E3890-CD65-4B80-AD09-C3A1ED9D7523}.Local|Any CPU.Build.0 = Local|Any CPU + {AA8E3890-CD65-4B80-AD09-C3A1ED9D7523}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA8E3890-CD65-4B80-AD09-C3A1ED9D7523}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA8E3890-CD65-4B80-AD09-C3A1ED9D7523}.Testing|Any CPU.ActiveCfg = Testing|Any CPU + {AA8E3890-CD65-4B80-AD09-C3A1ED9D7523}.Testing|Any CPU.Build.0 = Testing|Any CPU + {AA8E3890-CD65-4B80-AD09-C3A1ED9D7523}.Staging|Any CPU.ActiveCfg = Staging|Any CPU + {AA8E3890-CD65-4B80-AD09-C3A1ED9D7523}.Staging|Any CPU.Build.0 = Staging|Any CPU + {AA8E3890-CD65-4B80-AD09-C3A1ED9D7523}.Production|Any CPU.ActiveCfg = Local|Any CPU + {AA8E3890-CD65-4B80-AD09-C3A1ED9D7523}.Production|Any CPU.Build.0 = Local|Any CPU + {AA8E3890-CD65-4B80-AD09-C3A1ED9D7523}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA8E3890-CD65-4B80-AD09-C3A1ED9D7523}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2D8AB580-E186-48E6-BED8-53D6A4941986} + EndGlobalSection +EndGlobal diff --git a/src/AzureAdB2C/Api/LICENSE.md b/src/AzureAdB2C/Api/LICENSE.md new file mode 100644 index 00000000..4f592f86 --- /dev/null +++ b/src/AzureAdB2C/Api/LICENSE.md @@ -0,0 +1,35 @@ +--- +date: 2023-07-13T05:44:46:00-05:00Z +description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... +keywords: +- IP +- copyright +- license +- mit +permissions: +- commercial-use +- modifications +- distribution +- private-use +conditions: +- include-copyright +limitations: +- liability +- warranty +lastmod: 2024-01-0T00:39:00.0000+05:00Z +license: MIT +slug: mit-license +title: MIT License +type: license +--- + +# MIT License + +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/src/AzureAdB2C/Api/icon.png b/src/AzureAdB2C/Api/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..db07a039193d57c5147e651a7ab732121bfd20ba GIT binary patch literal 26997 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 CAey28 literal 0 HcmV?d00001 diff --git a/src/AzureAdB2C/Services/ClaimsGenerator.cs b/src/AzureAdB2C/Services/ClaimsGenerator.cs new file mode 100644 index 00000000..bc9bcc91 --- /dev/null +++ b/src/AzureAdB2C/Services/ClaimsGenerator.cs @@ -0,0 +1,25 @@ +namespace Dgmjr.AzureAdB2C.Services; + +using System.Security.Claims; + +using Dgmjr.AzureAdB2C.Models; + +using Microsoft.Extensions.Options; + +public interface IClaimsGenerator +{ + Task GenerateClaimsAsync( + ApiRequest request, + CancellationToken cancellationToken = default + ); + Version Version { get; } +} + +public abstract class ClaimsGenerator(IOptions version) : IClaimsGenerator +{ + public virtual Version Version => version.Value; + public abstract Task GenerateClaimsAsync( + ApiRequest request, + CancellationToken cancellationToken = default + ); +} diff --git a/src/AzureAdB2C/Services/ClaimsValidator.cs b/src/AzureAdB2C/Services/ClaimsValidator.cs new file mode 100644 index 00000000..0668532c --- /dev/null +++ b/src/AzureAdB2C/Services/ClaimsValidator.cs @@ -0,0 +1,43 @@ +namespace Dgmjr.AzureAdB2C.Services; + +using System.ComponentModel.DataAnnotations; +using System.Security.Claims; + +using Dgmjr.AzureAdB2C.Models; + +using Microsoft.Extensions.Options; + +public interface IClaimsValidator +{ + Task ValidateAsync( + ApiRequest request, + CancellationToken cancellationToken = default + ); + Version Version { get; } +} + +public class ClaimsValidatorOptions +{ + public virtual Version Version { get; set; } = default!; + public virtual Func ValidateClaim { get; set; } = kvp => ValidationResult.Success; + + public virtual ValidationResult Validate(StrKvp kvp) => ValidateClaim(kvp); + + public virtual ValidationResult Validate(ApiRequest request) + { + var results = request.Select(Validate); + var failures = results.Where(result => !IsNullOrWhiteSpace(result.ErrorMessage)); + var messages = failures.Select(result => $"{Join(", ", result.MemberNames)}: {result.ErrorMessage}"); + var message = Join(env.NewLine, messages); + return (failures.Any() ? new ValidationResult(message, failures.SelectMany(result => result.MemberNames)) : ValidationResult.Success)!; + } +} + +public abstract class ClaimsValidator(IOptions options) : IClaimsValidator +{ + public Version Version => options.Value.Version; + public abstract Task ValidateAsync( + ApiRequest request, + CancellationToken cancellationToken = default + ); +} diff --git a/src/AzureAdB2C/Services/Dgmjr.AzureAdB2C.Services.csproj b/src/AzureAdB2C/Services/Dgmjr.AzureAdB2C.Services.csproj new file mode 100644 index 00000000..933fe4d2 --- /dev/null +++ b/src/AzureAdB2C/Services/Dgmjr.AzureAdB2C.Services.csproj @@ -0,0 +1,29 @@ + + + + + net6.0;net8.0 + + + + + + + + + + + + + + diff --git a/src/AzureAdB2C/Services/Dgmjr.AzureAdB2C.Services.sln b/src/AzureAdB2C/Services/Dgmjr.AzureAdB2C.Services.sln new file mode 100644 index 00000000..aca64e77 --- /dev/null +++ b/src/AzureAdB2C/Services/Dgmjr.AzureAdB2C.Services.sln @@ -0,0 +1,70 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B283EBC2-E01F-412D-9339-FD56EF114549}" + ProjectSection(SolutionItems) = preProject + ..\..\..\Directory.Build.props = ..\..\..\Directory.Build.props + ..\..\..\..\..\Directory.Build.targets = ..\..\..\..\..\Directory.Build.targets + ..\..\..\..\..\global.json = ..\..\..\..\..\global.json + ..\..\..\..\..\Packages\Versions.Local.props = ..\..\..\..\..\Packages\Versions.Local.props + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dgmjr.Web.DownstreamApis", "..\..\DownstreamApis\Dgmjr.Web.DownstreamApis.csproj", "{0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dgmjr.Graph", "..\..\MicrosoftGraph\Dgmjr.Graph.csproj", "{630E503A-45AE-4D8C-A35F-3E499810DF91}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dgmjr.AzureAdB2C.Services", "Dgmjr.AzureAdB2C.Services.csproj", "{3CBB5BC6-4C01-4708-894C-37DFCB8523D1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Local|Any CPU = Local|Any CPU + Debug|Any CPU = Debug|Any CPU + Testing|Any CPU = Testing|Any CPU + Staging|Any CPU = Staging|Any CPU + Production|Any CPU = Production|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Local|Any CPU.ActiveCfg = Local|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Local|Any CPU.Build.0 = Local|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Testing|Any CPU.ActiveCfg = Testing|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Testing|Any CPU.Build.0 = Testing|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Staging|Any CPU.ActiveCfg = Staging|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Staging|Any CPU.Build.0 = Staging|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Production|Any CPU.ActiveCfg = Local|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Production|Any CPU.Build.0 = Local|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Release|Any CPU.Build.0 = Release|Any CPU + {630E503A-45AE-4D8C-A35F-3E499810DF91}.Local|Any CPU.ActiveCfg = Local|Any CPU + {630E503A-45AE-4D8C-A35F-3E499810DF91}.Local|Any CPU.Build.0 = Local|Any CPU + {630E503A-45AE-4D8C-A35F-3E499810DF91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {630E503A-45AE-4D8C-A35F-3E499810DF91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {630E503A-45AE-4D8C-A35F-3E499810DF91}.Testing|Any CPU.ActiveCfg = Testing|Any CPU + {630E503A-45AE-4D8C-A35F-3E499810DF91}.Testing|Any CPU.Build.0 = Testing|Any CPU + {630E503A-45AE-4D8C-A35F-3E499810DF91}.Staging|Any CPU.ActiveCfg = Staging|Any CPU + {630E503A-45AE-4D8C-A35F-3E499810DF91}.Staging|Any CPU.Build.0 = Staging|Any CPU + {630E503A-45AE-4D8C-A35F-3E499810DF91}.Production|Any CPU.ActiveCfg = Local|Any CPU + {630E503A-45AE-4D8C-A35F-3E499810DF91}.Production|Any CPU.Build.0 = Local|Any CPU + {630E503A-45AE-4D8C-A35F-3E499810DF91}.Release|Any CPU.ActiveCfg = Release|Any CPU + {630E503A-45AE-4D8C-A35F-3E499810DF91}.Release|Any CPU.Build.0 = Release|Any CPU + {3CBB5BC6-4C01-4708-894C-37DFCB8523D1}.Local|Any CPU.ActiveCfg = Local|Any CPU + {3CBB5BC6-4C01-4708-894C-37DFCB8523D1}.Local|Any CPU.Build.0 = Local|Any CPU + {3CBB5BC6-4C01-4708-894C-37DFCB8523D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3CBB5BC6-4C01-4708-894C-37DFCB8523D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CBB5BC6-4C01-4708-894C-37DFCB8523D1}.Testing|Any CPU.ActiveCfg = Testing|Any CPU + {3CBB5BC6-4C01-4708-894C-37DFCB8523D1}.Testing|Any CPU.Build.0 = Testing|Any CPU + {3CBB5BC6-4C01-4708-894C-37DFCB8523D1}.Staging|Any CPU.ActiveCfg = Staging|Any CPU + {3CBB5BC6-4C01-4708-894C-37DFCB8523D1}.Staging|Any CPU.Build.0 = Staging|Any CPU + {3CBB5BC6-4C01-4708-894C-37DFCB8523D1}.Production|Any CPU.ActiveCfg = Local|Any CPU + {3CBB5BC6-4C01-4708-894C-37DFCB8523D1}.Production|Any CPU.Build.0 = Local|Any CPU + {3CBB5BC6-4C01-4708-894C-37DFCB8523D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3CBB5BC6-4C01-4708-894C-37DFCB8523D1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CCAFD427-35E6-4064-B3B6-7E7AFCF69FC1} + EndGlobalSection +EndGlobal diff --git a/src/AzureAdB2C/Services/Graph/AppRolesService.cs b/src/AzureAdB2C/Services/Graph/AppRolesService.cs new file mode 100644 index 00000000..ff3d907d --- /dev/null +++ b/src/AzureAdB2C/Services/Graph/AppRolesService.cs @@ -0,0 +1,167 @@ +namespace Dgmjr.AzureAdB2C.Services.Graph; + +using System; +using System.Security.Claims; + +using Dgmjr.AzureAdB2C.Models; +using Dgmjr.Graph.Constants; +using Dgmjr.Graph.Options; +using Dgmjr.Graph.Services; +using Dgmjr.Graph.TokenProviders; +using Dgmjr.AspNetCore.Authentication.Basic; + +using Microsoft.Extensions.Options; +using Microsoft.Graph; +using System.Net.Http.Headers; +using Microsoft.Identity.Client; + +public interface IAppRolesService +{ + Version Version { get; } + + Task GenerateClaimsAsync( + ApiRequest request, + CancellationToken cancellationToken = default + ); +} + +public class AppRolesService( + IOptionsMonitor msidOptions, + IOptions version, + IOptions graphClientOptions, + GraphServiceClient graph, + ILogger logger +) : IClaimsGenerator, IAppRolesService, ILog +{ + public ILogger Logger => logger; + private readonly Dgmjr.AzureAd.Web.MicrosoftIdentityOptions _msidOptions = + msidOptions.CurrentValue; + public Version Version => version.Value; + + private string clientId => _msidOptions.ClientId; + private string clientSecret => _msidOptions.ClientSecret; + private string tenantId => _msidOptions.TenantId; + + private GraphServiceClient _graph; + private GraphServiceClient Graph => _graph ??= GetGraphServiceClient().Result; + + private readonly AzureAdB2CGraphOptions _graphClientOptions = graphClientOptions.Value; + + public async Task GenerateClaimsAsync( + ApiRequest request, + CancellationToken cancellationToken = default + ) + { + Logger.LogDebug("request: {request}", request); + + var app = await Graph.Applications[ + _graphClientOptions.AzureAdB2CExtensionsApplicationId.ToString() + ] + .Request() + .GetAsync(cancellationToken); + Logger.LogDebug("app: {app}", app); + + var user = await Graph.Users[request.ObjectId.ToString()] + // .Request(new Option[] { new QueryOption("$expand", "appRoleAssignments") }) + .Request() + .Expand("appRoleAssignments") + .GetAsync(cancellationToken); + Logger.LogDebug("user: {user}", user); + + var appRoles = app.AppRoles; + var myAppRoles = user.AppRoleAssignments + .Where(x => x.ResourceId == _graphClientOptions.AzureAdB2CExtensionsApplicationId) + .ToList(); + var myAppsAppRoles = myAppRoles.Select( + appRole => appRoles.FirstOrDefault(x => x.Id.ToString() == appRole.Id) + ); + return new ApiContinueResponse + { + Claims = new StringDictionary() { { ClaimTypes.Role, Serialize(myAppsAppRoles) } }, + Version = Version + }; + } + + + private async Task GetGraphServiceClient() + { + var app = ConfidentialClientApplicationBuilder + .Create(clientId) + .WithClientSecret(clientSecret) + .WithAuthority($"https://login.microsoftonline.com/{tenantId}") + .Build(); + + var authResult = await app.AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" }) + .ExecuteAsync(); + + var graphClient = new GraphServiceClient( + new DelegateAuthenticationProvider((requestMessage) => + { + requestMessage.Headers.Authorization = + new AuthenticationHeaderValue("Bearer", authResult.AccessToken); + + return Task.CompletedTask; + })); + return graphClient; + } + + + // public static async Task> GetApplicationRolesForUserAsync(string userId) + // { + // var applicationRoles = new List(); + + // var app = ConfidentialClientApplicationBuilder + // .Create(clientId) + // .WithClientSecret(clientSecret) + // .WithAuthority($"https://login.microsoftonline.com/{tenantId}") + // .Build(); + + // var authResult = await app.AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" }) + // .ExecuteAsync(); + + // var graphClient = new GraphServiceClient( + // new DelegateAuthenticationProvider((requestMessage) => + // { + // requestMessage.Headers.Authorization = + // new AuthenticationHeaderValue("Bearer", authResult.AccessToken); + + // return Task.CompletedTask; + // })); + + // try + // { + // // Query Microsoft Graph to get the user's assigned directory roles + // var userDirectoryRoles = await graphClient.Users[userId].MemberOf.Request().GetAsync(); + + // foreach (var directoryRole in userDirectoryRoles.OfType()) + // { + // // Query each directory role to get its assigned application roles + // var applicationRoleAssignments = await graphClient.DirectoryRoles[directoryRole.Id].AppRoleAssignedTo.Request().GetAsync(); + + // foreach (var applicationRoleAssignment in applicationRoleAssignments) + // { + // applicationRoles.Add(applicationRoleAssignment.AppRoleId); + // } + // } + // } + // catch (ServiceException ex) + // { + // Console.WriteLine($"Error retrieving application roles: {ex.Message}"); + // } + + // return applicationRoles; + // } + + // public static async Task Main(string[] args) + // { + // string userId = "user_id_goes_here"; + // List applicationRoles = await GetApplicationRolesForUserAsync(userId); + + // // Print the application roles for the user + // foreach (var role in applicationRoles) + // { + // Console.WriteLine(role); + // } + // } + // } +} diff --git a/src/AzureAdB2C/Services/LICENSE.md b/src/AzureAdB2C/Services/LICENSE.md new file mode 100644 index 00000000..4f592f86 --- /dev/null +++ b/src/AzureAdB2C/Services/LICENSE.md @@ -0,0 +1,35 @@ +--- +date: 2023-07-13T05:44:46:00-05:00Z +description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... +keywords: +- IP +- copyright +- license +- mit +permissions: +- commercial-use +- modifications +- distribution +- private-use +conditions: +- include-copyright +limitations: +- liability +- warranty +lastmod: 2024-01-0T00:39:00.0000+05:00Z +license: MIT +slug: mit-license +title: MIT License +type: license +--- + +# MIT License + +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/src/AzureAdB2C/Services/Models/ApiRequest.cs b/src/AzureAdB2C/Services/Models/ApiRequest.cs new file mode 100644 index 00000000..dc0f6278 --- /dev/null +++ b/src/AzureAdB2C/Services/Models/ApiRequest.cs @@ -0,0 +1,22 @@ +namespace Dgmjr.AzureAdB2C.Models; + +using System.Globalization; + +public class ApiRequest : StringDictionary +{ + public string? Email { get; set; } + public ICollection? Identities { get; set; } = new List(); + public guid ClientId { get; set; } + public guid ObjectId { get; set; } + public string Step { get; set; } = default!; + + [JsonConverter(typeof(JsonLocaleConverter))] + public CultureInfo? UiLocales { get; set; } +} + +public class UserIdentity +{ + public string SignInType { get; set; } = default!; + public string Issuer { get; set; } = default!; + public string IssuerAssignedId { get; set; } = default!; +} diff --git a/src/AzureAdB2C/Services/Models/ApiResponse.cs b/src/AzureAdB2C/Services/Models/ApiResponse.cs new file mode 100644 index 00000000..b74971ca --- /dev/null +++ b/src/AzureAdB2C/Services/Models/ApiResponse.cs @@ -0,0 +1,58 @@ +using Dgmjr.AzureAdB2C.Json; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; + +namespace Dgmjr.AzureAdB2C.Models; + +public abstract class ApiResponse : ActionResult, IStatusCodeActionResult +{ + [JIgnore] + public virtual bool IsSuccess => Action == ApiResponseAction.Continue; + [JProp("version")] + public virtual Version Version { get; set; } = default!; + [JProp("action")] + public virtual ApiResponseAction Action => ApiResponseAction.Continue; + [JProp("status")] + public virtual int? StatusCode => StatusCodes.Status200OK; +} + +[JsonConverter(typeof(JsonApiContinueResponseConverter))] +public class ApiContinueResponse : ApiResponse +{ + public override ApiResponseAction Action => ApiResponseAction.Continue; + + public virtual IStringDictionary Claims { get; set; } = new StringDictionary(); +} + +public abstract class ApiErrorResponse(string userMessage, string? developerMessage = default) + : ApiResponse +{ + public virtual string? UserMessage => userMessage; + public virtual string? DeveloperMessage => developerMessage ?? userMessage; +} + +public class ApiBlockResponse(string userMessage, string? developerMessage = default) + : ApiErrorResponse(userMessage, developerMessage) +{ + public override ApiResponseAction Action => ApiResponseAction.ShowBlockPage; +} + +public class ApiValidationErrorResponse( + string userMessage, + string? developerMessage = default +) : ApiErrorResponse(userMessage, developerMessage) +{ + public override int? StatusCode => StatusCodes.Status400BadRequest; + public override ApiResponseAction Action => ApiResponseAction.ValidationError; +} + +[JsonConverter(typeof(JStringEnumConverter))] +public enum ApiResponseAction +{ + Continue, + ShowBlockPage, + Redirect, + ValidationError +} diff --git a/src/AzureAdB2C/Services/Models/JsonApiContinueResponseConverter.cs b/src/AzureAdB2C/Services/Models/JsonApiContinueResponseConverter.cs new file mode 100644 index 00000000..de948cac --- /dev/null +++ b/src/AzureAdB2C/Services/Models/JsonApiContinueResponseConverter.cs @@ -0,0 +1,27 @@ +namespace Dgmjr.AzureAdB2C.Json; +using System; +using System.Text.Json; + +using Dgmjr.AzureAdB2C.Models; + +using Microsoft.AspNetCore.Http; +public class JsonApiContinueResponseConverter : JsonConverter +{ + public override ApiContinueResponse? Read(ref Utf8JsonReader reader, type typeToConvert, Jso options) + { + throw new NotImplementedException("This converter is only for writing"); + } + + public override void Write(Utf8JsonWriter writer, ApiContinueResponse value, Jso options) + { + writer.WriteStartObject(); + writer.WriteString("version", value.Version.ToString()); + writer.WriteString("action", value.Action.ToString()); + writer.WriteNumber("status", value.StatusCode ?? StatusCodes.Status200OK); + foreach(var (claimType, claimValue) in value.Claims) + { + writer.WriteString(claimType, claimValue); + } + writer.WriteEndObject(); + } +} diff --git a/src/AzureAdB2C/Services/UserHydrator.cs b/src/AzureAdB2C/Services/UserHydrator.cs new file mode 100644 index 00000000..412cda35 --- /dev/null +++ b/src/AzureAdB2C/Services/UserHydrator.cs @@ -0,0 +1,23 @@ +namespace Dgmjr.AzureAdB2C.Services; + +using System.Security.Claims; + +using Microsoft.AspNetCore.Http; + +public interface IUserHydrator +{ + Task HydrateAsync(HttpContext context, ClaimsPrincipal principal, CancellationToken cancellationToken = default); +} + +public class UserHydrator : IUserHydrator +{ + public virtual Task HydrateAsync( + HttpContext context, + ClaimsPrincipal principal, + CancellationToken cancellationToken = default + ) + { + + return Task.FromResult(principal); + } +} diff --git a/src/AzureAdB2C/Services/icon.png b/src/AzureAdB2C/Services/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..db07a039193d57c5147e651a7ab732121bfd20ba GIT binary patch literal 26997 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 CAey28 literal 0 HcmV?d00001 diff --git a/src/AzureAdB2C/api-connector-samples b/src/AzureAdB2C/api-connector-samples new file mode 160000 index 00000000..5e56afe5 --- /dev/null +++ b/src/AzureAdB2C/api-connector-samples @@ -0,0 +1 @@ +Subproject commit 5e56afe56885a37264271ba670521abcaf5fda83 diff --git a/src/Configuration/Abstractions/ConfigurationOrder.cs b/src/Configuration/Abstractions/ConfigurationOrder.cs index 8e16b058..b3670e3c 100644 --- a/src/Configuration/Abstractions/ConfigurationOrder.cs +++ b/src/Configuration/Abstractions/ConfigurationOrder.cs @@ -2,6 +2,7 @@ namespace Dgmjr.Configuration.Extensions; public enum ConfigurationOrder { + First = -1000, VeryEarly = 0, Early = 100, Middle = 200, diff --git a/src/Configuration/Configuration/AutoConfiguratorConfiguration.cs b/src/Configuration/Configuration/AutoConfiguratorConfiguration.cs index ec5e653b..497548b0 100644 --- a/src/Configuration/Configuration/AutoConfiguratorConfiguration.cs +++ b/src/Configuration/Configuration/AutoConfiguratorConfiguration.cs @@ -13,15 +13,30 @@ public class AutoConfiguratorConfigurator(IConfiguration configuration) public void Configure(AutoConfiguratorConfiguration options) { var section = configuration.GetSection(AutoConfiguratorConfiguration.SectionName); - Console.WriteLine(section.ToJson()); + Console.WriteLine(section?.ToJson()); if (section.Exists()) { - foreach (var configurator in section.GetChildren()) + foreach ( + var configurator in section + .GetChildren() + .Select( + configurator => + configurator.Value.Split( + ",", + StringSplitOptions.RemoveEmptyEntries + | StringSplitOptions.TrimEntries + ) + ) + ) { - var type = Type.GetType(configurator.Value); - if (type != null) + var assembly = Assembly.Load(configurator[1]); + if (assembly != null) { - options.Add(type); + var type = assembly.GetType(configurator[0]); + if (type != null) + { + options.Add(type); + } } } } diff --git a/src/Configuration/DependencyInjection/AutoConfigureIApplicationBuilderExtensions.cs b/src/Configuration/DependencyInjection/AutoConfigureIApplicationBuilderExtensions.cs index 41b28533..cba2bd3c 100644 --- a/src/Configuration/DependencyInjection/AutoConfigureIApplicationBuilderExtensions.cs +++ b/src/Configuration/DependencyInjection/AutoConfigureIApplicationBuilderExtensions.cs @@ -13,7 +13,7 @@ public static IApplicationBuilder AutoConfigure(this IApplicationBuilder builder .ToList(); Console.WriteLine( - $"Configuring IApplicationBuilder with the following configurators: {Join(", ", configurators.Select(configurator => configurator.GetType().Name))}." + $"Configuring IApplicationBuilder with the following configurators: {Join(env.NewLine, configurators.Select(configurator => $"{configurator.Order}: {configurator.GetType().Name}"))}." ); foreach (var configurator in configurators) diff --git a/src/Configuration/DependencyInjection/AutoConfigureIApplicationHostBuilderExtensions.cs b/src/Configuration/DependencyInjection/AutoConfigureIApplicationHostBuilderExtensions.cs index 3d5fd17f..00bd312b 100644 --- a/src/Configuration/DependencyInjection/AutoConfigureIApplicationHostBuilderExtensions.cs +++ b/src/Configuration/DependencyInjection/AutoConfigureIApplicationHostBuilderExtensions.cs @@ -12,7 +12,9 @@ namespace Microsoft.Extensions.DependencyInjection; public static class AutoConfigureIApplicationHostBuilderExtensions { public static WebApplicationBuilder AutoConfigure(this WebApplicationBuilder builder) + where TProgram : class { + builder.Configuration.AddUserSecrets(); builder.Configuration.AddKeyPerJsonFile( Path.Join(Path.GetDirectoryName(typeof(TProgram).Assembly.Location), "./Configuration/") ); @@ -83,7 +85,7 @@ public static WebApplicationBuilder AutoConfigure(this WebApplicationB .ToList(); Console.WriteLine( - $"Configuring IHostApplicationBuilder with the following configurators: {Join(", ", configurators.Select(configurator => configurator.GetType().Name))}." + $"Configuring IHostApplicationBuilder with the following configurators: {Join(env.NewLine, configurators.Select(configurator => $"{configurator.Order}: {configurator.GetType().Name}"))}." ); foreach (var configurator in configurators) diff --git a/src/Configuration/LICENSE.md b/src/Configuration/LICENSE.md index c80a6760..4f592f86 100755 --- a/src/Configuration/LICENSE.md +++ b/src/Configuration/LICENSE.md @@ -1,21 +1,21 @@ --- -date: 2023-07-13T05:44:46:00+05:00Z +date: 2023-07-13T05:44:46:00-05:00Z description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... keywords: - - IP - - copyright - - license - - mit +- IP +- copyright +- license +- mit permissions: - - commercial-use - - modifications - - distribution - - private-use +- commercial-use +- modifications +- distribution +- private-use conditions: - - include-copyright +- include-copyright limitations: - - liability - - warranty +- liability +- warranty lastmod: 2024-01-0T00:39:00.0000+05:00Z license: MIT slug: mit-license @@ -25,7 +25,7 @@ type: license # MIT License -## Copyright © 2022-2023 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore an email") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/DownstreamApis/LICENSE.md b/src/DownstreamApis/LICENSE.md index 4f608744..4f592f86 100755 --- a/src/DownstreamApis/LICENSE.md +++ b/src/DownstreamApis/LICENSE.md @@ -1,22 +1,22 @@ --- -date: 2023-07-13T05:44:46:00.048Z +date: 2023-07-13T05:44:46:00-05:00Z description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... keywords: - - IP - - copyright - - license - - mit +- IP +- copyright +- license +- mit permissions: - - commercial-use - - modifications - - distribution - - private-use +- commercial-use +- modifications +- distribution +- private-use conditions: - - include-copyright +- include-copyright limitations: - - liability - - warranty -lastmod: 2023-08-29T17:13:51:00.216Z +- liability +- warranty +lastmod: 2024-01-0T00:39:00.0000+05:00Z license: MIT slug: mit-license title: MIT License @@ -25,7 +25,7 @@ type: license # MIT License -## Copyright © 2022-2023 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send David an email") ([@dgmjr](https://github.com/dgmjr "Contact david on GitHub")), All Rights Reserved +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/Http/Extensions/LICENSE.md b/src/Http/Extensions/LICENSE.md index 4f608744..4f592f86 100755 --- a/src/Http/Extensions/LICENSE.md +++ b/src/Http/Extensions/LICENSE.md @@ -1,22 +1,22 @@ --- -date: 2023-07-13T05:44:46:00.048Z +date: 2023-07-13T05:44:46:00-05:00Z description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... keywords: - - IP - - copyright - - license - - mit +- IP +- copyright +- license +- mit permissions: - - commercial-use - - modifications - - distribution - - private-use +- commercial-use +- modifications +- distribution +- private-use conditions: - - include-copyright +- include-copyright limitations: - - liability - - warranty -lastmod: 2023-08-29T17:13:51:00.216Z +- liability +- warranty +lastmod: 2024-01-0T00:39:00.0000+05:00Z license: MIT slug: mit-license title: MIT License @@ -25,7 +25,7 @@ type: license # MIT License -## Copyright © 2022-2023 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send David an email") ([@dgmjr](https://github.com/dgmjr "Contact david on GitHub")), All Rights Reserved +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/Http/Headers/LICENSE.md b/src/Http/Headers/LICENSE.md index 4f608744..4f592f86 100755 --- a/src/Http/Headers/LICENSE.md +++ b/src/Http/Headers/LICENSE.md @@ -1,22 +1,22 @@ --- -date: 2023-07-13T05:44:46:00.048Z +date: 2023-07-13T05:44:46:00-05:00Z description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... keywords: - - IP - - copyright - - license - - mit +- IP +- copyright +- license +- mit permissions: - - commercial-use - - modifications - - distribution - - private-use +- commercial-use +- modifications +- distribution +- private-use conditions: - - include-copyright +- include-copyright limitations: - - liability - - warranty -lastmod: 2023-08-29T17:13:51:00.216Z +- liability +- warranty +lastmod: 2024-01-0T00:39:00.0000+05:00Z license: MIT slug: mit-license title: MIT License @@ -25,7 +25,7 @@ type: license # MIT License -## Copyright © 2022-2023 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send David an email") ([@dgmjr](https://github.com/dgmjr "Contact david on GitHub")), All Rights Reserved +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/Http/Http/LICENSE.md b/src/Http/Http/LICENSE.md index 4f608744..4f592f86 100755 --- a/src/Http/Http/LICENSE.md +++ b/src/Http/Http/LICENSE.md @@ -1,22 +1,22 @@ --- -date: 2023-07-13T05:44:46:00.048Z +date: 2023-07-13T05:44:46:00-05:00Z description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... keywords: - - IP - - copyright - - license - - mit +- IP +- copyright +- license +- mit permissions: - - commercial-use - - modifications - - distribution - - private-use +- commercial-use +- modifications +- distribution +- private-use conditions: - - include-copyright +- include-copyright limitations: - - liability - - warranty -lastmod: 2023-08-29T17:13:51:00.216Z +- liability +- warranty +lastmod: 2024-01-0T00:39:00.0000+05:00Z license: MIT slug: mit-license title: MIT License @@ -25,7 +25,7 @@ type: license # MIT License -## Copyright © 2022-2023 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send David an email") ([@dgmjr](https://github.com/dgmjr "Contact david on GitHub")), All Rights Reserved +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/Http/Mime/LICENSE.md b/src/Http/Mime/LICENSE.md index 4f608744..4f592f86 100755 --- a/src/Http/Mime/LICENSE.md +++ b/src/Http/Mime/LICENSE.md @@ -1,22 +1,22 @@ --- -date: 2023-07-13T05:44:46:00.048Z +date: 2023-07-13T05:44:46:00-05:00Z description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... keywords: - - IP - - copyright - - license - - mit +- IP +- copyright +- license +- mit permissions: - - commercial-use - - modifications - - distribution - - private-use +- commercial-use +- modifications +- distribution +- private-use conditions: - - include-copyright +- include-copyright limitations: - - liability - - warranty -lastmod: 2023-08-29T17:13:51:00.216Z +- liability +- warranty +lastmod: 2024-01-0T00:39:00.0000+05:00Z license: MIT slug: mit-license title: MIT License @@ -25,7 +25,7 @@ type: license # MIT License -## Copyright © 2022-2023 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send David an email") ([@dgmjr](https://github.com/dgmjr "Contact david on GitHub")), All Rights Reserved +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/Http/ResponseCodes/LICENSE.md b/src/Http/ResponseCodes/LICENSE.md index c80a6760..4f592f86 100755 --- a/src/Http/ResponseCodes/LICENSE.md +++ b/src/Http/ResponseCodes/LICENSE.md @@ -1,21 +1,21 @@ --- -date: 2023-07-13T05:44:46:00+05:00Z +date: 2023-07-13T05:44:46:00-05:00Z description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... keywords: - - IP - - copyright - - license - - mit +- IP +- copyright +- license +- mit permissions: - - commercial-use - - modifications - - distribution - - private-use +- commercial-use +- modifications +- distribution +- private-use conditions: - - include-copyright +- include-copyright limitations: - - liability - - warranty +- liability +- warranty lastmod: 2024-01-0T00:39:00.0000+05:00Z license: MIT slug: mit-license @@ -25,7 +25,7 @@ type: license # MIT License -## Copyright © 2022-2023 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore an email") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/Http/Services/.vscode/settings.json b/src/Http/Services/.vscode/settings.json new file mode 100644 index 00000000..40ac7cbc --- /dev/null +++ b/src/Http/Services/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.exclude": { + "**/.vs": false + } +} diff --git a/src/Http/Services/HttpServicesExtensions.AddHttpServices.cs b/src/Http/Services/HttpServicesExtensions.AddHttpServices.cs index dbe1b6cd..d85e636d 100644 --- a/src/Http/Services/HttpServicesExtensions.AddHttpServices.cs +++ b/src/Http/Services/HttpServicesExtensions.AddHttpServices.cs @@ -9,6 +9,8 @@ using Dgmjr.AspNetCore.Http; using Dgmjr.AspNetCore.Http.Services; using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Http; public static partial class HttpServicesExtensions { @@ -27,7 +29,8 @@ public static partial class HttpServicesExtensions public static WebApplicationBuilder AddHttpServices( this WebApplicationBuilder builder, - string configurationSectionKey = Http + string configurationSectionKey = Http, + ILogger? logger = null ) { var options = builder.Configuration @@ -42,6 +45,7 @@ public static WebApplicationBuilder AddHttpServices( { if (options.UseRequestDecompression) { + logger?.SettingUpHttpService(RequestDecompression, options.RequestDecompression); builder.Services.AddRequestDecompression( options => builder.Configuration.Bind( @@ -53,6 +57,7 @@ public static WebApplicationBuilder AddHttpServices( if (options.UseResponseCompression) { + logger?.SettingUpHttpService(ResponseCompression, options.ResponseCompression); builder.Services.AddResponseCompression(options => { options.EnableForHttps = true; @@ -67,6 +72,7 @@ public static WebApplicationBuilder AddHttpServices( if (options.UseResponseCaching) { + logger?.SettingUpHttpService(ResponseCaching, options.ResponseCaching); builder.Services.AddResponseCaching( options => builder.Configuration.Bind( @@ -78,6 +84,7 @@ public static WebApplicationBuilder AddHttpServices( if (options.UseOutputCaching) { + logger?.SettingUpHttpService(OutputCache, options.OutputCache); builder.Services.AddOutputCache( options => builder.Configuration.Bind( @@ -89,6 +96,7 @@ public static WebApplicationBuilder AddHttpServices( if (options.UseSession) { + logger?.SettingUpHttpService(Session, options.Session); builder.Services.AddSession( options => builder.Configuration.Bind($"{configurationSectionKey}:{Session}", options) @@ -97,17 +105,23 @@ public static WebApplicationBuilder AddHttpServices( if (options.UseFileServer && options.FileServer.EnableDirectoryBrowsing) { + logger?.SettingUpHttpService( + $"{nameof(options.FileServer)}:{nameof(options.FileServer.EnableDirectoryBrowsing)}", + options.FileServer + ); builder.Services.AddDirectoryBrowser(); } if (options.UseCookiePolicy) { + logger?.SettingUpHttpService(Cookies, options.CookiePolicy); builder.Services.AddCookiePolicy( policy => builder.Configuration.Bind($"{configurationSectionKey}:{Cookies}", policy) ); } + logger?.SettingUpHttpService(Cors, options.Cors); builder.Services.AddCors(); if (options.UseCors) @@ -122,6 +136,7 @@ public static WebApplicationBuilder AddHttpServices( if (options.UseHsts) { + logger?.SettingUpHttpService(Hsts, options.Hsts); builder.Services.AddHsts( options => builder.Configuration.Bind($"{configurationSectionKey}:{Hsts}", options) @@ -130,6 +145,7 @@ public static WebApplicationBuilder AddHttpServices( if (options.IIS != null) { + logger?.SettingUpHttpService(IIS, options.IIS); builder.Services.Configure( options => builder.Configuration.Bind($"{configurationSectionKey}:{IIS}", options) @@ -138,6 +154,7 @@ public static WebApplicationBuilder AddHttpServices( if (options.Kestrel != null) { + logger?.SettingUpHttpService(Kestrel, options.Kestrel); builder.Services.Configure( options => builder.Configuration.Bind($"{configurationSectionKey}:{Kestrel}", options) @@ -146,6 +163,7 @@ public static WebApplicationBuilder AddHttpServices( if (options.ExceptionHandling != null) { + logger?.SettingUpHttpService(ExceptionHandling, options.ExceptionHandling); builder.Services.Configure( options => builder.Configuration.Bind( @@ -157,6 +175,7 @@ public static WebApplicationBuilder AddHttpServices( if (options.AddHttpContextAccessor) { + logger?.SettingUpHttpService(nameof(IHttpContextAccessor), options.AddHttpContextAccessor); builder.Services.AddHttpContextAccessor(); builder.Services.AddSingleton(); } diff --git a/src/Http/Services/HttpServicesExtensions.UseHttpServices.cs b/src/Http/Services/HttpServicesExtensions.UseHttpServices.cs index f913215a..831015de 100644 --- a/src/Http/Services/HttpServicesExtensions.UseHttpServices.cs +++ b/src/Http/Services/HttpServicesExtensions.UseHttpServices.cs @@ -5,25 +5,30 @@ namespace Microsoft.Extensions.DependencyInjection; using System.Runtime.Serialization; using AspNetCorsOptions = Microsoft.AspNetCore.Cors.Infrastructure.CorsOptions; using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.Extensions.Logging; +using Dgmjr.AspNetCore.Http.Services; public static partial class HttpServicesExtensions { - public static IApplicationBuilder UseHttpServices(this IApplicationBuilder app) + public static IApplicationBuilder UseHttpServices(this IApplicationBuilder app, ILogger logger = null) { var options = app.ApplicationServices.GetRequiredService>().Value; if(options.UseRequestDecompression) { + logger?.AddingHttpServiceToThePipeline(nameof(RequestDecompression)); app.UseRequestDecompression(); } if (options.UseResponseCompression) { + logger?.AddingHttpServiceToThePipeline(nameof(ResponseCompression)); app.UseResponseCompression(); } if(options.UseFileServer) { + logger?.AddingHttpServiceToThePipeline(nameof(options.FileServer)); app.UseFileServer(options.FileServer); if(options.FileServer.EnableDefaultFiles) @@ -44,16 +49,19 @@ public static IApplicationBuilder UseHttpServices(this IApplicationBuilder app) if(options.UseResponseCaching) { + logger?.AddingHttpServiceToThePipeline(nameof(ResponseCaching)); app.UseResponseCaching(); } if(options.UseForwardedHeaders) { + logger?.AddingHttpServiceToThePipeline(nameof(options.ForwardedHeaders)); app.UseForwardedHeaders(options.ForwardedHeaders); } if(options.UseCors) { + logger?.AddingHttpServiceToThePipeline(nameof(Cors)); var corsOptions = app.ApplicationServices.GetRequiredService>().Value; app.UseCors(builder => { @@ -88,36 +96,43 @@ public static IApplicationBuilder UseHttpServices(this IApplicationBuilder app) if(options.UseCookiePolicy) { + logger?.AddingHttpServiceToThePipeline(Cookies); app.UseCookiePolicy(options.CookiePolicy); } if(options.UseSession) { + logger?.AddingHttpServiceToThePipeline(Session); app.UseSession(options.Session); } if(options.UseHsts) { + logger?.AddingHttpServiceToThePipeline(Hsts); app.UseHsts(); } if(options.UseHttpsRedirection) { + logger?.AddingHttpServiceToThePipeline(nameof(options.UseHttpsRedirection)); app.UseHttpsRedirection(); } if(options.ExceptionHandling?.UseDeveloperExceptionPage == true) { + logger?.AddingHttpServiceToThePipeline($"{nameof(options.ExceptionHandling)}:{nameof(options.ExceptionHandling.UseDeveloperExceptionPage)}"); app.UseDeveloperExceptionPage(); } if(options.UseExceptionHandler) { + logger?.AddingHttpServiceToThePipeline(nameof(options.ExceptionHandling)); app.UseExceptionHandler(options.ExceptionHandling); } if(options.UseWelcomePage) { + logger?.AddingHttpServiceToThePipeline(nameof(options.WelcomePage)); app.UseWelcomePage(options.WelcomePage); } diff --git a/src/Http/Services/HttpServicesOptionsAutoConfigurator.cs b/src/Http/Services/HttpServicesOptionsAutoConfigurator.cs index 1cbe81c4..1a185db9 100644 --- a/src/Http/Services/HttpServicesOptionsAutoConfigurator.cs +++ b/src/Http/Services/HttpServicesOptionsAutoConfigurator.cs @@ -1,22 +1,28 @@ namespace Microsoft.Extensions.DependencyInjection; +using Dgmjr.AspNetCore.Http.Services; using Dgmjr.Configuration.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; -public class HttpServicesOptionsAutoConfigurator +public class HttpServicesOptionsAutoConfigurator(ILogger? logger = null) : IConfigureIHostApplicationBuilder, - IConfigureIApplicationBuilder + IConfigureIApplicationBuilder, ILog { - public ConfigurationOrder Order => ConfigurationOrder.AnyTime; + public ILogger? Logger => logger; + + public ConfigurationOrder Order => ConfigurationOrder.VeryEarly; public void Configure(WebApplicationBuilder builder) { - builder.AddHttpServices(); + Logger?.HttpServicesOptionsAutoConfiguratorConfigureWebApplicationBuilder(); + builder.AddHttpServices(logger: Logger); } - public void Configure(IApplicationBuilder builder) + public void Configure(IApplicationBuilder app) { - builder.UseHttpServices(); + Logger?.HttpServicesOptionsAutoConfiguratorConfigureIApplicationBuilder(); + app.UseHttpServices(logger: Logger); } } diff --git a/src/Http/Services/LICENSE.md b/src/Http/Services/LICENSE.md index c80a6760..4f592f86 100755 --- a/src/Http/Services/LICENSE.md +++ b/src/Http/Services/LICENSE.md @@ -1,21 +1,21 @@ --- -date: 2023-07-13T05:44:46:00+05:00Z +date: 2023-07-13T05:44:46:00-05:00Z description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... keywords: - - IP - - copyright - - license - - mit +- IP +- copyright +- license +- mit permissions: - - commercial-use - - modifications - - distribution - - private-use +- commercial-use +- modifications +- distribution +- private-use conditions: - - include-copyright +- include-copyright limitations: - - liability - - warranty +- liability +- warranty lastmod: 2024-01-0T00:39:00.0000+05:00Z license: MIT slug: mit-license @@ -25,7 +25,7 @@ type: license # MIT License -## Copyright © 2022-2023 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore an email") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/Http/Services/LoggerExtensions.cs b/src/Http/Services/LoggerExtensions.cs new file mode 100644 index 00000000..a86d053b --- /dev/null +++ b/src/Http/Services/LoggerExtensions.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.Logging; + +namespace Dgmjr.AspNetCore.Http.Services; + +public static partial class LoggerExtensions +{ + [LoggerMessage( + EventId = 1, + Level = LogLevel.Information, + Message = "HttpServicesOptionsAutoConfigurator.Configure(WebApplicationBuilder)")] + public static partial void HttpServicesOptionsAutoConfiguratorConfigureWebApplicationBuilder(this ILogger logger); + + [LoggerMessage( + EventId = 2, + Level = LogLevel.Information, + Message = "HttpServicesOptionsAutoConfigurator.Configure(IApplicationBuilder)")] + public static partial void HttpServicesOptionsAutoConfiguratorConfigureIApplicationBuilder(this ILogger logger); + + // [LoggerMessage( + // EventId = 3, + // Level = LogLevel.Information, + // Message = "MvcAutoConfigurator.Configure(WebApplicationBuilder)")] + // public static partial void MvcAutoConfiguratorConfigureWebApplicationBuilder(this ILogger logger); + + // [LoggerMessage( + // EventId = 4, + // Level = LogLevel.Information, + // Message = "MvcAutoConfigurator.Configure(IApplicationBuilder)")] + // public static partial void MvcAutoConfiguratorConfigureIApplicationBuilder(this ILogger logger); + + [LoggerMessage( + EventId = 5, + Level = LogLevel.Information, + Message = "Setting up HTTP service: {ServiceName}")] + public static partial void SettingUpHttpService(this ILogger logger, string serviceName); + + [LoggerMessage( + EventId = 6, + Level = LogLevel.Information, + Message = "Setting up HTTP service: {ServiceName} with options: {Options}")] + public static partial void SettingUpHttpService(this ILogger logger, string serviceName, object options); + + [LoggerMessage( + EventId = 7, + Level = LogLevel.Information, + Message = "Adding HTTP service to the pipeline: {ServiceName}")] + public static partial void AddingHttpServiceToThePipeline(this ILogger logger, string serviceName); +} diff --git a/src/MicrosoftGraph/.vscode/settings.json b/src/MicrosoftGraph/.vscode/settings.json new file mode 100644 index 00000000..40ac7cbc --- /dev/null +++ b/src/MicrosoftGraph/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.exclude": { + "**/.vs": false + } +} diff --git a/src/MicrosoftGraph/Configuration/UserAppRolesConfigurator.cs b/src/MicrosoftGraph/Configuration/UserAppRolesConfigurator.cs new file mode 100644 index 00000000..f482f0b9 --- /dev/null +++ b/src/MicrosoftGraph/Configuration/UserAppRolesConfigurator.cs @@ -0,0 +1,95 @@ +namespace Dgmjr.Graph.Configuration; + +using System; +using System.Security.Claims; +using System.Security.Extensions; + +using Dgmjr.Graph.TokenProviders; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Kiota.Abstractions.Authentication; + +public class UserAppRolesConfigurator(ILogger logger, IHttpContextAccessor httpContextAccessor) : IConfigureOptions, ILog +{ + public ILogger Logger => logger; + private HttpContext HttpContext => httpContextAccessor.HttpContext!; + private IServiceProvider Services => HttpContext.RequestServices; + + public void Configure(MicrosoftIdentityOptions options) + { + options.Events.OnTokenValidated += OnTokenValidated; + options.Events.OnAuthenticationFailed += OnAuthenticationFailed; + options.Events.OnAuthorizationCodeReceived += OnAuthorizationCodeReceived; + options.Events.OnMessageReceived += OnMessageReceived; + options.Events.OnRedirectToIdentityProvider += OnRedirectToIdentityProvider; + options.Events.OnRemoteFailure += OnRemoteFailure; + } + + private async Task OnRemoteFailure(RemoteFailureContext context) + { + Logger.RemoteFailure(context.Failure); + } + + private async Task OnRedirectToIdentityProvider(RedirectContext context) + { + Logger.RedirectToIdentityProvider(context.ProtocolMessage); + } + + private async Task OnMessageReceived(MessageReceivedContext context) + { + Logger.MessageReceived(context.ProtocolMessage); + } + + private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context) + { + Logger.AuthorizationCodeReceived(context.TokenEndpointResponse); + } + + private async Task OnAuthenticationFailed(AuthenticationFailedContext context) + { + Logger.AuthenticationFailed(context.Exception); + } + + private static async Task OnTokenValidated(TokenValidatedContext context) + { + using var scope = context.HttpContext.RequestServices.CreateScope(); + using var activity = Dgmjr.Graph.Telemetry.Activities.TokenAcquisitionActivitySource.StartActivity( + nameof(OnTokenValidated), + ActivityKind.Client + ); + var logger = scope.ServiceProvider.GetRequiredService>(); + logger.BeginningSupplementaryTokenAcquisitionAndCreation(context.Principal); + activity.AddTag("UserObjectId", context.Principal.GetObjectId()); + activity.AddTag("UserTenantId", context.Principal.GetTenantId()); + + var services = scope.ServiceProvider; + var tokenAcquisition = services.GetRequiredService(); + var graphClientOptions = services.GetRequiredService>().Value; + + var graphClient = new GraphServiceClient( + (Microsoft.Graph.IAuthenticationProvider)(new BaseBearerTokenAuthenticationProvider( + new TokenAcquisitionTokenProvider( + tokenAcquisition, + [MsGraphScopes.Default], + context.Principal + ) + ) as IAuthenticationProvider) + ); + var me = await graphClient.Me.Request().GetAsync(); + var app = await graphClient.Applications[graphClientOptions.AzureAdB2CExtensionsApplicationId.ToString()].Request().GetAsync(); + var appRoles = app.AppRoles; + var myAppRoles = me.AppRoleAssignments.Where(x => x.ResourceId == graphClientOptions.AzureAdB2CExtensionsApplicationId).ToList(); + var claims = new List(); + foreach (var appRole in myAppRoles) + { + var theAppRole = appRoles.FirstOrDefault(x => x.Id.ToString() == appRole.Id); + if(theAppRole != null) + { + claims.Add(new(ClaimTypes.Role, theAppRole.Value)); + } + } + logger.SupplementaryTokenAcquisitionAndCreationComplete(context.Principal); + } +} diff --git a/src/MicrosoftGraph/Constants/MsGraphConstants.cs b/src/MicrosoftGraph/Constants/MsGraphConstants.cs index 29979f0b..205ab78e 100644 --- a/src/MicrosoftGraph/Constants/MsGraphConstants.cs +++ b/src/MicrosoftGraph/Constants/MsGraphConstants.cs @@ -22,4 +22,13 @@ public static class MsGraphConstants public const string DownstreamApis_MicrosoftGraph_AzureAdB2CExtensionsApplicationId = $"{DownstreamApis_MicrosoftGraph}:{AzureAdB2CExtensionsApplicationId}"; + + public static readonly string[] ValidMsGraphHosts = + [ + "graph.microsoft.com", + "graph.microsoft.us", + "dod-graph.microsoft.us", + "graph.microsoft.de", + "microsoftgraph.chinacloudapi.cn" + ]; } diff --git a/src/MicrosoftGraph/Constants/ODataUris.cs b/src/MicrosoftGraph/Constants/ODataUris.cs new file mode 100644 index 00000000..0f2960eb --- /dev/null +++ b/src/MicrosoftGraph/Constants/ODataUris.cs @@ -0,0 +1,10 @@ +namespace Dgmjr.Graph.Constants; + +public static class ODataUris +{ + /// https://graph.microsoft.com/v1.0/$metadata#Collection(extensionProperty) + public const string ExtensionPropertyODataContextString = "https://graph.microsoft.com/v1.0/$metadata#Collection(extensionProperty)"; + + /// #microsoft.graph.extensionProperty + public const string ExtensionPropertyODataTypeString = "#microsoft.graph.extensionProperty"; +} diff --git a/src/MicrosoftGraph/Constants/Scopes.cs b/src/MicrosoftGraph/Constants/Scopes.cs index fb016f9a..f0d04ae2 100644 --- a/src/MicrosoftGraph/Constants/Scopes.cs +++ b/src/MicrosoftGraph/Constants/Scopes.cs @@ -65,4 +65,16 @@ public static class EnableDisableAccount public const string All = Base + ".All"; } } + + public static class Directory + { + public const string Base = MsGraphScopes.Base + "Directory."; + + public static class Read + { + public const string Base = Directory.Base + "Read"; + + public const string All = Base + ".All"; + } + } } diff --git a/src/MicrosoftGraph/Controllers/DirectoryObjectsController.cs b/src/MicrosoftGraph/Controllers/DirectoryObjectsController.cs index 8db2bb83..68622dee 100644 --- a/src/MicrosoftGraph/Controllers/DirectoryObjectsController.cs +++ b/src/MicrosoftGraph/Controllers/DirectoryObjectsController.cs @@ -16,6 +16,7 @@ namespace Dgmjr.Graph.Controllers; using Microsoft.Extensions.DependencyInjection; [Route($"{MsGraphApi}{DirectoryObjects}")] +[AuthorizeForScopes(Scopes = [MsGraphScopes.Directory.Read.All])] public class DirectoryObjectsController( ILogger logger, IServiceProvider services diff --git a/src/MicrosoftGraph/Controllers/MeController.cs b/src/MicrosoftGraph/Controllers/MeController.cs index a83ff349..5f3bb052 100644 --- a/src/MicrosoftGraph/Controllers/MeController.cs +++ b/src/MicrosoftGraph/Controllers/MeController.cs @@ -6,6 +6,7 @@ namespace Dgmjr.Graph.Controllers; using Dgmjr.Abstractions; using Microsoft.Extensions.Logging; using Microsoft.Identity.Web.Resource; +using User = Microsoft.Graph.User; [AuthorizeForScopes(Scopes = [MsGraphScopes.User.Read.Base])] [RequiredScope([MsGraphScopes.User.Read.Base])] diff --git a/src/MicrosoftGraph/Controllers/UsersController.cs b/src/MicrosoftGraph/Controllers/UsersController.cs index dad1e88a..e5b351cd 100644 --- a/src/MicrosoftGraph/Controllers/UsersController.cs +++ b/src/MicrosoftGraph/Controllers/UsersController.cs @@ -5,6 +5,7 @@ namespace Dgmjr.Graph.Controllers; using Microsoft.Extensions.DependencyInjection; using Application = Dgmjr.Mime.Application; using Dgmjr.Abstractions; +using User = Microsoft.Graph.User; [AuthorizeForScopes(ScopeKeySection = DownstreamApis_MicrosoftGraph_Scopes)] [Route($"{MsGraphApi}{Users}")] diff --git a/src/MicrosoftGraph/Dgmjr.Graph.csproj b/src/MicrosoftGraph/Dgmjr.Graph.csproj index b19f76f1..694dca57 100644 --- a/src/MicrosoftGraph/Dgmjr.Graph.csproj +++ b/src/MicrosoftGraph/Dgmjr.Graph.csproj @@ -10,6 +10,7 @@ + diff --git a/src/MicrosoftGraph/Dgmjr.Graph.sln b/src/MicrosoftGraph/Dgmjr.Graph.sln index 39551486..c0a8eeec 100644 --- a/src/MicrosoftGraph/Dgmjr.Graph.sln +++ b/src/MicrosoftGraph/Dgmjr.Graph.sln @@ -10,7 +10,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dgmjr.Web.DownstreamApis", "..\DownstreamApis\Dgmjr.Web.DownstreamApis.csproj", "{0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dgmjr.Graph", "Dgmjr.Graph.csproj", "{0F27DFD4-7490-4E35-BA32-7D2B44EC3C13}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dgmjr.Graph", "Dgmjr.Graph.csproj", "{04A895B2-EFA5-4512-92EA-A28B0917C7E2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -34,18 +34,18 @@ Global {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Production|Any CPU.Build.0 = Local|Any CPU {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Release|Any CPU.ActiveCfg = Release|Any CPU {0EB63FC8-4D68-471C-BF25-FF7FBE57A33F}.Release|Any CPU.Build.0 = Release|Any CPU - {0F27DFD4-7490-4E35-BA32-7D2B44EC3C13}.Local|Any CPU.ActiveCfg = Local|Any CPU - {0F27DFD4-7490-4E35-BA32-7D2B44EC3C13}.Local|Any CPU.Build.0 = Local|Any CPU - {0F27DFD4-7490-4E35-BA32-7D2B44EC3C13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0F27DFD4-7490-4E35-BA32-7D2B44EC3C13}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0F27DFD4-7490-4E35-BA32-7D2B44EC3C13}.Testing|Any CPU.ActiveCfg = Testing|Any CPU - {0F27DFD4-7490-4E35-BA32-7D2B44EC3C13}.Testing|Any CPU.Build.0 = Testing|Any CPU - {0F27DFD4-7490-4E35-BA32-7D2B44EC3C13}.Staging|Any CPU.ActiveCfg = Staging|Any CPU - {0F27DFD4-7490-4E35-BA32-7D2B44EC3C13}.Staging|Any CPU.Build.0 = Staging|Any CPU - {0F27DFD4-7490-4E35-BA32-7D2B44EC3C13}.Production|Any CPU.ActiveCfg = Local|Any CPU - {0F27DFD4-7490-4E35-BA32-7D2B44EC3C13}.Production|Any CPU.Build.0 = Local|Any CPU - {0F27DFD4-7490-4E35-BA32-7D2B44EC3C13}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0F27DFD4-7490-4E35-BA32-7D2B44EC3C13}.Release|Any CPU.Build.0 = Release|Any CPU + {04A895B2-EFA5-4512-92EA-A28B0917C7E2}.Local|Any CPU.ActiveCfg = Local|Any CPU + {04A895B2-EFA5-4512-92EA-A28B0917C7E2}.Local|Any CPU.Build.0 = Local|Any CPU + {04A895B2-EFA5-4512-92EA-A28B0917C7E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04A895B2-EFA5-4512-92EA-A28B0917C7E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04A895B2-EFA5-4512-92EA-A28B0917C7E2}.Testing|Any CPU.ActiveCfg = Testing|Any CPU + {04A895B2-EFA5-4512-92EA-A28B0917C7E2}.Testing|Any CPU.Build.0 = Testing|Any CPU + {04A895B2-EFA5-4512-92EA-A28B0917C7E2}.Staging|Any CPU.ActiveCfg = Staging|Any CPU + {04A895B2-EFA5-4512-92EA-A28B0917C7E2}.Staging|Any CPU.Build.0 = Staging|Any CPU + {04A895B2-EFA5-4512-92EA-A28B0917C7E2}.Production|Any CPU.ActiveCfg = Local|Any CPU + {04A895B2-EFA5-4512-92EA-A28B0917C7E2}.Production|Any CPU.Build.0 = Local|Any CPU + {04A895B2-EFA5-4512-92EA-A28B0917C7E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04A895B2-EFA5-4512-92EA-A28B0917C7E2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/MicrosoftGraph/Extensions/LoggingExtensions.cs b/src/MicrosoftGraph/Extensions/LoggingExtensions.cs index 050e9f9c..2ea2bf75 100644 --- a/src/MicrosoftGraph/Extensions/LoggingExtensions.cs +++ b/src/MicrosoftGraph/Extensions/LoggingExtensions.cs @@ -1,6 +1,8 @@ +namespace Dgmjr.Graph; using System.Security.Claims; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; -namespace Dgmjr.Graph; +using ClaimsPrincipal = System.Security.Claims.ClaimsPrincipal; internal static partial class LoggingExtensions { @@ -26,6 +28,62 @@ public static partial void SupplementaryTokenAcquisitionAndCreationComplete( ClaimsPrincipal user ); + [LoggerMessage( + EventId = 3, + Level = LogLevel.Information, + Message = "Authentication failed: {Exception}", + EventName = nameof(AuthenticationFailed) + )] + public static partial void AuthenticationFailed( + this ILogger logger, + Exception exception + ); + + [LoggerMessage( + EventId = 4, + Level = LogLevel.Information, + Message = "Authorization code received: {TokenEndpointResponse}", + EventName = nameof(AuthorizationCodeReceived) + )] + public static partial void AuthorizationCodeReceived( + this ILogger logger, + OpenIdConnectMessage tokenEndpointResponse + ); + + [LoggerMessage( + EventId = 5, + Level = LogLevel.Information, + Message = "Message received: {ProtocolMessage}", + EventName = nameof(MessageReceived) + )] + public static partial void MessageReceived( + this ILogger logger, + OpenIdConnectMessage protocolMessage + ); + + [LoggerMessage( + EventId = 6, + Level = LogLevel.Information, + Message = "Redirect to identity provider: {ProtocolMessage}", + EventName = nameof(RedirectToIdentityProvider) + )] + public static partial void RedirectToIdentityProvider( + this ILogger logger, + OpenIdConnectMessage protocolMessage + ); + + [LoggerMessage( + EventId = 7, + Level = LogLevel.Information, + Message = "Remote failure: {Failure}", + EventName = nameof(RemoteFailure) + )] + public static partial void RemoteFailure( + this ILogger logger, + Exception? failure + ); + + // [LoggerMessage( // EventId = 1, // Level = LogLevel.Information, diff --git a/src/MicrosoftGraph/Extensions/MicrosoftGraphServiceCollectionExtensions.cs b/src/MicrosoftGraph/Extensions/MicrosoftGraphServiceCollectionExtensions.cs index f804222a..478fd674 100644 --- a/src/MicrosoftGraph/Extensions/MicrosoftGraphServiceCollectionExtensions.cs +++ b/src/MicrosoftGraph/Extensions/MicrosoftGraphServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using Dgmjr.Graph.TokenProviders; using IAuthenticationProvider = Microsoft.Graph.IAuthenticationProvider; using Microsoft.ApplicationInsights; +using Dgmjr.Graph.Configuration; namespace Microsoft.Extensions.DependencyInjection; @@ -22,13 +23,11 @@ IConfiguration config .AddMicrosoftIdentityConsentHandler() .ConfigureDownstreamApi(MicrosoftGraph, configSection); services.AddScoped(); + services.AddScoped(); services.Configure(configSection); services.AddScoped(); - services.ConfigureAll(options => - { - options.Events.OnTokenValidated = OnTokenValidated; - }); services.AddPassphraseGenerator(config); + services.AddSingleton, UserAppRolesConfigurator>(); return services; } @@ -43,45 +42,4 @@ IConfiguration config services.AddSingleton(); return services; } - - private static async Task OnTokenValidated(TokenValidatedContext context) - { - using var scope = context.HttpContext.RequestServices.CreateScope(); - using var activity = Dgmjr.Graph.Telemetry.Activities.TokenAcquisitionActivitySource.StartActivity( - nameof(OnTokenValidated), - ActivityKind.Client - ); - var logger = scope.ServiceProvider.GetRequiredService>(); - logger.BeginningSupplementaryTokenAcquisitionAndCreation(context.Principal); - activity.AddTag("UserObjectId", context.Principal.GetObjectId()); - activity.AddTag("UserTenantId", context.Principal.GetTenantId()); - - var services = scope.ServiceProvider; - var tokenAcquisition = services.GetRequiredService(); - var graphClientOptions = services.GetRequiredService>().Value; - - var graphClient = new GraphServiceClient( - new BaseBearerTokenAuthenticationProvider( - new TokenAcquisitionTokenProvider( - tokenAcquisition, - [MsGraphScopes.Default], - context.Principal - ) - ) as IAuthenticationProvider - ); - var me = await graphClient.Me.Request().GetAsync(); - var app = await graphClient.Applications[graphClientOptions.AzureAdB2CExtensionsApplicationId.ToString()].Request().GetAsync(); - var appRoles = app.AppRoles; - var myAppRoles = me.AppRoleAssignments.Where(x => x.ResourceId == graphClientOptions.AzureAdB2CExtensionsApplicationId).ToList(); - var claims = new List(); - foreach (var appRole in myAppRoles) - { - var theAppRole = appRoles.FirstOrDefault(x => x.Id.ToString() == appRole.Id); - if(theAppRole != null) - { - claims.Add(new(ClaimTypes.Role, theAppRole.Value)); - } - } - logger.SupplementaryTokenAcquisitionAndCreationComplete(context.Principal); - } } diff --git a/src/MicrosoftGraph/LICENSE.md b/src/MicrosoftGraph/LICENSE.md index c80a6760..4f592f86 100755 --- a/src/MicrosoftGraph/LICENSE.md +++ b/src/MicrosoftGraph/LICENSE.md @@ -1,21 +1,21 @@ --- -date: 2023-07-13T05:44:46:00+05:00Z +date: 2023-07-13T05:44:46:00-05:00Z description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... keywords: - - IP - - copyright - - license - - mit +- IP +- copyright +- license +- mit permissions: - - commercial-use - - modifications - - distribution - - private-use +- commercial-use +- modifications +- distribution +- private-use conditions: - - include-copyright +- include-copyright limitations: - - liability - - warranty +- liability +- warranty lastmod: 2024-01-0T00:39:00.0000+05:00Z license: MIT slug: mit-license @@ -25,7 +25,7 @@ type: license # MIT License -## Copyright © 2022-2023 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore an email") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/MicrosoftGraph/Models/ExtensionProperty.cs b/src/MicrosoftGraph/Models/ExtensionProperty.cs index e7798107..31ce8f1d 100644 --- a/src/MicrosoftGraph/Models/ExtensionProperty.cs +++ b/src/MicrosoftGraph/Models/ExtensionProperty.cs @@ -1,12 +1,12 @@ namespace Dgmjr.Graph.Models; using System.Runtime.InteropServices; -[StructLayout(LayoutKind.Sequential, Pack = 1)] +[StructLayout(LayoutKind.Auto, Pack = 1)] public partial record struct ExtensionProperty { - /// The name of the extension property - [JProp("@odata.context")] - public Uri OdataContext { get; init; } + /// + [JProp("@odata.type")] + public Uri OdataContext { get; init; } = new(ODataUris.ExtensionPropertyODataTypeString); /// The name of the extension property [JProp("id")] diff --git a/src/MicrosoftGraph/Services/UsersService.cs b/src/MicrosoftGraph/Services/UsersService.cs index 1090f5f2..a05cbfd9 100644 --- a/src/MicrosoftGraph/Services/UsersService.cs +++ b/src/MicrosoftGraph/Services/UsersService.cs @@ -1,4 +1,5 @@ namespace Dgmjr.Graph.Services; +using User = Microsoft.Graph.User; public class UsersService(GraphServiceClient graph, ILogger logger, IOptionsMonitor options, IOptionsMonitor msidOptions, IDistributedCache cache) : MsGraphService(graph, logger, options, msidOptions, cache), IUsersService { diff --git a/src/MicrosoftGraph/TokenProviders/ClientSecretTokenProvider.cs b/src/MicrosoftGraph/TokenProviders/ClientSecretTokenProvider.cs new file mode 100644 index 00000000..2cf07827 --- /dev/null +++ b/src/MicrosoftGraph/TokenProviders/ClientSecretTokenProvider.cs @@ -0,0 +1,22 @@ +// using System; +// using System.Collections.Generic; +// using System.Threading; +// using System.Threading.Tasks; + +// using Microsoft.Kiota.Abstractions.Authentication; +// using Microsoft.Identity.Web; + +// namespace Dgmjr.Graph.TokenProviders; + +// public class ClientSecretTokenProvider(IOptionsMonitor options, ITokenAcquisition tokenAcquisition) : IAccessTokenProvider +// { +// private MicrosoftIdentityOptions Options => options.CurrentValue; +// public AllowedHostsValidator AllowedHostsValidator => new (ValidMsGraphHosts); + +// public async Task GetAuthorizationTokenAsync(Uri uri, Dictionary? additionalAuthenticationContext = null, CancellationToken cancellationToken = default) +// { +// TokenAcquirerAppTokenCredential tokenAcquisitionAppTokenCredential = new (tokenAcquisition); +// await tokenAcquisitionAppTokenCredential.GetTokenAsync(new Azure.Core.TokenRequestContext([MsGraphScopes.Default], tenantId: Options.TenantId), cancellationToken); + +// } +// } diff --git a/src/MicrosoftGraph/TokenProviders/TokenAcquisitionTokenProvider.cs b/src/MicrosoftGraph/TokenProviders/TokenAcquisitionTokenProvider.cs index ed039179..97a2bc7d 100644 --- a/src/MicrosoftGraph/TokenProviders/TokenAcquisitionTokenProvider.cs +++ b/src/MicrosoftGraph/TokenProviders/TokenAcquisitionTokenProvider.cs @@ -6,6 +6,8 @@ using Microsoft.Identity.Web; using Microsoft.Kiota.Abstractions.Authentication; +using ClaimsPrincipal = System.Security.Claims.ClaimsPrincipal; + namespace Dgmjr.Graph.TokenProviders { /// diff --git a/src/Mvc/.vscode/settings.json b/src/Mvc/.vscode/settings.json new file mode 100644 index 00000000..40ac7cbc --- /dev/null +++ b/src/Mvc/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.exclude": { + "**/.vs": false + } +} diff --git a/src/Mvc/Dgmjr.AspNetCore.Mvc.csproj b/src/Mvc/Dgmjr.AspNetCore.Mvc.csproj index 845a91f3..d97fb127 100644 --- a/src/Mvc/Dgmjr.AspNetCore.Mvc.csproj +++ b/src/Mvc/Dgmjr.AspNetCore.Mvc.csproj @@ -35,6 +35,7 @@ + diff --git a/src/Mvc/IHostApplicationBuilderMvcExtensions.cs b/src/Mvc/IHostApplicationBuilderMvcExtensions.cs index 5539b8e5..618f72d2 100644 --- a/src/Mvc/IHostApplicationBuilderMvcExtensions.cs +++ b/src/Mvc/IHostApplicationBuilderMvcExtensions.cs @@ -9,15 +9,17 @@ namespace Microsoft.Extensions.DependencyInjection; #endif using MvcOptions = Dgmjr.AspNetCore.Mvc.MvcOptions; using Microsoft.Identity.Web.UI; +using Microsoft.Extensions.Logging; +using static Dgmjr.AspNetCore.Mvc.ServiceNames; public static class IHostApplicationBuilderMvcExtensions { private const string Mvc = nameof(Mvc); - private const string JsonSerializer = nameof(JsonSerializer); public static WebApplicationBuilder AddMvc( this WebApplicationBuilder builder, - string configurationSectionKey = Mvc + string configurationSectionKey = Mvc, + ILogger? logger = null ) { var mvcBuilder = builder.Services.AddMvc( @@ -25,48 +27,57 @@ public static WebApplicationBuilder AddMvc( ); var mvcOptionsSection = builder.Configuration.GetSection(configurationSectionKey); - builder.Services.Configure(mvcOptionsSection); var mvcOptions = mvcOptionsSection.Get(); + builder.Services.Configure(mvcOptionsSection); + builder.Services.Configure(mvc => mvcOptions.CopyTo(mvc)); if (mvcOptions is not null) { if (mvcOptions.EnableEndpointRouting) { + logger?.SettingUpMvcService(EndpointRouting); builder.Services.AddRouting(); } if (mvcOptions.AddControllersWithViews) { + logger?.SettingUpMvcService(ControllersWithViews); builder.Services.AddControllersWithViews(); } if (mvcOptions.AddRazorPages) { + logger?.SettingUpMvcService(RazorPages); builder.Services.AddRazorPages(); } if (mvcOptions.AddControllers) { + logger?.SettingUpMvcService(Controllers); builder.Services.AddControllers(); } if (mvcOptions.AddControllersAsServices) { + logger?.SettingUpMvcService(ControllersAsServices); mvcBuilder.AddControllersAsServices(); } if (mvcOptions.AddXmlSerializerFormatters) { + logger?.SettingUpMvcService(XmlSerializerFormatters); mvcBuilder.AddXmlSerializerFormatters(); } if (mvcOptions.AddXmlDataContractSerializerFormatters) { + logger?.SettingUpMvcService(XmlDataContractSerializerFormatters); mvcBuilder.AddXmlDataContractSerializerFormatters(); } if (mvcOptions.AddMicrosoftIdentityUI) { + logger?.SettingUpMvcService(MicrosoftIdentityUI); mvcBuilder.AddMicrosoftIdentityUI(); } @@ -77,12 +88,20 @@ public static WebApplicationBuilder AddMvc( if (mvcOptions.AddJsonOptions) { + logger?.SettingUpMvcService(JsonSerializer); builder.Services.Configure( options => builder.Configuration .GetSection(JsonSerializer) .Bind(options.JsonSerializerOptions) ); + builder.Services.AddSingleton>( + new TypeNameAndAssemblyConfigurator( + builder.Configuration, + $"{JsonSerializer}:{nameof(Jso.Converters)}", + options => options.JsonSerializerOptions.Converters + ) + ); } } @@ -91,7 +110,8 @@ public static WebApplicationBuilder AddMvc( public static IApplicationBuilder UseMvc( this IApplicationBuilder app, - string configurationSectionKey = Mvc + string configurationSectionKey = Mvc, + ILogger? logger = null ) { var webApp = app as WebApplication; @@ -104,21 +124,25 @@ public static IApplicationBuilder UseMvc( { if (mvcOptions.EnableEndpointRouting) { + logger?.AddingMvcServiceToThePipeline(EndpointRouting); webApp?.UseRouting(); } if (mvcOptions.AddControllersWithViews) { + logger?.AddingMvcServiceToThePipeline(ControllersWithViews); webApp?.MapControllers(); } if (mvcOptions.AddRazorPages) { + logger?.AddingMvcServiceToThePipeline(RazorPages); webApp?.MapRazorPages(); } if (mvcOptions.AddControllers) { + logger?.AddingMvcServiceToThePipeline(Controllers); webApp?.MapControllers(); } } diff --git a/src/Mvc/LoggerExtensions.cs b/src/Mvc/LoggerExtensions.cs new file mode 100644 index 00000000..f55ee960 --- /dev/null +++ b/src/Mvc/LoggerExtensions.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.Logging; + +namespace Dgmjr.AspNetCore.Mvc; + +public static partial class LoggerExtensions +{ + // [LoggerMessage( + // EventId = 1, + // Level = LogLevel.Information, + // Message = "MvcServicesOptionsAutoConfigurator.Configure(WebApplicationBuilder)")] + // public static partial void MvcServicesOptionsAutoConfiguratorConfigureWebApplicationBuilder(this ILogger logger); + + // [LoggerMessage( + // EventId = 2, + // Level = LogLevel.Information, + // Message = "MvcServicesOptionsAutoConfigurator.Configure(IApplicationBuilder)")] + // public static partial void MvcServicesOptionsAutoConfiguratorConfigureIApplicationBuilder(this ILogger logger); + + [LoggerMessage( + EventId = 3, + Level = LogLevel.Information, + Message = "MvcAutoConfigurator.Configure(WebApplicationBuilder)")] + public static partial void MvcAutoConfiguratorConfigureWebApplicationBuilder(this ILogger logger); + + [LoggerMessage( + EventId = 4, + Level = LogLevel.Information, + Message = "MvcAutoConfigurator.Configure(IApplicationBuilder)")] + public static partial void MvcAutoConfiguratorConfigureIApplicationBuilder(this ILogger logger); + + [LoggerMessage( + EventId = 5, + Level = LogLevel.Information, + Message = "Setting up Mvc service: {ServiceName}")] + public static partial void SettingUpMvcService(this ILogger logger, string serviceName); + + [LoggerMessage( + EventId = 6, + Level = LogLevel.Information, + Message = "Setting up Mvc service: {ServiceName} with options: {Options}")] + public static partial void SettingUpMvcService(this ILogger logger, string serviceName, object options); + + [LoggerMessage( + EventId = 7, + Level = LogLevel.Information, + Message = "Adding Mvc service to the pipeline: {ServiceName}")] + public static partial void AddingMvcServiceToThePipeline(this ILogger logger, string serviceName); +} diff --git a/src/Mvc/MvcAutoConfigurator.cs b/src/Mvc/MvcAutoConfigurator.cs index 01a2f0ed..8da0f229 100644 --- a/src/Mvc/MvcAutoConfigurator.cs +++ b/src/Mvc/MvcAutoConfigurator.cs @@ -3,20 +3,25 @@ namespace Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Builder; using Dgmjr.Configuration.Extensions; using static Microsoft.Extensions.DependencyInjection.IHostApplicationBuilderMvcExtensions; +using Microsoft.Extensions.Logging; +using Dgmjr.Abstractions; -public class MvcAutoConfigurator : IConfigureIHostApplicationBuilder, IConfigureIApplicationBuilder +public class MvcAutoConfigurator(ILogger logger) : IConfigureIHostApplicationBuilder, IConfigureIApplicationBuilder, ILog { + public ILogger? Logger => logger; private const string Mvc = nameof(Mvc); private const string JsonSerializer = nameof(JsonSerializer); - public ConfigurationOrder Order => ConfigurationOrder.AnyTime; + public ConfigurationOrder Order => ConfigurationOrder.Late; public void Configure(WebApplicationBuilder builder) { - builder.AddMvc(); + Logger?.MvcAutoConfiguratorConfigureWebApplicationBuilder(); + builder.AddMvc(Mvc, Logger); } public void Configure(IApplicationBuilder builder) { - builder.UseMvc(Mvc); + Logger.MvcAutoConfiguratorConfigureIApplicationBuilder(); + builder.UseMvc(Mvc, Logger); } } diff --git a/src/Mvc/MvcOptions.cs b/src/Mvc/MvcOptions.cs index 916fd53e..3441f46c 100644 --- a/src/Mvc/MvcOptions.cs +++ b/src/Mvc/MvcOptions.cs @@ -1,133 +1,227 @@ +namespace Dgmjr.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; -namespace Dgmjr.AspNetCore.Mvc; +using OneOf.Types; + +using MsMvcOptions = Microsoft.AspNetCore.Mvc.MvcOptions; -public class MvcOptions : Microsoft.AspNetCore.Mvc.MvcOptions +public class MvcOptions(MsMvcOptions mvc) { + public MvcOptions() + : this( + new MsMvcOptions + { + AllowEmptyInputInBodyModelBinding = false, + RespectBrowserAcceptHeader = true + } + ) { } + /// if you want to add controllers with views, otherwise - public bool AddControllersWithViews { get; set; } = false; + public virtual bool AddControllersWithViews { get; set; } = false; + public virtual bool AddControllersAsServices { get; set; } = false; /// if you want to add controllers with views, otherwise - public bool AddRazorPages { get; set; } = false; + public virtual bool AddRazorPages { get; set; } = false; /// if you want to add controllers with views, otherwise - public bool AddControllersAsServices { get; set; } = false; + /// public virtual bool AddControllersAsServices { get; set; } = false; /// if you want to add the Microsoft Identity UI, otherwise - public bool AddMicrosoftIdentityUI { get; set; } = false; + public virtual bool AddMicrosoftIdentityUI { get; set; } = false; - /// if you want to add JSON serializer options, otherwise - public bool AddJsonOptions { get; set; } = false; + /// if you want to add JSON serializer otherwise + public virtual bool AddJsonOptions { get; set; } = false; /// if you want to add XML serializer formatters, otherwise - public bool AddXmlSerializerFormatters { get; set; } = false; + public virtual bool AddXmlSerializerFormatters { get; set; } = false; /// if you want to add XML data contract serializer formatters, otherwise - public bool AddXmlDataContractSerializerFormatters { get; set; } = false; + public virtual bool AddXmlDataContractSerializerFormatters { get; set; } = false; /// if you want to add the DGMJR MVC conventions, otherwise - public bool AddMvcConventions { get; set; } = false; + public virtual bool AddMvcConventions { get; set; } = false; /// if you want to add controllers, otherwise - public bool AddControllers { get; set; } = false; + public virtual bool AddControllers { get; set; } = false; - /// - public new bool AllowEmptyInputInBodyModelBinding - { - get => base.AllowEmptyInputInBodyModelBinding; - set => base.AllowEmptyInputInBodyModelBinding = value; - } + /// + public virtual bool AllowEmptyInputInBodyModelBinding { get; set; } = + mvc.AllowEmptyInputInBodyModelBinding; - /// - public new bool EnableActionInvokers - { - get => base.EnableActionInvokers; - set => base.EnableActionInvokers = value; - } + /// + public virtual bool EnableActionInvokers { get; set; } = mvc.EnableActionInvokers; - /// - public new bool EnableEndpointRouting - { - get => base.EnableEndpointRouting; - set => base.EnableEndpointRouting = value; - } + /// + public virtual bool EnableEndpointRouting { get; set; } = mvc.EnableEndpointRouting; - /// - public new bool RespectBrowserAcceptHeader - { - get => base.RespectBrowserAcceptHeader; - set => base.RespectBrowserAcceptHeader = value; - } + /// + public virtual bool RespectBrowserAcceptHeader { get; set; } = mvc.RespectBrowserAcceptHeader; - /// - public new bool RequireHttpsPermanent - { - get => base.RequireHttpsPermanent; - set => base.RequireHttpsPermanent = value; - } + /// + public virtual bool RequireHttpsPermanent { get; set; } = mvc.RequireHttpsPermanent; - /// - public new bool SuppressAsyncSuffixInActionNames - { - get => base.SuppressAsyncSuffixInActionNames; - set => base.SuppressAsyncSuffixInActionNames = value; - } + /// + public virtual bool SuppressAsyncSuffixInActionNames { get; set; } = + mvc.SuppressAsyncSuffixInActionNames; - /// - public new bool SuppressInputFormatterBuffering - { - get => base.SuppressInputFormatterBuffering; - set => base.SuppressInputFormatterBuffering = value; - } + /// + public virtual bool SuppressInputFormatterBuffering { get; set; } = + mvc.SuppressInputFormatterBuffering; - /// - public new bool SuppressOutputFormatterBuffering - { - get => base.SuppressOutputFormatterBuffering; - set => base.SuppressOutputFormatterBuffering = value; - } + /// + public virtual bool SuppressOutputFormatterBuffering { get; set; } = + mvc.SuppressOutputFormatterBuffering; - /// - public new bool ValidateComplexTypesIfChildValidationFails - { - get => base.ValidateComplexTypesIfChildValidationFails; - set => base.ValidateComplexTypesIfChildValidationFails = value; - } + /// + public virtual bool ValidateComplexTypesIfChildValidationFails { get; set; } = + mvc.ValidateComplexTypesIfChildValidationFails; - /// - public new bool SuppressImplicitRequiredAttributeForNonNullableReferenceTypes - { - get => base.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes; - set => base.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = value; - } + /// + public virtual bool SuppressImplicitRequiredAttributeForNonNullableReferenceTypes { get; set; } = + mvc.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes; - /// - public new bool ReturnHttpNotAcceptable - { - get => base.ReturnHttpNotAcceptable; - set => base.ReturnHttpNotAcceptable = value; - } + /// + public virtual bool ReturnHttpNotAcceptable { get; set; } = mvc.ReturnHttpNotAcceptable; + + /// + public virtual IDictionary CacheProfiles { get; set; } = + mvc.CacheProfiles; + + /// + public virtual IList Conventions { get; set; } = mvc.Conventions; + + /// + public virtual FilterCollection Filters { get; set; } = mvc.Filters; + + /// + public virtual FormatterMappings FormatterMappings { get; set; } = mvc.FormatterMappings; - /// - public new IDictionary CacheProfiles + /// + public virtual FormatterCollection InputFormatters { get; set; } = + mvc.InputFormatters; + + /// + public virtual int MaxModelValidationErrors { get; set; } = mvc.MaxModelValidationErrors; + + /// + public virtual IList ModelBinderProviders { get; set; } = + mvc.ModelBinderProviders; + + /// + public virtual DefaultModelBindingMessageProvider ModelBindingMessageProvider { get; set; } = + mvc.ModelBindingMessageProvider; + + /// + public virtual IList ModelMetadataDetailsProviders { get; set; } = + mvc.ModelMetadataDetailsProviders; + + /// + public virtual IList ModelValidatorProviders { get; set; } = + mvc.ModelValidatorProviders; + + /// + public virtual FormatterCollection OutputFormatters { get; set; } = + mvc.OutputFormatters; + + /// + public virtual IList ValueProviderFactories { get; set; } = + mvc.ValueProviderFactories; + + /// + public virtual int? SslPort { get; set; } = mvc.SslPort; + + /// + public virtual int? MaxValidationDepth { get; set; } = mvc.MaxValidationDepth; + + /// + public virtual int MaxModelBindingCollectionSize { get; set; } = + mvc.MaxModelBindingCollectionSize; + + /// + public virtual int MaxModelBindingRecursionDepth { get; set; } = + mvc.MaxModelBindingRecursionDepth; + + /// + public virtual int MaxIAsyncEnumerableBufferLimit { get; set; } = + mvc.MaxIAsyncEnumerableBufferLimit; + + public static implicit operator MsMvcOptions(MvcOptions options) => + options.CopyTo(new MsMvcOptions()); + + public virtual MsMvcOptions CopyTo(MsMvcOptions mvcOpts) { - get => base.CacheProfiles; - set + mvcOpts.AllowEmptyInputInBodyModelBinding = AllowEmptyInputInBodyModelBinding; + mvcOpts.EnableActionInvokers = EnableActionInvokers; + mvcOpts.EnableEndpointRouting = EnableEndpointRouting; + mvcOpts.MaxIAsyncEnumerableBufferLimit = MaxIAsyncEnumerableBufferLimit; + mvcOpts.MaxModelBindingCollectionSize = MaxModelBindingCollectionSize; + mvcOpts.MaxModelBindingRecursionDepth = MaxModelBindingRecursionDepth; + mvcOpts.MaxModelValidationErrors = MaxModelValidationErrors; + mvcOpts.MaxValidationDepth = MaxValidationDepth; + mvcOpts.RequireHttpsPermanent = RequireHttpsPermanent; + mvcOpts.RespectBrowserAcceptHeader = RespectBrowserAcceptHeader; + mvcOpts.ReturnHttpNotAcceptable = ReturnHttpNotAcceptable; + mvcOpts.SslPort = SslPort; + mvcOpts.SuppressAsyncSuffixInActionNames = SuppressAsyncSuffixInActionNames; + mvcOpts.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = + SuppressImplicitRequiredAttributeForNonNullableReferenceTypes; + mvcOpts.SuppressInputFormatterBuffering = SuppressInputFormatterBuffering; + mvcOpts.SuppressOutputFormatterBuffering = SuppressOutputFormatterBuffering; + mvcOpts.ValidateComplexTypesIfChildValidationFails = + ValidateComplexTypesIfChildValidationFails; + + // mvcOpts.CacheProfiles.Clear(); + foreach (var profile in CacheProfiles) { - base.CacheProfiles.Clear(); - ForEach(value.ToArray(), cp => base.CacheProfiles.Add(cp)); + mvcOpts.CacheProfiles[profile.Key] = profile.Value; } - } - - /// - public new IList Conventions - { - get => base.Conventions; - set + // mvcOpts.Filters.Clear(); + foreach (var v in Filters) + { + mvcOpts.Filters.Add(v); + } + // mvcOpts.InputFormatters.Clear(); + foreach (var v in InputFormatters) + { + mvcOpts.InputFormatters.Add(v); + } + // mvcOpts.OutputFormatters.Clear(); + foreach (var v in OutputFormatters) + { + mvcOpts.OutputFormatters.Add(v); + } + // mvcOpts.Conventions.Clear(); + foreach (var v in Conventions) + { + mvcOpts.Conventions.Add(v); + } + // mvcOpts.ValueProviderFactories.Clear(); + foreach (var v in ValueProviderFactories) + { + mvcOpts.ValueProviderFactories.Add(v); + } + // mvcOpts.ModelBinderProviders.Clear(); + foreach (var v in ModelBinderProviders) + { + mvcOpts.ModelBinderProviders.Add(v); + } + // mvcOpts.ModelMetadataDetailsProviders.Clear(); + foreach (var v in ModelMetadataDetailsProviders) + { + mvcOpts.ModelMetadataDetailsProviders.Add(v); + } + // mvcOpts.ModelValidatorProviders.Clear(); + foreach (var v in ModelValidatorProviders) { - base.Conventions.Clear(); - ForEach(value.ToArray(), c => base.Conventions.Add(c)); + mvcOpts.ModelValidatorProviders.Add(v); } + return mvcOpts; } } diff --git a/src/Mvc/ServiceNames.cs b/src/Mvc/ServiceNames.cs new file mode 100644 index 00000000..0be1d25d --- /dev/null +++ b/src/Mvc/ServiceNames.cs @@ -0,0 +1,15 @@ +namespace Dgmjr.AspNetCore.Mvc; + +public static class ServiceNames +{ + public const string JsonSerializer = nameof(JsonSerializer); + public const string EndpointRouting = nameof(EndpointRouting); + public const string ControllersWithViews = nameof(ControllersWithViews); + public const string RazorPages = nameof(RazorPages); + public const string Controllers = nameof(Controllers); + public const string ControllersAsServices = nameof(ControllersAsServices); + public const string XmlSerializerFormatters = nameof(XmlSerializerFormatters); + public const string XmlDataContractSerializerFormatters = nameof(XmlDataContractSerializerFormatters); + public const string Cors = nameof(Cors); + public const string MicrosoftIdentityUI = nameof(MicrosoftIdentityUI); +} diff --git a/src/Mvc/TypeNameAndAssemblyConfigurator.cs b/src/Mvc/TypeNameAndAssemblyConfigurator.cs new file mode 100644 index 00000000..67b827a0 --- /dev/null +++ b/src/Mvc/TypeNameAndAssemblyConfigurator.cs @@ -0,0 +1,48 @@ +using System.Collections; +using System.Runtime.Loader; + +namespace Dgmjr.AspNetCore.Mvc; + +public class TypeNameAndAssemblyConfigurator( + IConfiguration configuration, + string? configurationSectionKey = null, + Expression>>? property = null +) : IConfigureOptions + where T : class + where U : class +{ + public virtual void Configure(T options) + { + var configurationSection = configuration; + if (configurationSectionKey is not null) + { + configurationSection = configuration.GetRequiredSection(configurationSectionKey); + } + + var list = property.Compile().Invoke(options); + + foreach (var (assemblyName, typeName) in configurationSection.GetChildren().Select(GetType)) + { + var u = Activator.CreateInstance(assemblyName, typeName).Unwrap() as U; + if(u is not null) + { + list.Add(u); + } + } + } + + private (string assemblyName, string typeName) GetType(IConfigurationSection section) + { + var parts = section.Value.Split( + ',', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries + ); + var assemblyName = parts[1]; + var typeName = parts[0]; + return (assemblyName, typeName); + // var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName( + // new AssemblyName(assemblyName) + // ); + // return assembly.GetType(typeName); + } +} diff --git a/src/Payloads/LICENSE.md b/src/Payloads/LICENSE.md index 4f608744..4f592f86 100755 --- a/src/Payloads/LICENSE.md +++ b/src/Payloads/LICENSE.md @@ -1,22 +1,22 @@ --- -date: 2023-07-13T05:44:46:00.048Z +date: 2023-07-13T05:44:46:00-05:00Z description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... keywords: - - IP - - copyright - - license - - mit +- IP +- copyright +- license +- mit permissions: - - commercial-use - - modifications - - distribution - - private-use +- commercial-use +- modifications +- distribution +- private-use conditions: - - include-copyright +- include-copyright limitations: - - liability - - warranty -lastmod: 2023-08-29T17:13:51:00.216Z +- liability +- warranty +lastmod: 2024-01-0T00:39:00.0000+05:00Z license: MIT slug: mit-license title: MIT License @@ -25,7 +25,7 @@ type: license # MIT License -## Copyright © 2022-2023 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send David an email") ([@dgmjr](https://github.com/dgmjr "Contact david on GitHub")), All Rights Reserved +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/Security/Authentication.Basic/BasicAuthenticationHandler.cs b/src/Security/Authentication.Basic/BasicAuthenticationHandler.cs new file mode 100644 index 00000000..0097d59a --- /dev/null +++ b/src/Security/Authentication.Basic/BasicAuthenticationHandler.cs @@ -0,0 +1,18 @@ +namespace Dgmjr.AspNetCore.Authentication.Basic; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Text.Encodings.Web; + +public abstract class BasicAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory loggerFactory, + UrlEncoder urlEncoder, + ISystemClock clock +) : AuthenticationHandler(options, loggerFactory, urlEncoder, clock), ILog + where TOptions : AuthenticationSchemeOptions, new() +{ + private ILogger _logger; + public new virtual ILogger Logger => _logger ??= loggerFactory.CreateLogger(GetType().FullName); +} diff --git a/src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationHandlerWithUsers.cs b/src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationHandlerWithUsers.cs new file mode 100644 index 00000000..885f3f75 --- /dev/null +++ b/src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationHandlerWithUsers.cs @@ -0,0 +1,74 @@ +namespace Dgmjr.AspNetCore.Authentication.Basic; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using System.Collections; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +public class BasicAuthenticationWithUsersHandler( + IOptionsMonitor options, + ILoggerFactory loggerFactory, + UrlEncoder urlEncoder, + ISystemClock clock +) + : BasicAuthenticationHandler( + options, + loggerFactory, + urlEncoder, + clock + ) +{ + private ICollection Users => Options.Users; + + protected override async Task HandleAuthenticateAsync() + { + var authHeader = Request.Headers[HReqH.Authorization.DisplayName].FirstOrDefault(); + if (authHeader == null) + { + return AuthenticateResult.NoResult(); + } + + var authHeaderVal = AuthenticationHeaderValue.Parse(authHeader); + if ( + !authHeaderVal.Scheme.Equals( + BasicAuthenticationWithUsersDefaults.AuthenticationScheme, + OrdinalIgnoreCase + ) + ) + { + return AuthenticateResult.NoResult(); + } + + var credentialBytes = Convert.FromBase64String(authHeaderVal.Parameter); + var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':', 2); + if (credentials.Length != 2) + { + return AuthenticateResult.Fail("Invalid Basic authentication header"); + } + + var (username, password) = (credentials[0], credentials[1]); + if (!Users.Any(u => u.Username == username && u.Password == password)) + { + return AuthenticateResult.Fail("Invalid username or password"); + } + + return await Task.FromResult( + AuthenticateResult.Success( + new AuthenticationTicket( + new ClaimsPrincipal( + new ClaimsIdentity( + new[] { new Claim(ClaimTypes.Name, username) }, + BasicAuthenticationWithUsersDefaults.AuthenticationScheme + ) + ), + BasicAuthenticationWithUsersDefaults.AuthenticationScheme + ) + ) + ); + } +} diff --git a/src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationWithUsersAutoConfigurator.cs b/src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationWithUsersAutoConfigurator.cs new file mode 100644 index 00000000..0e97e69e --- /dev/null +++ b/src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationWithUsersAutoConfigurator.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Dgmjr.AspNetCore.Authentication.Basic; + +public class BasicAuthenticationWithUsersAutoConfigurator + : IConfigureIHostApplicationBuilder, + IConfigureIApplicationBuilder +{ + public ConfigurationOrder Order => ConfigurationOrder.AnyTime; + + public void Configure(WebApplicationBuilder builder) + { + var config = builder.Configuration.GetSection( + BasicAuthenticationWithUsersOptions.ConfigurationKey + ); + builder.Services + .AddAuthentication(BasicAuthenticationWithUsersDefaults.AuthenticationScheme) + .AddScheme( + BasicAuthenticationWithUsersDefaults.AuthenticationScheme, + config.Bind + ); + } + + public void Configure(IApplicationBuilder app) { } +} diff --git a/src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationWithUsersDefaults.cs b/src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationWithUsersDefaults.cs new file mode 100644 index 00000000..00a44b4a --- /dev/null +++ b/src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationWithUsersDefaults.cs @@ -0,0 +1,6 @@ +namespace Dgmjr.AspNetCore.Authentication.Basic; + +public static class BasicAuthenticationWithUsersDefaults +{ + public const string AuthenticationScheme = "Basic"; +} diff --git a/src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationWithUsersOptions.cs b/src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationWithUsersOptions.cs new file mode 100644 index 00000000..5eac1ab4 --- /dev/null +++ b/src/Security/Authentication.Basic/BasicWithUsers/BasicAuthenticationWithUsersOptions.cs @@ -0,0 +1,31 @@ +namespace Dgmjr.AspNetCore.Authentication.Basic; + +using System.Collections; + +using Microsoft.AspNetCore.Authentication; + +public class BasicAuthenticationWithUsersOptions : AuthenticationSchemeOptions +{ + public const string ConfigurationKey = $"{nameof(Authentication)}:{nameof(Basic)}"; + + public BasicAuthenticationWithUsersOptions() + { + ForwardAuthenticate = BasicAuthenticationWithUsersDefaults.AuthenticationScheme; + ForwardChallenge = BasicAuthenticationWithUsersDefaults.AuthenticationScheme; + ForwardForbid = BasicAuthenticationWithUsersDefaults.AuthenticationScheme; + ForwardSignIn = BasicAuthenticationWithUsersDefaults.AuthenticationScheme; + ForwardSignOut = BasicAuthenticationWithUsersDefaults.AuthenticationScheme; + ClaimsIssuer = BasicAuthenticationWithUsersDefaults.AuthenticationScheme; + ForwardDefault = BasicAuthenticationWithUsersDefaults.AuthenticationScheme; + ForwardDefaultSelector = context => + BasicAuthenticationWithUsersDefaults.AuthenticationScheme; + } + + public ICollection Users { get; set; } = new List(); +} + +public class UsernameAndPassword +{ + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} diff --git a/src/Security/Authentication.Basic/Dgmjr.AspNetCore.Authentication.Basic.csproj b/src/Security/Authentication.Basic/Dgmjr.AspNetCore.Authentication.Basic.csproj new file mode 100644 index 00000000..0d4213b0 --- /dev/null +++ b/src/Security/Authentication.Basic/Dgmjr.AspNetCore.Authentication.Basic.csproj @@ -0,0 +1,25 @@ + + + + + net6.0;net8.0 + + + + + + + + + + diff --git a/src/Security/Authentication.Basic/Dgmjr.AspNetCore.Authentication.Basic.sln b/src/Security/Authentication.Basic/Dgmjr.AspNetCore.Authentication.Basic.sln new file mode 100644 index 00000000..d266d3dd --- /dev/null +++ b/src/Security/Authentication.Basic/Dgmjr.AspNetCore.Authentication.Basic.sln @@ -0,0 +1,42 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B283EBC2-E01F-412D-9339-FD56EF114549}" + ProjectSection(SolutionItems) = preProject + ..\..\..\Directory.Build.props = ..\..\..\Directory.Build.props + ..\..\..\..\..\Directory.Build.targets = ..\..\..\..\..\Directory.Build.targets + ..\..\..\..\..\global.json = ..\..\..\..\..\global.json + ..\..\..\..\..\Packages\Versions.Local.props = ..\..\..\..\..\Packages\Versions.Local.props + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dgmjr.AspNetCore.Authentication.Basic", "Dgmjr.AspNetCore.Authentication.Basic.csproj", "{F93943D3-352A-4FD7-9882-B74DE93B5DF0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Local|Any CPU = Local|Any CPU + Debug|Any CPU = Debug|Any CPU + Testing|Any CPU = Testing|Any CPU + Staging|Any CPU = Staging|Any CPU + Production|Any CPU = Production|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F93943D3-352A-4FD7-9882-B74DE93B5DF0}.Local|Any CPU.ActiveCfg = Local|Any CPU + {F93943D3-352A-4FD7-9882-B74DE93B5DF0}.Local|Any CPU.Build.0 = Local|Any CPU + {F93943D3-352A-4FD7-9882-B74DE93B5DF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F93943D3-352A-4FD7-9882-B74DE93B5DF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F93943D3-352A-4FD7-9882-B74DE93B5DF0}.Testing|Any CPU.ActiveCfg = Testing|Any CPU + {F93943D3-352A-4FD7-9882-B74DE93B5DF0}.Testing|Any CPU.Build.0 = Testing|Any CPU + {F93943D3-352A-4FD7-9882-B74DE93B5DF0}.Staging|Any CPU.ActiveCfg = Staging|Any CPU + {F93943D3-352A-4FD7-9882-B74DE93B5DF0}.Staging|Any CPU.Build.0 = Staging|Any CPU + {F93943D3-352A-4FD7-9882-B74DE93B5DF0}.Production|Any CPU.ActiveCfg = Local|Any CPU + {F93943D3-352A-4FD7-9882-B74DE93B5DF0}.Production|Any CPU.Build.0 = Local|Any CPU + {F93943D3-352A-4FD7-9882-B74DE93B5DF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F93943D3-352A-4FD7-9882-B74DE93B5DF0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7D1A0553-1A76-4B61-9F06-FEC9FB77B19B} + EndGlobalSection +EndGlobal diff --git a/src/Security/Authentication.Basic/LICENSE.md b/src/Security/Authentication.Basic/LICENSE.md new file mode 100644 index 00000000..4f592f86 --- /dev/null +++ b/src/Security/Authentication.Basic/LICENSE.md @@ -0,0 +1,35 @@ +--- +date: 2023-07-13T05:44:46:00-05:00Z +description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... +keywords: +- IP +- copyright +- license +- mit +permissions: +- commercial-use +- modifications +- distribution +- private-use +conditions: +- include-copyright +limitations: +- liability +- warranty +lastmod: 2024-01-0T00:39:00.0000+05:00Z +license: MIT +slug: mit-license +title: MIT License +type: license +--- + +# MIT License + +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/src/Security/Authentication.Basic/icon.png b/src/Security/Authentication.Basic/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..db07a039193d57c5147e651a7ab732121bfd20ba GIT binary patch literal 26997 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 CAey28 literal 0 HcmV?d00001 diff --git a/src/Security/Authentication/AddApiAuthentication.cs b/src/Security/Authentication/AddApiAuthentication.cs deleted file mode 100644 index 589181ca..00000000 --- a/src/Security/Authentication/AddApiAuthentication.cs +++ /dev/null @@ -1,61 +0,0 @@ -// /* -// * AddApiAuthentication.cs -// * -// * Created: 2022-12-10-08:00:57 -// * Modified: 2022-12-10-08:00:57 -// * -// * Author: David G. Moore, Jr, -// * -// * Copyright © 2022-2023 David G. Moore, Jr,, All Rights Reserved -// * License: MIT (https://opensource.org/licenses/MIT) -// */ -// #pragma warning disable -// using Dgmjr.AspNetCore.Authentication; -// using Dgmjr.AspNetCore.Authentication.Handlers; -// using Dgmjr.AspNetCore.Authentication.Options; -// using Dgmjr.Identity; -// using Dgmjr.Identity.Models; -// using Microsoft.AspNetCore.Authentication; -// using Microsoft.AspNetCore.Builder; -// using Microsoft.AspNetCore.Identity; -// using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -// using IdentityDbContext = Dgmjr.Identity.IdentityDbContext; - -// namespace Microsoft.Extensions.DependencyInjection; - -// public static class AddApiAuthenticationExtensions -// { -// // /// Registers the API authentication middleware. -// // public static WebApplicationBuilder AddApiAuthentication( -// // this WebApplicationBuilder builder, -// // Action? basicConfig = null, -// // Action? jwtConfig = null, -// // Action? sharedSecretConfig = null -// // ) -// // { -// // builder.Services.AddScoped>(); -// // builder.Services.AddAuthentication().AddApiBasicAuthentication(); -// // return builder; -// // } - -// public static AuthenticationBuilder AddApiBasicAuthentication( -// this AuthenticationBuilder builder -// ) -// where TUser : class, IIdentityUserBase, IHaveATelegramUsername, IIdentifiable -// where TRole : class, IIdentityRoleBase, IIdentifiable => -// builder.AddScheme>( -// ApiAuthenticationOptions.BasicAuthenticationSchemeName, -// ApiAuthenticationOptions.BasicAuthenticationSchemeName, -// _ => { } -// ); - -// public static WebApplication UseApiBasicAuthentication(this WebApplication app) -// where TUser : class, IIdentityUserBase, IHaveATelegramUsername, IIdentifiable -// where TRole : class, IIdentityRoleBase, IIdentifiable -// { -// // app.UseAuthentication(); -// // return app; -// app.UseMiddleware>(); -// return app; -// } -// } diff --git a/src/Security/Authentication/AllowAnonymousAttribute.cs b/src/Security/Authentication/AllowAnonymousAttribute.cs deleted file mode 100644 index 5b0237da..00000000 --- a/src/Security/Authentication/AllowAnonymousAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -/* - * AllowAnonymousAttribute.cs - * - * Created: 2022-12-14-10:52:11 - * Modified: 2022-12-14-10:52:11 - * - * Author: David G. Moore, Jr, - * - * Copyright © 2022-2023 David G. Moore, Jr,, All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -// namespace Dgmjr.AspNetCore.Authorization; - -// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -// public class AllowAnonymousAttribute : Attribute { } diff --git a/src/Security/Authentication/ApiAuthenticationExtensions.cs b/src/Security/Authentication/ApiAuthenticationExtensions.cs deleted file mode 100644 index f7e73016..00000000 --- a/src/Security/Authentication/ApiAuthenticationExtensions.cs +++ /dev/null @@ -1,224 +0,0 @@ -// using System; -// using System; -// /* -// * ApiAuthenticationExtensions.cs -// * -// * Created: 2023-03-29-11:16:18 -// * Modified: 2023-03-29-11:16:19 -// * -// * Author: David G. Moore, Jr. -// * -// * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved -// * License: MIT (https://opensource.org/licenses/MIT) -// */ - -// using System.IdentityModel.Tokens.Jwt; -// using System.Net.Http.Headers; -// using System.Text; -// using Dgmjr.AspNetCore.Authentication.JwtBearer; -// using Dgmjr.AspNetCore.Authentication.Options; -// using Dgmjr.Identity; -// using Microsoft.AspNetCore.Authentication; -// using Microsoft.Extensions.Configuration; -// using Microsoft.Extensions.Logging; -// using Microsoft.IdentityModel.Tokens; - -// namespace Dgmjr.AspNetCore.Authentication; - -// public class ApiAuthenticationExtensions : ILog -// where TKey : IEquatable, IComparable -// where TUser : class, IIdentityUser, IIdentityUserBase -// where TRole : class, IIdentityRole, IIdentityRoleBase -// { -// public const string Basic = nameof(Basic); -// public const string Bearer = nameof(Bearer); - -// public ILogger Logger { get; } -// public IConfiguration Configuration { get; } -// public UserManager UserManager { get; } - -// public ApiAuthenticationExtensions( -// ILogger> logger, -// IConfiguration config, -// UserManager userManager -// ) -// { -// Logger = logger; -// Configuration = config; -// UserManager = userManager; -// } - -// public JwtConfigurationOptions GetJwtConfigurationOptions() -// { -// var jwtConfig = new JwtConfigurationOptions(); -// Configuration.GetSection(nameof(JwtConfigurationOptions)).Bind(jwtConfig); -// return jwtConfig; -// } - -// public TokenValidationParameters GetTokenValidationParameters() -// { -// var jwtConfig = GetJwtConfigurationOptions(); -// return new TokenValidationParameters -// { -// ValidateIssuerSigningKey = true, -// IssuerSigningKey = new SymmetricSecurityKey(jwtConfig.Secret.ToUTF8Bytes()), -// ValidateIssuer = true, -// ValidIssuer = jwtConfig.Issuer, -// ValidateAudience = true, -// ValidAudience = jwtConfig.Audience, -// ValidateLifetime = true, -// ClockSkew = TimeSpan.Zero, -// RequireExpirationTime = true -// }; -// } - -// public async Task AuthenticateAsync(HttpContext context) -// { -// var jwtConfig = GetJwtConfigurationOptions(); -// var tokenHandler = new JwtSecurityTokenHandler(); -// Logger.LogBeginAuthentication( -// context.TraceIdentifier, -// context.Request.Path, -// context.Request.Method -// ); -// try -// { -// var authHeader = AuthenticationHeaderValue.Parse( -// context.Request.Headers["Authorization"] -// ); -// switch (authHeader.Scheme) -// { -// case Basic: -// var credentialBytes = authHeader.Parameter.FromBase64String(); -// var credentials = credentialBytes.ToUTF8String().Split(':', 2); -// var authUsername = credentials[0]; -// var authPassword = credentials[1]; -// Logger.LogAuthenticatingUser(authUsername, Basic); - -// // authenticate credentials with user service and attach user to http context -// var user = await UserManager.FindByNameAsync(authUsername); -// if ( -// user is not null && await UserManager.CheckPasswordAsync(user, authPassword) -// ) -// { -// var identity = new ClaimsIdentity( -// ApiAuthenticationOptions.BasicAuthenticationSchemeName -// ); -// var userClaims = await UserManager.GetClaimsAsync(user); - -// userClaims.Add( -// new(TelegramID.ClaimTypes.UserId.UriString, user.Id.ToString()) -// ); -// userClaims.Add(new(DgmjrCt.NameIdentifier.UriString, user.Id.ToString())); -// userClaims.Add( -// new( -// DgmjrCt.AuthenticationInstant.UriString, -// DateTimeOffset.UtcNow.ToString() -// ) -// ); -// userClaims.Add( -// new( -// DgmjrCt.AuthenticationMethod.UriString, -// ApiAuthenticationOptions.BasicAuthenticationSchemeName -// ) -// ); -// userClaims.Add(new(DgmjrCt.CommonName.UriString, user.GoByName)); -// userClaims.Add(new(DgmjrCt.GivenName.UriString, user.GivenName)); -// userClaims.Add(new(DgmjrCt.Surname.UriString, user.Surname)); - -// if ( -// !IsNullOrEmpty(user.TelegramUsername) -// && !userClaims.Any( -// c => c.Type == TelegramID.ClaimTypes.Username.UriString -// ) -// ) -// { -// userClaims.Add( -// new(TelegramID.ClaimTypes.Username.UriString, user.TelegramUsername) -// ); -// } - -// if ( -// !user.PhoneNumber.IsEmpty -// && !userClaims.Any(c => c.Type == DgmjrCt.HomePhone.UriString) -// ) -// { -// userClaims.Add(new(DgmjrCt.HomePhone.UriString, user.PhoneNumber)); -// } - -// if ( -// !user.EmailAddress.IsEmpty -// && !userClaims.Any(c => c.Type == DgmjrCt.Email.UriString) -// ) -// { -// userClaims.Add(new(DgmjrCt.Email.UriString, user.EmailAddress)); -// } - -// identity.AddClaims(userClaims); -// var principal = new ClaimsPrincipal(identity); -// var tokenDescriptor = new SecurityTokenDescriptor -// { -// Subject = identity, -// Audience = jwtConfig.Audience, -// Issuer = jwtConfig.Issuer, -// IssuedAt = datetime.UtcNow, -// NotBefore = datetime.UtcNow, -// Expires = datetime.UtcNow + jwtConfig.TokenLifetime, -// }; -// var token = tokenHandler.CreateToken(tokenDescriptor); -// var tokenString = tokenHandler.WriteToken(token); -// return AuthenticationResult.SuccessTokenIssued.WithToken(tokenString); -// } -// else -// { -// Logger.UserAuthenticationFailed(authUsername); -// return AuthenticationResult.FailInvalidPassword.Instance; -// } -// case Bearer: -// if (tokenHandler.CanReadToken(authHeader.Parameter)) -// { -// var tokenValidationParameters = GetTokenValidationParameters(); -// var principal = tokenHandler.ValidateToken( -// authHeader.Parameter, -// tokenValidationParameters, -// out var validatedToken -// ); -// var jwtToken = (JwtSecurityToken)validatedToken; -// var username = jwtToken.Claims -// .First(x => x.Type == Telegram.Identity.ClaimTypes.Username.UriString) -// ?.Value; -// if (jwtToken.IssuedAt + jwtConfig.TokenLifetime > datetime.UtcNow) -// { -// Logger.LogExpiredToken( -// context.TraceIdentifier, -// username, -// jwtToken.IssuedAt, -// jwtToken.IssuedAt + jwtConfig.TokenLifetime, -// datetime.UtcNow -// ); -// // context.Response.Headers["WWW-Authenticate"] = "Bearer"; -// // context.Response.StatusCode = (int)Unauthorized; -// return AuthenticationResult.FailExpiredToken.Instance; -// } -// Logger.LogUserAuthenticated(username, jwtToken.Claims.Count()); -// context.User = principal; -// } -// else -// { -// Logger.LogInvalidAuthHeader(context.TraceIdentifier); -// return AuthenticationResult.FailInvalidAuthHeader.Instance; -// } -// ; -// break; -// default: -// return AuthenticationResult.FailInvalidAuthHeader.Instance; -// } -// } -// catch -// { -// Logger.LogInvalidAuthHeader(context.TraceIdentifier); -// return AuthenticationResult.FailInvalidAuthHeader.Instance; -// } -// return AuthenticationResult.FailUnknownReason.Instance; -// } -// } diff --git a/src/Security/Authentication/ApiAuthorizeAttribute.cs b/src/Security/Authentication/ApiAuthorizeAttribute.cs deleted file mode 100644 index 6f1b2331..00000000 --- a/src/Security/Authentication/ApiAuthorizeAttribute.cs +++ /dev/null @@ -1,47 +0,0 @@ -// /* -// * ApiAuthorizeAttribute.cs -// * -// * Created: 2022-12-10-07:15:41 -// * Modified: 2022-12-10-07:15:41 -// * -// * Author: David G. Moore, Jr, -// * -// * Copyright © 2022-2023 David G. Moore, Jr,, All Rights Reserved -// * License: MIT (https://opensource.org/licenses/MIT) -// */ - -// using Dgmjr.Identity.Models; -// using Microsoft.AspNetCore.Authorization; -// using Microsoft.AspNetCore.Http; -// using Microsoft.AspNetCore.Mvc; -// using Microsoft.AspNetCore.Mvc.Filters; - -// namespace Dgmjr.AspNetCore.Authentication; - -// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -// public class ApiAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter -// { -// public void OnAuthorization(AuthorizationFilterContext context) -// { -// // skip authorization if action is decorated with [AllowAnonymous] attribute -// var allowAnonymous = context.ActionDescriptor.EndpointMetadata -// .OfType() -// .Any(); -// if (allowAnonymous) -// return; - -// var user = context.HttpContext.Items[nameof(User)] as User; -// if (user is null) -// { -// // not logged in - return 401 unauthorized -// context.Result = new JsonResult(new { message = "Unauthorized" }) -// { -// StatusCode = StatusCodes.Status401Unauthorized -// }; - -// // set 'WWW-Authenticate' header to trigger login popup in browsers -// context.HttpContext.Response.Headers["WWW-Authenticate"] = -// "Basic realm=\"\", charset=\"UTF-8\""; -// } -// } -// } diff --git a/src/Security/Authentication/AuthenticationHandler.cs b/src/Security/Authentication/AuthenticationHandler.cs new file mode 100644 index 00000000..aa1017f4 --- /dev/null +++ b/src/Security/Authentication/AuthenticationHandler.cs @@ -0,0 +1,15 @@ +/* + * AuthenticationHandler.cs + * + * Created: 2024-13-30T02:13:57-05:00 + * Modified: 2024-13-30T02:13:57-05:00 + * + * Author: David G. Moore, Jr. + * + * Copyright © 2024 David G. Moore, Jr., All Rights Reserved + * License: MIT (https://opensource.org/licenses/MIT) + */ + +namespace Dgmjr.AspNetCore.Authentication; + +public class AuthenticationHandler { } diff --git a/src/Security/Authentication/AuthenticationResult.cs b/src/Security/Authentication/AuthenticationResult.cs deleted file mode 100644 index bfdd1b44..00000000 --- a/src/Security/Authentication/AuthenticationResult.cs +++ /dev/null @@ -1,64 +0,0 @@ -/* - * AuthenticationResult.cs - * - * Created: 2023-03-30-08:13:27 - * Modified: 2023-03-30-08:13:27 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -namespace Dgmjr.AspNetCore.Authentication.Enums -{ - [GenerateEnumerationRecordStruct( - nameof(AuthenticationResult), - "Dgmjr.AspNetCore.Authentication" - )] - public enum AuthenticationResult - { - SuccessTokenIssued, - SuccessTokenValidated, - FailExpiredToken, - FailInvalidToken, - FailInvalidCredentials, - FailInvalidUser, - FailInvalidPassword, - FailInvalidAuthHeader, - FailUnknownReason - } -} - -namespace Dgmjr.AspNetCore.Authentication.Abstractions -{ - public partial interface IAuthenticationResult { } -} - -namespace Dgmjr.AspNetCore.Authentication -{ - public partial record struct AuthenticationResult - { - public partial record struct SuccessTokenValidated : Abstractions.IAuthenticationResult - { - public string Token { get; init; } - - public static Abstractions.IAuthenticationResult WithToken(string token) => - new SuccessTokenValidated() with - { - Token = token - }; - } - - public partial record struct SuccessTokenIssued : Abstractions.IAuthenticationResult - { - public string Token { get; init; } - - public static Abstractions.IAuthenticationResult WithToken(string token) => - new SuccessTokenIssued() with - { - Token = token - }; - } - } -} diff --git a/src/Security/Authentication/AuthorizationHeaderParameterOperationFilter.cs b/src/Security/Authentication/AuthorizationHeaderParameterOperationFilter.cs deleted file mode 100644 index b6001263..00000000 --- a/src/Security/Authentication/AuthorizationHeaderParameterOperationFilter.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* - * AuthorizationHeaderParameterOperationFilter.cs - * - * Created: 2022-12-10-07:05:54 - * Modified: 2022-12-10-07:05:55 - * - * Author: David G. Moore, Jr, - * - * Copyright © 2022-2023 David G. Moore, Jr,, All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -namespace Dgmjr.AspNetCore.Authentication; - -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Mvc.Authorization; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -public class AuthorizationHeaderParameterOperationFilter : IOperationFilter -{ - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - var filterPipeline = context.ApiDescription.ActionDescriptor.FilterDescriptors; - var isAuthorized = filterPipeline - .Select(filterInfo => filterInfo.Filter) - .Any(filter => filter is AuthorizeFilter); - var allowAnonymous = filterPipeline - .Select(filterInfo => filterInfo.Filter) - .Any(filter => filter is IAllowAnonymousFilter); - - if (isAuthorized && !allowAnonymous) - { - if (operation.Parameters == null) - operation.Parameters = new List(); - - operation.Parameters.Add( - new OpenApiParameter - { - Name = "Authorization", - In = ParameterLocation.Header, - Description = "access token", - Required = true, - Schema = new() { Type = "string", Default = new OpenApiString("Bearer ") } - } - ); - } - } -} diff --git a/src/Security/Authentication/Basic/BasicApiAuthHandler.cs b/src/Security/Authentication/Basic/BasicApiAuthHandler.cs deleted file mode 100644 index ce5040cb..00000000 --- a/src/Security/Authentication/Basic/BasicApiAuthHandler.cs +++ /dev/null @@ -1,150 +0,0 @@ -/* - * BasicApiAuthHandler.cs - * - * Created: 2022-12-19-06:50:32 - * Modified: 2022-12-31-05:07:17 - * - * Author: David G. Moore, Jr, - * - * Copyright © 2022-2023 David G. Moore, Jr,, All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -namespace Dgmjr.AspNetCore.Authentication.Handlers; - -using System; -using System.Net.Http.Headers; -using System.Security.Claims; -using System.Text; -using System.Text.Encodings.Web; -using Dgmjr.Abstractions; -using Dgmjr.AspNetCore.Authentication.Options; -using Dgmjr.Identity; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; - -public class BasicApiAuthHandler - : AuthenticationHandler, - IHttpContextAccessor, - ILog - where TUser : class, IIdentityUserBase - where TRole : class, IIdentityRoleBase -{ - public new virtual ILogger Logger { get; } - private readonly UserManager _userManager; - private readonly BasicAuthenticationSchemeOptions _options; - public HttpContext? HttpContext - { - get => Request.HttpContext; - set { } - } - - public BasicApiAuthHandler( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock, - UserManager userManager - ) - : base(options, logger, encoder, clock) - { - _userManager = userManager; - _options = options.CurrentValue; - Logger = logger.CreateLogger>(); - } - - protected virtual string? AuthenticationSchemeName => - (_options as IBasicAuthenticationSchemeOptions)?.AuthenticationSchemeName; - - protected override async Task HandleAuthenticateAsync() - { - try - { - var authHeader = AuthenticationHeaderValue.Parse( - Request.Headers[nameof(HttpRequestHeaders.Authorization)] - ); - var credentialBytes = authHeader.Parameter.FromBase64String(); - var credentials = credentialBytes.ToUTF8String().Split(':', 2); - var authUsername = credentials[0]; - var authPassword = credentials[1]; - Logger.LogAuthenticatingUser( - authUsername, - BasicAuthenticationSchemeOptions.DefaultAuthenticationSchemeName - ); - - // authenticate credentials with user service and attach user to http context - var user = await _userManager.FindByNameAsync(authUsername); - if (user is not null && await _userManager.CheckPasswordAsync(user, authPassword)) - { - var identity = new ClaimsIdentity(AuthenticationSchemeName); - var userClaims = await _userManager.GetClaimsAsync(user); - - // userClaims.Add(new(TelegramID.ClaimTypes.UserId.UriString, user.Id.ToString())); - // userClaims.Add(new(DgmjrCt.NameIdentifier.UriString, user.Id.ToString())); - userClaims.Add( - new(DgmjrCt.AuthenticationInstant.UriString, DateTimeOffset.UtcNow.ToString()) - ); - userClaims.Add( - new(DgmjrCt.AuthenticationMethod.UriString, AuthenticationSchemeName) - ); - userClaims.Add(new(DgmjrCt.CommonName.UriString, user.GoByName)); - userClaims.Add(new(DgmjrCt.GivenName.UriString, user.GivenName)); - userClaims.Add(new(DgmjrCt.Surname.UriString, user.Surname)); - - // if ( - // !IsNullOrEmpty(user.TelegramUsername) - // && !userClaims.Any(c => c.Type == TelegramID.ClaimTypes.Username.UriString) - // ) - // { - // userClaims.Add( - // new(TelegramID.ClaimType.Username.UriString, user.TelegramUsername) - // ); - // } - - if ( - !user.PhoneNumber.IsEmpty - && !userClaims.Any(c => c.Type == DgmjrCt.HomePhone.UriString) - ) - { - userClaims.Add(new(DgmjrCt.HomePhone.UriString, user.PhoneNumber)); - } - - if ( - !user.EmailAddress.IsEmpty - && !userClaims.Any(c => c.Type == DgmjrCt.Email.UriString) - ) - { - userClaims.Add(new(DgmjrCt.Email.UriString, user.EmailAddress)); - } - - identity.AddClaims(userClaims); - var principal = new ClaimsPrincipal(identity); - // var tokenDescriptor = new SecurityTokenDescriptor - // { - // Subject = identity, - // Expires = UtcNow + _options.TokenLifetime, - // }; - var ticket = new AuthenticationTicket(principal, AuthenticationSchemeName); - Logger.LogUserAuthenticated(authUsername, userClaims.Count); - return AuthenticateResult.Success(ticket); - } - else if (user is null) - { - Logger.UserAuthenticationFailed(authUsername); - return AuthenticateResult.Fail("Invalid username or password."); - } - Logger.UserAuthenticationFailed(authUsername); - return AuthenticateResult.Fail( - "An unknown error occurred while authenticating the user." - ); - } - catch (Exception ex) - { - Logger.LogAuthenticationError(ex.Message, ex.StackTrace); - return AuthenticateResult.Fail("An error occurred while authenticating the user."); - } - } -} diff --git a/src/Security/Authentication/Constants.cs b/src/Security/Authentication/Constants.cs deleted file mode 100644 index 17b77c03..00000000 --- a/src/Security/Authentication/Constants.cs +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Constants.cs - * - * Created: 2023-04-02-06:13:16 - * Modified: 2023-04-02-06:13:16 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -using Microsoft.AspNetCore.Authentication.JwtBearer; - -namespace Dgmjr.AspNetCore.Authentication; - -public static class Constants -{ - public static class AuthenticationSchemes - { - public static class Api - { - public const string Name = "Api"; - public const string DisplayName = "API Combined Auhentication"; - } - - public static class Basic - { - public const string Name = "Basic"; - public const string DisplayName = "Basic Authentication with username and password"; - } - - public static class Bearer - { - public const string Name = JwtBearerDefaults.AuthenticationScheme; - public const string DisplayName = JwtBearerDefaults.AuthenticationScheme; - } - - public static class JwtBearer - { - public const string Name = nameof(JwtBearer); - public const string DisplayName = nameof(JwtBearer); - } - - public static class SharedSecret - { - public const string Name = "SharedSecret"; - public const string DisplayName = "Shared Secret"; - } - - public static class Cookies - { - public const string Name = Microsoft - .AspNetCore - .Authentication - .Cookies - .CookieAuthenticationDefaults - .AuthenticationScheme; - public const string DisplayName = Microsoft - .AspNetCore - .Authentication - .Cookies - .CookieAuthenticationDefaults - .AuthenticationScheme; - } - - public static class OAuth - { - public const string Name = nameof(OAuth); - public const string DisplayName = nameof(OAuth); - } - } -} diff --git a/src/Security/Authentication/Dgmjr.AspNetCore.Authentication.csproj b/src/Security/Authentication/Dgmjr.AspNetCore.Authentication.csproj index 8ba3b216..2961ef48 100644 --- a/src/Security/Authentication/Dgmjr.AspNetCore.Authentication.csproj +++ b/src/Security/Authentication/Dgmjr.AspNetCore.Authentication.csproj @@ -1,58 +1,20 @@ - netstandard2.0;netstandard2.1;net6.0;net8.0 - - security + net6.0;net8.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/Security/Authentication/Dgmjr.AspNetCore.Authentication.props b/src/Security/Authentication/Dgmjr.AspNetCore.Authentication.props deleted file mode 100644 index 763c212e..00000000 --- a/src/Security/Authentication/Dgmjr.AspNetCore.Authentication.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/src/Security/Authentication/Dgmjr.AspNetCore.Authentication.sln b/src/Security/Authentication/Dgmjr.AspNetCore.Authentication.sln index ce3abd1f..5b0d68e0 100644 --- a/src/Security/Authentication/Dgmjr.AspNetCore.Authentication.sln +++ b/src/Security/Authentication/Dgmjr.AspNetCore.Authentication.sln @@ -2,13 +2,13 @@ # Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B283EBC2-E01F-412D-9339-FD56EF114549}" ProjectSection(SolutionItems) = preProject - ..\..\..\..\..\Directory.Build.props = ..\..\..\..\..\Directory.Build.props + ..\..\..\Directory.Build.props = ..\..\..\Directory.Build.props ..\..\..\..\..\Directory.Build.targets = ..\..\..\..\..\Directory.Build.targets ..\..\..\..\..\global.json = ..\..\..\..\..\global.json ..\..\..\..\..\Packages\Versions.Local.props = ..\..\..\..\..\Packages\Versions.Local.props EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dgmjr.AspNetCore.Authentication", "Dgmjr.AspNetCore.Authentication.csproj", "{710FC597-9D1D-4E75-A5B8-411A1EF56065}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dgmjr.AspNetCore.Authentication", "Dgmjr.AspNetCore.Authentication.csproj", "{6ED2A526-1BA1-42C9-806D-739DD3D92F25}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -20,23 +20,23 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {710FC597-9D1D-4E75-A5B8-411A1EF56065}.Local|Any CPU.ActiveCfg = Local|Any CPU - {710FC597-9D1D-4E75-A5B8-411A1EF56065}.Local|Any CPU.Build.0 = Local|Any CPU - {710FC597-9D1D-4E75-A5B8-411A1EF56065}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {710FC597-9D1D-4E75-A5B8-411A1EF56065}.Debug|Any CPU.Build.0 = Debug|Any CPU - {710FC597-9D1D-4E75-A5B8-411A1EF56065}.Testing|Any CPU.ActiveCfg = Testing|Any CPU - {710FC597-9D1D-4E75-A5B8-411A1EF56065}.Testing|Any CPU.Build.0 = Testing|Any CPU - {710FC597-9D1D-4E75-A5B8-411A1EF56065}.Staging|Any CPU.ActiveCfg = Staging|Any CPU - {710FC597-9D1D-4E75-A5B8-411A1EF56065}.Staging|Any CPU.Build.0 = Staging|Any CPU - {710FC597-9D1D-4E75-A5B8-411A1EF56065}.Production|Any CPU.ActiveCfg = Local|Any CPU - {710FC597-9D1D-4E75-A5B8-411A1EF56065}.Production|Any CPU.Build.0 = Local|Any CPU - {710FC597-9D1D-4E75-A5B8-411A1EF56065}.Release|Any CPU.ActiveCfg = Release|Any CPU - {710FC597-9D1D-4E75-A5B8-411A1EF56065}.Release|Any CPU.Build.0 = Release|Any CPU + {6ED2A526-1BA1-42C9-806D-739DD3D92F25}.Local|Any CPU.ActiveCfg = Local|Any CPU + {6ED2A526-1BA1-42C9-806D-739DD3D92F25}.Local|Any CPU.Build.0 = Local|Any CPU + {6ED2A526-1BA1-42C9-806D-739DD3D92F25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6ED2A526-1BA1-42C9-806D-739DD3D92F25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6ED2A526-1BA1-42C9-806D-739DD3D92F25}.Testing|Any CPU.ActiveCfg = Testing|Any CPU + {6ED2A526-1BA1-42C9-806D-739DD3D92F25}.Testing|Any CPU.Build.0 = Testing|Any CPU + {6ED2A526-1BA1-42C9-806D-739DD3D92F25}.Staging|Any CPU.ActiveCfg = Staging|Any CPU + {6ED2A526-1BA1-42C9-806D-739DD3D92F25}.Staging|Any CPU.Build.0 = Staging|Any CPU + {6ED2A526-1BA1-42C9-806D-739DD3D92F25}.Production|Any CPU.ActiveCfg = Local|Any CPU + {6ED2A526-1BA1-42C9-806D-739DD3D92F25}.Production|Any CPU.Build.0 = Local|Any CPU + {6ED2A526-1BA1-42C9-806D-739DD3D92F25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6ED2A526-1BA1-42C9-806D-739DD3D92F25}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {C0DF569E-7097-4B7B-925D-9023CDD69A93} + SolutionGuid = {141FF18A-6A36-47BC-9F3D-FFBC944FB2E1} EndGlobalSection EndGlobal diff --git a/src/Security/Authentication/Dgmjr.AspNetCore.Authentication.targets b/src/Security/Authentication/Dgmjr.AspNetCore.Authentication.targets deleted file mode 100644 index e349d9e8..00000000 --- a/src/Security/Authentication/Dgmjr.AspNetCore.Authentication.targets +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/Security/Authentication/Generated/GeneratedAuthenticationSchemeOptions.cs b/src/Security/Authentication/Generated/GeneratedAuthenticationSchemeOptions.cs deleted file mode 100644 index b0441cb9..00000000 --- a/src/Security/Authentication/Generated/GeneratedAuthenticationSchemeOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -/* - * GeneratedAuthenticationSchemeOptions.cs - * - * Created: 2023-04-01-02:39:31 - * Modified: 2023-04-01-02:39:31 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -using Microsoft.AspNetCore.Authentication; - -namespace Dgmjr.AspNetCore.Authentication.Generated; - -public class GeneratedAuthenticationSchemeOptions : AuthenticationSchemeOptions -{ - public string Secret { get; set; } = default!; - public string Issuer { get; set; } = DgmjrCt.DgmjrClaims.UriString; - public string Audience { get; set; } = DgmjrCt.DgmjrClaims.UriString; - public duration TokenLifetime { get; set; } = duration.FromMinutes(60); -} diff --git a/src/Security/Authentication/Generated/GeneratedCustomAuthHandler.cs b/src/Security/Authentication/Generated/GeneratedCustomAuthHandler.cs deleted file mode 100644 index 57a82f4a..00000000 --- a/src/Security/Authentication/Generated/GeneratedCustomAuthHandler.cs +++ /dev/null @@ -1,257 +0,0 @@ -/* - * GeneratedCustomAuthHandler.cs - * - * Created: 2023-04-01-02:38:20 - * Modified: 2023-04-01-02:38:20 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -namespace Dgmjr.AspNetCore.Authentication.Generated; - -using System; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Net.Http.Headers; -using System.Security.Claims; -using System.Text; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Dgmjr.Identity; -using Dgmjr.Identity.Models; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using Dgmjr.AspNetCore.Authentication.Options; - -public class JwtBearerBasicAuthHandler - : AuthenticationHandler - where TUser : class, IIdentityUserBase, IHaveATelegramUsername, IIdentifiable - where TRole : class, IIdentityRoleBase -{ - private const string AuthorizationHeaderName = HReqH.Authorization.Name; - private const string BasicSchemeName = "Basic"; - private const string BearerSchemeName = "Bearer"; - protected virtual UserManager UserManager { get; init; } - - private readonly SymmetricSecurityKey _jwtKey; - - public JwtBearerBasicAuthHandler( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock, - UserManager userManager - ) - : base(options, logger, encoder, clock) - { - if (options?.CurrentValue?.Secret.IsNullOrWhitespace() ?? true) - { - throw new ArgumentNullException( - $"{nameof(options)}.{nameof(options.CurrentValue)}.{nameof(options.CurrentValue.Secret)}" - ); - } - var jwtSecret = options?.CurrentValue?.Secret.ToUTF8Bytes(); - _jwtKey = new SymmetricSecurityKey(jwtSecret); - } - - protected override async Task HandleAuthenticateAsync() - { - if (Request.Headers.TryGetValue(AuthorizationHeaderName, out var authHeaderStringValues)) - { - var authHeaderStringValue = authHeaderStringValues.Join(","); - if (authHeaderStringValue.IsNullOrWhitespace()) - { - return AuthenticateResult.NoResult(); - } - - var authHeader = AuthenticationHeaderValue.Parse(authHeaderStringValue); - - if (authHeader.Scheme.Equals(BasicSchemeName, OrdinalIgnoreCase)) - { - var credentials = authHeader.Parameter[(BasicSchemeName.Length + 1)..] - .FromBase64String() - .ToUTF8String() - .Split(':', 2); - var username = credentials[0]; - var password = credentials[1]; - - if ( - await IsAuthenticated( - username, - password, - out var user, - out var ticket, - out var identity - ) - ) - { - return AuthenticateResult.Success(ticket); - } - else - { - return AuthenticateResult.Fail("Invalid username or password"); - } - } - else if (authHeader.Scheme.Equals(BearerSchemeName, OrdinalIgnoreCase)) - { - var token = authHeader.Parameter; - var validationParameters = new TokenValidationParameters - { - ValidateIssuerSigningKey = true, - IssuerSigningKey = _jwtKey, - ValidateIssuer = true, - ValidateAudience = true, - ValidIssuers = new[] { DgmjrCt.DgmjrClaims.BaseUri }, - ValidAudiences = new[] { DgmjrCt.DgmjrClaims.BaseUri }, - ClockSkew = TimeSpan.Zero - }; - - try - { - var handler = new JwtSecurityTokenHandler(); - var claimsPrincipal = handler.ValidateToken( - token, - validationParameters, - out var securityToken - ); - var authenticationTicket = new AuthenticationTicket( - claimsPrincipal, - Scheme.Name - ); - return AuthenticateResult.Success(authenticationTicket); - } - catch (Exception ex) - { - return AuthenticateResult.Fail(ex.Message); - } - } - } - - return AuthenticateResult.NoResult(); - } - - private Task IsAuthenticated( - string username, - string password, - out TUser? user, - out AuthenticationTicket? ticket, - out ClaimsIdentity? identity - ) - { - user = UserManager.FindByNameAsync(username).Result; - if (user is not null && UserManager.CheckPasswordAsync(user, password).Result) - { - identity = new ClaimsIdentity(ApiAuthenticationOptions.BasicAuthenticationSchemeName); - var userClaims = UserManager.GetClaimsAsync(user).Result; - - userClaims.Add(new(TelegramID.ClaimTypes.UserId.UriString, user.Id.ToString())); - userClaims.Add(new(DgmjrCt.NameIdentifier.UriString, user.Id.ToString())); - userClaims.Add( - new(DgmjrCt.AuthenticationInstant.UriString, DateTimeOffset.UtcNow.ToString()) - ); - userClaims.Add( - new( - DgmjrCt.AuthenticationMethod.UriString, - ApiAuthenticationOptions.BasicAuthenticationSchemeName - ) - ); - userClaims.Add( - new( - DgmjrCt.CommonName.UriString, - user.GoByName, - Options.Issuer, - Options.Audience, - TelegramID.ClaimTypes.TelegramClaimType.UriString - ) - ); - userClaims.Add( - new( - DgmjrCt.GivenName.UriString, - user.GivenName, - Options.Issuer, - Options.Audience, - TelegramID.ClaimTypes.TelegramClaimType.UriString - ) - ); - userClaims.Add( - new( - DgmjrCt.Surname.UriString, - user.Surname, - Options.Issuer, - Options.Audience, - TelegramID.ClaimTypes.TelegramClaimType.UriString - ) - ); - - if ( - !IsNullOrEmpty(user.TelegramUsername) - && !userClaims.Any(c => c.Type == TelegramID.ClaimTypes.Username.UriString) - ) - { - userClaims.Add( - new( - TelegramID.ClaimTypes.Username.UriString, - user.TelegramUsername, - Options.Issuer, - Options.Audience, - TelegramID.ClaimTypes.TelegramClaimType.UriString - ) - ); - } - - if ( - !user.PhoneNumber.IsEmpty - && !userClaims.Any(c => c.Type == DgmjrCt.HomePhone.UriString) - ) - { - userClaims.Add( - new( - DgmjrCt.HomePhone.UriString, - user.PhoneNumber, - Options.Issuer, - Options.Audience, - TelegramID.ClaimTypes.TelegramClaimType.UriString - ) - ); - } - - if ( - !user.EmailAddress.IsEmpty - && !userClaims.Any(c => c.Type == DgmjrCt.Email.UriString) - ) - { - userClaims.Add( - new( - DgmjrCt.Email.UriString, - user.EmailAddress, - Options.Issuer, - Options.Audience, - Options.Issuer - ) - ); - } - - identity.AddClaims(userClaims); - var principal = new ClaimsPrincipal(identity); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = identity, - Expires = datetime.UtcNow + Options.TokenLifetime, - }; - ticket = new AuthenticationTicket( - principal, - ApiAuthenticationOptions.BasicAuthenticationSchemeName - ); - Logger.LogUserAuthenticated(username, userClaims.Count); - return Task.FromResult(true); - } - ticket = null; - identity = null; - return Task.FromResult(false); - } -} diff --git a/src/Security/Authentication/Handlers/ApiAuthHandlerBase.cs b/src/Security/Authentication/Handlers/ApiAuthHandlerBase.cs deleted file mode 100644 index 7960ea0c..00000000 --- a/src/Security/Authentication/Handlers/ApiAuthHandlerBase.cs +++ /dev/null @@ -1,44 +0,0 @@ -/* - * ApiAuthHandlerBase.cs - * - * Created: 2023-04-07-07:41:16 - * Modified: 2023-04-07-07:41:16 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -using System.Text.Encodings.Web; -using Dgmjr.AspNetCore.Authentication.Options; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Options; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Dgmjr.AspNetCore.Authentication.Handlers; - -public abstract class ApiAuthHandlerBase - : AuthenticationHandler, - IAuthenticationSignOutHandler, - IAuthenticationSignInHandler, - ILog - where TOptions : AuthenticationSchemeOptions, IAuthenticationSchemeOptions, new() -{ - public new virtual ILogger Logger { get; init; } - - public ApiAuthHandlerBase( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock - ) - : base(options, logger, encoder, clock) - { - Logger = logger.CreateLogger(GetType().FullName); - } - - public abstract Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties); - public abstract Task SignOutAsync(AuthenticationProperties? properties); -} diff --git a/src/Security/Authentication/Handlers/ApiMultiAuthenticationHandler.cs b/src/Security/Authentication/Handlers/ApiMultiAuthenticationHandler.cs deleted file mode 100644 index e4a95887..00000000 --- a/src/Security/Authentication/Handlers/ApiMultiAuthenticationHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -/* - * ApiMultiAuthenticationHandler.cs - * - * Created: 2023-04-06-04:20:28 - * Modified: 2023-04-06-04:20:29 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -using System.Text.Encodings.Web; -using Dgmjr.AspNetCore.Authentication.Options; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Dgmjr.AspNetCore.Authentication.Handlers; - -public class ApiMultiAuthenticationHandler : AuthenticationHandler -{ - public ApiMultiAuthenticationHandler( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock - ) - : base(options, logger, encoder, clock) { } - - protected override Task HandleAuthenticateAsync() - { - throw new NotImplementedException(); - } -} diff --git a/src/Security/Authentication/Handlers/IAuthenticationHandler.cs b/src/Security/Authentication/Handlers/IAuthenticationHandler.cs deleted file mode 100644 index 4632fb05..00000000 --- a/src/Security/Authentication/Handlers/IAuthenticationHandler.cs +++ /dev/null @@ -1,16 +0,0 @@ -/* - * IAuthenticationHandler.cs - * - * Created: 2023-05-07-05:14:17 - * Modified: 2023-05-07-05:14:17 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -namespace Dgmjr.AspNetCore.Authentication.Handlers.Abstractions; - -[GenerateInterface(typeof(Microsoft.AspNetCore.Authentication.AuthenticationHandler<>))] -public partial interface IAuthenticationHandler { } diff --git a/src/Security/Authentication/Handlers/JwtAuthHandler.cs b/src/Security/Authentication/Handlers/JwtAuthHandler.cs deleted file mode 100644 index 80e975a9..00000000 --- a/src/Security/Authentication/Handlers/JwtAuthHandler.cs +++ /dev/null @@ -1,164 +0,0 @@ -/* - * JwtAuthHandler.cs - * - * Created: 2023-04-06-02:15:37 - * Modified: 2023-04-06-02:15:53 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - - -namespace Dgmjr.AspNetCore.Authentication.Handlers; - -using System; -using System.Net.Http.Headers; -using System.Security.Claims; -using System.Text; -using System.Text.Encodings.Web; -using Dgmjr.Abstractions; -using Dgmjr.AspNetCore.Authentication.Options; -using Dgmjr.Identity; -using Dgmjr.Identity.Abstractions; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Extensions.Configuration; - -public class JwtAuthHandler : JwtBearerHandler, IHttpContextAccessor, ILog - where TUser : class, IIdentityUserBase, IHaveATelegramUsername, IIdentifiable - where TRole : class, IIdentityRoleBase -{ - public IConfiguration Configuration { get; } - public new ILogger Logger { get; } - private readonly UserManager _userManager; - private readonly JwtConfigurationOptions _options; - public HttpContext? HttpContext - { - get => Request.HttpContext; - set { } - } - - public JwtAuthHandler( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock, - UserManager userManager, - IConfiguration configuration - ) - : base(options, logger, encoder, clock) - { - _userManager = userManager; - _options = options.CurrentValue; - Logger = logger.CreateLogger>(); - Configuration = configuration; - } - - protected virtual string? AuthenticationSchemeName => _options?.AuthenticationSchemeName; - - protected override async Task HandleAuthenticateAsync() - { - try - { - var authHeader = AuthenticationHeaderValue.Parse( - Request.Headers[nameof(HttpRequestHeaders.Authorization)] - ); - var authUsername = authHeader.Parameter.Split(':')[0]; - var authPassword = authHeader.Parameter.Split(':')[1].FromBase64String().ToUTF8String(); - // opts.TokenValidationParameters = new TokenValidationParameters - // { - // ValidIssuer = _options?.ClaimsIssuer ?? DgmjrCt.DgmjrClaims.BaseUri, - // ValidAudience = Configuration[ - // $"{nameof(JwtConfigurationOptions)}:{nameof(JwtConfigurationOptions.Audience)}" - // ], - // IssuerSigningKey = new SymmetricSecurityKey( - // Configuration[ - // $"{nameof(JwtConfigurationOptions)}:{nameof(JwtConfigurationOptions.Secret)}" - // ].ToUTF8Bytes() - // ), - // ValidateIssuer = true, - // ValidateAudience = true, - // ValidateIssuerSigningKey = true - // }; - Logger.LogAuthenticatingUser(authUsername, nameof(JwtAuthHandler)); - - // authenticate credentials with user service and attach user to http context - var user = await _userManager.FindByNameAsync(authUsername); - if (user is not null && await _userManager.CheckPasswordAsync(user, authPassword)) - { - var identity = new ClaimsIdentity(AuthenticationSchemeName); - var userClaims = await _userManager.GetClaimsAsync(user); - - userClaims.Add(new(TelegramID.ClaimTypes.UserId.UriString, user.Id.ToString())); - userClaims.Add(new(DgmjrCt.NameIdentifier.UriString, user.Id.ToString())); - userClaims.Add( - new(DgmjrCt.AuthenticationInstant.UriString, DateTimeOffset.UtcNow.ToString()) - ); - userClaims.Add( - new(DgmjrCt.AuthenticationMethod.UriString, AuthenticationSchemeName) - ); - userClaims.Add(new(DgmjrCt.CommonName.UriString, user.GoByName)); - userClaims.Add(new(DgmjrCt.GivenName.UriString, user.GivenName)); - userClaims.Add(new(DgmjrCt.Surname.UriString, user.Surname)); - - if ( - !IsNullOrEmpty(user.TelegramUsername) - && !userClaims.Any(c => c.Type == TelegramID.ClaimTypes.Username.UriString) - ) - { - userClaims.Add( - new(TelegramID.ClaimTypes.Username.UriString, user.TelegramUsername) - ); - } - - if ( - !user.PhoneNumber.IsEmpty - && !userClaims.Any(c => c.Type == DgmjrCt.HomePhone.UriString) - ) - { - userClaims.Add(new(DgmjrCt.HomePhone.UriString, user.PhoneNumber)); - } - - if ( - !user.EmailAddress.IsEmpty - && !userClaims.Any(c => c.Type == DgmjrCt.Email.UriString) - ) - { - userClaims.Add(new(DgmjrCt.Email.UriString, user.EmailAddress)); - } - - identity.AddClaims(userClaims); - var principal = new ClaimsPrincipal(identity); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = identity, - Expires = datetime.UtcNow + _options.TokenLifetime, - }; - var ticket = new AuthenticationTicket(principal, AuthenticationSchemeName); - HttpContext.User = principal; - Logger.LogUserAuthenticated(authUsername, userClaims.Count); - return AuthenticateResult.Success(ticket); - } - else if (user is null) - { - Logger.UserAuthenticationFailed(authUsername); - return AuthenticateResult.Fail("Invalid username or password."); - } - Logger.UserAuthenticationFailed(authUsername); - return AuthenticateResult.Fail( - "An unknown error occurred while authenticating the user." - ); - } - catch (Exception ex) - { - Logger.LogAuthenticationError(ex.Message, ex.StackTrace); - return AuthenticateResult.Fail("An error occurred while authenticating the user."); - } - } -} diff --git a/src/Security/Authentication/Handlers/JwtExtensions.cs b/src/Security/Authentication/Handlers/JwtExtensions.cs deleted file mode 100644 index 0506cc0f..00000000 --- a/src/Security/Authentication/Handlers/JwtExtensions.cs +++ /dev/null @@ -1,96 +0,0 @@ -/* - * JwtExtensions.cs - * - * Created: 2023-03-29-04:05:26 - * Modified: 2023-03-29-04:05:26 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -namespace Dgmjr.AspNetCore.Authentication.JwtBearer; - -using System.Buffers; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; -using Dgmjr.AspNetCore.Authentication.Options; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.IdentityModel.Tokens; -using static System.Text.Encoding; - -public static class JwtExtensions -{ - public static AuthenticationBuilder AddJwtBearerAuthentication( - this WebApplicationBuilder builder, - Action configureOptions - ) - { - return builder.Services - .AddAuthentication(opts => - { - opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - opts.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer(opts => - { - configureOptions(opts); - opts.SaveToken = true; - opts.TokenValidationParameters = new TokenValidationParameters - { - ValidIssuer = builder.Configuration[ - $"{nameof(JwtConfigurationOptions)}:{nameof(JwtConfigurationOptions.ClaimsIssuer)}" - ], - ValidAudience = builder.Configuration[ - $"{nameof(JwtConfigurationOptions)}:{nameof(JwtConfigurationOptions.Audience)}" - ], - IssuerSigningKey = new SymmetricSecurityKey( - builder.Configuration[ - $"{nameof(JwtConfigurationOptions)}:{nameof(JwtConfigurationOptions.Secret)}" - ].ToUTF8Bytes() - ), - ValidateIssuer = true, - ValidateAudience = true, - ValidateIssuerSigningKey = true - }; - }); - } - - public static WebApplication UseJwtBearerAuthentication(this WebApplication app) - { - _ = app.MapGet( - "/auth/token", - async context => - { - var claims = new[] { new C(Ct.Name, "dgmjr"), new C(Ct.Role, "admin") }; - var key = new SymmetricSecurityKey( - app.Configuration[ - $"{nameof(JwtConfigurationOptions)}:{nameof(JwtConfigurationOptions.Secret)}" - ].ToUTF8Bytes() - ); - var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); - var token = new JwtSecurityToken( - issuer: app.Configuration[ - $"{nameof(JwtConfigurationOptions)}:{nameof(JwtConfigurationOptions.Issuer)}" - ], - audience: app.Configuration[ - $"{nameof(JwtConfigurationOptions)}:{nameof(JwtConfigurationOptions.Audience)}" - ], - claims: claims, - expires: datetime.UtcNow.AddMinutes(value: 5252600), - signingCredentials: creds - ); - await context.Response.WriteAsync(new JwtSecurityTokenHandler().WriteToken(token)); - } - ); - app.UseAuthentication(); - return app; - } -} diff --git a/src/Security/Authentication/Handlers/SharedSecretAuthHandler.cs b/src/Security/Authentication/Handlers/SharedSecretAuthHandler.cs deleted file mode 100644 index 76f1477c..00000000 --- a/src/Security/Authentication/Handlers/SharedSecretAuthHandler.cs +++ /dev/null @@ -1,154 +0,0 @@ -/* - * SharedSecretAuthHandler.cs - * - * Created: 2022-12-31-05:06:39 - * Modified: 2022-12-31-05:07:21 - * - * Author: David G. Moore, Jr, - * - * Copyright © 2022-2023 David G. Moore, Jr,, All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -namespace Dgmjr.AspNetCore.Authorization.Handlers; - -using System.Net.Http.Headers; -using System.Security.Claims; -using System.Text.Encodings.Web; -using Dgmjr.Abstractions; -using Dgmjr.AspNetCore.Authentication; -using Dgmjr.AspNetCore.Authentication.Options; -using Dgmjr.Identity; -using Dgmjr.Identity.Abstractions; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using static System.Text.TextEncodingExtensions; -using ClaimTypes = DgmjrCt; - -public class SharedSecretAuthHandler - : AuthenticationHandler, - IHttpContextAccessor, - ILog - where TUser : class, IIdentityUserBase, IHaveATelegramUsername, IIdentifiable - where TRole : class, IIdentityRoleBase -{ - public new virtual ILogger Logger { get; } = - new NullLogger>(); - private readonly UserManager _userManager; - private readonly SharedSecretAuthenticationOptions _options; - public HttpContext? HttpContext - { - get => Request.HttpContext; - set { } - } - - public SharedSecretAuthHandler( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock, - UserManager userManager - ) - : base(options, logger, encoder, clock) - { - _userManager = userManager; - _options = options.CurrentValue; - } - - protected override async Task HandleAuthenticateAsync() - { - try - { - var authHeader = AuthenticationHeaderValue.Parse( - Request.Headers[HReqH.Authorization.DisplayName] - ); - var credentialBytes = authHeader.Parameter.FromBase64String(); - var credentials = GetUTF8String(credentialBytes).Split(':', 2); - var authUsername = credentials[0]; - var authSecret = credentials[1]; - Logger.LogAuthenticatingUser( - authUsername, - SharedSecretAuthenticationOptions.SharedSecretAuthenticationSchemeName - ); - - // authenticate credentials with user service and attach user to http context - var user = await _userManager.FindByNameAsync(authUsername); - if ( - user is not null - && authSecret == _options.Secret - && (await _userManager.GetClaimsAsync(user)).Any( - c => c.Type is DgmjrCt.Role.UriString or DgmjrR.AnonymousUser.UriString - ) - ) - { - var identity = new ClaimsIdentity( - SharedSecretAuthenticationOptions.SharedSecretAuthenticationSchemeName - ); - var userClaims = await _userManager.GetClaimsAsync(user); - - userClaims.Add( - new(TelegramID.ClaimTypes.Username.UriString, user.TelegramUsername) - ); - userClaims.Add(new(TelegramID.ClaimTypes.UserId.UriString, user.Id.ToString())); - if (!user.EmailAddress.IsEmpty) - { - userClaims.Add(new(ClaimTypes.Email.UriString, user.EmailAddress.ToString())); - } - - identity.AddClaims(userClaims); - var principal = new ClaimsPrincipal(identity); - var ticket = new AuthenticationTicket( - principal, - SharedSecretAuthenticationOptions.SharedSecretAuthenticationSchemeName - ); - Logger.LogUserAuthenticated(authUsername, userClaims.Count); - return AuthenticateResult.Success(ticket); - } - else if (user is null) - { - Logger.UserAuthenticationFailed(authUsername); - return AuthenticateResult.Fail("Invalid username or password."); - } - Logger.UserAuthenticationFailed(authUsername); - return AuthenticateResult.Fail( - "An unknown error occurred while authenticating the user." - ); - } - catch (Exception ex) - { - Logger.LogError(ex, "An error occurred while authenticating the user."); - return AuthenticateResult.Fail("An error occurred while authenticating the user."); - } - } - - // public async Task GetHandlerAsync(HttpContext context, string authenticationScheme) - // { - // if (authenticationScheme == ApiBasicAuthenticationOptions.AuthenticationSchemeName) - // { - // await InitializeAsync(new AuthenticationScheme(authenticationScheme, authenticationScheme, GetType()), context); - // return this; - // } - // return null; - // } - - // public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) - // { - // HttpContext = context; - // return Task.CompletedTask; - // } - // public Task AuthenticateAsync() => HandleAuthenticateAsync(); - // public Task ChallengeAsync(AuthenticationProperties? properties) - // { - // HttpContext.Response.StatusCode = 401; - // HttpContext.Response.Headers["WWW-Authenticate"] = "Basic"; - // return Task.CompletedTask; - // } - // public Task ForbidAsync(AuthenticationProperties? properties) - // { - // HttpContext.Response.StatusCode = 403; - // return Task.CompletedTask; - // } -} diff --git a/src/Security/Authentication/IBasicApiAuthMiddleware.cs b/src/Security/Authentication/IBasicApiAuthMiddleware.cs deleted file mode 100644 index 8d222a85..00000000 --- a/src/Security/Authentication/IBasicApiAuthMiddleware.cs +++ /dev/null @@ -1,23 +0,0 @@ -/* - * BasicApiAuthMiddleware.cs - * - * Created: 2022-12-14-05:48:24 - * Modified: 2022-12-14-05:48:25 - * - * Author: David G. Moore, Jr, - * - * Copyright © 2022-2023 David G. Moore, Jr,, All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -namespace Dgmjr.AspNetCore.Authentication; - -using Dgmjr.Identity; -using Microsoft.AspNetCore.Http; - -public interface IBasicApiAuthMiddleware : IMiddleware - where TUser : class, IIdentityUserBase - where TRole : class, IIdentityRoleBase -{ - UserManager UserManager { get; } -} diff --git a/src/Security/Authentication/Jwt/botmessage.json b/src/Security/Authentication/Jwt/botmessage.json deleted file mode 100644 index a322c97d..00000000 --- a/src/Security/Authentication/Jwt/botmessage.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "bot": { - "external_id": "1865072231", - "id": "000000000000000000000", - "name": "Channel Users Bot", - "url": "https: //t.me/ChannelUsersBot" - }, - "contact": { - "id": "000000000000000000000", - "last_message": " ➕ #USER_JOINED_CHANNEL,\\n • Of: Wilver Pena [1305486235] \\n • Channel: S L A M | V I D S (Channel) \\n #id1305486235", - "name": "Channel Users Bot", - "telegram_id": "1305486235", - "username": "ChannelUsersBot" - }, - "date": "1680078685", - "info": { - "message": { - "channel_data": { - "message": { - "text": " ➕ #USER_JOINED_CHANNEL,\\n • Of: Wilver Pena [1305486235] \\n • Channel: S L A M | V I D S (Channel) \\n #id1305486235" - }, - "message_id": "2650" - }, - "id": "000000000000000000000000" - } - }, - "service": "telegram", - "title": "incoming_message" -} diff --git a/src/Security/Authentication/LoggerExtensions.cs b/src/Security/Authentication/LoggerExtensions.cs deleted file mode 100644 index 4e46edab..00000000 --- a/src/Security/Authentication/LoggerExtensions.cs +++ /dev/null @@ -1,86 +0,0 @@ -/* - * BasicApiAuthMiddleware.cs - * - * Created: 2022-12-14-05:48:24 - * Modified: 2022-12-14-05:48:25 - * - * Author: David G. Moore, Jr, - * - * Copyright © 2022-2023 David G. Moore, Jr,, All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -namespace Dgmjr.AspNetCore.Authentication; - -using Microsoft.Extensions.Logging; -using static Microsoft.Extensions.Logging.LogLevel; - -internal static partial class LoggerExtensions -{ - [LoggerMessage(0, Debug, "Begin authentication for request {TraceIdentifier} {Method} {Path}")] - public static partial void LogBeginAuthentication( - this ILogger logger, - string traceIdentifier, - string path, - string method - ); - - [LoggerMessage(1, Debug, "Authenticating user {Username} via {Method}")] - public static partial void LogAuthenticatingUser( - this ILogger logger, - string Username, - string Method - ); - - [LoggerMessage(100, Debug, "User {Username} authenticated successfully (claims: {claimCount}")] - public static partial void LogUserAuthenticated( - this ILogger logger, - string Username, - int claimCount - ); - - [LoggerMessage(-1, Debug, "User {Username} failed to authenticate")] - public static partial void UserAuthenticationFailed(this ILogger logger, string Username); - - [LoggerMessage(-2, Debug, "User {Username} failed to authenticate: {Reason}")] - public static partial void LogUserAuthenticationFailed( - this ILogger logger, - string Username, - string Reason - ); - - [LoggerMessage(-3, Debug, "{TraceIdentifier} presented an invalid or missing auth header.")] - public static partial void LogInvalidAuthHeader(this ILogger logger, string TraceIdentifier); - - [LoggerMessage( - -4, - Debug, - "{TraceIdentifier} presented an expired auth token for use {username}. Originally issued: {initiallyIssued}; Expiration: {originalExpiration}; Now: {now}" - )] - public static partial void LogExpiredToken( - this ILogger logger, - string traceIdentifier, - string? username, - DateTimeOffset initiallyIssued, - DateTimeOffset originalExpiration, - DateTimeOffset now - ); - - [LoggerMessage( - -5, - Debug, - "No authenticationSchemeOptions are configured. Configure named options for this middleware with services.AddAuthentication(\"SchemeName\").AddScheme(\"SchemeName\", o => {{ }})" - )] - public static partial void LogNoDefaultAuthenticationScheme(this ILogger logger); - - [LoggerMessage( - -6, - Error, - "An error occurred while authenticating the user: {message}, {stacktrace}" - )] - public static partial void LogAuthenticationError( - this ILogger logger, - string message, - string stackTrace - ); -} diff --git a/src/Security/Authentication/Middlewares/AuthMidewareBase.cs b/src/Security/Authentication/Middlewares/AuthMidewareBase.cs deleted file mode 100644 index 37209e01..00000000 --- a/src/Security/Authentication/Middlewares/AuthMidewareBase.cs +++ /dev/null @@ -1,97 +0,0 @@ -/* - * AuthMidewareBase.cs - * - * Created: 2023-04-02-05:53:30 - * Modified: 2023-04-02-05:53:30 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Options; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Dgmjr.AspNetCore.Authentication.Middlewares; - -public abstract class AuthMiddlewareBase : ILog, IMiddleware - where TOptions : AuthenticationSchemeOptions, IAuthenticationSchemeOptions, new() - where THandler : AuthenticationHandler -{ - protected readonly RequestDelegate _next; - protected readonly IOptionsMonitor _options; - protected readonly UrlEncoder _encoder; - protected readonly ISystemClock _clock; - protected readonly THandler _handler; - - public ILogger Logger { get; init; } - - // Use constructor injection to initialize dependencies. - protected AuthMiddlewareBase( - RequestDelegate next, - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock, - THandler handler - ) - { - _next = next; - _options = options; - Logger = logger.CreateLogger(GetType().FullName); - _encoder = encoder; - _clock = clock; - _handler = handler; // Initialize the handler field. - } - - public async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - var options = _options.CurrentValue?.ForwardDefault; // Cache the options value. - if (options == null) - { - Logger.LogNoDefaultAuthenticationScheme(); - context.Response.StatusCode = 500; - return; - } - - if (ShouldHandleScheme(_options.CurrentValue.AuthenticationSchemeName)) - { - await HandleAuthenticateOnceAsync(context, _options.CurrentValue); - return; // Return after authentication to avoid calling _next twice. - } - - await _next(context); // Call _next only once outside of the if statement. - } - - protected virtual bool ShouldHandleScheme(string authenticationScheme) - { - try - { - _options?.CurrentValue?.Validate(authenticationScheme); - return true; - } - catch - { - return false; - } - } - - protected virtual async Task HandleAuthenticateOnceAsync(HttpContext context, TOptions options) - { - var authResult = await _handler.AuthenticateAsync(); // Use the cached handler field. - - if (authResult?.Principal != null) - { - await context.SignInAsync(authResult.Principal, authResult.Properties); - } - else - { - context.Response.StatusCode = 401; - return; // Return after setting status code to avoid calling _next twice. - } - } -} diff --git a/src/Security/Authentication/Middlewares/BasicApiAuthMiddleware.cs b/src/Security/Authentication/Middlewares/BasicApiAuthMiddleware.cs deleted file mode 100644 index 49456c0d..00000000 --- a/src/Security/Authentication/Middlewares/BasicApiAuthMiddleware.cs +++ /dev/null @@ -1,130 +0,0 @@ -// /* -// * BasicApiAuthMiddleware.cs -// * -// * Created: 2022-12-14-05:48:24 -// * Modified: 2022-12-14-05:48:25 -// * -// * Author: David G. Moore, Jr, -// * -// * Copyright © 2022-2023 David G. Moore, Jr,, All Rights Reserved -// * License: MIT (https://opensource.org/licenses/MIT) -// */ - -// namespace Dgmjr.AspNetCore.Authentication.Middlewares; - -// using System.Net.Http.Headers; -// using System.Security.Claims; -// using System.Text; -// using Dgmjr.Abstractions; -// using Dgmjr.AspNetCore.Authentication.Handlers; -// using Dgmjr.AspNetCore.Authentication.Options; -// using Dgmjr.Identity; -// using Microsoft.AspNetCore.Authentication; -// using Microsoft.AspNetCore.Http; -// using Microsoft.Extensions.Logging; -// using Microsoft.Extensions.Options; -// using Microsoft.IdentityModel.Tokens; - -// public class BasicApiAuthMiddleware : AuthMiddlewareBase, IBasicApiAuthMiddleware, ILog -// { -// public UserManager UserManager { get; } - -// public ILogger Logger { get; init; } -// private readonly IOptions _options; - -// public BasicApiAuthMiddleware(IOptions options, UserManager userManager, ILogger logger) -// { -// UserManager = userManager; -// Logger = logger; -// _options = options; -// } - -// protected string? AuthenticationSchemeName => _options?.Value.; - - -// public override async Task HandleAuthenticateOnceAsync(HttpContext context, RequestDelegate next) -// { -// var authHeader = AuthenticationHeaderValue.Parse( -// context.Request.Headers[nameof(HttpRequestHeaders.Authorization)] -// ); -// var credentialBytes = authHeader.Parameter.FromBase64String(); -// var credentials = credentialBytes.ToUTF8String().Split(':', 2); -// var authUsername = credentials[0]; -// var authPassword = credentials[1]; -// Logger.LogAuthenticatingUser(authUsername); - -// // authenticate credentials with user service and attach user to http context -// var user = await UserManager.FindByNameAsync(authUsername); -// if (user is not null && await UserManager.CheckPasswordAsync(user, authPassword)) -// { -// var identity = new ClaimsIdentity( -// AuthenticationSchemeName -// ); -// var userClaims = await UserManager.GetClaimsAsync(user); - -// userClaims.Add(new(TelegramID.ClaimType.UserId.Uri, user.Id.ToString())); -// userClaims.Add(new(DgmjrCt.NameIdentifier, user.Id.ToString())); -// userClaims.Add(new(DgmjrCt.AuthenticationInstant, DateTimeOffset.UtcNow.ToString())); -// userClaims.Add( -// new( -// type: DgmjrCt.AuthenticationMethod, -// AuthenticationSchemeName -// ) -// ); -// userClaims.Add(new(DgmjrCt.CommonName, user.GoByName)); -// userClaims.Add(new(DgmjrCt.GivenName, user.GivenName)); -// userClaims.Add(new(DgmjrCt.Surname, user.Surname)); - -// if ( -// !IsNullOrEmpty(user.TelegramUsername) -// && !userClaims.Any(c => c.Type == TelegramID.ClaimType.Username.Uri) -// ) -// { -// userClaims.Add(new(TelegramID.ClaimType.Username.Uri, user.TelegramUsername)); -// } - -// if (user.Phone.HasValue && !userClaims.Any(c => c.Type == DgmjrCt.HomePhone)) -// { -// userClaims.Add(new(DgmjrCt.HomePhone, user.PhoneNumber)); -// } - -// if (user.EmailAddress.HasValue && !userClaims.Any(c => c.Type == DgmjrCt.Email)) -// { -// userClaims.Add(new(DgmjrCt.Email, user.EmailAddress)); -// } - -// identity.AddClaims(userClaims); -// var principal = new ClaimsPrincipal(identity); -// var tokenDescriptor = new SecurityTokenDescriptor -// { -// Subject = identity, -// Expires = UtcNow + _options.TokenLifetime, -// }; -// var ticket = new AuthenticationTicket( -// principal, -// AuthenticationSchemeName -// ); -// Logger.LogUserAuthenticated(authUsername, userClaims.Count); -// return AuthenticateResult.Success(ticket); -// } -// else if (user is null) -// { -// Logger.UserAuthenticationFailed(authUsername); -// } -// Logger.UserAuthenticationFailed(authUsername); -// ); -// } -// catch (Exception ex) -// { -// Logger.LogError(ex, "An error occurred while authenticating the user."); -// } -// await next(context); -// } - -// public virtual Task HandleChallengeAsync(HttpContext context) -// { -// context.Response.StatusCode = 401; -// context.Response.Headers["WWW-Authenticate"] = "Basic"; -// return Task.CompletedTask; -// } -// } diff --git a/src/Security/Authentication/Old/ApiAuthorizationServerProvider.cs b/src/Security/Authentication/Old/ApiAuthorizationServerProvider.cs deleted file mode 100644 index e020f1bd..00000000 --- a/src/Security/Authentication/Old/ApiAuthorizationServerProvider.cs +++ /dev/null @@ -1,132 +0,0 @@ -// /* -// * ApiAuthorizationServerProvider.cs -// * -// * Created: 2022-12-10-07:18:59 -// * Modified: 2022-12-10-07:18:59 -// * -// * Author: David G. Moore, Jr, -// * -// * Copyright © 2022-2023 David G. Moore, Jr,, All Rights Reserved -// * License: MIT (https://opensource.org/licenses/MIT) -// */ -// #pragma warning disable -// namespace Dgmjr.AspNetCore.Authentication; - -// using System.Net.Http.Headers; -// using System.Security.Claims; -// using System.Text; -// using System.Text.Encodings.Web; -// using System.Text.RegularExpressions; -// using Dgmjr.Identity; -// using Dgmjr.Identity.Models; -// using Microsoft.AspNetCore.Authentication; -// using Microsoft.AspNetCore.Http; -// using Microsoft.AspNetCore.Identity; -// using Microsoft.AspNetCore.Mvc; -// using Microsoft.AspNetCore.Mvc.Infrastructure; -// using Microsoft.Extensions.Logging; -// using Microsoft.Extensions.Options; - -// public class ApiKeyAuthenticationHandler : AuthenticationHandler -// { -// private readonly UserManager _userManager; - -// public ApiKeyAuthenticationHandler( -// IOptionsMonitor options, -// ILoggerFactory logger, -// UrlEncoder encoder, -// ISystemClock clock, -// UserManager userManager -// ) : base(options, logger, encoder, clock) -// { -// _userManager = userManager; -// } - -// protected override async Task HandleAuthenticateAsync() -// { -// if ( -// Request.Path.StartsWithSegments("/swagger") -// || Request.Path.StartsWithSegments("/api-docs") -// || Request.Path.StartsWithSegments("/api-docs.json") -// ) -// return AuthenticateResult.NoResult(); -// else if ( -// Request.Path.StartsWithSegments("/api") -// && Request.Method.Equals( -// HttpRequestMethodNames.Options, -// StringComparison.InvariantCultureIgnoreCase -// ) -// ) -// return AuthenticateResult.NoResult(); -// if (Request.Headers.TryGetValue(HttpRequestHeaderNames.Authorization, out var authHeader)) -// { -// var authHeaderVal = Regex.Replace( -// authHeader.ToString(), -// "Basic ", -// "", -// RegexOptions.IgnoreCase -// ); -// var authHeaderValDecoded = System.Text.Encoding.UTF8.GetString( -// FromBase64String(authHeaderVal) -// ); -// var authUsername = authHeaderVal.Substring(0, authHeaderValDecoded.IndexOf(':')); -// var authPassword = authHeaderVal.Substring(authHeaderValDecoded.IndexOf(':') + 1); -// var user = await _userManager.FindByNameAsync(authUsername); -// if (user != null && await _userManager.CheckPasswordAsync(user, authPassword)) -// { -// var identity = new ClaimsIdentity( -// ApiBasicAuthenticationOptions.AuthenticationSchemeName -// ); -// var userClaims = await _userManager.GetClaimsAsync(user); -// identity.AddClaims(userClaims); -// var principal = new ClaimsPrincipal(identity); -// var ticket = new AuthenticationTicket( -// principal, -// ApiBasicAuthenticationOptions.AuthenticationSchemeName -// ); -// Context.User = principal; -// await Context.SignInAsync( -// ApiBasicAuthenticationOptions.AuthenticationSchemeName, -// principal -// ); -// return AuthenticateResult.Success(ticket); -// } -// else -// { -// Response.StatusCode = 401; -// return AuthenticateResult.Fail("Invalid username or password"); -// } -// } -// return AuthenticateResult.Fail("No Authorization header found"); - -// // var identity = new ClaimsIdentity(ApiKeyAuthenticationOptions.AuthenticationSchemeName); -// // identity.AddClaim(new Claim(ClaimTypes.Role, "user")); -// // identity.AddClaim(new Claim("username", "user")); -// // identity.AddClaim(new Claim(ClaimTypes.Name, "Hi User")); -// // var principal = new ClaimsPrincipal(identity); -// // var ticket = new AuthenticationTicket(principal, ApiKeyAuthenticationOptions.AuthenticationSchemeName); -// // Context.User = principal; -// // await Context.SignInAsync(ApiKeyAuthenticationOptions.AuthenticationSchemeName, principal); -// // return AuthenticateResult.Success(ticket); -// // } -// // else if(Request.Headers.TryGetValue(ApiKeyAuthenticationOptions.AdminHeaderName, out var adminApiKey) && adminApiKey == Options.AdminApiKey) -// // { -// // var identity = new ClaimsIdentity(ApiKeyAuthenticationOptions.AuthenticationSchemeName); -// // identity.AddClaim(new Claim(ClaimTypes.Role, "admin")); -// // identity.AddClaim(new Claim("username", "admin")); -// // identity.AddClaim(new Claim(ClaimTypes.Name, "Hi Admin")); -// // var principal = new ClaimsPrincipal(identity); -// // var ticket = new AuthenticationTicket(principal, ApiKeyAuthenticationOptions.AuthenticationSchemeName); -// // Context.User = principal; -// // await Context.SignInAsync(ApiKeyAuthenticationOptions.AuthenticationSchemeName, principal); -// // return AuthenticateResult.Success(ticket); -// // } -// // else if(!IsNullOrEmpty(apiKey.ToString()) || !IsNullOrEmpty(adminApiKey.ToString())) -// // { -// // Response.StatusCode = 401; -// // return AuthenticateResult.Fail("Invalid API Key"); -// // } -// // Response.StatusCode = 401; -// // return AuthenticateResult.Fail("Missing API Key"); -// } -// } diff --git a/src/Security/Authentication/Options/ApiAuthenticationOptions.cs b/src/Security/Authentication/Options/ApiAuthenticationOptions.cs deleted file mode 100644 index 4c622007..00000000 --- a/src/Security/Authentication/Options/ApiAuthenticationOptions.cs +++ /dev/null @@ -1,238 +0,0 @@ -/* - * ApiAuthorizationServerProvider.cs - * - * Created: 2022-12-10-07:18:59 - * Modified: 2022-12-10-07:18:59 - * - * Author: David G. Moore, Jr, - * - * Copyright © 2022-2023 David G. Moore, Jr,, All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ -#pragma warning disable -namespace Dgmjr.AspNetCore.Authentication.Options; - -using System.Diagnostics; -using Dgmjr.AspNetCore.Authentication.Options; -using Dgmjr.AspNetCore.Authentication.Schemes; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Options; -using Microsoft.Identity.Client; -using static Dgmjr.AspNetCore.Authentication.Constants; -using static Dgmjr.AspNetCore.Authentication.Constants.AuthenticationSchemes; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Extensions.Configuration; - -public class ApiAuthenticationOptions - : AuthenticationSchemeOptions, - IAuthenticationSchemeOptions, - IJwtConfigurationOptions, - IBasicAuthenticationSchemeOptions, - ISharedSecretAuthenticationSchemeOptions -{ - public const string BasicAuthenticationSchemeName = AuthenticationSchemes.Basic.Name; - public const string BasicAuthenticationSchemeDisplayName = AuthenticationSchemes - .Basic - .DisplayName; - - public string AuthenticationSchemeName { get; set; } = Api.Name; - public string AuthenticationSchemeDisplayName { get; set; } = Api.DisplayName; - - public ApiAuthenticationOptions() - { - this.ClaimsIssuer = Dgmjr.Identity.ClaimTypes.DgmjrClaims.BaseUri; - this.ForwardAuthenticate = Basic.AuthenticationSchemeName; - this.ForwardChallenge = Basic.AuthenticationSchemeName; - this.ForwardForbid = Basic.AuthenticationSchemeName; - this.ForwardSignIn = Basic.AuthenticationSchemeName; - this.ForwardSignOut = Basic.AuthenticationSchemeName; - this.ForwardDefault = Basic.AuthenticationSchemeName; - } - - public ISharedSecretAuthenticationSchemeOptions SharedSecret { get; set; } = - new SharedSecretAuthenticationOptions(); - public IBasicAuthenticationSchemeOptions Basic { get; set; } = - new BasicAuthenticationSchemeOptions(); - public IJwtConfigurationOptions Jwt { get; set; } = new JwtConfigurationOptions(); - - #region IJwtConfigurationOptions - string? IJwtConfigurationOptions.ClaimsIssuer - { - get => Jwt.ClaimsIssuer; - set => Jwt.ClaimsIssuer = value; - } - public duration TokenLifetime - { - get => Jwt.TokenLifetime; - set => Jwt.TokenLifetime = value; - } - public duration AutomaticRefreshInterval - { - get => Jwt.AutomaticRefreshInterval; - set => Jwt.AutomaticRefreshInterval = value; - } - public duration RefreshInterval - { - get => Jwt.RefreshInterval; - set => Jwt.RefreshInterval = value; - } - public bool RequireHttpsMetadata - { - get => Jwt.RequireHttpsMetadata; - set => Jwt.RequireHttpsMetadata = value; - } - public bool RefreshOnIssuerKeyNotFound - { - get => Jwt.RefreshOnIssuerKeyNotFound; - set => Jwt.RefreshOnIssuerKeyNotFound = value; - } - public bool SaveToken - { - get => Jwt.SaveToken; - set => Jwt.SaveToken = value; - } - public bool IncludeErrorDetails - { - get => Jwt.IncludeErrorDetails; - set => Jwt.IncludeErrorDetails = value; - } - public bool MapInboundClaims - { - get => Jwt.MapInboundClaims; - set => Jwt.MapInboundClaims = value; - } - public bool UseSecurityTokenValidators - { - get => Jwt.UseSecurityTokenValidators; - set => Jwt.UseSecurityTokenValidators = value; - } - public string MetadataAddress - { - get => Jwt.MetadataAddress; - set => Jwt.MetadataAddress = value; - } - public string Authority - { - get => Jwt.Authority; - set => Jwt.Authority = value; - } - public string Challenge - { - get => Jwt.Challenge; - set => Jwt.Challenge = value; - } - public string Audience - { - get => Jwt.Audience; - set => Jwt.Audience = value; - } - public virtual global::System.Net.Http.HttpMessageHandler BackchannelHttpHandler - { - get => Jwt.BackchannelHttpHandler; - set => Jwt.BackchannelHttpHandler = value; - } - public virtual global::System.Net.Http.HttpClient Backchannel - { - get => Jwt.Backchannel; - set => Jwt.Backchannel = value; - } - public virtual duration BackchannelTimeout - { - get => Jwt.BackchannelTimeout; - set => Jwt.BackchannelTimeout = value; - } - public virtual JwtBearerEvents Events - { - get => Jwt.Events; - set => Jwt.Events = value; - } - public Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration Configuration - { - get => Jwt.Configuration; - set => Jwt.Configuration = value; - } - public Microsoft.IdentityModel.Protocols.IConfigurationManager ConfigurationManager - { - get => Jwt.ConfigurationManager; - set => Jwt.ConfigurationManager = value; - } - public IList SecurityTokenValidators => - Jwt.SecurityTokenValidators; - public IList TokenHandlers => Jwt.TokenHandlers; - public Microsoft.IdentityModel.Tokens.TokenValidationParameters TokenValidationParameters - { - get => Jwt.TokenValidationParameters; - set => Jwt.TokenValidationParameters = value; - } - #endregion - - #region ISharedSecretAuthenticationSchemeOptions - string ISharedSecretAuthenticationSchemeOptions.Secret - { - get => this.SharedSecret.Secret; - set => SharedSecret.Secret = value; - } - long ISharedSecretAuthenticationSchemeOptions.UserId - { - get => SharedSecret.UserId; - set => SharedSecret.UserId = value; - } - - public AuthenticationScheme ToAuthenticationScheme() - { - return new ApiMultiAuthenticationScheme( - this.AuthenticationSchemeName, - this.AuthenticationSchemeDisplayName, - typeof(Handlers.ApiMultiAuthenticationHandler) - ); - } - #endregion - - public virtual void Validate() - { - if (IsNullOrWhiteSpace(this.AuthenticationSchemeName)) - { - throw new ArgumentNullException( - nameof(this.AuthenticationSchemeName), - "The Authentication Scheme Name cannot be null or empty." - ); - } - - if (IsNullOrWhiteSpace(this.AuthenticationSchemeDisplayName)) - { - throw new ArgumentNullException( - nameof(this.AuthenticationSchemeDisplayName), - "The Authentication Scheme Display Name cannot be null or empty." - ); - } - - if (IsNullOrWhiteSpace(this.SharedSecret.Secret)) - { - throw new ArgumentNullException( - nameof(this.SharedSecret.Secret), - "The Shared Secret cannot be null or empty." - ); - } - - if (IsNullOrWhiteSpace(this.Jwt.ClaimsIssuer)) - { - throw new ArgumentNullException( - nameof(this.Jwt.ClaimsIssuer), - "The JWT Claims Issuer cannot be null or empty." - ); - } - - if (IsNullOrWhiteSpace(this.Jwt.Audience)) - { - throw new ArgumentNullException( - nameof(this.Jwt.Audience), - "The JWT Audience cannot be null or empty." - ); - } - } - - public virtual void Validate(string scheme) - { - Validate(); - } -} diff --git a/src/Security/Authentication/Options/BasicAuthenticationSchemeOptions.cs b/src/Security/Authentication/Options/BasicAuthenticationSchemeOptions.cs deleted file mode 100644 index 2c8c49a3..00000000 --- a/src/Security/Authentication/Options/BasicAuthenticationSchemeOptions.cs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * BasicAuthenticationSchemeOptions.cs - * - * Created: 2023-04-02-08:17:37 - * Modified: 2023-04-02-08:17:37 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -using Microsoft.AspNetCore.Authentication; - -namespace Dgmjr.AspNetCore.Authentication.Options; - -using Dgmjr.AspNetCore.Authentication.Handlers; -using Microsoft.AspNetCore.Authentication.Options; - -public class BasicAuthenticationSchemeOptions - : AuthenticationSchemeOptions, - IBasicAuthenticationSchemeOptions -{ - public const string Basic = nameof(Basic); - public const string DefaultAuthenticationSchemeName = Basic; - public const string DefaultAuthenticationSchemeDisplayName = - $"{Basic} Authentication (Client ID + Client Secret)"; - - public BasicAuthenticationSchemeOptions() - { - ((IBasicAuthenticationSchemeOptions)this).AuthenticationSchemeName = Basic; - ClaimsIssuer = DgmjrCt.DgmjrClaims.UriString; - ForwardAuthenticate = ((IBasicAuthenticationSchemeOptions)this).AuthenticationSchemeName; - ForwardChallenge = ((IBasicAuthenticationSchemeOptions)this).AuthenticationSchemeName; - ForwardDefault = ((IBasicAuthenticationSchemeOptions)this).AuthenticationSchemeName; - ForwardForbid = ((IBasicAuthenticationSchemeOptions)this).AuthenticationSchemeName; - ForwardForbid = ((IBasicAuthenticationSchemeOptions)this).AuthenticationSchemeName; - ForwardSignIn = ((IBasicAuthenticationSchemeOptions)this).AuthenticationSchemeName; - ForwardSignOut = ((IBasicAuthenticationSchemeOptions)this).AuthenticationSchemeName; - } - - public AuthenticationScheme ToAuthenticationScheme() => - new( - ((IBasicAuthenticationSchemeOptions)this).AuthenticationSchemeName, - ((IBasicAuthenticationSchemeOptions)this).AuthenticationSchemeDisplayName, - typeof(BasicApiAuthHandler) - ); - - AuthenticationScheme IAuthenticationSchemeOptions.ToAuthenticationScheme() - { - throw new NotImplementedException(); - } - - string IAuthenticationSchemeOptions.AuthenticationSchemeName { get; set; } - string IAuthenticationSchemeOptions.AuthenticationSchemeDisplayName { get; set; } - - public override void Validate() { } - - public override void Validate(string scheme) - { - Validate(); - } -} diff --git a/src/Security/Authentication/Options/IAuthenticationSchemeOptions.cs b/src/Security/Authentication/Options/IAuthenticationSchemeOptions.cs deleted file mode 100644 index 37f0d440..00000000 --- a/src/Security/Authentication/Options/IAuthenticationSchemeOptions.cs +++ /dev/null @@ -1,37 +0,0 @@ -/* - * IAuthenticationSchemeOptions.cs - * - * Created: 2023-04-02-04:39:15 - * Modified: 2023-04-02-04:39:15 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -namespace Microsoft.AspNetCore.Authentication.Options; - -using Microsoft.AspNetCore.Authentication; - -// [GenerateInterface(typeof(AuthenticationSchemeOptions))] -public partial interface IAuthenticationSchemeOptions -{ - string AuthenticationSchemeName { get; set; } - string AuthenticationSchemeDisplayName { get; set; } - AuthenticationScheme ToAuthenticationScheme(); - - string? ClaimsIssuer { get; set; } - object? Events { get; set; } - type? EventsType { get; set; } - string? ForwardDefault { get; set; } - string? ForwardAuthenticate { get; set; } - string? ForwardChallenge { get; set; } - string? ForwardForbid { get; set; } - string? ForwardSignIn { get; set; } - string? ForwardSignOut { get; set; } - Func? ForwardDefaultSelector { get; set; } - TimeProvider? TimeProvider { get; set; } - void Validate(); - void Validate(string scheme); -} diff --git a/src/Security/Authentication/Options/IBasicAuthenticationSchemeOptions.cs b/src/Security/Authentication/Options/IBasicAuthenticationSchemeOptions.cs deleted file mode 100644 index 86358172..00000000 --- a/src/Security/Authentication/Options/IBasicAuthenticationSchemeOptions.cs +++ /dev/null @@ -1,18 +0,0 @@ -/* - * IBasicAuthenticationSchemeOptions.cs - * - * Created: 2023-04-02-06:25:56 - * Modified: 2023-04-02-06:25:56 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Options; - -namespace Dgmjr.AspNetCore.Authentication.Options; - -public interface IBasicAuthenticationSchemeOptions : IAuthenticationSchemeOptions { } diff --git a/src/Security/Authentication/Options/IJwtConfigurationOptions.cs b/src/Security/Authentication/Options/IJwtConfigurationOptions.cs deleted file mode 100644 index fc0ddec0..00000000 --- a/src/Security/Authentication/Options/IJwtConfigurationOptions.cs +++ /dev/null @@ -1,45 +0,0 @@ -/* - * IJwtConfigurationOptions.cs - * - * Created: 2023-04-02-04:36:58 - * Modified: 2023-04-02-04:36:58 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -using Microsoft.AspNetCore.Authentication.JwtBearer; - -namespace Dgmjr.AspNetCore.Authentication.Options; - -// [GenerateInterface(typeof(JwtBearerOptions))] -public interface IJwtConfigurationOptions -{ - string? ClaimsIssuer { get; set; } - string? Audience { get; set; } - TimeSpan TokenLifetime { get; set; } - - bool RequireHttpsMetadata { get; set; } - - string MetadataAddress { get; set; } - string? Authority { get; set; } - string Challenge { get; set; } - global::Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents Events { get; set; } - global::System.Net.Http.HttpMessageHandler? BackchannelHttpHandler { get; set; } - global::System.Net.Http.HttpClient Backchannel { get; set; } - global::System.TimeSpan BackchannelTimeout { get; set; } - global::Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration? Configuration { get; set; } - global::Microsoft.IdentityModel.Protocols.IConfigurationManager? ConfigurationManager { get; set; } - bool RefreshOnIssuerKeyNotFound { get; set; } - global::System.Collections.Generic.IList SecurityTokenValidators { get; } - global::System.Collections.Generic.IList TokenHandlers { get; } - global::Microsoft.IdentityModel.Tokens.TokenValidationParameters TokenValidationParameters { get; set; } - bool SaveToken { get; set; } - bool IncludeErrorDetails { get; set; } - bool MapInboundClaims { get; set; } - global::System.TimeSpan AutomaticRefreshInterval { get; set; } - global::System.TimeSpan RefreshInterval { get; set; } - bool UseSecurityTokenValidators { get; set; } -} diff --git a/src/Security/Authentication/Options/ISharedSecretAuthenticationOptions.cs b/src/Security/Authentication/Options/ISharedSecretAuthenticationOptions.cs deleted file mode 100644 index 603c5b0e..00000000 --- a/src/Security/Authentication/Options/ISharedSecretAuthenticationOptions.cs +++ /dev/null @@ -1,22 +0,0 @@ -/* - * IAuthenticationSchemeOptions.cs - * - * Created: 2023-04-02-04:39:15 - * Modified: 2023-04-02-04:39:15 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Options; - -namespace Dgmjr.AspNetCore.Authentication.Options; - -public partial interface ISharedSecretAuthenticationSchemeOptions : IAuthenticationSchemeOptions -{ - string Secret { get; set; } - long UserId { get; set; } -} diff --git a/src/Security/Authentication/Options/JwtConfigurationOptions.cs b/src/Security/Authentication/Options/JwtConfigurationOptions.cs deleted file mode 100644 index 0593deb5..00000000 --- a/src/Security/Authentication/Options/JwtConfigurationOptions.cs +++ /dev/null @@ -1,96 +0,0 @@ -/* - * JwtConfigurationOptions.cs - * - * Created: 2023-03-29-04:13:06 - * Modified: 2023-03-29-04:13:06 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - - -using Dgmjr.AspNetCore.Authentication.Handlers; -using Dgmjr.AspNetCore.Authentication.Schemes; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authentication.Options; -using Microsoft.IdentityModel.Tokens; - -namespace Dgmjr.AspNetCore.Authentication.Options; - -// [GenerateInterface(typeof(JwtBearerOptions))] -public class JwtConfigurationOptions - : JwtBearerOptions, - IJwtConfigurationOptions, - IAuthenticationSchemeOptions -{ - public const int DefaultTokenLifetimeMinutes = 60; - - public string Secret { get; set; } = default!; - public string Issuer { get; set; } = default!; - - public JwtConfigurationOptions() - { - TokenLifetime = duration.FromMinutes(DefaultTokenLifetimeMinutes); - ClaimsIssuer = DgmjrCt.DgmjrClaims.BaseUri; - Audience = DgmjrCt.DgmjrClaims.BaseUri; - AuthenticationSchemeName = AuthenticationSchemes.JwtBearer.Name; - AuthenticationSchemeDisplayName = JwtBearerDefaults.AuthenticationScheme; - RequireHttpsMetadata = true; - SaveToken = true; - base.TokenValidationParameters = new() - { - ValidIssuer = DgmjrCt.DgmjrClaims.BaseUri, - ValidAudience = DgmjrCt.DgmjrClaims.BaseUri, - ValidateIssuerSigningKey = true, - ValidateLifetime = true, - ValidateIssuer = true, - ValidateAudience = true, - RequireExpirationTime = true, - RequireSignedTokens = true, - RequireAudience = true, - NameClaimType = DgmjrCt.Name.UriString, - RoleClaimType = DgmjrCt.Role.UriString, - AuthenticationType = AuthenticationSchemes.JwtBearer.Name, - }; - } - - public duration TokenLifetime { get; set; } - public string AuthenticationSchemeName { get; set; } - public string AuthenticationSchemeDisplayName { get; set; } - - public AuthenticationScheme ToAuthenticationScheme() - { - return new JwtAuthenticationScheme( - AuthenticationSchemeName, - AuthenticationSchemeName, - typeof(JwtAuthHandler) - ); - } - - public override void Validate() - { - if (IsNullOrWhiteSpace(ClaimsIssuer)) - { - throw new ArgumentNullException( - nameof(ClaimsIssuer), - "The JWT Claims Issuer cannot be null or empty." - ); - } - - if (IsNullOrWhiteSpace(Audience)) - { - throw new ArgumentNullException( - nameof(Audience), - "The JWT Audience cannot be null or empty." - ); - } - } - - public override void Validate(string scheme) - { - Validate(); - } -} diff --git a/src/Security/Authentication/Options/SharedSecretAuthenticationOptions.cs b/src/Security/Authentication/Options/SharedSecretAuthenticationOptions.cs deleted file mode 100644 index f487d47e..00000000 --- a/src/Security/Authentication/Options/SharedSecretAuthenticationOptions.cs +++ /dev/null @@ -1,96 +0,0 @@ -/* - * SharedSecretAuthenticationOptions.cs - * - * Created: 2023-03-18-02:56:43 - * Modified: 2023-04-03-08:38:22 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - - -// #pragma warning disable -namespace Dgmjr.AspNetCore.Authentication.Options; - -using System.Diagnostics; -using Dgmjr.AspNetCore.Authentication.Options; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Options; - -[DebuggerDisplay($"Scheme: {{{nameof(IAuthenticationSchemeOptions.AuthenticationSchemeName)}}}")] -public class SharedSecretAuthenticationOptions - : AuthenticationSchemeOptions, - ISharedSecretAuthenticationSchemeOptions -{ - public SharedSecretAuthenticationOptions() - { - ClaimsIssuer = DgmjrCt.DgmjrClaims.UriString; - Secret = guid.NewGuid().ToByteArray().ToHexString(); - UserId = DefaultUserIdForSharedSecret; - ForwardAuthenticate = SharedSecretAuthenticationSchemeName; - ForwardChallenge = SharedSecretAuthenticationSchemeName; - ForwardForbid = SharedSecretAuthenticationSchemeName; - ForwardSignIn = SharedSecretAuthenticationSchemeName; - ForwardSignOut = SharedSecretAuthenticationSchemeName; - ForwardDefault = SharedSecretAuthenticationSchemeName; - ((ISharedSecretAuthenticationSchemeOptions)this).AuthenticationSchemeName = - SharedSecretAuthenticationSchemeName; - ((ISharedSecretAuthenticationSchemeOptions)this).AuthenticationSchemeDisplayName = - AuthenticationSchemeDisplayName; - } - - public string Audience { get; set; } = string.Empty; - - public long UserId { get; set; } - - public string Secret { get; set; } = string.Empty; - string IAuthenticationSchemeOptions.AuthenticationSchemeName { get; set; } = - SharedSecretAuthenticationSchemeName; - string IAuthenticationSchemeOptions.AuthenticationSchemeDisplayName { get; set; } = - AuthenticationSchemeDisplayName; - - public const int DefaultUserIdForSharedSecret = -1; - public const string SharedSecretAuthenticationSchemeName = "SharedSecret"; - public const string AuthenticationSchemeDisplayName = "Shared Secret"; - public const string AuthenticationSchemeDescription = - "Insecure authentication with a common shared secret"; - - public AuthenticationScheme ToAuthenticationScheme() - { - throw new NotImplementedException(); - } - - public override void Validate() - { - if (IsNullOrWhiteSpace(Secret)) - { - throw new ArgumentNullException( - nameof(Secret), - "The JWT Secret cannot be null or empty." - ); - } - - if (IsNullOrWhiteSpace(ClaimsIssuer)) - { - throw new ArgumentNullException( - nameof(ClaimsIssuer), - "The JWT Claims Issuer cannot be null or empty." - ); - } - - if (IsNullOrWhiteSpace(Audience)) - { - throw new ArgumentNullException( - nameof(Audience), - "The JWT Audience cannot be null or empty." - ); - } - } - - public override void Validate(string scheme) - { - Validate(); - } -} diff --git a/src/Security/Authentication/README.md b/src/Security/Authentication/README.md deleted file mode 100644 index de6ad274..00000000 --- a/src/Security/Authentication/README.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: -lastmod: 2023-58-30T20:58:51.7611-04:00Z -date: 2023-58-30T20:58:51.7612-04:00Z -license: MIT -slug: Dgmjr.AspNetCore.Authentication-readme -version: -authors: - - dgmjr; -description: Dgmjr.AspNetCore.Authentication Readme #TODO: write description for Dgmjr.AspNetCore.Authentication Readme -keywords: -- Dgmjr.AspNetCore.Authentication - - dgmjr - - dgmjr-io -type: readme ---- -# Dgmjr.AspNetCore.Authentication Readme - -## Package Description -## Getting Started -## Prerequisites -## Installation -## Usage -## Contributing -## Versioning -Built from [commit on branch at ] -(/tree/) diff --git a/src/Security/Authentication/Schemes/ApiMultiAuthenticationScheme.cs b/src/Security/Authentication/Schemes/ApiMultiAuthenticationScheme.cs deleted file mode 100644 index 252e59f7..00000000 --- a/src/Security/Authentication/Schemes/ApiMultiAuthenticationScheme.cs +++ /dev/null @@ -1,27 +0,0 @@ -/* - * ApiMultiAuthenticationScheme.cs - * - * Created: 2023-04-04-03:12:40 - * Modified: 2023-04-04-03:12:40 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Abstractions; - -namespace Dgmjr.AspNetCore.Authentication.Schemes; - -public class ApiMultiAuthenticationScheme : AuthenticationScheme, IAuthenticationScheme -{ - public ApiMultiAuthenticationScheme( - string name, - string? displayName, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - type handlerType - ) - : base(name, displayName, handlerType) { } -} diff --git a/src/Security/Authentication/Schemes/BasicAuthenticationScheme.cs b/src/Security/Authentication/Schemes/BasicAuthenticationScheme.cs deleted file mode 100644 index ec9bfd48..00000000 --- a/src/Security/Authentication/Schemes/BasicAuthenticationScheme.cs +++ /dev/null @@ -1,29 +0,0 @@ -/* - * BasicAuthenticationScheme.cs - * - * Created: 2023-04-04-12:58:12 - * Modified: 2023-04-04-12:58:12 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -using System.Net; -using Dgmjr.AspNetCore.Authentication.Handlers; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Abstractions; - -namespace Dgmjr.AspNetCore.Authentication.Schemes; - -public class BasicAuthenticationScheme : AuthenticationScheme, IAuthenticationScheme -{ - public BasicAuthenticationScheme( - string name = Constants.AuthenticationSchemes.Basic.Name, - string? displayName = Constants.AuthenticationSchemes.Basic.DisplayName, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - type? handlerType = default - ) - : base(name, displayName, handlerType ?? typeof(BasicApiAuthHandler)) { } -} diff --git a/src/Security/Authentication/Schemes/IAuthenticationScheme.cs b/src/Security/Authentication/Schemes/IAuthenticationScheme.cs deleted file mode 100644 index c76f1ecd..00000000 --- a/src/Security/Authentication/Schemes/IAuthenticationScheme.cs +++ /dev/null @@ -1,21 +0,0 @@ -/* - * IAuthenticationScheme.cs - * - * Created: 2023-04-03-11:26:39 - * Modified: 2023-04-03-11:26:39 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -namespace Microsoft.AspNetCore.Authentication.Abstractions; - -// [GenerateInterfaceAttribute(typeof(AuthenticationScheme))] -public interface IAuthenticationScheme -{ - string Name { get; } - string? DisplayName { get; } - global::System.Type HandlerType { get; } -} diff --git a/src/Security/Authentication/Schemes/JwtAuthenticationScheme.cs b/src/Security/Authentication/Schemes/JwtAuthenticationScheme.cs deleted file mode 100644 index 370f32d3..00000000 --- a/src/Security/Authentication/Schemes/JwtAuthenticationScheme.cs +++ /dev/null @@ -1,29 +0,0 @@ -/* - * JwtAuthenticationScheme.cs - * - * Created: 2023-04-04-03:10:50 - * Modified: 2023-04-04-03:10:50 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Abstractions; -using Microsoft.AspNetCore.Authentication.JwtBearer; - -namespace Dgmjr.AspNetCore.Authentication.Schemes; - -public class JwtAuthenticationScheme : AuthenticationScheme, IAuthenticationScheme -{ - public JwtAuthenticationScheme( - string name = Constants.AuthenticationSchemes.JwtBearer.Name, - string? displayName = Constants.AuthenticationSchemes.JwtBearer.DisplayName, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - type? handlerType = default - ) - : base(name, displayName, handlerType ?? typeof(Handlers.JwtAuthHandler)) - { } -} diff --git a/src/Security/Authentication/Schemes/SharedSecretAuthencationScheme.cs b/src/Security/Authentication/Schemes/SharedSecretAuthencationScheme.cs deleted file mode 100644 index 0e10fae6..00000000 --- a/src/Security/Authentication/Schemes/SharedSecretAuthencationScheme.cs +++ /dev/null @@ -1,29 +0,0 @@ -/* - * BasicAuthenticationScheme.cs - * - * Created: 2023-04-04-12:58:12 - * Modified: 2023-04-04-12:58:12 - * - * Author: David G. Moore, Jr. - * - * Copyright © 2022 - 2023 David G. Moore, Jr., All Rights Reserved - * License: MIT (https://opensource.org/licenses/MIT) - */ - -using Dgmjr.AspNetCore.Authorization.Handlers; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Abstractions; - -namespace Dgmjr.AspNetCore.Authentication.Schemes; - -public class SharedSecretAuthenticationScheme : AuthenticationScheme, IAuthenticationScheme -{ - public SharedSecretAuthenticationScheme( - string name = Constants.AuthenticationSchemes.SharedSecret.Name, - string? displayName = Constants.AuthenticationSchemes.SharedSecret.DisplayName, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - type? handlerType = default - ) - : base(name, displayName, handlerType ?? typeof(SharedSecretAuthHandler)) - { } -} diff --git a/src/Security/Security/icon.png b/src/Security/Security/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..db07a039193d57c5147e651a7ab732121bfd20ba GIT binary patch literal 26997 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 CAey28 literal 0 HcmV?d00001 diff --git a/src/Swagger/LICENSE.md b/src/Swagger/LICENSE.md index c80a6760..4f592f86 100755 --- a/src/Swagger/LICENSE.md +++ b/src/Swagger/LICENSE.md @@ -1,21 +1,21 @@ --- -date: 2023-07-13T05:44:46:00+05:00Z +date: 2023-07-13T05:44:46:00-05:00Z description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... keywords: - - IP - - copyright - - license - - mit +- IP +- copyright +- license +- mit permissions: - - commercial-use - - modifications - - distribution - - private-use +- commercial-use +- modifications +- distribution +- private-use conditions: - - include-copyright +- include-copyright limitations: - - liability - - warranty +- liability +- warranty lastmod: 2024-01-0T00:39:00.0000+05:00Z license: MIT slug: mit-license @@ -25,7 +25,7 @@ type: license # MIT License -## Copyright © 2022-2023 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore an email") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/TagHelpers/.vscode/settings.json b/src/TagHelpers/.vscode/settings.json index fd02d624..b777c8a2 100644 --- a/src/TagHelpers/.vscode/settings.json +++ b/src/TagHelpers/.vscode/settings.json @@ -2,5 +2,8 @@ "cSpell.words": [ "ihaaaca", "ihagiaiis" - ] + ], + "files.exclude": { + "**/.vs": false + } } diff --git a/src/TagHelpers/Bootstrap/ActiveAnchorTagHelper.cs b/src/TagHelpers/Bootstrap/ActiveAnchorTagHelper.cs index b8b369e5..aec2c6e5 100644 --- a/src/TagHelpers/Bootstrap/ActiveAnchorTagHelper.cs +++ b/src/TagHelpers/Bootstrap/ActiveAnchorTagHelper.cs @@ -47,32 +47,48 @@ public override void Process(TagHelperContext context, TagHelperOutput output) private bool ShouldBeActive() { - var currentController = ViewContext.RouteData.Values["Controller"]?.ToString(); - var currentAction = ViewContext.RouteData.Values["Action"]?.ToString(); + // Get the current page's area + var currentArea = ViewContext.RouteData.Values["area"]?.ToString(); + // Get the current page's controller + var currentController = ViewContext.RouteData.Values["controller"]?.ToString(); + + // Get the current page's page name + var currentPage = ViewContext.RouteData.Values["page"]?.ToString(); + + // Get the current page's action + var currentAction = ViewContext.RouteData.Values["action"]?.ToString(); + + // Get the anchor's area + var anchorArea = Area; + + // Get the anchor's controller + var anchorController = Controller; + + // Get the anchor's page name + var anchorPage = Page; + + // Get the anchor's action + var anchorAction = Action; + + // Compare the current page's area, controller, page name, and action to the anchor's properties + // If they all match, return true if ( - !IsNullOrWhiteSpace(Controller) && Controller?.ToLower() != currentController?.ToLower() + string.Equals(currentArea, anchorArea, OrdinalIgnoreCase) + && string.Equals(currentController, anchorController, OrdinalIgnoreCase) + && string.Equals(currentPage, anchorPage, OrdinalIgnoreCase) + && string.Equals(currentAction, anchorAction, OrdinalIgnoreCase) ) { - return false; - } - - if (!IsNullOrWhiteSpace(Action) && Action?.ToLower() != currentAction?.ToLower()) - { - return false; + return true; } - foreach (var routeValue in RouteValues) + if(anchorPage != null && currentPage?.StartsWith(anchorPage, OrdinalIgnoreCase) == true) { - if ( - !ViewContext.RouteData.Values.ContainsKey(routeValue.Key) - || ViewContext.RouteData.Values[routeValue.Key].ToString() != routeValue.Value - ) - { - return false; - } + return true; } - return true; + // If any of the above comparisons fail, return false + return false; } } diff --git a/src/TagHelpers/Bootstrap/PageElements/Footer.cs b/src/TagHelpers/Bootstrap/PageElements/Footer.cs index 151b9611..9d630205 100644 --- a/src/TagHelpers/Bootstrap/PageElements/Footer.cs +++ b/src/TagHelpers/Bootstrap/PageElements/Footer.cs @@ -48,6 +48,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu { output.TagName = TagNames.Footer; var childContent = await output.GetChildContentAsync(); + output.Content.AppendHtml(childContent); output.AddCssClass( $"footer border-top bg-{BackgroundColor.GetName()} text-{TextColor.GetName()} {Position.GetName()}" @@ -92,7 +93,6 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu {{{CopyrightHolder}}} - | """ ); } @@ -101,6 +101,13 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu output.PreContent.AppendHtml( $$$""" {{{CopyrightHolder}}} + """ + ); + } + if(!IsNullOrWhiteSpace(childContent.GetContent())) + { + output.PreContent.AppendHtml( + """ | diff --git a/src/TagHelpers/LICENSE.md b/src/TagHelpers/LICENSE.md index 376423df..4f592f86 100644 --- a/src/TagHelpers/LICENSE.md +++ b/src/TagHelpers/LICENSE.md @@ -1,5 +1,5 @@ --- -date: 2023-07-13T05:44:46:00+05:00Z +date: 2023-07-13T05:44:46:00-05:00Z description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... keywords: - IP @@ -25,7 +25,7 @@ type: license # MIT License -## Copyright © 2022-2023 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore an email") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: