From 335851bf66a93e0ebe0d91a8caa7c8f4a6a6c47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=E2=96=88=E2=96=88=E2=96=88=E2=96=88=E2=96=88?= Date: Wed, 8 Oct 2025 19:15:17 -0400 Subject: [PATCH 1/2] chore: formatting --- .../Nullinside.Api.Common.AspNetCore.csproj | 4 ++-- ...e.Api.Common.AspNetCore.csproj.DotSettings | 3 ++- .../Nullinside.Api.Common.csproj | 6 ++--- .../Nullinside.Api.Common.csproj.DotSettings | 3 ++- src/Nullinside.Api.Model/Ddl/ITableModel.cs | 2 +- src/Nullinside.Api.Model/Ddl/User.cs | 4 ++-- .../Nullinside.Api.Model.csproj | 4 ++-- .../Nullinside.Api.Model.csproj.DotSettings | 3 ++- .../Shared/UserHelpers.cs | 8 +++---- .../Shared/UserHelpersTests.cs | 7 +++--- .../Nullinside.Api.Tests.csproj | 10 ++++----- .../Nullinside.Api.Tests.csproj.DotSettings | 3 ++- .../Controllers/UserControllerTests.cs | 19 +++++++--------- src/Nullinside.Api/Constants.cs | 4 ++-- .../Controllers/UserController.cs | 22 +++++++++---------- src/Nullinside.Api/Nullinside.Api.csproj | 8 +++---- .../Nullinside.Api.csproj.DotSettings | 3 ++- .../Shared/IWebSocketPersister.cs | 2 +- 18 files changed, 58 insertions(+), 57 deletions(-) diff --git a/src/Nullinside.Api.Common.AspNetCore/Nullinside.Api.Common.AspNetCore.csproj b/src/Nullinside.Api.Common.AspNetCore/Nullinside.Api.Common.AspNetCore.csproj index 87518b8..5ce8b39 100644 --- a/src/Nullinside.Api.Common.AspNetCore/Nullinside.Api.Common.AspNetCore.csproj +++ b/src/Nullinside.Api.Common.AspNetCore/Nullinside.Api.Common.AspNetCore.csproj @@ -22,8 +22,8 @@ all - - + + diff --git a/src/Nullinside.Api.Common.AspNetCore/Nullinside.Api.Common.AspNetCore.csproj.DotSettings b/src/Nullinside.Api.Common.AspNetCore/Nullinside.Api.Common.AspNetCore.csproj.DotSettings index 4527ed0..d0d789b 100644 --- a/src/Nullinside.Api.Common.AspNetCore/Nullinside.Api.Common.AspNetCore.csproj.DotSettings +++ b/src/Nullinside.Api.Common.AspNetCore/Nullinside.Api.Common.AspNetCore.csproj.DotSettings @@ -1,4 +1,5 @@ - Library \ No newline at end of file diff --git a/src/Nullinside.Api.Common/Nullinside.Api.Common.csproj b/src/Nullinside.Api.Common/Nullinside.Api.Common.csproj index 872d9a0..28032d3 100644 --- a/src/Nullinside.Api.Common/Nullinside.Api.Common.csproj +++ b/src/Nullinside.Api.Common/Nullinside.Api.Common.csproj @@ -18,9 +18,9 @@ - - - + + + diff --git a/src/Nullinside.Api.Common/Nullinside.Api.Common.csproj.DotSettings b/src/Nullinside.Api.Common/Nullinside.Api.Common.csproj.DotSettings index 4527ed0..d0d789b 100644 --- a/src/Nullinside.Api.Common/Nullinside.Api.Common.csproj.DotSettings +++ b/src/Nullinside.Api.Common/Nullinside.Api.Common.csproj.DotSettings @@ -1,4 +1,5 @@ - Library \ No newline at end of file diff --git a/src/Nullinside.Api.Model/Ddl/ITableModel.cs b/src/Nullinside.Api.Model/Ddl/ITableModel.cs index c445665..31c6538 100644 --- a/src/Nullinside.Api.Model/Ddl/ITableModel.cs +++ b/src/Nullinside.Api.Model/Ddl/ITableModel.cs @@ -10,5 +10,5 @@ public interface ITableModel { /// The method used to configure the POCOs of the table. /// /// The model builder. - public void OnModelCreating(ModelBuilder modelBuilder); + void OnModelCreating(ModelBuilder modelBuilder); } \ No newline at end of file diff --git a/src/Nullinside.Api.Model/Ddl/User.cs b/src/Nullinside.Api.Model/Ddl/User.cs index 8227484..5178e72 100644 --- a/src/Nullinside.Api.Model/Ddl/User.cs +++ b/src/Nullinside.Api.Model/Ddl/User.cs @@ -27,12 +27,12 @@ public class User : ITableModel { /// The user's auth token for interacting with the site's API. /// public string? Token { get; set; } - + /// /// The user's auth token for interacting with the site's API. /// public string? RefreshToken { get; set; } - + /// /// The user's auth token for interacting with the site's API. /// diff --git a/src/Nullinside.Api.Model/Nullinside.Api.Model.csproj b/src/Nullinside.Api.Model/Nullinside.Api.Model.csproj index bf1f905..6514113 100644 --- a/src/Nullinside.Api.Model/Nullinside.Api.Model.csproj +++ b/src/Nullinside.Api.Model/Nullinside.Api.Model.csproj @@ -17,13 +17,13 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/Nullinside.Api.Model/Nullinside.Api.Model.csproj.DotSettings b/src/Nullinside.Api.Model/Nullinside.Api.Model.csproj.DotSettings index 4527ed0..d0d789b 100644 --- a/src/Nullinside.Api.Model/Nullinside.Api.Model.csproj.DotSettings +++ b/src/Nullinside.Api.Model/Nullinside.Api.Model.csproj.DotSettings @@ -1,4 +1,5 @@ - Library \ No newline at end of file diff --git a/src/Nullinside.Api.Model/Shared/UserHelpers.cs b/src/Nullinside.Api.Model/Shared/UserHelpers.cs index 184025e..7ed4202 100644 --- a/src/Nullinside.Api.Model/Shared/UserHelpers.cs +++ b/src/Nullinside.Api.Model/Shared/UserHelpers.cs @@ -23,7 +23,7 @@ public static class UserHelpers { /// The username of the user on twitch. /// The id of the user on twitch. /// The bearer token if successful, null otherwise. - public static async Task GenerateTokenAndSaveToDatabase(INullinsideContext dbContext, string email, + public static async Task GenerateTokenAndSaveToDatabase(INullinsideContext dbContext, string email, TimeSpan tokenExpires, string? authToken = null, string? refreshToken = null, DateTime? expires = null, string? twitchUsername = null, string? twitchId = null, CancellationToken cancellationToken = new()) { string bearerToken = AuthUtils.GenerateToken(); @@ -76,9 +76,9 @@ public static class UserHelpers { } await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - return new() { - AccessToken = bearerToken, - RefreshToken = bearerRefreshToken, + return new OAuthToken { + AccessToken = bearerToken, + RefreshToken = bearerRefreshToken, ExpiresUtc = expiresUtc }; } diff --git a/src/Nullinside.Api.Tests/Nullinside.Api.Model/Shared/UserHelpersTests.cs b/src/Nullinside.Api.Tests/Nullinside.Api.Model/Shared/UserHelpersTests.cs index b45c0ea..4a0d009 100644 --- a/src/Nullinside.Api.Tests/Nullinside.Api.Model/Shared/UserHelpersTests.cs +++ b/src/Nullinside.Api.Tests/Nullinside.Api.Model/Shared/UserHelpersTests.cs @@ -1,3 +1,4 @@ +using Nullinside.Api.Common.Auth; using Nullinside.Api.Model.Ddl; using Nullinside.Api.Model.Shared; @@ -23,7 +24,7 @@ public async Task GenerateTokenForExistingUser() { Assert.That(_db.Users.Count(), Is.EqualTo(1)); // Generate a new token - var token = await UserHelpers.GenerateTokenAndSaveToDatabase(_db, "email", Constants.OAUTH_TOKEN_TIME_LIMIT).ConfigureAwait(false); + OAuthToken? token = await UserHelpers.GenerateTokenAndSaveToDatabase(_db, "email", Constants.OAUTH_TOKEN_TIME_LIMIT).ConfigureAwait(false); Assert.That(token, Is.Not.Null); // Verify we still only have one user @@ -48,7 +49,7 @@ public async Task GenerateTokenForNewUser() { Assert.That(_db.Users.Count(), Is.EqualTo(1)); // Generate a new token - var token = await UserHelpers.GenerateTokenAndSaveToDatabase(_db, "email", Constants.OAUTH_TOKEN_TIME_LIMIT).ConfigureAwait(false); + OAuthToken? token = await UserHelpers.GenerateTokenAndSaveToDatabase(_db, "email", Constants.OAUTH_TOKEN_TIME_LIMIT).ConfigureAwait(false); Assert.That(token, Is.Not.Null); // Verify we have a new user @@ -65,7 +66,7 @@ public async Task GenerateTokenForNewUser() { [Test] public async Task HandleUnexpectedErrors() { // Force an error to occur. - var token = await UserHelpers.GenerateTokenAndSaveToDatabase(null!, "email", Constants.OAUTH_TOKEN_TIME_LIMIT).ConfigureAwait(false); + OAuthToken? token = await UserHelpers.GenerateTokenAndSaveToDatabase(null!, "email", Constants.OAUTH_TOKEN_TIME_LIMIT).ConfigureAwait(false); Assert.That(token, Is.Null); } } \ No newline at end of file diff --git a/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj b/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj index 43a5684..2bb4ee9 100644 --- a/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj +++ b/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj @@ -15,13 +15,13 @@ - - + + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj.DotSettings b/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj.DotSettings index 4527ed0..d0d789b 100644 --- a/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj.DotSettings +++ b/src/Nullinside.Api.Tests/Nullinside.Api.Tests.csproj.DotSettings @@ -1,4 +1,5 @@ - Library \ No newline at end of file diff --git a/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/UserControllerTests.cs b/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/UserControllerTests.cs index b934038..bf9c8a0 100644 --- a/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/UserControllerTests.cs +++ b/src/Nullinside.Api.Tests/Nullinside.Api/Controllers/UserControllerTests.cs @@ -1,6 +1,5 @@ using System.Security.Claims; using System.Text; -using System.Text.Unicode; using Google.Apis.Auth; @@ -21,8 +20,6 @@ using Nullinside.Api.Shared; using Nullinside.Api.Shared.Json; -using Org.BouncyCastle.Utilities.Encoders; - namespace Nullinside.Api.Tests.Nullinside.Api.Controllers; /// @@ -81,13 +78,13 @@ public async Task PerformGoogleLoginExisting() { // We should have been redirected to the successful route. Assert.That(obj.Url.StartsWith("/user/login?token="), Is.True); - var queryParam = obj.Url["/user/login?token=".Length..]; + string queryParam = obj.Url["/user/login?token=".Length..]; // No additional users should have been created. Assert.That(_db.Users.Count(), Is.EqualTo(1)); // We should have saved the token in the existing user's database. - var json = Encoding.UTF8.GetString(Convert.FromBase64String(queryParam)); + string json = Encoding.UTF8.GetString(Convert.FromBase64String(queryParam)); var oauth = JsonConvert.DeserializeObject(json); Assert.That(oauth?.AccessToken!, Is.EqualTo(_db.Users.First().Token!)); } @@ -104,13 +101,13 @@ public async Task PerformGoogleLoginNewUser() { // We should have been redirected to the successful route. Assert.That(obj.Url.StartsWith("/user/login?token="), Is.True); - var queryParam = obj.Url["/user/login?token=".Length..]; + string queryParam = obj.Url["/user/login?token=".Length..]; // No additional users should have been created. Assert.That(_db.Users.Count(), Is.EqualTo(1)); // We should have saved the token in the existing user's database. - var json = Encoding.UTF8.GetString(Convert.FromBase64String(queryParam)); + string json = Encoding.UTF8.GetString(Convert.FromBase64String(queryParam)); var oauth = JsonConvert.DeserializeObject(json); Assert.That(oauth?.AccessToken!, Is.EqualTo(_db.Users.First().Token!)); } @@ -172,13 +169,13 @@ public async Task PerformTwitchLoginExisting() { // We should have been redirected to the successful route. Assert.That(obj.Url.StartsWith("/user/login?token="), Is.True); - var queryParam = obj.Url["/user/login?token=".Length..]; + string queryParam = obj.Url["/user/login?token=".Length..]; // No additional users should have been created. Assert.That(_db.Users.Count(), Is.EqualTo(1)); // We should have saved the token in the existing user's database. - var json = Encoding.UTF8.GetString(Convert.FromBase64String(queryParam)); + string json = Encoding.UTF8.GetString(Convert.FromBase64String(queryParam)); var oauth = JsonConvert.DeserializeObject(json); Assert.That(oauth?.AccessToken!, Is.EqualTo(_db.Users.First().Token!)); } @@ -202,13 +199,13 @@ public async Task PerformTwitchLoginNewUser() { // We should have been redirected to the successful route. Assert.That(obj.Url.StartsWith("/user/login?token="), Is.True); - var queryParam = obj.Url["/user/login?token=".Length..]; + string queryParam = obj.Url["/user/login?token=".Length..]; // No additional users should have been created. Assert.That(_db.Users.Count(), Is.EqualTo(1)); // We should have saved the token in the existing user's database. - var json = Encoding.UTF8.GetString(Convert.FromBase64String(queryParam)); + string json = Encoding.UTF8.GetString(Convert.FromBase64String(queryParam)); var oauth = JsonConvert.DeserializeObject(json); Assert.That(oauth?.AccessToken!, Is.EqualTo(_db.Users.First().Token!)); } diff --git a/src/Nullinside.Api/Constants.cs b/src/Nullinside.Api/Constants.cs index ccd5fc5..ddb03a4 100644 --- a/src/Nullinside.Api/Constants.cs +++ b/src/Nullinside.Api/Constants.cs @@ -1,11 +1,11 @@ namespace Nullinside.Api; /// -/// Constants used throughout the application. +/// Constants used throughout the application. /// public static class Constants { /// - /// The amount of time a token is valid for. + /// The amount of time a token is valid for. /// public static readonly TimeSpan OAUTH_TOKEN_TIME_LIMIT = TimeSpan.FromHours(1); } \ No newline at end of file diff --git a/src/Nullinside.Api/Controllers/UserController.cs b/src/Nullinside.Api/Controllers/UserController.cs index c7fd591..59d56bb 100644 --- a/src/Nullinside.Api/Controllers/UserController.cs +++ b/src/Nullinside.Api/Controllers/UserController.cs @@ -21,8 +21,6 @@ using Nullinside.Api.Shared; using Nullinside.Api.Shared.Json; -using Org.BouncyCastle.Utilities.Encoders; - namespace Nullinside.Api.Controllers; /// @@ -81,19 +79,19 @@ public UserController(IConfiguration configuration, INullinsideContext dbContext return Redirect($"{siteUrl}/user/login?error=1"); } - var bearerToken = await UserHelpers.GenerateTokenAndSaveToDatabase(_dbContext, credentials.Email, Constants.OAUTH_TOKEN_TIME_LIMIT, cancellationToken: token).ConfigureAwait(false); + OAuthToken? bearerToken = await UserHelpers.GenerateTokenAndSaveToDatabase(_dbContext, credentials.Email, Constants.OAUTH_TOKEN_TIME_LIMIT, cancellationToken: token).ConfigureAwait(false); if (null == bearerToken) { return Redirect($"{siteUrl}/user/login?error=2"); } - - var json = JsonConvert.SerializeObject(bearerToken); + + string json = JsonConvert.SerializeObject(bearerToken); return Redirect($"{siteUrl}/user/login?token={Convert.ToBase64String(Encoding.UTF8.GetBytes(json))}"); } catch (InvalidJwtException) { return Redirect($"{siteUrl}/user/login?error=1"); } } - + /// /// Called to generate a new oauth token using the refresh token we previously provided. /// @@ -104,16 +102,16 @@ public UserController(IConfiguration configuration, INullinsideContext dbContext [HttpPost] [Route("token/refresh")] public async Task Refresh(AuthToken token, CancellationToken cancellationToken = new()) { - var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.RefreshToken == token.Token, cancellationToken).ConfigureAwait(false); + User? user = await _dbContext.Users.FirstOrDefaultAsync(u => u.RefreshToken == token.Token, cancellationToken).ConfigureAwait(false); if (null == user?.Email) { return Unauthorized(); } - - var bearerToken = await UserHelpers.GenerateTokenAndSaveToDatabase(_dbContext, user.Email, Constants.OAUTH_TOKEN_TIME_LIMIT, cancellationToken: cancellationToken).ConfigureAwait(false); + + OAuthToken? bearerToken = await UserHelpers.GenerateTokenAndSaveToDatabase(_dbContext, user.Email, Constants.OAUTH_TOKEN_TIME_LIMIT, cancellationToken: cancellationToken).ConfigureAwait(false); if (null == bearerToken) { return StatusCode(500); } - + return Ok(bearerToken); } @@ -155,12 +153,12 @@ public async Task TwitchLogin([FromQuery] string code, [FromServ return Redirect($"{siteUrl}/user/login?error=4"); } - var bearerToken = await UserHelpers.GenerateTokenAndSaveToDatabase(_dbContext, email, Constants.OAUTH_TOKEN_TIME_LIMIT, cancellationToken: token).ConfigureAwait(false); + OAuthToken? bearerToken = await UserHelpers.GenerateTokenAndSaveToDatabase(_dbContext, email, Constants.OAUTH_TOKEN_TIME_LIMIT, cancellationToken: token).ConfigureAwait(false); if (null == bearerToken) { return Redirect($"{siteUrl}/user/login?error=2"); } - var json = JsonConvert.SerializeObject(bearerToken); + string json = JsonConvert.SerializeObject(bearerToken); return Redirect($"{siteUrl}/user/login?token={Convert.ToBase64String(Encoding.UTF8.GetBytes(json))}"); } diff --git a/src/Nullinside.Api/Nullinside.Api.csproj b/src/Nullinside.Api/Nullinside.Api.csproj index 5457db3..a9881b9 100644 --- a/src/Nullinside.Api/Nullinside.Api.csproj +++ b/src/Nullinside.Api/Nullinside.Api.csproj @@ -23,7 +23,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -31,10 +31,10 @@ - + - - + + diff --git a/src/Nullinside.Api/Nullinside.Api.csproj.DotSettings b/src/Nullinside.Api/Nullinside.Api.csproj.DotSettings index 4527ed0..d0d789b 100644 --- a/src/Nullinside.Api/Nullinside.Api.csproj.DotSettings +++ b/src/Nullinside.Api/Nullinside.Api.csproj.DotSettings @@ -1,4 +1,5 @@ - Library \ No newline at end of file diff --git a/src/Nullinside.Api/Shared/IWebSocketPersister.cs b/src/Nullinside.Api/Shared/IWebSocketPersister.cs index 9046eb7..955e4c2 100644 --- a/src/Nullinside.Api/Shared/IWebSocketPersister.cs +++ b/src/Nullinside.Api/Shared/IWebSocketPersister.cs @@ -11,5 +11,5 @@ public interface IWebSocketPersister { /// /// A collection of web sockets key'd by an identifier for the web socket connection. /// - public ConcurrentDictionary WebSockets { get; set; } + ConcurrentDictionary WebSockets { get; set; } } \ No newline at end of file From 3f912378725b04da3af89cde5c8a2fe00cc36eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=E2=96=88=E2=96=88=E2=96=88=E2=96=88=E2=96=88?= Date: Wed, 8 Oct 2025 19:15:28 -0400 Subject: [PATCH 2/2] bug: fixing authenticating OPTIONS We want to skip the auth check for OPTION requests that are sent. These are just to test capabilities for CORS and do not do anything that is relevant to authentication. closes #40 --- .../Middleware/BasicAuthenticationHandler.cs | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Nullinside.Api.Common.AspNetCore/Middleware/BasicAuthenticationHandler.cs b/src/Nullinside.Api.Common.AspNetCore/Middleware/BasicAuthenticationHandler.cs index 2d5e613..2dccddd 100644 --- a/src/Nullinside.Api.Common.AspNetCore/Middleware/BasicAuthenticationHandler.cs +++ b/src/Nullinside.Api.Common.AspNetCore/Middleware/BasicAuthenticationHandler.cs @@ -45,6 +45,11 @@ public BasicAuthenticationHandler(IOptionsMonitor o /// /// The user and their roles if successful, otherwise. protected override async Task HandleAuthenticateAsync() { + // Skip options requests + if (Request.Method == "OPTIONS") { + return AuthenticateResult.NoResult(); + } + // Read token from HTTP request header string? authorizationHeader = Request.Headers.Authorization; if (string.IsNullOrEmpty(authorizationHeader) || !authorizationHeader.StartsWith("Bearer ")) { @@ -87,18 +92,27 @@ protected override async Task HandleAuthenticateAsync() { } } - ClaimsIdentity identity = new(claims, "BasicBearerToken"); - ClaimsPrincipal user = new(identity); - AuthenticationProperties authProperties = new() { - IsPersistent = true - }; - - AuthenticationTicket ticket = new(user, authProperties, "BasicBearerToken"); - return AuthenticateResult.Success(ticket); + return CreateAuth(claims); } catch (Exception ex) { _logger.Error("Failed to create an auth ticket after successful token validation", ex); return AuthenticateResult.Fail(ex); } } + + /// + /// Creates the authorization ticket. + /// + /// The list of claims to provide. + /// The authorization ticket. + private static AuthenticateResult CreateAuth(List claims) { + ClaimsIdentity identity = new(claims, "BasicBearerToken"); + ClaimsPrincipal user = new(identity); + AuthenticationProperties authProperties = new() { + IsPersistent = true + }; + + AuthenticationTicket ticket = new(user, authProperties, "BasicBearerToken"); + return AuthenticateResult.Success(ticket); + } } \ No newline at end of file