diff --git a/NDB.Security.Authentication.Identity/Abstractions/IAuthenticationOptions.cs b/NDB.Security.Authentication.Identity/Abstractions/IAuthenticationOptions.cs index a520e24..c837f4b 100644 --- a/NDB.Security.Authentication.Identity/Abstractions/IAuthenticationOptions.cs +++ b/NDB.Security.Authentication.Identity/Abstractions/IAuthenticationOptions.cs @@ -8,5 +8,6 @@ namespace NDB.Security.Authentication.Identity.Abstractions Func AuthenticateAsGuest { get; } int GuestUserId { get; } string GuestUserName { get; } + bool AcceptTokenFromQuery { get; } } } diff --git a/NDB.Security.Authentication.Identity/AuthenticationExtensions.cs b/NDB.Security.Authentication.Identity/AuthenticationExtensions.cs index 8f17d1c..d151e2b 100644 --- a/NDB.Security.Authentication.Identity/AuthenticationExtensions.cs +++ b/NDB.Security.Authentication.Identity/AuthenticationExtensions.cs @@ -10,7 +10,7 @@ namespace NDB.Security.Authentication.Identity { public static IServiceCollection AddIdentityAuthentication(this IServiceCollection services, string identityServerBaseAddress) { - services.AddIdentityAuthentication(identityServerBaseAddress, new Services.AuthenticationOptions()); + services.AddIdentityAuthentication(identityServerBaseAddress, new Models.AuthenticationOptions()); return services; } diff --git a/NDB.Security.Authentication.Identity/Constants/QueryParams.cs b/NDB.Security.Authentication.Identity/Constants/QueryParams.cs new file mode 100644 index 0000000..700cd7a --- /dev/null +++ b/NDB.Security.Authentication.Identity/Constants/QueryParams.cs @@ -0,0 +1,8 @@ +namespace NDB.Security.Authentication.Identity.Constants +{ + internal struct QueryParams + { + public const string + Token = "token"; + } +} diff --git a/NDB.Security.Authentication.Identity/IdentityAuthenticationHandler.cs b/NDB.Security.Authentication.Identity/IdentityAuthenticationHandler.cs index b0c7f6a..7993874 100644 --- a/NDB.Security.Authentication.Identity/IdentityAuthenticationHandler.cs +++ b/NDB.Security.Authentication.Identity/IdentityAuthenticationHandler.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NDB.Security.Authentication.Identity.Abstractions; +using c = NDB.Security.Authentication.Identity.Constants; using System.Collections.Generic; using System.Linq; using System.Net.Http.Headers; @@ -27,35 +28,54 @@ namespace NDB.Security.Authentication.Identity protected override async Task HandleAuthenticateAsync() { - if (!Request.Headers.ContainsKey("Authorization")) + var token = GetAuthorizationToken(); + if (token != null) { - var authenticateAsGuest = _authenticationOptions.AuthenticateAsGuest?.Invoke(Request) ?? false; - if (authenticateAsGuest) + TokenCore tokenCore; + try { - var guestTicket = GetGuestAuthenticationTicket(_authenticationOptions.GuestUserId, _authenticationOptions.GuestUserName); - return AuthenticateResult.Success(guestTicket); + tokenCore = await _identityService.Authorize(token); + } + catch + { + return AuthenticateResult.Fail("Invalid authorization"); } - return AuthenticateResult.Fail("Missing Authorization Header"); + if (tokenCore == null) + return AuthenticateResult.Fail("Invalid token"); + + var ticket = GetAuthenticationTicket(tokenCore); + return AuthenticateResult.Success(ticket); } - TokenCore tokenCore; - try + var authenticateAsGuest = _authenticationOptions.AuthenticateAsGuest?.Invoke(Request) ?? false; + if (authenticateAsGuest) + { + var guestTicket = GetGuestAuthenticationTicket(_authenticationOptions.GuestUserId, _authenticationOptions.GuestUserName); + return AuthenticateResult.Success(guestTicket); + } + + return AuthenticateResult.Fail("Missing authorization header"); + } + + private string GetAuthorizationToken() + { + if (Request.Headers.ContainsKey("Authorization")) { var authorizationHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); var token = authorizationHeader.Parameter; - tokenCore = await _identityService.Authorize(token); + return token; } - catch + + if (_authenticationOptions.AcceptTokenFromQuery + && Request.Query.Count > 0 + && Request.Query.ContainsKey(c.QueryParams.Token)) { - return AuthenticateResult.Fail("Invalid authorization header"); + var token = Request.Query[c.QueryParams.Token]; + return token.ToString(); } - if (tokenCore == null) - return AuthenticateResult.Fail("Invalid token"); - - var ticket = GetAuthenticationTicket(tokenCore); - return AuthenticateResult.Success(ticket); + return null; } private AuthenticationTicket GetGuestAuthenticationTicket(int guestId, string guestName) diff --git a/NDB.Security.Authentication.Identity/Services/AuthenticationOptions.cs b/NDB.Security.Authentication.Identity/Models/AuthenticationOptions.cs similarity index 76% rename from NDB.Security.Authentication.Identity/Services/AuthenticationOptions.cs rename to NDB.Security.Authentication.Identity/Models/AuthenticationOptions.cs index 8558e19..3bb6716 100644 --- a/NDB.Security.Authentication.Identity/Services/AuthenticationOptions.cs +++ b/NDB.Security.Authentication.Identity/Models/AuthenticationOptions.cs @@ -2,7 +2,7 @@ using NDB.Security.Authentication.Identity.Abstractions; using System; -namespace NDB.Security.Authentication.Identity.Services +namespace NDB.Security.Authentication.Identity.Models { public class AuthenticationOptions : IAuthenticationOptions { @@ -11,5 +11,6 @@ namespace NDB.Security.Authentication.Identity.Services public int GuestUserId { get; set; } public string GuestUserName { get; set; } + public bool AcceptTokenFromQuery { get; set; } } } diff --git a/NDB.Test.Api/Controllers/DebugController.cs b/NDB.Test.Api/Controllers/DebugController.cs new file mode 100644 index 0000000..32162cd --- /dev/null +++ b/NDB.Test.Api/Controllers/DebugController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; + +namespace NDB.Test.Api.Controllers +{ + [Authorize] + [ApiController] + [Route("[controller]")] + public class DebugController : ControllerBase + { + private readonly ILogger _logger; + + public DebugController(ILogger logger) + { + _logger = logger; + } + + [AllowAnonymous] + [HttpGet("ping")] + public IActionResult Ping() + { + return Ok($"Ping success. System datetime: {DateTime.Now}"); + } + + [HttpGet("ping-auth")] + public IActionResult PingAuth() + { + return Ok($"Secured ping success. System datetime: {DateTime.Now}"); + } + } +} diff --git a/NDB.Test.Api/Extensions/AuthenticationExtensions.cs b/NDB.Test.Api/Extensions/AuthenticationExtensions.cs new file mode 100644 index 0000000..b71f94b --- /dev/null +++ b/NDB.Test.Api/Extensions/AuthenticationExtensions.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using NDB.Security.Authentication.Identity; +using NDB.Security.Authentication.Identity.Models; +using System.Linq; + +namespace NDB.Test.Api.Extensions +{ + public static class AuthenticationExtensions + { + private record GuestRoute(string Route, int PathElements); + private static readonly GuestRoute[] _resourcesRoutes = new GuestRoute[] + { + new GuestRoute("/resources", 1), + new GuestRoute("/images", 1), + new GuestRoute("/spot", 2) + }; + + public static IServiceCollection AddIdentityAuthentication(this IServiceCollection services, string identityServerBaseAddress) + { + var authenticationOptions = new AuthenticationOptions() + { + AuthenticateAsGuest = (HttpRequest request) => + { + var authenticateAsGuest = AuthenticateAsGuest(request); + return authenticateAsGuest; + }, + GuestUserName = "Guest", + GuestUserId = -111, + AcceptTokenFromQuery = true + }; + + services.AddIdentityAuthentication(identityServerBaseAddress, authenticationOptions); + return services; + } + + private static bool AuthenticateAsGuest(HttpRequest request) + { + if (!request.Path.HasValue) + return false; + + var guestRoute = _resourcesRoutes.FirstOrDefault(z => request.Path.Value.StartsWith(z.Route)); + if (guestRoute == null) + return false; + + var resourceRequestedById = request.Query.Count > 0 && request.Query.ContainsKey("id"); + if (resourceRequestedById) + return false; + + var resourceRequestedByCode = request.Query.Count > 0 && request.Query.ContainsKey("code"); + if (resourceRequestedByCode) + return true; + + var resourceRequestedByName = request.Path.Value.Replace(guestRoute.Route, string.Empty).Substring(1).Split("/").Length == guestRoute.PathElements; + if (resourceRequestedByName) + return true; + + return false; + } + } +} diff --git a/NDB.Test.Api/NDB.Test.Api.csproj b/NDB.Test.Api/NDB.Test.Api.csproj new file mode 100644 index 0000000..0cf9856 --- /dev/null +++ b/NDB.Test.Api/NDB.Test.Api.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + + + + + + + + + + + + diff --git a/NDB.Test.Api/Program.cs b/NDB.Test.Api/Program.cs new file mode 100644 index 0000000..fe6c692 --- /dev/null +++ b/NDB.Test.Api/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace NDB.Test.Api +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/NDB.Test.Api/Properties/launchSettings.json b/NDB.Test.Api/Properties/launchSettings.json new file mode 100644 index 0000000..73c4cde --- /dev/null +++ b/NDB.Test.Api/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "NDB.Test.Api": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/NDB.Test.Api/Startup.cs b/NDB.Test.Api/Startup.cs new file mode 100644 index 0000000..a33d886 --- /dev/null +++ b/NDB.Test.Api/Startup.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using NDB.Extensions.Swagger; +using NDB.Extensions.Swagger.Constants; +using NDB.Test.Api.Extensions; + +namespace NDB.Test.Api +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // Add basic authentication + services.AddIdentityAuthentication(Configuration.GetSection("IdentityServer")["BaseAddress"]); + + services.AddControllers(); + services.AddSwagger("NDB.Test.Api", AuthorizationType.InhouseIdentity); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "NDB.Test.Api v1")); + } + + app.ConfigureSwagger("NDB.Test.Api v1"); + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/NDB.Test.Api/appsettings.json b/NDB.Test.Api/appsettings.json new file mode 100644 index 0000000..91185aa --- /dev/null +++ b/NDB.Test.Api/appsettings.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "IdentityServer": { + //"BaseAddress": "http://localhost:5063/" + "BaseAddress": "https://toodle.ddns.net/identity-server-api/" + } +} diff --git a/NDB.sln b/NDB.sln index db1e5b3..f1fe871 100644 --- a/NDB.sln +++ b/NDB.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29324.140 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NDB.Hosting.WindowsService", "NDB.Hosting.WindowsService\NDB.Hosting.WindowsService.csproj", "{F4AB0B71-6B14-402B-8B05-128AFB3E06D9}" EndProject @@ -54,7 +54,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NDB.Security.Authentication EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "caching", "caching", "{A206A484-3ACF-4032-8B36-AC93A72B0B88}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NDB.Extensions.Caching", "NDB.Extensions.Caching\NDB.Extensions.Caching.csproj", "{3E045EE6-A290-467C-B503-3A6CB0065C97}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NDB.Extensions.Caching", "NDB.Extensions.Caching\NDB.Extensions.Caching.csproj", "{3E045EE6-A290-467C-B503-3A6CB0065C97}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{CCEE458E-02A8-42FD-8F5F-A35481A23303}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NDB.Test.Api", "NDB.Test.Api\NDB.Test.Api.csproj", "{F717BE3D-F5F4-4D99-B96D-D0A23E8BED01}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -102,6 +106,10 @@ Global {3E045EE6-A290-467C-B503-3A6CB0065C97}.Debug|Any CPU.Build.0 = Debug|Any CPU {3E045EE6-A290-467C-B503-3A6CB0065C97}.Release|Any CPU.ActiveCfg = Release|Any CPU {3E045EE6-A290-467C-B503-3A6CB0065C97}.Release|Any CPU.Build.0 = Release|Any CPU + {F717BE3D-F5F4-4D99-B96D-D0A23E8BED01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F717BE3D-F5F4-4D99-B96D-D0A23E8BED01}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F717BE3D-F5F4-4D99-B96D-D0A23E8BED01}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F717BE3D-F5F4-4D99-B96D-D0A23E8BED01}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -127,6 +135,8 @@ Global {5C0637C8-6BA4-4EAE-97CA-BB8D98B2991A} = {B8132F39-6677-4D70-84CA-9747DC9086B3} {A206A484-3ACF-4032-8B36-AC93A72B0B88} = {B50B55F0-9E6E-4061-9100-E2329A44E76B} {3E045EE6-A290-467C-B503-3A6CB0065C97} = {A206A484-3ACF-4032-8B36-AC93A72B0B88} + {CCEE458E-02A8-42FD-8F5F-A35481A23303} = {E0202271-4E92-4DB8-900D-B5FD745B9278} + {F717BE3D-F5F4-4D99-B96D-D0A23E8BED01} = {CCEE458E-02A8-42FD-8F5F-A35481A23303} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87541BAB-3FAC-4ADB-A7FB-8228DA87843D}