From 5e36bf43168fa24839700a83461b724239c27fe4 Mon Sep 17 00:00:00 2001 From: RaidMax Date: Fri, 15 Feb 2019 22:19:59 -0600 Subject: [PATCH] begin implementation of token authentication replacing password authentication ingame precompile views for webfront issue #66 issue #52 --- Application/Application.csproj | 2 +- Application/ApplicationManager.cs | 8 ++ Application/{ => Misc}/Logger.cs | 0 Application/{ => Misc}/PageList.cs | 0 Application/Misc/TokenAuthentication.cs | 94 +++++++++++++++++++ Plugins/Stats/Web/Views/Stats/Index.cshtml | 2 +- Plugins/Stats/Web/Views/Stats/_List.cshtml | 4 +- .../Stats/Web/Views/Stats/_PenaltyInfo.cshtml | 4 +- .../Commands/GenerateTokenCommand.cs | 22 +++++ SharedLibraryCore/Interfaces/IManager.cs | 1 + .../Interfaces/ITokenAuthentication.cs | 23 +++++ SharedLibraryCore/SharedLibraryCore.csproj | 4 +- WebfrontCore/Controllers/AccountController.cs | 16 +++- WebfrontCore/WebfrontCore.csproj | 8 +- 14 files changed, 172 insertions(+), 16 deletions(-) rename Application/{ => Misc}/Logger.cs (100%) rename Application/{ => Misc}/PageList.cs (100%) create mode 100644 Application/Misc/TokenAuthentication.cs create mode 100644 SharedLibraryCore/Commands/GenerateTokenCommand.cs create mode 100644 SharedLibraryCore/Interfaces/ITokenAuthentication.cs diff --git a/Application/Application.csproj b/Application/Application.csproj index 3097038e4..23357d6bc 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -3,7 +3,7 @@ Exe netcoreapp2.2 - 2.2.1 + 2.2.2 false RaidMax.IW4MAdmin.Application 2.2.5.0 diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs index 7d13f754a..da9823031 100644 --- a/Application/ApplicationManager.cs +++ b/Application/ApplicationManager.cs @@ -1,5 +1,6 @@ using IW4MAdmin.Application.API.Master; using IW4MAdmin.Application.EventParsers; +using IW4MAdmin.Application.Misc; using IW4MAdmin.Application.RconParsers; using SharedLibraryCore; using SharedLibraryCore.Commands; @@ -39,6 +40,10 @@ namespace IW4MAdmin.Application public IList AdditionalRConParsers { get; } public IList AdditionalEventParsers { get; } + public ITokenAuthentication TokenAuthenticator => Authenticator; + + public ITokenAuthentication Authenticator => _authenticator; + static ApplicationManager Instance; readonly List TaskStatuses; List Commands; @@ -52,6 +57,7 @@ namespace IW4MAdmin.Application readonly IPageList PageList; readonly SemaphoreSlim ProcessingEvent = new SemaphoreSlim(1, 1); readonly Dictionary Loggers = new Dictionary(); + readonly ITokenAuthentication _authenticator; private ApplicationManager() { @@ -70,6 +76,7 @@ namespace IW4MAdmin.Application AdditionalRConParsers = new List(); OnServerEvent += OnGameEvent; OnServerEvent += EventApi.OnGameEvent; + _authenticator = new TokenAuthentication(); } private async void OnGameEvent(object sender, GameEventArgs args) @@ -369,6 +376,7 @@ namespace IW4MAdmin.Application Commands.Add(new CPing()); Commands.Add(new CSetGravatar()); Commands.Add(new CNextMap()); + Commands.Add(new GenerateTokenCommand()); foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands) { diff --git a/Application/Logger.cs b/Application/Misc/Logger.cs similarity index 100% rename from Application/Logger.cs rename to Application/Misc/Logger.cs diff --git a/Application/PageList.cs b/Application/Misc/PageList.cs similarity index 100% rename from Application/PageList.cs rename to Application/Misc/PageList.cs diff --git a/Application/Misc/TokenAuthentication.cs b/Application/Misc/TokenAuthentication.cs new file mode 100644 index 000000000..41249d79a --- /dev/null +++ b/Application/Misc/TokenAuthentication.cs @@ -0,0 +1,94 @@ +using SharedLibraryCore.Interfaces; +using System; +using System.Collections.Concurrent; +using System.Security.Cryptography; +using System.Text; + +namespace IW4MAdmin.Application.Misc +{ + class TokenAuthentication : ITokenAuthentication + { + private readonly ConcurrentDictionary _tokens; + private readonly RNGCryptoServiceProvider _random; + private readonly static TimeSpan _timeoutPeriod = new TimeSpan(0, 0, 30); + private const short TOKEN_LENGTH = 4; + + private class TokenState + { + public long NetworkId { get; set; } + public DateTime RequestTime { get; set; } = DateTime.Now; + public string Token { get; set; } + } + + public TokenAuthentication() + { + _tokens = new ConcurrentDictionary(); + _random = new RNGCryptoServiceProvider(); + } + + public bool AuthorizeToken(long networkId, string token) + { + bool authorizeSuccessful = _tokens.ContainsKey(networkId) && _tokens[networkId].Token == token; + + if (authorizeSuccessful) + { + _tokens.TryRemove(networkId, out TokenState _); + } + + return authorizeSuccessful; + } + + public string GenerateNextToken(long networkId) + { + TokenState state = null; + if (_tokens.ContainsKey(networkId)) + { + state = _tokens[networkId]; + + if ((DateTime.Now - state.RequestTime) < _timeoutPeriod) + { + return null; + } + + else + { + _tokens.TryRemove(networkId, out TokenState _); + } + } + + state = new TokenState() + { + NetworkId = networkId, + Token = _generateToken() + }; + + _tokens.TryAdd(networkId, state); + return state.Token; + } + + public string _generateToken() + { + bool validCharacter(char c) + { + // this ensure that the characters are 0-9, A-Z, a-z + return (c > 47 && c < 58) || (c > 64 && c < 91) || (c > 96 && c < 123); + } + + StringBuilder token = new StringBuilder(); + + while (token.Length < TOKEN_LENGTH) + { + byte[] charSet = new byte[1]; + _random.GetBytes(charSet); + + if (validCharacter((char)charSet[0])) + { + token.Append((char)charSet[0]); + } + } + + _random.Dispose(); + return token.ToString(); + } + } +} diff --git a/Plugins/Stats/Web/Views/Stats/Index.cshtml b/Plugins/Stats/Web/Views/Stats/Index.cshtml index 2537882ea..3d251ccb0 100644 --- a/Plugins/Stats/Web/Views/Stats/Index.cshtml +++ b/Plugins/Stats/Web/Views/Stats/Index.cshtml @@ -1,4 +1,4 @@ -@model List +@model List

@ViewBag.Title

diff --git a/Plugins/Stats/Web/Views/Stats/_List.cshtml b/Plugins/Stats/Web/Views/Stats/_List.cshtml index 66588a0f3..9bd53b4f6 100644 --- a/Plugins/Stats/Web/Views/Stats/_List.cshtml +++ b/Plugins/Stats/Web/Views/Stats/_List.cshtml @@ -1,4 +1,4 @@ -@model List +@model List @{ Layout = null; var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex.Set; @@ -46,7 +46,7 @@
} - @Html.ActionLink(stat.Name, "ProfileAsync", "Client", new { id = stat.ClientId }) + @Html.ActionLink((string)stat.Name, "ProfileAsync", "Client", new { id = stat.ClientId }) @stat.Performance @loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"]
diff --git a/Plugins/Stats/Web/Views/Stats/_PenaltyInfo.cshtml b/Plugins/Stats/Web/Views/Stats/_PenaltyInfo.cshtml index 08f091a30..a3059776a 100644 --- a/Plugins/Stats/Web/Views/Stats/_PenaltyInfo.cshtml +++ b/Plugins/Stats/Web/Views/Stats/_PenaltyInfo.cshtml @@ -1,4 +1,4 @@ -@model IEnumerable +@model IEnumerable @{ Layout = null; } @@ -7,7 +7,7 @@ @foreach (var snapshot in Model) { - var snapProperties = typeof(IW4MAdmin.Plugins.Stats.Models.EFACSnapshot).GetProperties(); + var snapProperties = Model.First().GetType().GetProperties(); foreach (var prop in snapProperties) { diff --git a/SharedLibraryCore/Commands/GenerateTokenCommand.cs b/SharedLibraryCore/Commands/GenerateTokenCommand.cs new file mode 100644 index 000000000..596837aa5 --- /dev/null +++ b/SharedLibraryCore/Commands/GenerateTokenCommand.cs @@ -0,0 +1,22 @@ +using SharedLibraryCore.Database.Models; +using System.Threading.Tasks; + +namespace SharedLibraryCore.Commands +{ + public class GenerateTokenCommand : Command + { + public GenerateTokenCommand() : + base("generatetoken", Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_DESC"], "gt", EFClient.Permission.Trusted, false) + { } + + public override Task ExecuteAsync(GameEvent E) + { + string token = E.Owner.Manager.TokenAuthenticator.GenerateNextToken(E.Origin.NetworkId); + var _event = token == null ? + E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_FAIL"]) : + E.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_SUCCESS"]} {token}"); + + return Task.CompletedTask; + } +} +} diff --git a/SharedLibraryCore/Interfaces/IManager.cs b/SharedLibraryCore/Interfaces/IManager.cs index 7b2d383e2..8c0e79dee 100644 --- a/SharedLibraryCore/Interfaces/IManager.cs +++ b/SharedLibraryCore/Interfaces/IManager.cs @@ -45,5 +45,6 @@ namespace SharedLibraryCore.Interfaces IRConParser GenerateDynamicRConParser(); IEventParser GenerateDynamicEventParser(); string Version { get;} + ITokenAuthentication TokenAuthenticator { get; } } } diff --git a/SharedLibraryCore/Interfaces/ITokenAuthentication.cs b/SharedLibraryCore/Interfaces/ITokenAuthentication.cs new file mode 100644 index 000000000..f9752862b --- /dev/null +++ b/SharedLibraryCore/Interfaces/ITokenAuthentication.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SharedLibraryCore.Interfaces +{ + public interface ITokenAuthentication + { + /// + /// generates and returns a token for the given network id + /// + /// network id of the players to generate the token for + /// 4 character string token + string GenerateNextToken(long networkId); + + /// + /// authorizes given token + /// + /// token to authorize + /// true if token authorized successfully, false otherwise + bool AuthorizeToken(long networkId, string token); + } +} diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index dae6cb631..9fb48da62 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -3,11 +3,11 @@ Library netcoreapp2.2 - 2.2.1 + 2.2.2 RaidMax.IW4MAdmin.SharedLibraryCore - 2.1.0 + 2.2.0 RaidMax Forever None Debug;Release;Prerelease diff --git a/WebfrontCore/Controllers/AccountController.cs b/WebfrontCore/Controllers/AccountController.cs index 9465f6586..60d947322 100644 --- a/WebfrontCore/Controllers/AccountController.cs +++ b/WebfrontCore/Controllers/AccountController.cs @@ -1,9 +1,10 @@ -using Microsoft.AspNetCore.Mvc; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; -using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using System; -using Microsoft.AspNetCore.Authentication; +using System.Security.Claims; +using System.Threading.Tasks; namespace WebfrontCore.Controllers { @@ -60,5 +61,12 @@ namespace WebfrontCore.Controllers await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return RedirectToAction("Index", "Home"); } + + [HttpGet] + [Authorize] + public async Task GenerateLoginTokenAsync() + { + return Json(new { token = Manager.TokenAuthenticator.GenerateNextToken(Client.NetworkId) }); + } } } diff --git a/WebfrontCore/WebfrontCore.csproj b/WebfrontCore/WebfrontCore.csproj index 384924a32..56525595e 100644 --- a/WebfrontCore/WebfrontCore.csproj +++ b/WebfrontCore/WebfrontCore.csproj @@ -2,9 +2,9 @@ netcoreapp2.2 - 2.2.1 - false - false + 2.2.2 + true + true true 2.6 RaidMax.IW4MAdmin.WebfrontCore @@ -13,7 +13,7 @@ Forever None IW4MAdmin IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated server - 2018 + 2019 https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE https://raidmax.org/IW4MAdmin https://github.com/RaidMax/IW4M-Admin