mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-09-06 09:36:20 +00:00
Rework login and registration systems (#600)
* Initial work for verifying login ticket signatures * Add candidate psn public key * Add candidate psn public key and fix nuget packages * Finalize npticket changes * Add support for ticket version 3.0 * Rework login system to link platform accounts instead of using ip addresses * Make linked accounts green instead of blue * Fix api building * Fix unit tests * Actually fix unit tests * Set unit test user's linked platform * Why was this the wrong default value? * Fix username change code * Make TicketHash hash the entire ticket instead of just the serial * Send password setup email when user sets their email for the first time * Changes from self review
This commit is contained in:
parent
ff7969a147
commit
19ea44e0e2
37 changed files with 836 additions and 449 deletions
|
@ -4,62 +4,38 @@
|
|||
|
||||
@{
|
||||
Layout = "Layouts/BaseLayout";
|
||||
Model.Title = "Authentication";
|
||||
Model.Title = "Linked Accounts";
|
||||
string timeZone = Model.GetTimeZone();
|
||||
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
|
||||
}
|
||||
|
||||
@if (Model.AuthenticationAttempts.Count == 0)
|
||||
@if (Model.LinkAttempts.Count == 0)
|
||||
{
|
||||
<p>You have no pending authentication attempts.</p>
|
||||
<p>You have no pending link attempts.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>You have @Model.AuthenticationAttempts.Count authentication attempts pending.</p>
|
||||
<p>You have @Model.LinkAttempts.Count authentication attempts pending.</p>
|
||||
@if (Model.IpAddress != null)
|
||||
{
|
||||
<p>This device's IP address is <b>@(Model.IpAddress.ToString())</b>. If this matches with an authentication attempt below, then it's safe to assume the authentication attempt came from the same network as this device.</p>
|
||||
<p>This device's IP address is <b>@(Model.IpAddress.ToString())</b>. If this matches with a link attempt below, then it's safe to assume the link attempt came from the same network as this device.</p>
|
||||
}
|
||||
}
|
||||
|
||||
@if (Model.User!.ApprovedIPAddress != null)
|
||||
@foreach (PlatformLinkAttempt authAttempt in Model.LinkAttempts)
|
||||
{
|
||||
<a href="/authentication/revokeAutoApproval">
|
||||
<button class="ui red button">
|
||||
<i class="trash icon"></i>
|
||||
<span>Revoke automatically approved IP Address (@Model.User!.ApprovedIPAddress)</span>
|
||||
</button>
|
||||
</a>
|
||||
}
|
||||
@if (Model.AuthenticationAttempts.Count > 1)
|
||||
{
|
||||
<a href="/authentication/denyAll">
|
||||
<button class="ui red button">
|
||||
<i class="x icon"></i>
|
||||
<span>Deny all</span>
|
||||
</button>
|
||||
</a>
|
||||
}
|
||||
|
||||
@foreach (AuthenticationAttempt authAttempt in Model.AuthenticationAttempts)
|
||||
{
|
||||
DateTimeOffset timestamp = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp), timeZoneInfo);
|
||||
DateTimeOffset timestamp = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(authAttempt.Timestamp), timeZoneInfo);
|
||||
<div class="ui red segment">
|
||||
<p>A <b>@authAttempt.Platform</b> authentication request was logged at <b>@timestamp.ToString("M/d/yyyy @ h:mm tt")</b> from the IP address <b>@authAttempt.IPAddress</b>.</p>
|
||||
<p>A <b>@authAttempt.Platform</b> link request was logged at <b>@timestamp.ToString("M/d/yyyy @ h:mm tt")</b> from the IP address <b>@authAttempt.IPAddress</b>.</p>
|
||||
<p><i class="yellow warning icon"></i> If you approve this request it will override any other linked accounts you have</p>
|
||||
<div>
|
||||
<a href="/authentication/autoApprove/@authAttempt.AuthenticationAttemptId">
|
||||
<a href="/authentication/approve/@authAttempt.PlatformLinkAttemptId">
|
||||
<button class="ui small green button">
|
||||
<i class="check icon"></i>
|
||||
<span>Automatically approve every time</span>
|
||||
<span>Approve</span>
|
||||
</button>
|
||||
</a>
|
||||
<a href="/authentication/approve/@authAttempt.AuthenticationAttemptId">
|
||||
<button class="ui small yellow button">
|
||||
<i class="check icon"></i>
|
||||
<span>Approve this time</span>
|
||||
</button>
|
||||
</a>
|
||||
<a href="/authentication/deny/@authAttempt.AuthenticationAttemptId">
|
||||
<a href="/authentication/deny/@authAttempt.PlatformLinkAttemptId">
|
||||
<button class="ui small red button">
|
||||
<i class="x icon"></i>
|
||||
<span>Deny</span>
|
||||
|
@ -67,4 +43,42 @@ else
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<div style="display: inline-block">
|
||||
<h3 style="display: inline-block">PSN: </h3>
|
||||
@if (Model.User?.LinkedPsnId != 0)
|
||||
{
|
||||
<div class="ui green button" style="cursor: default; pointer-events: none">
|
||||
Linked
|
||||
</div>
|
||||
<a href="/authentication/unlink/psn" style="display: block; color: orangered">
|
||||
Click here to unlink this platform
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="ui button" style="cursor: default; pointer-events: none">
|
||||
Unlinked
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div>
|
||||
<h3 style="display: inline-block">RPCN: </h3>
|
||||
@if (Model.User?.LinkedRpcnId != 0)
|
||||
{
|
||||
<div class="ui green button" style="cursor: default; pointer-events: none">
|
||||
Linked
|
||||
</div>
|
||||
<a href="/authentication/unlink/rpcn" style="display: block; color: orangered">
|
||||
Click here to unlink this platform
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="ui button" style="cursor: default; pointer-events: none">
|
||||
Unlinked
|
||||
</button>
|
||||
}
|
||||
</div>
|
|
@ -1,18 +1,15 @@
|
|||
#nullable enable
|
||||
using System.Net;
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData;
|
||||
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
|
||||
using LBPUnion.ProjectLighthouse.Types;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.ExternalAuth;
|
||||
|
||||
public class AuthenticationPage : BaseLayout
|
||||
{
|
||||
|
||||
public List<AuthenticationAttempt> AuthenticationAttempts = new();
|
||||
public List<PlatformLinkAttempt> LinkAttempts = new();
|
||||
|
||||
public IPAddress? IpAddress;
|
||||
public AuthenticationPage(Database database) : base(database)
|
||||
|
@ -20,16 +17,14 @@ public class AuthenticationPage : BaseLayout
|
|||
|
||||
public IActionResult OnGet()
|
||||
{
|
||||
if (!ServerConfiguration.Instance.Authentication.UseExternalAuth) return this.NotFound();
|
||||
if (this.User == null) return this.StatusCode(403, "");
|
||||
|
||||
this.IpAddress = this.HttpContext.Connection.RemoteIpAddress;
|
||||
|
||||
this.AuthenticationAttempts = this.Database.AuthenticationAttempts.Include
|
||||
(a => a.GameToken)
|
||||
.Where(a => a.GameToken.UserId == this.User.UserId)
|
||||
.OrderByDescending(a => a.Timestamp)
|
||||
.ToList();
|
||||
this.LinkAttempts = this.Database.PlatformLinkAttempts
|
||||
.Where(l => l.UserId == this.User.UserId)
|
||||
.OrderByDescending(a => a.Timestamp)
|
||||
.ToList();
|
||||
|
||||
return this.Page();
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
@if (Model.User != null)
|
||||
{
|
||||
<p>@Model.Translate(LandingPageStrings.LoggedInAs, Model.User.Username)</p>
|
||||
if (ServerConfiguration.Instance.Authentication.UseExternalAuth && Model.PendingAuthAttempts > 0)
|
||||
if (Model.PendingAuthAttempts > 0)
|
||||
{
|
||||
<p>
|
||||
<b><a href="/authentication">@Model.Translate(LandingPageStrings.AuthAttemptsPending, Model.PendingAuthAttempts)</a></b>
|
||||
|
@ -30,18 +30,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
@if (Model.PlayersOnline.Count == 1)
|
||||
{
|
||||
<p>@Model.Translate(LandingPageStrings.UsersSingle)</p>
|
||||
}
|
||||
|
||||
else if (Model.PlayersOnline.Count == 0)
|
||||
@switch (Model.PlayersOnline.Count)
|
||||
{
|
||||
<p>@Model.Translate(LandingPageStrings.UsersNone)</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>@Model.Translate(LandingPageStrings.UsersMultiple, Model.PlayersOnline.Count)</p>
|
||||
case 0:
|
||||
<p>@Model.Translate(LandingPageStrings.UsersNone)</p>
|
||||
break;
|
||||
case 1:
|
||||
<p>@Model.Translate(LandingPageStrings.UsersSingle)</p>
|
||||
break;
|
||||
default:
|
||||
<p>@Model.Translate(LandingPageStrings.UsersMultiple, Model.PlayersOnline.Count)</p>
|
||||
break;
|
||||
}
|
||||
|
||||
@{
|
||||
|
@ -49,7 +49,12 @@ else
|
|||
foreach (User user in Model.PlayersOnline)
|
||||
{
|
||||
i++;
|
||||
@await user.ToLink(Html, ViewData, language, timeZone, true)if (i != Model.PlayersOnline.Count){<span>,</span>} @* whitespace has forced my hand *@
|
||||
@await user.ToLink(Html, ViewData, language, timeZone, true)
|
||||
@* whitespace has forced my hand *@
|
||||
if (i != Model.PlayersOnline.Count)
|
||||
{
|
||||
<span>,</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,9 +27,8 @@ public class LandingPage : BaseLayout
|
|||
if (user != null && user.PasswordResetRequired) return this.Redirect("~/passwordResetRequired");
|
||||
|
||||
if (user != null)
|
||||
this.PendingAuthAttempts = await this.Database.AuthenticationAttempts.Include
|
||||
(a => a.GameToken)
|
||||
.CountAsync(a => a.GameToken.UserId == user.UserId);
|
||||
this.PendingAuthAttempts = await this.Database.PlatformLinkAttempts
|
||||
.CountAsync(l => l.UserId == user.UserId);
|
||||
|
||||
List<int> userIds = await this.Database.LastContacts.Where(l => TimeHelper.Timestamp - l.Timestamp < 300).Select(l => l.UserId).ToListAsync();
|
||||
|
||||
|
|
|
@ -12,10 +12,7 @@
|
|||
}
|
||||
else
|
||||
{
|
||||
if (ServerConfiguration.Instance.Authentication.UseExternalAuth)
|
||||
{
|
||||
Model.NavigationItems.Add(new PageNavigationItem(BaseLayoutStrings.HeaderAuthentication, "/authentication", "key"));
|
||||
}
|
||||
Model.NavigationItems.Add(new PageNavigationItem(BaseLayoutStrings.HeaderAuthentication, "/authentication", "key"));
|
||||
|
||||
@if (Model.User.IsAdmin)
|
||||
{
|
||||
|
|
|
@ -60,7 +60,7 @@ public class LoginForm : BaseLayout
|
|||
}
|
||||
}
|
||||
|
||||
if (user == null)
|
||||
if (user == null || user.Password == null)
|
||||
{
|
||||
Logger.Warn($"User {username} failed to login on web due to invalid username", LogArea.Login);
|
||||
this.Error = ServerConfiguration.Instance.Mail.MailEnabled
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using JetBrains.Annotations;
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
|
@ -36,7 +35,7 @@ public class PasswordResetRequestForm : BaseLayout
|
|||
return this.Page();
|
||||
}
|
||||
|
||||
if (!new EmailAddressAttribute().IsValid(email))
|
||||
if (!SanitizationHelper.IsValidEmail(email))
|
||||
{
|
||||
this.Error = "This email is in an invalid format";
|
||||
return this.Page();
|
||||
|
|
|
@ -25,30 +25,9 @@ public class RegisterForm : BaseLayout
|
|||
[SuppressMessage("ReSharper", "SpecifyStringComparison")]
|
||||
public async Task<IActionResult> OnPost(string username, string password, string confirmPassword, string emailAddress)
|
||||
{
|
||||
if (ServerConfiguration.Instance.Authentication.PrivateRegistration)
|
||||
{
|
||||
if (this.Request.Query.ContainsKey("token"))
|
||||
{
|
||||
string? token = this.Request.Query["token"];
|
||||
if (!this.Database.IsRegistrationTokenValid(token))
|
||||
return this.StatusCode(403, this.Translate(ErrorStrings.TokenInvalid));
|
||||
if (this.Database.UserFromWebRequest(this.Request) != null) return this.Redirect("~/");
|
||||
|
||||
string? tokenUsername = await this.Database.RegistrationTokens.Where(r => r.Token == token)
|
||||
.Select(u => u.Username)
|
||||
.FirstOrDefaultAsync();
|
||||
if (tokenUsername == null) return this.BadRequest();
|
||||
|
||||
username = tokenUsername;
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.NotFound();
|
||||
}
|
||||
}
|
||||
else if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled)
|
||||
{
|
||||
return this.NotFound();
|
||||
}
|
||||
if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled) return this.NotFound();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
|
@ -74,7 +53,8 @@ public class RegisterForm : BaseLayout
|
|||
return this.Page();
|
||||
}
|
||||
|
||||
if (await this.Database.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower()) != null)
|
||||
User? existingUser = await this.Database.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower());
|
||||
if (existingUser != null)
|
||||
{
|
||||
this.Error = this.Translate(ErrorStrings.UsernameTaken);
|
||||
return this.Page();
|
||||
|
@ -93,11 +73,6 @@ public class RegisterForm : BaseLayout
|
|||
return this.Page();
|
||||
}
|
||||
|
||||
if (this.Request.Query.ContainsKey("token"))
|
||||
{
|
||||
await this.Database.RemoveRegistrationToken(this.Request.Query["token"]);
|
||||
}
|
||||
|
||||
User user = await this.Database.CreateUser(username, CryptoHelper.BCryptHash(password), emailAddress);
|
||||
|
||||
WebToken webToken = new()
|
||||
|
@ -112,35 +87,17 @@ public class RegisterForm : BaseLayout
|
|||
|
||||
this.Response.Cookies.Append("LighthouseToken", webToken.UserToken);
|
||||
|
||||
if (ServerConfiguration.Instance.Mail.MailEnabled) return this.Redirect("~/login/sendVerificationEmail");
|
||||
|
||||
return this.Redirect("~/");
|
||||
return ServerConfiguration.Instance.Mail.MailEnabled ?
|
||||
this.Redirect("~/login/sendVerificationEmail") :
|
||||
this.Redirect("~/");
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[SuppressMessage("ReSharper", "SpecifyStringComparison")]
|
||||
public async Task<IActionResult> OnGet()
|
||||
public IActionResult OnGet()
|
||||
{
|
||||
this.Error = string.Empty;
|
||||
if (ServerConfiguration.Instance.Authentication.PrivateRegistration)
|
||||
{
|
||||
if (this.Request.Query.ContainsKey("token"))
|
||||
{
|
||||
string? token = this.Request.Query["token"];
|
||||
if (!this.Database.IsRegistrationTokenValid(token))
|
||||
return this.StatusCode(403, this.Translate(ErrorStrings.TokenInvalid));
|
||||
|
||||
string? tokenUsername = await this.Database.RegistrationTokens.Where(r => r.Token == token)
|
||||
.Select(u => u.Username)
|
||||
.FirstAsync();
|
||||
this.Username = tokenUsername;
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.NotFound();
|
||||
}
|
||||
}
|
||||
else if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled)
|
||||
if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled)
|
||||
{
|
||||
return this.NotFound();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.Files;
|
||||
|
@ -19,8 +18,6 @@ public class UserSettingsPage : BaseLayout
|
|||
public UserSettingsPage(Database database) : base(database)
|
||||
{}
|
||||
|
||||
private static bool IsValidEmail(string? email) => !string.IsNullOrWhiteSpace(email) && new EmailAddressAttribute().IsValid(email);
|
||||
|
||||
[SuppressMessage("ReSharper", "SpecifyStringComparison")]
|
||||
public async Task<IActionResult> OnPost([FromRoute] int userId, [FromForm] string? avatar, [FromForm] string? username, [FromForm] string? email, [FromForm] string? biography, [FromForm] string? timeZone, [FromForm] string? language)
|
||||
{
|
||||
|
@ -39,7 +36,9 @@ public class UserSettingsPage : BaseLayout
|
|||
|
||||
if (this.ProfileUser.Biography != biography && biography.Length <= 512) this.ProfileUser.Biography = biography;
|
||||
|
||||
if (ServerConfiguration.Instance.Mail.MailEnabled && IsValidEmail(email) && (this.User == this.ProfileUser || this.User.IsAdmin))
|
||||
if (ServerConfiguration.Instance.Mail.MailEnabled &&
|
||||
SanitizationHelper.IsValidEmail(email) &&
|
||||
(this.User == this.ProfileUser || this.User.IsAdmin))
|
||||
{
|
||||
// if email hasn't already been used
|
||||
if (!await this.Database.Users.AnyAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == email!.ToLower()))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue