diff --git a/DotBased.AspNet.Authority.EFCore/Extensions.cs b/DotBased.AspNet.Authority.EFCore/Extensions.cs index d48aa30..698dfbc 100644 --- a/DotBased.AspNet.Authority.EFCore/Extensions.cs +++ b/DotBased.AspNet.Authority.EFCore/Extensions.cs @@ -7,13 +7,13 @@ namespace DotBased.AspNet.Authority.EFCore; public static class Extensions { - public static IServiceCollection AddAuthorityContext(this IServiceCollection services, Action options) + public static AuthorityBuilder AddAuthorityContext(this AuthorityBuilder builder, Action options) { - services.AddDbContextFactory(options); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - return services; + builder.Services.AddDbContextFactory(options); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + return builder; } } \ No newline at end of file diff --git a/DotBased.AspNet.Authority/AuthorityDefaults.cs b/DotBased.AspNet.Authority/AuthorityDefaults.cs index a0b4726..8a437d6 100755 --- a/DotBased.AspNet.Authority/AuthorityDefaults.cs +++ b/DotBased.AspNet.Authority/AuthorityDefaults.cs @@ -4,8 +4,24 @@ public static class AuthorityDefaults { public static class Scheme { - public const string AuthenticationScheme = "Authority.Scheme.Authentication"; - public const string ExternalScheme = "Authority.Scheme.External"; + public static class Cookie + { + public const string Default = "Authority.Scheme.Cookie"; + public const string CookieName = "AuthorityAuth"; + } + + public static class Token + { + public const string Default = "Authority.Scheme.Token"; + public const string TokenName = "AuthorityAuthToken"; + } } + public static class Paths + { + public const string Default = "/"; + public const string Login = "/auth/login"; + public const string Logout = "/auth/logout"; + public const string Forbidden = "/forbidden"; + } } \ No newline at end of file diff --git a/DotBased.AspNet.Authority/AuthorityProviderExtensions.cs b/DotBased.AspNet.Authority/AuthorityProviderExtensions.cs index 9018d7a..7363a87 100755 --- a/DotBased.AspNet.Authority/AuthorityProviderExtensions.cs +++ b/DotBased.AspNet.Authority/AuthorityProviderExtensions.cs @@ -1,7 +1,10 @@ using DotBased.AspNet.Authority.Crypto; using DotBased.AspNet.Authority.Managers; using DotBased.AspNet.Authority.Models.Options; +using DotBased.AspNet.Authority.Models.Options.Auth; +using DotBased.AspNet.Authority.Services; using DotBased.AspNet.Authority.Validators; +using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -9,13 +12,13 @@ namespace DotBased.AspNet.Authority; public static class AuthorityProviderExtensions { - public static AuthorityBuilder AddAuthority(this IServiceCollection services, Action? optionsAction = null) + public static AuthorityBuilder AddAuthority(this IServiceCollection services) => AddAuthority(services, _ => { }); + + public static AuthorityBuilder AddAuthority(this IServiceCollection services, Action optionsAction) { - if (optionsAction != null) - { - services.AddOptions(); - services.Configure(optionsAction); - } + services.AddOptions(); + ArgumentNullException.ThrowIfNull(optionsAction); + services.Configure(optionsAction); services.TryAddScoped(); services.TryAddScoped(); @@ -26,9 +29,49 @@ public static class AuthorityProviderExtensions services.TryAddScoped(); services.TryAddScoped();*/ services.TryAddScoped(); + return new AuthorityBuilder(services); } + public static AuthenticationBuilder AddAuthorityAuth(this AuthorityBuilder builder) => AddAuthorityAuth(builder, _ => { }); + + public static AuthenticationBuilder AddAuthorityAuth(this AuthorityBuilder builder, Action configureOptions) + { + ArgumentNullException.ThrowIfNull(configureOptions); + builder.Services.Configure(configureOptions); + + builder.Services.AddScoped(); + var authBuilder = builder.Services.AddAuthentication(options => + { + + }); + return authBuilder; + } + + public static AuthenticationBuilder AddAuthorityCookie(this AuthenticationBuilder builder, string scheme = AuthorityDefaults.Scheme.Cookie.Default) + { + builder.AddCookie(options => + { + options.Cookie.Name = AuthorityDefaults.Scheme.Cookie.CookieName; + options.Cookie.Path = AuthorityDefaults.Paths.Default; + options.Cookie.Expiration = TimeSpan.FromDays(1); + options.Cookie.HttpOnly = true; + options.Cookie.IsEssential = true; + options.LoginPath = AuthorityDefaults.Paths.Login; + options.LogoutPath = AuthorityDefaults.Paths.Logout; + options.AccessDeniedPath = AuthorityDefaults.Paths.Forbidden; + options.SlidingExpiration = true; + //options.SessionStore + }); + return builder; + } + + public static AuthenticationBuilder AddAuthorityToken(this AuthenticationBuilder builder, string scheme = AuthorityDefaults.Scheme.Token.Default) + { + + return builder; + } + public static AuthorityBuilder AddAuthorityRepository(this AuthorityBuilder authorityBuilder) where TRepository : class { return authorityBuilder; @@ -38,20 +81,4 @@ public static class AuthorityProviderExtensions { return builder; } - - private static Type GetBaseGenericArgumentType(Type baseType) - { - var userGenericBaseTypeDefinition = typeof(TModel).BaseType?.GetGenericTypeDefinition(); - if (userGenericBaseTypeDefinition != null && userGenericBaseTypeDefinition == baseType) - { - var userBaseGenericArguments = userGenericBaseTypeDefinition.GetGenericArguments(); - if (userBaseGenericArguments.Length <= 0) - { - throw new ArgumentException("Base implementation does not have the required generic argument.", nameof(TModel)); - } - - return userBaseGenericArguments[0]; - } - throw new ArgumentException($"Given object {typeof(TModel).Name} does not have the base implementation type of: {baseType.Name}", nameof(TModel)); - } } \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Controllers/AuthorityController.cs b/DotBased.AspNet.Authority/Controllers/AuthorityController.cs new file mode 100644 index 0000000..1568196 --- /dev/null +++ b/DotBased.AspNet.Authority/Controllers/AuthorityController.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc; + +namespace DotBased.AspNet.Authority.Controllers; + +[ApiController] +[Route("[controller]")] +public class AuthorityController : ControllerBase +{ + +} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/DotBased.AspNet.Authority.csproj b/DotBased.AspNet.Authority/DotBased.AspNet.Authority.csproj index b0a8901..332f6b0 100755 --- a/DotBased.AspNet.Authority/DotBased.AspNet.Authority.csproj +++ b/DotBased.AspNet.Authority/DotBased.AspNet.Authority.csproj @@ -17,11 +17,14 @@ - + + + + diff --git a/DotBased.AspNet.Authority/Handlers/AuthorityAuthenticationHandler.cs b/DotBased.AspNet.Authority/Handlers/AuthorityAuthenticationHandler.cs new file mode 100644 index 0000000..3385bc6 --- /dev/null +++ b/DotBased.AspNet.Authority/Handlers/AuthorityAuthenticationHandler.cs @@ -0,0 +1,38 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + +namespace DotBased.AspNet.Authority.Handlers; + +public class AuthorityAuthenticationHandler : IAuthenticationHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler +{ + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + throw new NotImplementedException(); + } + + public Task AuthenticateAsync() + { + throw new NotImplementedException(); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Models/Data/Auth/AuthenticationSessionType.cs b/DotBased.AspNet.Authority/Models/Data/Auth/AuthenticationSessionType.cs new file mode 100644 index 0000000..bfc1868 --- /dev/null +++ b/DotBased.AspNet.Authority/Models/Data/Auth/AuthenticationSessionType.cs @@ -0,0 +1,7 @@ +namespace DotBased.AspNet.Authority.Models.Data.Auth; + +public class AuthenticationSessionType +{ + public string Id { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Models/Data/Auth/AuthenticationType.cs b/DotBased.AspNet.Authority/Models/Data/Auth/AuthenticationType.cs new file mode 100644 index 0000000..4adbf42 --- /dev/null +++ b/DotBased.AspNet.Authority/Models/Data/Auth/AuthenticationType.cs @@ -0,0 +1,16 @@ +namespace DotBased.AspNet.Authority.Models.Data.Auth; + +public class AuthenticationType +{ + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Provider { get; set; } = string.Empty; + public bool Redirects { get; set; } + public AuthenticationTypePaths Paths { get; set; } = new(); +} + +public class AuthenticationTypePaths +{ + public string Login { get; set; } = string.Empty; + public string Logout { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Models/Data/System/AboutModel.cs b/DotBased.AspNet.Authority/Models/Data/System/AboutModel.cs new file mode 100644 index 0000000..65dd126 --- /dev/null +++ b/DotBased.AspNet.Authority/Models/Data/System/AboutModel.cs @@ -0,0 +1,10 @@ +using DotBased.AspNet.Authority.Models.Data.Auth; + +namespace DotBased.AspNet.Authority.Models.Data.System; + +public class AboutModel +{ + public string Name { get; set; } = "Authority.Server"; + public List AuthenticationTypes { get; set; } = []; + public List SessionTypes { get; set; } = []; +} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Models/Options/Auth/AuthenticationSecurityOptions.cs b/DotBased.AspNet.Authority/Models/Options/Auth/AuthenticationSecurityOptions.cs new file mode 100644 index 0000000..bada294 --- /dev/null +++ b/DotBased.AspNet.Authority/Models/Options/Auth/AuthenticationSecurityOptions.cs @@ -0,0 +1,14 @@ +namespace DotBased.AspNet.Authority.Models.Options.Auth; + +public class AuthenticationSecurityOptions +{ + public SecurityMode SecurityMode { get; set; } = SecurityMode.Normal; + public List AllowedLoginMethods { get; set; } = ["*"]; +} + +public enum SecurityMode +{ + Loose = 0, + Normal = 1, + Strict = 2 +} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Models/Options/Auth/AuthorityAuthenticationOptions.cs b/DotBased.AspNet.Authority/Models/Options/Auth/AuthorityAuthenticationOptions.cs new file mode 100644 index 0000000..4b16122 --- /dev/null +++ b/DotBased.AspNet.Authority/Models/Options/Auth/AuthorityAuthenticationOptions.cs @@ -0,0 +1,8 @@ +namespace DotBased.AspNet.Authority.Models.Options.Auth; + +public class AuthorityAuthenticationOptions +{ + public AuthenticationSecurityOptions Security { get; set; } = new AuthenticationSecurityOptions(); + public SessionOptions Session { get; set; } = new SessionOptions(); + public string DefaultScheme { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Models/Options/Auth/SessionOptions.cs b/DotBased.AspNet.Authority/Models/Options/Auth/SessionOptions.cs new file mode 100644 index 0000000..eda2152 --- /dev/null +++ b/DotBased.AspNet.Authority/Models/Options/Auth/SessionOptions.cs @@ -0,0 +1,6 @@ +namespace DotBased.AspNet.Authority.Models.Options.Auth; + +public class SessionOptions +{ + public TimeSpan RefreshInterval { get; set; } = TimeSpan.FromMinutes(30); +} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Services/AuthorityAuthenticationService.cs b/DotBased.AspNet.Authority/Services/AuthorityAuthenticationService.cs new file mode 100644 index 0000000..7b31945 --- /dev/null +++ b/DotBased.AspNet.Authority/Services/AuthorityAuthenticationService.cs @@ -0,0 +1,20 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + +namespace DotBased.AspNet.Authority.Services; + +public class AuthorityAuthenticationService(IAuthenticationSchemeProvider schemes, IAuthenticationHandlerProvider handlers, IClaimsTransformation transform) : AuthenticationService(schemes, handlers, transform) +{ + public override Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) + { + //TODO: Get from query parameters which auth scheme to use or fallback to configured default. + return base.SignInAsync(context, scheme, principal, properties); + } + + public override Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties) + { + //TODO: Figure out which type of auth is used and logout with the scheme. + return base.SignOutAsync(context, scheme, properties); + } +} \ No newline at end of file diff --git a/TestWebApi/Controllers/WeatherController.cs b/TestWebApi/Controllers/WeatherController.cs new file mode 100644 index 0000000..8df836d --- /dev/null +++ b/TestWebApi/Controllers/WeatherController.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace TestWebApi.Controllers; + +[ApiController] +[Route("[controller]")] +[Authorize] +public class WeatherController : ControllerBase +{ + private readonly string[] _summaries = + [ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + ]; + + [HttpGet("GetWeatherForecast")] + public async Task>> GetForecast() + { + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + _summaries[Random.Shared.Next(_summaries.Length)] + )) + .ToList(); + await Task.Delay(TimeSpan.FromSeconds(1)); + return forecast; + } +} \ No newline at end of file diff --git a/TestWebApi/Program.cs b/TestWebApi/Program.cs index 4c03d50..0b917f6 100755 --- a/TestWebApi/Program.cs +++ b/TestWebApi/Program.cs @@ -21,20 +21,15 @@ LogService.AddLogAdapter(new BasedSerilogAdapter(serilogLogger)); builder.Logging.ClearProviders(); builder.Logging.AddDotBasedLoggerProvider(LogService.Options); -builder.Services.AddAuthorityContext(options => -{ - options.UseSqlite("Data Source=dev-dotbased.db", c => c.MigrationsAssembly("TestWebApi")); -}); -builder.Services.AddAuthority(options => -{ - -}); -/*builder.Services.AddAuthentication(options => +builder.Services.AddControllers(); + +builder.Services.AddAuthority().AddAuthorityContext(options => { - options.DefaultScheme = BasedAuthenticationDefaults.BasedAuthenticationScheme; - options.DefaultChallengeScheme = BasedAuthenticationDefaults.BasedAuthenticationScheme; -}).AddCookie();*/ + options.UseSqlite("Data Source=dev-authority.db", c => c.MigrationsAssembly("TestWebApi")); +}).AddAuthorityAuth() +.AddAuthorityCookie() +.AddAuthorityToken(); // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle @@ -53,28 +48,13 @@ if (app.Environment.IsDevelopment()) } app.UseHttpsRedirection(); +app.MapControllers(); -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; - -app.MapGet("/weatherforecast", () => - { - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; - }) - .WithName("GetWeatherForecast") - .WithOpenApi(); +app.UseAuthentication(); +app.UseAuthorization(); app.Run(); +return; ILogger SetupSerilog() { @@ -84,7 +64,7 @@ ILogger SetupSerilog() return logConfig.CreateLogger(); } -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } \ No newline at end of file