begin implementation of token authentication

replacing password authentication ingame
precompile views for webfront
issue #66
issue #52
This commit is contained in:
RaidMax 2019-02-15 22:19:59 -06:00
parent a362caebac
commit 5e36bf4316
14 changed files with 172 additions and 16 deletions

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework> <TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.2.1</RuntimeFrameworkVersion> <RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish> <MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<PackageId>RaidMax.IW4MAdmin.Application</PackageId> <PackageId>RaidMax.IW4MAdmin.Application</PackageId>
<Version>2.2.5.0</Version> <Version>2.2.5.0</Version>

View File

@ -1,5 +1,6 @@
using IW4MAdmin.Application.API.Master; using IW4MAdmin.Application.API.Master;
using IW4MAdmin.Application.EventParsers; using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.Misc;
using IW4MAdmin.Application.RconParsers; using IW4MAdmin.Application.RconParsers;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Commands; using SharedLibraryCore.Commands;
@ -39,6 +40,10 @@ namespace IW4MAdmin.Application
public IList<IRConParser> AdditionalRConParsers { get; } public IList<IRConParser> AdditionalRConParsers { get; }
public IList<IEventParser> AdditionalEventParsers { get; } public IList<IEventParser> AdditionalEventParsers { get; }
public ITokenAuthentication TokenAuthenticator => Authenticator;
public ITokenAuthentication Authenticator => _authenticator;
static ApplicationManager Instance; static ApplicationManager Instance;
readonly List<AsyncStatus> TaskStatuses; readonly List<AsyncStatus> TaskStatuses;
List<Command> Commands; List<Command> Commands;
@ -52,6 +57,7 @@ namespace IW4MAdmin.Application
readonly IPageList PageList; readonly IPageList PageList;
readonly SemaphoreSlim ProcessingEvent = new SemaphoreSlim(1, 1); readonly SemaphoreSlim ProcessingEvent = new SemaphoreSlim(1, 1);
readonly Dictionary<long, ILogger> Loggers = new Dictionary<long, ILogger>(); readonly Dictionary<long, ILogger> Loggers = new Dictionary<long, ILogger>();
readonly ITokenAuthentication _authenticator;
private ApplicationManager() private ApplicationManager()
{ {
@ -70,6 +76,7 @@ namespace IW4MAdmin.Application
AdditionalRConParsers = new List<IRConParser>(); AdditionalRConParsers = new List<IRConParser>();
OnServerEvent += OnGameEvent; OnServerEvent += OnGameEvent;
OnServerEvent += EventApi.OnGameEvent; OnServerEvent += EventApi.OnGameEvent;
_authenticator = new TokenAuthentication();
} }
private async void OnGameEvent(object sender, GameEventArgs args) private async void OnGameEvent(object sender, GameEventArgs args)
@ -369,6 +376,7 @@ namespace IW4MAdmin.Application
Commands.Add(new CPing()); Commands.Add(new CPing());
Commands.Add(new CSetGravatar()); Commands.Add(new CSetGravatar());
Commands.Add(new CNextMap()); Commands.Add(new CNextMap());
Commands.Add(new GenerateTokenCommand());
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands) foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
{ {

View File

@ -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<long, TokenState> _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<long, TokenState>();
_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();
}
}
}

View File

@ -1,4 +1,4 @@
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo> @model List<dynamic>
<h4 class="pb-2 text-center ">@ViewBag.Title</h4> <h4 class="pb-2 text-center ">@ViewBag.Title</h4>
<div id="stats_top_players" class="striped border-top border-bottom"> <div id="stats_top_players" class="striped border-top border-bottom">

View File

@ -1,4 +1,4 @@
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo> @model List<dynamic>
@{ @{
Layout = null; Layout = null;
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex.Set; var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex.Set;
@ -46,7 +46,7 @@
</div> </div>
} }
<span class="text-muted pl-1 pr-1" style="font-size: 1.25rem;">&mdash;</span> <span class="text-muted pl-1 pr-1" style="font-size: 1.25rem;">&mdash;</span>
@Html.ActionLink(stat.Name, "ProfileAsync", "Client", new { id = stat.ClientId }) @Html.ActionLink((string)stat.Name, "ProfileAsync", "Client", new { id = stat.ClientId })
</div> </div>
<span class="text-primary">@stat.Performance</span><span class="text-muted"> @loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"]</span><br /> <span class="text-primary">@stat.Performance</span><span class="text-muted"> @loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"]</span><br />

View File

@ -1,4 +1,4 @@
@model IEnumerable<IW4MAdmin.Plugins.Stats.Models.EFACSnapshot> @model IEnumerable<dynamic>
@{ @{
Layout = null; Layout = null;
} }
@ -7,7 +7,7 @@
@foreach (var snapshot in Model) @foreach (var snapshot in Model)
{ {
<!-- this is not ideal, but I didn't want to manually write out all the properties--> <!-- this is not ideal, but I didn't want to manually write out all the properties-->
var snapProperties = typeof(IW4MAdmin.Plugins.Stats.Models.EFACSnapshot).GetProperties(); var snapProperties = Model.First().GetType().GetProperties();
foreach (var prop in snapProperties) foreach (var prop in snapProperties)
{ {
<!-- this is another ugly hack--> <!-- this is another ugly hack-->

View File

@ -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;
}
}
}

View File

@ -45,5 +45,6 @@ namespace SharedLibraryCore.Interfaces
IRConParser GenerateDynamicRConParser(); IRConParser GenerateDynamicRConParser();
IEventParser GenerateDynamicEventParser(); IEventParser GenerateDynamicEventParser();
string Version { get;} string Version { get;}
ITokenAuthentication TokenAuthenticator { get; }
} }
} }

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SharedLibraryCore.Interfaces
{
public interface ITokenAuthentication
{
/// <summary>
/// generates and returns a token for the given network id
/// </summary>
/// <param name="networkId">network id of the players to generate the token for</param>
/// <returns>4 character string token</returns>
string GenerateNextToken(long networkId);
/// <summary>
/// authorizes given token
/// </summary>
/// <param name="token">token to authorize</param>
/// <returns>true if token authorized successfully, false otherwise</returns>
bool AuthorizeToken(long networkId, string token);
}
}

View File

@ -3,11 +3,11 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework> <TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.2.1</RuntimeFrameworkVersion> <RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
<ApplicationIcon /> <ApplicationIcon />
<StartupObject /> <StartupObject />
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId> <PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2.1.0</Version> <Version>2.2.0</Version>
<Authors>RaidMax</Authors> <Authors>RaidMax</Authors>
<Company>Forever None</Company> <Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>

View File

@ -1,9 +1,10 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authentication;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System; using System;
using Microsoft.AspNetCore.Authentication; using System.Security.Claims;
using System.Threading.Tasks;
namespace WebfrontCore.Controllers namespace WebfrontCore.Controllers
{ {
@ -60,5 +61,12 @@ namespace WebfrontCore.Controllers
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction("Index", "Home"); return RedirectToAction("Index", "Home");
} }
[HttpGet]
[Authorize]
public async Task<IActionResult> GenerateLoginTokenAsync()
{
return Json(new { token = Manager.TokenAuthenticator.GenerateNextToken(Client.NetworkId) });
}
} }
} }

View File

@ -2,9 +2,9 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework> <TargetFramework>netcoreapp2.2</TargetFramework>
<RuntimeFrameworkVersion>2.2.1</RuntimeFrameworkVersion> <RuntimeFrameworkVersion>2.2.2</RuntimeFrameworkVersion>
<RazorCompileOnBuild>false</RazorCompileOnBuild> <RazorCompileOnBuild>true</RazorCompileOnBuild>
<RazorCompileOnPublish>false</RazorCompileOnPublish> <RazorCompileOnPublish>true</RazorCompileOnPublish>
<PreserveCompilationContext>true</PreserveCompilationContext> <PreserveCompilationContext>true</PreserveCompilationContext>
<TypeScriptToolsVersion>2.6</TypeScriptToolsVersion> <TypeScriptToolsVersion>2.6</TypeScriptToolsVersion>
<PackageId>RaidMax.IW4MAdmin.WebfrontCore</PackageId> <PackageId>RaidMax.IW4MAdmin.WebfrontCore</PackageId>
@ -13,7 +13,7 @@
<Company>Forever None</Company> <Company>Forever None</Company>
<Product>IW4MAdmin</Product> <Product>IW4MAdmin</Product>
<Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated server</Description> <Description>IW4MAdmin is a complete server administration tool for IW4x and most Call of Duty® dedicated server</Description>
<Copyright>2018</Copyright> <Copyright>2019</Copyright>
<PackageLicenseUrl>https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/RaidMax/IW4M-Admin/blob/master/LICENSE</PackageLicenseUrl>
<PackageProjectUrl>https://raidmax.org/IW4MAdmin</PackageProjectUrl> <PackageProjectUrl>https://raidmax.org/IW4MAdmin</PackageProjectUrl>
<RepositoryUrl>https://github.com/RaidMax/IW4M-Admin</RepositoryUrl> <RepositoryUrl>https://github.com/RaidMax/IW4M-Admin</RepositoryUrl>