From 63f66c5e656ba458a4125139f2610ca492fcd322 Mon Sep 17 00:00:00 2001 From: Mihir Dilip Date: Thu, 9 Jan 2025 22:31:17 +0000 Subject: [PATCH] AOT support added --- README.md | 2 +- samples/SampleWebApi_AOT/Program.cs | 184 ++++++++++++++++++ .../Properties/launchSettings.json | 17 ++ .../SampleWebApi_AOT/SampleWebApi_AOT.csproj | 22 +++ .../appsettings.Development.json | 8 + samples/SampleWebApi_AOT/appsettings.json | 9 + src/AspNetCore.Authentication.Basic.sln | 8 + .../AspNetCore.Authentication.Basic.csproj | 5 + .../BasicExtensions.cs | 26 +++ .../BasicOptions.cs | 4 + 10 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 samples/SampleWebApi_AOT/Program.cs create mode 100644 samples/SampleWebApi_AOT/Properties/launchSettings.json create mode 100644 samples/SampleWebApi_AOT/SampleWebApi_AOT.csproj create mode 100644 samples/SampleWebApi_AOT/appsettings.Development.json create mode 100644 samples/SampleWebApi_AOT/appsettings.json diff --git a/README.md b/README.md index 753637a..2ba9d23 100644 --- a/README.md +++ b/README.md @@ -300,7 +300,7 @@ public void ConfigureServices(IServiceCollection services) ## Release Notes | Version |           Notes | |---------|-------| -|9.0.0 | | +|9.0.0 | | |8.0.0 | | |7.0.0 | | |6.0.1 | | diff --git a/samples/SampleWebApi_AOT/Program.cs b/samples/SampleWebApi_AOT/Program.cs new file mode 100644 index 0000000..2bb5d2d --- /dev/null +++ b/samples/SampleWebApi_AOT/Program.cs @@ -0,0 +1,184 @@ +using AspNetCore.Authentication.Basic; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using SampleWebApi.Repositories; +using SampleWebApi.Services; +using System.Text; +using System.Text.Json.Serialization; + +var builder = WebApplication.CreateSlimBuilder(args); +builder.WebHost.UseKestrelHttpsConfiguration(); + +// Add User repository to the dependency container. +builder.Services.AddTransient(); + +// Add the Basic scheme authentication here.. +// It requires Realm to be set in the options if SuppressWWWAuthenticateHeader is not set. +// If an implementation of IBasicUserValidationService interface is registered in the dependency register as well as OnValidateCredentials delegete on options.Events is also set then this delegate will be used instead of an implementation of IBasicUserValidationService. +builder.Services.AddAuthentication(BasicDefaults.AuthenticationScheme) + + // The below AddBasic without type parameter will require OnValidateCredentials delegete on options.Events to be set unless an implementation of IBasicUserValidationService interface is registered in the dependency register. + // Please note if both the delgate and validation server are set then the delegate will be used instead of BasicUserValidationService. + //.AddBasic(options => + + // The below AddBasic with type parameter will add the BasicUserValidationService to the dependency register. + // Please note if OnValidateCredentials delegete on options.Events is also set then this delegate will be used instead of BasicUserValidationService. + .AddBasic(options => + { + options.Realm = "Sample Web API"; + + //// Optional option to suppress the browser login dialog for ajax calls. + //options.SuppressWWWAuthenticateHeader = true; + + //// Optional option to ignore authentication if AllowAnonumous metadata/filter attribute is added to an endpoint. + //options.IgnoreAuthenticationIfAllowAnonymous = true; + + //// Optional events to override the basic original logic with custom logic. + //// Only use this if you know what you are doing at your own risk. Any of the events can be assigned. + options.Events = new BasicEvents + { + + //// A delegate assigned to this property will be invoked just before validating credentials. + //OnValidateCredentials = async (context) => + //{ + // // custom code to handle credentials, create principal and call Success method on context. + // var userRepository = context.HttpContext.RequestServices.GetRequiredService(); + // var user = await userRepository.GetUserByUsername(context.Username); + // var isValid = user != null && user.Password == context.Password; + // if (isValid) + // { + // context.Response.Headers.Add("ValidationCustomHeader", "From OnValidateCredentials"); + // var claims = new[] + // { + // new Claim(ClaimTypes.NameIdentifier, context.Username, ClaimValueTypes.String, context.Options.ClaimsIssuer), + // new Claim(ClaimTypes.Name, context.Username, ClaimValueTypes.String, context.Options.ClaimsIssuer), + // new Claim("CustomClaimType", "Custom Claim Value - from OnValidateCredentials") + // }; + // context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); + // context.Success(); + // } + // else + // { + // context.NoResult(); + // } + //}, + + //// A delegate assigned to this property will be invoked just before validating credentials. + //// NOTE: Same as above delegate but slightly different implementation which will give same result. + //OnValidateCredentials = async (context) => + //{ + // // custom code to handle credentials, create principal and call Success method on context. + // var userRepository = context.HttpContext.RequestServices.GetRequiredService(); + // var user = await userRepository.GetUserByUsername(context.Username); + // var isValid = user != null && user.Password == context.Password; + // if (isValid) + // { + // context.Response.Headers.Add("ValidationCustomHeader", "From OnValidateCredentials"); + // var claims = new[] + // { + // new Claim("CustomClaimType", "Custom Claim Value - from OnValidateCredentials") + // }; + // context.ValidationSucceeded(claims); // claims are optional + // } + // else + // { + // context.ValidationFailed(); + // } + //}, + + //// A delegate assigned to this property will be invoked before a challenge is sent back to the caller when handling unauthorized response. + //OnHandleChallenge = async (context) => + //{ + // // custom code to handle authentication challenge unauthorized response. + // context.Response.StatusCode = StatusCodes.Status401Unauthorized; + // context.Response.Headers.Add("ChallengeCustomHeader", "From OnHandleChallenge"); + // await context.Response.WriteAsync("{\"CustomBody\":\"From OnHandleChallenge\"}"); + // context.Handled(); // important! do not forget to call this method at the end. + //}, + + //// A delegate assigned to this property will be invoked if Authorization fails and results in a Forbidden response. + //OnHandleForbidden = async (context) => + //{ + // // custom code to handle forbidden response. + // context.Response.StatusCode = StatusCodes.Status403Forbidden; + // context.Response.Headers.Add("ForbidCustomHeader", "From OnHandleForbidden"); + // await context.Response.WriteAsync("{\"CustomBody\":\"From OnHandleForbidden\"}"); + // context.Handled(); // important! do not forget to call this method at the end. + //}, + + //// A delegate assigned to this property will be invoked when the authentication succeeds. It will not be called if OnValidateCredentials delegate is assigned. + //// It can be used for adding claims, headers, etc to the response. + //OnAuthenticationSucceeded = (context) => + //{ + // //custom code to add extra bits to the success response. + // context.Response.Headers.Add("SuccessCustomHeader", "From OnAuthenticationSucceeded"); + // var customClaims = new List + // { + // new Claim("CustomClaimType", "Custom Claim Value - from OnAuthenticationSucceeded") + // }; + // context.AddClaims(customClaims); + // //or can add like this - context.Principal.AddIdentity(new ClaimsIdentity(customClaims)); + // return Task.CompletedTask; + //}, + + //// A delegate assigned to this property will be invoked when the authentication fails. + //OnAuthenticationFailed = (context) => + //{ + // // custom code to handle failed authentication. + // context.Fail("Failed to authenticate"); + // return Task.CompletedTask; + //} + + }; + }); + +// All the requests will need to be authorized. +// Alternatively, add [Authorize] attribute to Controller or Action Method where necessary. +builder.Services.AddAuthorizationBuilder() + .SetFallbackPolicy( + new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build() + ); + + +builder.Services.ConfigureHttpJsonOptions(options => +{ + options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); +}); + +var app = builder.Build(); + +app.UseHttpsRedirection(); + +app.UseAuthentication(); // NOTE: DEFAULT TEMPLATE DOES NOT HAVE THIS, THIS LINE IS REQUIRED AND HAS TO BE ADDED!!! + +app.UseAuthorization(); + +var valuesApi = app.MapGroup("/api/values") + .RequireAuthorization(); +valuesApi.MapGet("/", () => new[] { "value1", "value2" }); +valuesApi.MapGet("/claims", async (context) => +{ + var sb = new StringBuilder(); + foreach (var claim in context.User.Claims) + { + sb.AppendLine($"{claim.Type}: {claim.Value}"); + } + context.Response.StatusCode = 200; + await context.Response.WriteAsync(sb.ToString()); +}); +valuesApi.MapGet("/forbid", async (context) => +{ + await context.ForbidAsync(); +}); + +app.Run(); + + +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(string[]))] +internal partial class AppJsonSerializerContext : JsonSerializerContext +{ + +} diff --git a/samples/SampleWebApi_AOT/Properties/launchSettings.json b/samples/SampleWebApi_AOT/Properties/launchSettings.json new file mode 100644 index 0000000..1781183 --- /dev/null +++ b/samples/SampleWebApi_AOT/Properties/launchSettings.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "api/values", + "applicationUrl": "https://localhost:44304;http://localhost:3920", + "sslPort": 44304, + "useSSL": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/SampleWebApi_AOT/SampleWebApi_AOT.csproj b/samples/SampleWebApi_AOT/SampleWebApi_AOT.csproj new file mode 100644 index 0000000..233cc84 --- /dev/null +++ b/samples/SampleWebApi_AOT/SampleWebApi_AOT.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + true + true + + + + + + + + + + + + diff --git a/samples/SampleWebApi_AOT/appsettings.Development.json b/samples/SampleWebApi_AOT/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/SampleWebApi_AOT/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/SampleWebApi_AOT/appsettings.json b/samples/SampleWebApi_AOT/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/samples/SampleWebApi_AOT/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/AspNetCore.Authentication.Basic.sln b/src/AspNetCore.Authentication.Basic.sln index e159288..ca17524 100644 --- a/src/AspNetCore.Authentication.Basic.sln +++ b/src/AspNetCore.Authentication.Basic.sln @@ -40,6 +40,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWebApi_8_0", "..\samp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWebApi_9_0", "..\samples\SampleWebApi_9_0\SampleWebApi_9_0.csproj", "{53DBA160-9C57-4A01-A189-34FD4D052CD1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWebApi_AOT", "..\samples\SampleWebApi_AOT\SampleWebApi_AOT.csproj", "{79C7438E-72FC-4C43-B255-7B5D1AB94761}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -86,6 +88,10 @@ Global {53DBA160-9C57-4A01-A189-34FD4D052CD1}.Debug|Any CPU.Build.0 = Debug|Any CPU {53DBA160-9C57-4A01-A189-34FD4D052CD1}.Release|Any CPU.ActiveCfg = Release|Any CPU {53DBA160-9C57-4A01-A189-34FD4D052CD1}.Release|Any CPU.Build.0 = Release|Any CPU + {79C7438E-72FC-4C43-B255-7B5D1AB94761}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79C7438E-72FC-4C43-B255-7B5D1AB94761}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79C7438E-72FC-4C43-B255-7B5D1AB94761}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79C7438E-72FC-4C43-B255-7B5D1AB94761}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -101,6 +107,7 @@ Global {D5C8BCC5-C997-475E-9E71-5BF807294BB6} = {CF13271D-BF3F-4167-BEBA-DD02D33992F2} {548DBE4A-0C06-4FB3-BEA6-6AB4A0386EEF} = {CF13271D-BF3F-4167-BEBA-DD02D33992F2} {53DBA160-9C57-4A01-A189-34FD4D052CD1} = {CF13271D-BF3F-4167-BEBA-DD02D33992F2} + {79C7438E-72FC-4C43-B255-7B5D1AB94761} = {CF13271D-BF3F-4167-BEBA-DD02D33992F2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {70815049-1680-480A-BF5A-00536D6C9C20} @@ -110,6 +117,7 @@ Global ..\samples\SampleWebApi.Shared\SampleWebApi.Shared.projitems*{2705db4c-3bce-4cfc-9a30-b4bfd1f28c56}*SharedItemsImports = 5 ..\samples\SampleWebApi.Shared\SampleWebApi.Shared.projitems*{53dba160-9c57-4a01-a189-34fd4d052cd1}*SharedItemsImports = 5 ..\samples\SampleWebApi.Shared\SampleWebApi.Shared.projitems*{548dbe4a-0c06-4fb3-bea6-6ab4a0386eef}*SharedItemsImports = 5 + ..\samples\SampleWebApi.Shared\SampleWebApi.Shared.projitems*{79c7438e-72fc-4c43-b255-7b5d1ab94761}*SharedItemsImports = 5 ..\samples\SampleWebApi.Shared\SampleWebApi.Shared.projitems*{897e5c9c-8c0a-4fb6-960c-4d11aafd4491}*SharedItemsImports = 5 ..\samples\SampleWebApi.Shared\SampleWebApi.Shared.projitems*{9232da41-ca69-4fe3-b0c9-d8d85fec272a}*SharedItemsImports = 5 ..\samples\SampleWebApi.Shared\SampleWebApi.Shared.projitems*{b82830a0-fdfc-469d-b2a8-d657cd216451}*SharedItemsImports = 5 diff --git a/src/AspNetCore.Authentication.Basic/AspNetCore.Authentication.Basic.csproj b/src/AspNetCore.Authentication.Basic/AspNetCore.Authentication.Basic.csproj index 99a5435..47386b0 100644 --- a/src/AspNetCore.Authentication.Basic/AspNetCore.Authentication.Basic.csproj +++ b/src/AspNetCore.Authentication.Basic/AspNetCore.Authentication.Basic.csproj @@ -9,6 +9,10 @@ - net9.0 support added - Sample project for net9.0 added - Readme updated +- Nullable reference types enabled +- Language version set to latest +- Implicit usings enabled +- AOT support added Easy to use and very light weight Microsoft style Basic Scheme Authentication implementation for ASP.NET Core. Mihir Dilip @@ -21,6 +25,7 @@ latest enable enable + true true LICENSE.txt README.md diff --git a/src/AspNetCore.Authentication.Basic/BasicExtensions.cs b/src/AspNetCore.Authentication.Basic/BasicExtensions.cs index b8ff529..bfa523e 100644 --- a/src/AspNetCore.Authentication.Basic/BasicExtensions.cs +++ b/src/AspNetCore.Authentication.Basic/BasicExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; +using System.Diagnostics.CodeAnalysis; namespace AspNetCore.Authentication.Basic { @@ -84,8 +85,13 @@ public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, /// /// /// The instance of +#if NET5_0_OR_GREATER + public static AuthenticationBuilder AddBasic<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TBasicUserValidationService>(this AuthenticationBuilder builder) where TBasicUserValidationService : class, IBasicUserValidationService + => builder.AddBasic(BasicDefaults.AuthenticationScheme); +#else public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder) where TBasicUserValidationService : class, IBasicUserValidationService => builder.AddBasic(BasicDefaults.AuthenticationScheme); +#endif /// /// Adds basic authentication scheme to the project. It takes a implementation of as type parameter. @@ -95,8 +101,13 @@ public static AuthenticationBuilder AddBasic(this A /// /// The authentication scheme. /// The instance of +#if NET5_0_OR_GREATER + public static AuthenticationBuilder AddBasic<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TBasicUserValidationService>(this AuthenticationBuilder builder, string authenticationScheme) where TBasicUserValidationService : class, IBasicUserValidationService + => builder.AddBasic(authenticationScheme, configureOptions: null); +#else public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme) where TBasicUserValidationService : class, IBasicUserValidationService => builder.AddBasic(authenticationScheme, configureOptions: null); +#endif /// /// Adds basic authentication scheme to the project. It takes a implementation of as type parameter. @@ -106,8 +117,13 @@ public static AuthenticationBuilder AddBasic(this A /// /// The . /// The instance of +#if NET5_0_OR_GREATER + public static AuthenticationBuilder AddBasic<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TBasicUserValidationService>(this AuthenticationBuilder builder, Action? configureOptions) where TBasicUserValidationService : class, IBasicUserValidationService + => builder.AddBasic(BasicDefaults.AuthenticationScheme, configureOptions); +#else public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, Action? configureOptions) where TBasicUserValidationService : class, IBasicUserValidationService => builder.AddBasic(BasicDefaults.AuthenticationScheme, configureOptions); +#endif /// /// Adds basic authentication scheme to the project. It takes a implementation of as type parameter. @@ -118,8 +134,13 @@ public static AuthenticationBuilder AddBasic(this A /// The authentication scheme. /// The . /// The instance of +#if NET5_0_OR_GREATER + public static AuthenticationBuilder AddBasic<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TBasicUserValidationService>(this AuthenticationBuilder builder, string authenticationScheme, Action? configureOptions) where TBasicUserValidationService : class, IBasicUserValidationService + => builder.AddBasic(authenticationScheme, displayName: null, configureOptions: configureOptions); +#else public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme, Action? configureOptions) where TBasicUserValidationService : class, IBasicUserValidationService => builder.AddBasic(authenticationScheme, displayName: null, configureOptions: configureOptions); +#endif /// /// Adds basic authentication scheme to the project. It takes a implementation of as type parameter. @@ -131,8 +152,13 @@ public static AuthenticationBuilder AddBasic(this A /// The display name. /// The . /// The instance of +#if NET5_0_OR_GREATER + public static AuthenticationBuilder AddBasic<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TBasicUserValidationService>(this AuthenticationBuilder builder, string authenticationScheme, string? displayName, Action? configureOptions) + where TBasicUserValidationService : class, IBasicUserValidationService +#else public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme, string? displayName, Action? configureOptions) where TBasicUserValidationService : class, IBasicUserValidationService +#endif { // Adds implementation of IBasicUserValidationService to the dependency container. builder.Services.AddTransient(); diff --git a/src/AspNetCore.Authentication.Basic/BasicOptions.cs b/src/AspNetCore.Authentication.Basic/BasicOptions.cs index 6b63521..0bd7181 100644 --- a/src/AspNetCore.Authentication.Basic/BasicOptions.cs +++ b/src/AspNetCore.Authentication.Basic/BasicOptions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using Microsoft.AspNetCore.Authentication; +using System.Diagnostics.CodeAnalysis; namespace AspNetCore.Authentication.Basic { @@ -53,6 +54,9 @@ public BasicOptions() public bool IgnoreAuthenticationIfAllowAnonymous { get; set; } #endif +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif internal Type? BasicUserValidationServiceType { get; set; } = null; } }