From 21ff43c1db1b56ba1f39b7872dce8c527caeeac2 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Sat, 7 Apr 2018 01:00:27 -0400 Subject: [PATCH] Preliminary AuthenticationContext stuff --- src/Tgstation.Server.Api/Headers.cs | 109 ++++++++++++++++++ src/Tgstation.Server.Host/Core/Application.cs | 4 +- .../Core/ApplicationBuilderExtensions.cs | 16 +++ .../Core/AuthenticationContext.cs | 63 ++++++++++ .../Core/HttpContextExtensions.cs | 12 ++ .../Core/IAuthenticationContext.cs | 15 +++ 6 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 src/Tgstation.Server.Api/Headers.cs create mode 100644 src/Tgstation.Server.Host/Core/ApplicationBuilderExtensions.cs create mode 100644 src/Tgstation.Server.Host/Core/AuthenticationContext.cs create mode 100644 src/Tgstation.Server.Host/Core/HttpContextExtensions.cs create mode 100644 src/Tgstation.Server.Host/Core/IAuthenticationContext.cs diff --git a/src/Tgstation.Server.Api/Headers.cs b/src/Tgstation.Server.Api/Headers.cs new file mode 100644 index 0000000000..9f5f62955a --- /dev/null +++ b/src/Tgstation.Server.Api/Headers.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Tgstation.Server.Api +{ + public sealed class Headers + { + const string userAgentHeader = "User-Agent"; + const string apiVersionHeader = "Api-Version"; + const string tokenHeader = "Token"; + const string usernameHeader = "Username"; + const string passwordHeader = "Password"; + + public static readonly Version CurrentApiVersion = Assembly.GetExecutingAssembly().GetName().Version; + + public IReadOnlyDictionary HeaderEntries => headerEntries; + + public string UserAgent { + get => headerEntries[userAgentHeader]; + private set => headerEntries[userAgentHeader] = value; + } + + public Version ApiVersion + { + get => new Version(headerEntries[apiVersionHeader]); + private set => headerEntries[userAgentHeader] = value.ToString(); + } + + public string Token + { + get => headerEntries[tokenHeader]; + private set => headerEntries[tokenHeader] = value; + } + + public string Username + { + get => headerEntries[usernameHeader]; + private set => headerEntries[usernameHeader] = value; + } + + public string Password + { + get => headerEntries[passwordHeader]; + private set => headerEntries[passwordHeader] = value; + } + + public bool IsTokenAuthentication => headerEntries.TryGetValue(tokenHeader, out string value); + + readonly Dictionary headerEntries; + + public Headers(string userAgent, string token) : this(userAgent, token, null, null) + { + if (userAgent == null) + throw new ArgumentNullException(nameof(userAgent)); + if (token == null) + throw new ArgumentNullException(nameof(token)); + } + + public Headers(string userAgent, string username, string password) : this(userAgent, null, username, password) + { + if (userAgent == null) + throw new ArgumentNullException(nameof(userAgent)); + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (password == null) + throw new ArgumentNullException(nameof(password)); + } + + public Headers(IReadOnlyDictionary headerEntries) + { + this.headerEntries = headerEntries?.ToDictionary(x => x.Key, x => x.Value) ?? throw new ArgumentNullException(nameof(headerEntries)); + AssertHeader(userAgentHeader); + AssertHeader(apiVersionHeader); + try + { + AssertHeader(usernameHeader); + AssertHeader(passwordHeader); + } + catch (InvalidOperationException) + { + AssertHeader(tokenHeader); + } + } + + void AssertHeader(string headerName) + { + try + { + var headerValue = headerEntries[headerName]; + } + catch(Exception e) + { + throw new InvalidOperationException("Missing required header!", e); + } + } + + Headers(string userAgent, string token, string username, string password) + { + headerEntries = new Dictionary(); + ApiVersion = CurrentApiVersion; + UserAgent = userAgent; + Token = token; + Username = username; + Password = password; + } + } +} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 83e0a44524..1c4cf52f9c 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -69,7 +69,9 @@ namespace Tgstation.Server.Host.Core SupportedCultures = supportedCultures, SupportedUICultures = supportedCultures, }); - + + applicationBuilder.UseSystemAuthentication(); + applicationBuilder.UseMvc(); } } diff --git a/src/Tgstation.Server.Host/Core/ApplicationBuilderExtensions.cs b/src/Tgstation.Server.Host/Core/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..a1714d6b5d --- /dev/null +++ b/src/Tgstation.Server.Host/Core/ApplicationBuilderExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Builder; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Tgstation.Server.Host.Core +{ + static class ApplicationBuilderExtensions + { + public static IApplicationBuilder UseSystemAuthentication(this IApplicationBuilder applicationBuilder) + { + AuthenticationContext.AddToPipeline(applicationBuilder); + return applicationBuilder; + } + } +} diff --git a/src/Tgstation.Server.Host/Core/AuthenticationContext.cs b/src/Tgstation.Server.Host/Core/AuthenticationContext.cs new file mode 100644 index 0000000000..6c770faa5b --- /dev/null +++ b/src/Tgstation.Server.Host/Core/AuthenticationContext.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Builder; +using System; +using System.Net; +using System.Linq; +using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Core +{ + sealed class AuthenticationContext : IAuthenticationContext + { + static readonly object contextKey = new object(); + + public static IAuthenticationContext Current(HttpContext httpContext) => (IAuthenticationContext)httpContext.Items[contextKey]; + + public static void AddToPipeline(IApplicationBuilder applicationBuilder) => applicationBuilder.Use(async (httpContext, next) => + { + Headers headers; + try + { + headers = new Headers(httpContext.Request.Headers.ToDictionary(x => x.Key, x => x.Value.First())); + } + catch (InvalidOperationException) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + return; + } + + using (var authContext = new AuthenticationContext(headers)) + { + if (!authContext.Valid) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + return; + } + httpContext.Items[contextKey] = authContext; + await next().ConfigureAwait(false); + } + }); + + public AuthenticationContext(Headers headers) + { + throw new NotImplementedException(); + } + + ~AuthenticationContext() => Dispose(); + + public void Dispose() + { + + GC.SuppressFinalize(this); + } + + public IAuthenticationContext Clone() => throw new NotImplementedException(); + + bool Valid => throw new NotImplementedException(); + + public User User => throw new NotImplementedException(); + + public InstanceUser InstanceUser => throw new NotImplementedException(); + } +} diff --git a/src/Tgstation.Server.Host/Core/HttpContextExtensions.cs b/src/Tgstation.Server.Host/Core/HttpContextExtensions.cs new file mode 100644 index 0000000000..7bca0daf5b --- /dev/null +++ b/src/Tgstation.Server.Host/Core/HttpContextExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Tgstation.Server.Host.Core +{ + static class HttpContextExtensions + { + public static IAuthenticationContext AuthenticationContext(this HttpContext httpContext) => Core.AuthenticationContext.Current(httpContext); + } +} diff --git a/src/Tgstation.Server.Host/Core/IAuthenticationContext.cs b/src/Tgstation.Server.Host/Core/IAuthenticationContext.cs new file mode 100644 index 0000000000..dacb668c86 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/IAuthenticationContext.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Core +{ + interface IAuthenticationContext : IDisposable + { + User User { get; } + + InstanceUser InstanceUser { get; } + + IAuthenticationContext Clone(); + } +} \ No newline at end of file