From 65e7771ce34c83fd84b62264f8725a8453382123 Mon Sep 17 00:00:00 2001 From: jvyden Date: Mon, 13 Dec 2021 15:59:33 -0500 Subject: [PATCH] Add ability to approve an IP address --- .../Controllers/LoginController.cs | 51 ++++++++------ .../Controllers/MessageController.cs | 12 +++- .../ExternalAuth/AutoApprovalController.cs | 67 +++++++++++++++++++ ProjectLighthouse/Database.cs | 2 +- ...211213195540_AddUserApprovedIpAddresses.cs | 50 ++++++++++++++ .../Migrations/DatabaseModelSnapshot.cs | 30 +++++++++ .../ExternalAuth/AuthenticationPage.cshtml | 26 +++---- .../ManageUserApprovedIpAddressesPage.cshtml | 22 ++++++ ...anageUserApprovedIpAddressesPage.cshtml.cs | 29 ++++++++ .../Types/Settings/ServerSettings.cs | 4 +- .../Types/UserApprovedIpAddress.cs | 18 +++++ 11 files changed, 273 insertions(+), 38 deletions(-) create mode 100644 ProjectLighthouse/Controllers/Website/ExternalAuth/AutoApprovalController.cs create mode 100644 ProjectLighthouse/Migrations/20211213195540_AddUserApprovedIpAddresses.cs create mode 100644 ProjectLighthouse/Pages/ExternalAuth/ManageUserApprovedIpAddressesPage.cshtml create mode 100644 ProjectLighthouse/Pages/ExternalAuth/ManageUserApprovedIpAddressesPage.cshtml.cs create mode 100644 ProjectLighthouse/Types/UserApprovedIpAddress.cs diff --git a/ProjectLighthouse/Controllers/LoginController.cs b/ProjectLighthouse/Controllers/LoginController.cs index a8126a94..d5085096 100644 --- a/ProjectLighthouse/Controllers/LoginController.cs +++ b/ProjectLighthouse/Controllers/LoginController.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -43,12 +44,12 @@ namespace LBPUnion.ProjectLighthouse.Controllers } if (loginData == null) return this.BadRequest(); - IPAddress? ipAddress = this.HttpContext.Connection.RemoteIpAddress; - if (ipAddress == null) return this.StatusCode(403, ""); // 403 probably isnt the best status code for this, but whatever + IPAddress? remoteIpAddress = this.HttpContext.Connection.RemoteIpAddress; + if (remoteIpAddress == null) return this.StatusCode(403, ""); // 403 probably isnt the best status code for this, but whatever - string userLocation = ipAddress.ToString(); + string ipAddress = remoteIpAddress.ToString(); - GameToken? token = await this.database.AuthenticateUser(loginData, userLocation, titleId); + GameToken? token = await this.database.AuthenticateUser(loginData, ipAddress, titleId); if (token == null) return this.StatusCode(403, ""); User? user = await this.database.UserFromGameToken(token, true); @@ -56,28 +57,38 @@ namespace LBPUnion.ProjectLighthouse.Controllers if (ServerSettings.Instance.UseExternalAuth) { - string ipAddressAndName = $"{token.UserLocation}|{user.Username}"; - if (DeniedAuthenticationHelper.RecentlyDenied(ipAddressAndName) || DeniedAuthenticationHelper.GetAttempts(ipAddressAndName) > 3) + if (ServerSettings.Instance.BlockDeniedUsers) { - this.database.AuthenticationAttempts.RemoveRange - (this.database.AuthenticationAttempts.Include(a => a.GameToken).Where(a => a.GameToken.UserId == user.UserId)); + string ipAddressAndName = $"{token.UserLocation}|{user.Username}"; + if (DeniedAuthenticationHelper.RecentlyDenied(ipAddressAndName) || DeniedAuthenticationHelper.GetAttempts(ipAddressAndName) > 3) + { + this.database.AuthenticationAttempts.RemoveRange + (this.database.AuthenticationAttempts.Include(a => a.GameToken).Where(a => a.GameToken.UserId == user.UserId)); - DeniedAuthenticationHelper.AddAttempt(ipAddressAndName); + DeniedAuthenticationHelper.AddAttempt(ipAddressAndName); - await this.database.SaveChangesAsync(); - return this.StatusCode(403, ""); + await this.database.SaveChangesAsync(); + return this.StatusCode(403, ""); + } } - AuthenticationAttempt authAttempt = new() - { - GameToken = token, - GameTokenId = token.TokenId, - Timestamp = TimestampHelper.Timestamp, - IPAddress = userLocation, - Platform = token.GameVersion == GameVersion.LittleBigPlanetVita ? Platform.Vita : Platform.PS3, // TODO: properly identify RPCS3 - }; + List approvedIpAddresses = await this.database.UserApprovedIpAddresses.Where(a => a.UserId == user.UserId).ToListAsync(); + bool ipAddressApproved = approvedIpAddresses.Select(a => a.IpAddress).Contains(ipAddress); - this.database.AuthenticationAttempts.Add(authAttempt); + if (ipAddressApproved) token.Approved = true; + else + { + AuthenticationAttempt authAttempt = new() + { + GameToken = token, + GameTokenId = token.TokenId, + Timestamp = TimestampHelper.Timestamp, + IPAddress = ipAddress, + Platform = token.GameVersion == GameVersion.LittleBigPlanetVita ? Platform.Vita : Platform.PS3, // TODO: properly identify RPCS3 + }; + + this.database.AuthenticationAttempts.Add(authAttempt); + } } else { diff --git a/ProjectLighthouse/Controllers/MessageController.cs b/ProjectLighthouse/Controllers/MessageController.cs index 80851bbe..83531921 100644 --- a/ProjectLighthouse/Controllers/MessageController.cs +++ b/ProjectLighthouse/Controllers/MessageController.cs @@ -1,3 +1,4 @@ +#nullable enable using System.IO; using System.Threading.Tasks; using Kettu; @@ -27,10 +28,15 @@ namespace LBPUnion.ProjectLighthouse.Controllers [HttpGet("announce")] public async Task Announce() { - User user = await this.database.UserFromGameRequest(this.Request, true); - if (user == null) return this.StatusCode(403, ""); + (User, GameToken)? userAndToken = await this.database.UserAndGameTokenFromRequest(this.Request); - if (ServerSettings.Instance.UseExternalAuth) + if (userAndToken == null) return this.StatusCode(403, ""); + + // ReSharper disable once PossibleInvalidOperationException + User user = userAndToken.Value.Item1; + GameToken gameToken = userAndToken.Value.Item2; + + if (ServerSettings.Instance.UseExternalAuth && !gameToken.Approved) return this.Ok ( "Please stay on this screen.\n" + diff --git a/ProjectLighthouse/Controllers/Website/ExternalAuth/AutoApprovalController.cs b/ProjectLighthouse/Controllers/Website/ExternalAuth/AutoApprovalController.cs new file mode 100644 index 00000000..f801b055 --- /dev/null +++ b/ProjectLighthouse/Controllers/Website/ExternalAuth/AutoApprovalController.cs @@ -0,0 +1,67 @@ +#nullable enable +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Types; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Controllers.Website.ExternalAuth +{ + [ApiController] + [Route("/authentication")] + public class AutoApprovalController : ControllerBase + { + private readonly Database database; + + public AutoApprovalController(Database database) + { + this.database = database; + } + + [HttpGet("autoApprove/{id:int}")] + public async Task AutoApprove([FromRoute] int id) + { + User? user = this.database.UserFromWebRequest(this.Request); + if (user == null) return this.Redirect("/login"); + + AuthenticationAttempt? authAttempt = await this.database.AuthenticationAttempts.Include + (a => a.GameToken) + .FirstOrDefaultAsync(a => a.AuthenticationAttemptId == id); + + if (authAttempt == null) return this.BadRequest(); + if (authAttempt.GameToken.UserId != user.UserId) return this.Redirect("/login"); + + authAttempt.GameToken.Approved = true; + + UserApprovedIpAddress approvedIpAddress = new() + { + UserId = user.UserId, + User = user, + IpAddress = authAttempt.IPAddress, + }; + + this.database.UserApprovedIpAddresses.Add(approvedIpAddress); + this.database.AuthenticationAttempts.Remove(authAttempt); + + await this.database.SaveChangesAsync(); + + return this.Redirect("/authentication"); + } + + [HttpGet("revokeAutoApproval/{id:int}")] + public async Task RevokeAutoApproval([FromRoute] int id) + { + User? user = this.database.UserFromWebRequest(this.Request); + if (user == null) return this.Redirect("/login"); + + UserApprovedIpAddress? approvedIpAddress = await this.database.UserApprovedIpAddresses.FirstOrDefaultAsync(a => a.UserApprovedIpAddressId == id); + if (approvedIpAddress == null) return this.BadRequest(); + if (approvedIpAddress.UserId != user.UserId) return this.Redirect("/login"); + + this.database.UserApprovedIpAddresses.Remove(approvedIpAddress); + + await this.database.SaveChangesAsync(); + + return this.Redirect("/authentication/autoApprovals"); + } + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Database.cs b/ProjectLighthouse/Database.cs index 2af08e30..9fda3cab 100644 --- a/ProjectLighthouse/Database.cs +++ b/ProjectLighthouse/Database.cs @@ -34,6 +34,7 @@ namespace LBPUnion.ProjectLighthouse public DbSet AuthenticationAttempts { get; set; } public DbSet Reviews { get; set; } public DbSet RatedReviews { get; set; } + public DbSet UserApprovedIpAddresses { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseMySql(ServerSettings.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion); @@ -66,7 +67,6 @@ namespace LBPUnion.ProjectLighthouse #nullable enable public async Task AuthenticateUser(LoginData loginData, string userLocation, string titleId = "") { - // TODO: don't use psn name to authenticate User? user = await this.Users.FirstOrDefaultAsync(u => u.Username == loginData.Username); if (user == null) return null; diff --git a/ProjectLighthouse/Migrations/20211213195540_AddUserApprovedIpAddresses.cs b/ProjectLighthouse/Migrations/20211213195540_AddUserApprovedIpAddresses.cs new file mode 100644 index 00000000..4f9264df --- /dev/null +++ b/ProjectLighthouse/Migrations/20211213195540_AddUserApprovedIpAddresses.cs @@ -0,0 +1,50 @@ +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20211213195540_AddUserApprovedIpAddresses")] + public partial class AddUserApprovedIpAddresses : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserApprovedIpAddresses", + columns: table => new + { + UserApprovedIpAddressId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + UserId = table.Column(type: "int", nullable: false), + IpAddress = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_UserApprovedIpAddresses", x => x.UserApprovedIpAddressId); + table.ForeignKey( + name: "FK_UserApprovedIpAddresses_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_UserApprovedIpAddresses_UserId", + table: "UserApprovedIpAddresses", + column: "UserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserApprovedIpAddresses"); + } + } +} diff --git a/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs b/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs index 4afc7b51..64dfac1f 100644 --- a/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs @@ -587,6 +587,25 @@ namespace ProjectLighthouse.Migrations b.ToTable("Users"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.UserApprovedIpAddress", b => + { + b.Property("UserApprovedIpAddressId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("IpAddress") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("UserApprovedIpAddressId"); + + b.HasIndex("UserId"); + + b.ToTable("UserApprovedIpAddresses"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.WebToken", b => { b.Property("TokenId") @@ -829,6 +848,17 @@ namespace ProjectLighthouse.Migrations b.Navigation("Location"); }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.UserApprovedIpAddress", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); #pragma warning restore 612, 618 } } diff --git a/ProjectLighthouse/Pages/ExternalAuth/AuthenticationPage.cshtml b/ProjectLighthouse/Pages/ExternalAuth/AuthenticationPage.cshtml index 97278a6a..d5a56773 100644 --- a/ProjectLighthouse/Pages/ExternalAuth/AuthenticationPage.cshtml +++ b/ProjectLighthouse/Pages/ExternalAuth/AuthenticationPage.cshtml @@ -18,21 +18,21 @@ else {

This device's IP address is @(Model.IpAddress.ToString()). 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.

} - - - - - - - } + + + + + + + @foreach (AuthenticationAttempt authAttempt in Model.AuthenticationAttempts) { DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp); diff --git a/ProjectLighthouse/Pages/ExternalAuth/ManageUserApprovedIpAddressesPage.cshtml b/ProjectLighthouse/Pages/ExternalAuth/ManageUserApprovedIpAddressesPage.cshtml new file mode 100644 index 00000000..dde2fa8a --- /dev/null +++ b/ProjectLighthouse/Pages/ExternalAuth/ManageUserApprovedIpAddressesPage.cshtml @@ -0,0 +1,22 @@ +@page "/authentication/autoApprovals" +@using LBPUnion.ProjectLighthouse.Types +@model LBPUnion.ProjectLighthouse.Pages.ExternalAuth.ManageUserApprovedIpAddressesPage + +@{ + Layout = "Layouts/BaseLayout"; + Model.Title = "Automatically approved IP addresses"; +} + + +@foreach (UserApprovedIpAddress approvedIpAddress in Model.ApprovedIpAddresses) +{ +
+

@approvedIpAddress.IpAddress

+ + + +
+} \ No newline at end of file diff --git a/ProjectLighthouse/Pages/ExternalAuth/ManageUserApprovedIpAddressesPage.cshtml.cs b/ProjectLighthouse/Pages/ExternalAuth/ManageUserApprovedIpAddressesPage.cshtml.cs new file mode 100644 index 00000000..00dfa3be --- /dev/null +++ b/ProjectLighthouse/Pages/ExternalAuth/ManageUserApprovedIpAddressesPage.cshtml.cs @@ -0,0 +1,29 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Pages.Layouts; +using LBPUnion.ProjectLighthouse.Types; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Pages.ExternalAuth +{ + public class ManageUserApprovedIpAddressesPage : BaseLayout + { + public ManageUserApprovedIpAddressesPage(Database database) : base(database) + {} + + public List ApprovedIpAddresses; + + public async Task OnGet() + { + User? user = this.Database.UserFromWebRequest(this.Request); + if (user == null) return this.Redirect("/login"); + + this.ApprovedIpAddresses = await this.Database.UserApprovedIpAddresses.Where(a => a.UserId == user.UserId).ToListAsync(); + + return this.Page(); + } + } +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Settings/ServerSettings.cs b/ProjectLighthouse/Types/Settings/ServerSettings.cs index ef1943e3..bdbb7676 100644 --- a/ProjectLighthouse/Types/Settings/ServerSettings.cs +++ b/ProjectLighthouse/Types/Settings/ServerSettings.cs @@ -12,7 +12,7 @@ namespace LBPUnion.ProjectLighthouse.Types.Settings public class ServerSettings { - public const int CurrentConfigVersion = 12; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE! + public const int CurrentConfigVersion = 13; // MUST BE INCREMENTED FOR EVERY CONFIG CHANGE! static ServerSettings() { if (ServerStatics.IsUnitTesting) return; // Unit testing, we don't want to read configurations here since the tests will provide their own @@ -97,6 +97,8 @@ namespace LBPUnion.ProjectLighthouse.Types.Settings public string GoogleAnalyticsId { get; set; } = ""; + public bool BlockDeniedUsers = true; + #region Meta [NotNull] diff --git a/ProjectLighthouse/Types/UserApprovedIpAddress.cs b/ProjectLighthouse/Types/UserApprovedIpAddress.cs new file mode 100644 index 00000000..06c84183 --- /dev/null +++ b/ProjectLighthouse/Types/UserApprovedIpAddress.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace LBPUnion.ProjectLighthouse.Types +{ + public class UserApprovedIpAddress + { + [Key] + public int UserApprovedIpAddressId { get; set; } + + public int UserId { get; set; } + + [ForeignKey(nameof(UserId))] + public User User { get; set; } + + public string IpAddress { get; set; } + } +} \ No newline at end of file