From 320d939c7674342a84f835427a81ec409b953962 Mon Sep 17 00:00:00 2001 From: littlefoot Date: Thu, 14 Oct 2021 20:54:58 -0400 Subject: [PATCH] Authentication finally moved to the dotnet way in webapi, ready to be added to to deal with users and such Introspection access point properly uses basic auth of client id and secret to access --- Web/Pages/CreateServer.razor | 2 +- WebAPI/Auth/CustomAuthorizationFilter.cs | 64 ++++++++++++++++++++++ WebAPI/Auth/OIDCAuthHandler.cs | 58 ++++++++++++++++++++ WebAPI/Controllers/HelloWorldController.cs | 13 ++--- WebAPI/Data/AppSettings.cs | 4 +- WebAPI/Data/OIDCService.cs | 23 ++++++-- WebAPI/Startup.cs | 23 +++++++- WebAPI/appsettings.Development.json | 4 +- WebAPI/appsettings.json | 4 +- 9 files changed, 176 insertions(+), 19 deletions(-) create mode 100644 WebAPI/Auth/CustomAuthorizationFilter.cs create mode 100644 WebAPI/Auth/OIDCAuthHandler.cs diff --git a/Web/Pages/CreateServer.razor b/Web/Pages/CreateServer.razor index 1a3029e..a7c81a3 100644 --- a/Web/Pages/CreateServer.razor +++ b/Web/Pages/CreateServer.razor @@ -7,7 +7,7 @@ @code { - protected async override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { base.OnInitializedAsync(); } diff --git a/WebAPI/Auth/CustomAuthorizationFilter.cs b/WebAPI/Auth/CustomAuthorizationFilter.cs new file mode 100644 index 0000000..698e510 --- /dev/null +++ b/WebAPI/Auth/CustomAuthorizationFilter.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using WebAPI.Data; + +namespace WebAPI.Auth +{ + public class OIDCAuthorization : ServiceFilterAttribute + { + public OIDCAuthorization() : base(typeof(CustomAuthorizationFilter)) + { + + } + } + /// + /// Authorization filter for checking the bearer token against the OIDC service. + /// Will short circuit on an invalid token + /// + /// + public class CustomAuthorizationFilter: IAsyncAuthorizationFilter + { + private readonly ILogger _logger; + private readonly OIDCService _oidcService; + private readonly AppDbContext _appDbContext; + + public CustomAuthorizationFilter(ILogger logger, OIDCService oidcService, AppDbContext appDbContext) + { + _logger = logger; + _oidcService = oidcService; + _appDbContext = appDbContext; + } + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + try + { + var httpContext = context.HttpContext; + var bearerToken = httpContext?.Request?.Headers[HeaderNames.Authorization].FirstOrDefault(); + if (string.IsNullOrEmpty(bearerToken)) + { + throw new Exception("Need a token"); + } + + string token = bearerToken.Split(" ").ElementAt(1); + if (!await _oidcService.ValidateAccessToken(token)) + { + throw new Exception("bad token"); + } + + } + catch (Exception e) + { + context.Result = new ForbidResult(e.Message); + } + } + } +} \ No newline at end of file diff --git a/WebAPI/Auth/OIDCAuthHandler.cs b/WebAPI/Auth/OIDCAuthHandler.cs new file mode 100644 index 0000000..53021aa --- /dev/null +++ b/WebAPI/Auth/OIDCAuthHandler.cs @@ -0,0 +1,58 @@ +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using WebAPI.Data; + +namespace WebAPI.Auth +{ + public class OIDCTokenAuthenticationHandler : AuthenticationHandler + { + private readonly OIDCService _oidcService; + public OIDCTokenAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, OIDCService oidcService) : base(options, logger, encoder, clock) + { + _oidcService = oidcService; + } + + protected override async Task HandleAuthenticateAsync() + { + var bearerToken = Request.Headers[HeaderNames.Authorization].FirstOrDefault() ?? ""; + if (string.IsNullOrEmpty(bearerToken)) + { + return AuthenticateResult.Fail("no token"); + } + + var token = bearerToken.Split(" ").ElementAt(1); + if (!await _oidcService.ValidateAccessToken(token)) + { + return AuthenticateResult.Fail("failed to validate token"); + } + + var identity = new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.Authentication, token) + }, Scheme.Name); + var principal = new ClaimsPrincipal(identity); + return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name)); + } + } + + public class OIDCTokenAuthenticationOptions : AuthenticationSchemeOptions + { + public string OIDCIntrospectionEndpoint { get; set; } + public string OIDCClientId { get; set; } + public string OIDCClientSecret { get; set; } + + } + + public class OIDCTokenAuthenticationDefaults + { + public static string DefaultScheme => "OIDCAuthentication"; + } +} \ No newline at end of file diff --git a/WebAPI/Controllers/HelloWorldController.cs b/WebAPI/Controllers/HelloWorldController.cs index e0b434c..ea37667 100644 --- a/WebAPI/Controllers/HelloWorldController.cs +++ b/WebAPI/Controllers/HelloWorldController.cs @@ -2,33 +2,30 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using WebAPI.Auth; using WebAPI.Data; namespace WebAPI.Controllers { + [Authorize(Policy = "test")] [Route("api/[controller]")] [ApiController] public class HelloWorldController : BaseController { private readonly PterodactylService _pterodactylService; - private readonly OIDCService _oidcService; - public HelloWorldController(PterodactylService pterodactylService, OIDCService oidcService) + public HelloWorldController(PterodactylService pterodactylService) { _pterodactylService = pterodactylService; - _oidcService = oidcService; } [HttpGet] public async Task HelloWorld() { - if (await _oidcService.ValidateAccessToken(BearerToken)) - { - return "Validated"; - } - return "Failed"; + return "Success"; } } diff --git a/WebAPI/Data/AppSettings.cs b/WebAPI/Data/AppSettings.cs index 4711cf1..f8ef49e 100644 --- a/WebAPI/Data/AppSettings.cs +++ b/WebAPI/Data/AppSettings.cs @@ -7,7 +7,9 @@ namespace WebAPI.Data { public static string PterodactylAPIKey { get; private set; } public static string PterodactylPanelURL { get; private set; } - public static string OIDCUserInfoEndpoint { get; private set; } + public static string OIDCIntrospectionEndpoint { get; private set; } + public static string OIDCClientId { get; private set; } + public static string OIDCClientSecret { get; set; } public static void Init(IConfiguration configuration) { var fields = typeof(AppSettings).GetProperties(); diff --git a/WebAPI/Data/OIDCService.cs b/WebAPI/Data/OIDCService.cs index 2ed297f..de8a665 100644 --- a/WebAPI/Data/OIDCService.cs +++ b/WebAPI/Data/OIDCService.cs @@ -1,14 +1,21 @@ using System; +using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; namespace WebAPI.Data { public class OIDCService { + protected class IntrospectionResponse + { + public bool Active { get; set; } + } private HttpClient _httpClient { get; set; } private ILogger _logger { get; set; } public OIDCService(ILogger logger) @@ -24,15 +31,23 @@ namespace WebAPI.Data /// success public async Task ValidateAccessToken(string accessToken) { - Uri requestUri = new Uri($"{AppSettings.OIDCUserInfoEndpoint}"); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + Uri requestUri = new Uri($"https://{AppSettings.OIDCIntrospectionEndpoint}"); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Content = new FormUrlEncodedContent(new Dictionary() + { + {"token", accessToken} + }); + string encodedAuth = Convert.ToBase64String(Encoding.GetEncoding(Encoding.Latin1.CodePage) + .GetBytes($"{AppSettings.OIDCClientId}:{AppSettings.OIDCClientSecret}")); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", encodedAuth); HttpResponseMessage response = await _httpClient.SendAsync(request); if (!response.IsSuccessStatusCode) { return false; } - return true; + + var responsecontent = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responsecontent).Active; } } diff --git a/WebAPI/Startup.cs b/WebAPI/Startup.cs index 28bf8ed..37c54ac 100644 --- a/WebAPI/Startup.cs +++ b/WebAPI/Startup.cs @@ -1,7 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpsPolicy; @@ -12,6 +16,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; +using WebAPI.Auth; using WebAPI.Data; namespace WebAPI @@ -61,8 +66,21 @@ namespace WebAPI }); }); services.AddDbContext(options => options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"))); - services.AddSingleton(); - services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddAuthentication(opt => + { + opt.DefaultScheme = OIDCTokenAuthenticationDefaults.DefaultScheme; + }) + .AddScheme( + OIDCTokenAuthenticationDefaults.DefaultScheme, + opt => + { + opt.OIDCClientId = AppSettings.OIDCClientId; + opt.OIDCClientSecret = AppSettings.OIDCClientSecret; + opt.OIDCIntrospectionEndpoint = AppSettings.OIDCIntrospectionEndpoint; + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -79,6 +97,7 @@ namespace WebAPI app.UseRouting(); + app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); diff --git a/WebAPI/appsettings.Development.json b/WebAPI/appsettings.Development.json index 2900c0a..182c7fb 100644 --- a/WebAPI/appsettings.Development.json +++ b/WebAPI/appsettings.Development.json @@ -4,8 +4,8 @@ }, "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", + "Default": "Debug", + "Microsoft": "Debug", "Microsoft.Hosting.Lifetime": "Information" } } diff --git a/WebAPI/appsettings.json b/WebAPI/appsettings.json index 01fe3bc..36832eb 100644 --- a/WebAPI/appsettings.json +++ b/WebAPI/appsettings.json @@ -12,5 +12,7 @@ "AllowedHosts": "*", "PterodactylAPIKey": "REPLACE_ME", "PterodactylPanelURL": "https://panel.orfl.xyz", - "OIDCUserInfoEndpoint": "https://authentik.mattstop.com/application/o/userinfo/" + "OIDCIntrospectionEndpoint": "authentik.mattstop.com/application/o/introspect/", + "OIDCClientId": "", + "OIDCClientSecret": "" }