From 588afeec2054b26831fe5d02a26b33b3f3906e64 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 4 Jun 2024 17:38:59 +0200 Subject: [PATCH] feat: add debug registration endpoint, fix snowflake serialization --- .../Controllers/ApiControllerBase.cs | 4 +- .../Controllers/DebugController.cs | 32 ++ .../Controllers/UsersController.cs | 6 +- Foxnouns.Backend/Database/DatabaseContext.cs | 2 + .../20240604142522_AddPassword.Designer.cs | 478 ++++++++++++++++++ .../Migrations/20240604142522_AddPassword.cs | 28 + .../DatabaseContextModelSnapshot.cs | 4 + .../Database/Models/Application.cs | 2 +- Foxnouns.Backend/Database/Models/User.cs | 1 + Foxnouns.Backend/Database/Snowflake.cs | 17 + .../Extensions/WebApplicationExtensions.cs | 3 +- Foxnouns.Backend/Services/AuthService.cs | 57 +++ Foxnouns.Backend/Utils/OauthUtils.cs | 20 +- Foxnouns.Backend/config.ini | 2 +- 14 files changed, 646 insertions(+), 10 deletions(-) create mode 100644 Foxnouns.Backend/Controllers/DebugController.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.Designer.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.cs create mode 100644 Foxnouns.Backend/Services/AuthService.cs diff --git a/Foxnouns.Backend/Controllers/ApiControllerBase.cs b/Foxnouns.Backend/Controllers/ApiControllerBase.cs index b52ca2c..d30803e 100644 --- a/Foxnouns.Backend/Controllers/ApiControllerBase.cs +++ b/Foxnouns.Backend/Controllers/ApiControllerBase.cs @@ -8,6 +8,6 @@ namespace Foxnouns.Backend.Controllers; [Authenticate] public class ApiControllerBase : ControllerBase { - internal Token? Token => HttpContext.GetToken(); - internal new User? User => HttpContext.GetUser(); + internal Token? CurrentToken => HttpContext.GetToken(); + internal User? CurrentUser => HttpContext.GetUser(); } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/DebugController.cs b/Foxnouns.Backend/Controllers/DebugController.cs new file mode 100644 index 0000000..328bf3d --- /dev/null +++ b/Foxnouns.Backend/Controllers/DebugController.cs @@ -0,0 +1,32 @@ +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Services; +using Microsoft.AspNetCore.Mvc; +using NodaTime; + +namespace Foxnouns.Backend.Controllers; + +[Route("/api/v2/debug")] +public class DebugController(DatabaseContext db, AuthService authSvc, IClock clock, ILogger logger) : ApiControllerBase +{ + [HttpPost("users")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))] + public async Task CreateUser([FromBody] CreateUserRequest req) + { + logger.Debug("Creating user with username {Username} and email {Email}", req.Username, req.Email); + + var user = await authSvc.CreateUserWithPasswordAsync(req.Username, req.Email, req.Password); + var frontendApp = await db.GetFrontendApplicationAsync(); + + var (tokenStr, token) = + authSvc.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365)); + db.Add(token); + + await db.SaveChangesAsync(); + + return Ok(new AuthResponse(user.Id, user.Username, tokenStr)); + } + + public record CreateUserRequest(string Username, string Password, string Email); + + public record AuthResponse(Snowflake Id, string Username, string Token); +} \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 108a9f0..26ae497 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -13,15 +13,15 @@ public class UsersController(DatabaseContext db, UserRendererService userRendere public async Task GetUser(string userRef) { var user = await db.ResolveUserAsync(userRef); - return Ok(await userRendererService.RenderUserAsync(user, selfUser: User)); + return Ok(await userRendererService.RenderUserAsync(user, selfUser: CurrentUser)); } [HttpGet("@me")] [Authorize("identify")] public async Task GetMe() { - var user = await db.ResolveUserAsync(User!.Id); - return Ok(await userRendererService.RenderUserAsync(user, selfUser: User)); + var user = await db.ResolveUserAsync(CurrentUser!.Id); + return Ok(await userRendererService.RenderUserAsync(user, selfUser: CurrentUser)); } [HttpPatch("@me")] diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index 30223b5..f9ec686 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -2,6 +2,7 @@ using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Diagnostics; using Npgsql; namespace Foxnouns.Backend.Database; @@ -32,6 +33,7 @@ public class DatabaseContext : DbContext protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder + .ConfigureWarnings(c => c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)) .UseNpgsql(_dataSource, o => o.UseNodaTime()) .UseSnakeCaseNamingConvention(); diff --git a/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.Designer.cs new file mode 100644 index 0000000..2c92566 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.Designer.cs @@ -0,0 +1,478 @@ +// +using Foxnouns.Backend.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240604142522_AddPassword")] + partial class AddPassword + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("RedirectUris") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("redirect_uris"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.HasKey("Id") + .HasName("pk_applications"); + + b.ToTable("applications", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("AuthType") + .HasColumnType("integer") + .HasColumnName("auth_type"); + + b.Property("FediverseApplicationId") + .HasColumnType("bigint") + .HasColumnName("fediverse_application_id"); + + b.Property("RemoteId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("remote_id"); + + b.Property("RemoteUsername") + .HasColumnType("text") + .HasColumnName("remote_username"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_auth_methods"); + + b.HasIndex("FediverseApplicationId") + .HasDatabaseName("ix_auth_methods_fediverse_application_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_auth_methods_user_id"); + + b.ToTable("auth_methods", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("Domain") + .IsRequired() + .HasColumnType("text") + .HasColumnName("domain"); + + b.Property("InstanceType") + .HasColumnType("integer") + .HasColumnName("instance_type"); + + b.HasKey("Id") + .HasName("pk_fediverse_applications"); + + b.ToTable("fediverse_applications", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("Links") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("links"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Unlisted") + .HasColumnType("boolean") + .HasColumnName("unlisted"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_members"); + + b.HasIndex("UserId", "Name") + .IsUnique() + .HasDatabaseName("ix_members_user_id_name"); + + b.ToTable("members", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("bigint") + .HasColumnName("application_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hash"); + + b.Property("ManuallyExpired") + .HasColumnType("boolean") + .HasColumnName("manually_expired"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tokens"); + + b.HasIndex("ApplicationId") + .HasDatabaseName("ix_tokens_application_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tokens_user_id"); + + b.ToTable("tokens", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("Links") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("links"); + + b.Property("ListHidden") + .HasColumnType("boolean") + .HasColumnName("list_hidden"); + + b.Property("MemberTitle") + .HasColumnType("text") + .HasColumnName("member_title"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Username") + .IsUnique() + .HasDatabaseName("ix_users_username"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication") + .WithMany() + .HasForeignKey("FediverseApplicationId") + .HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany("AuthMethods") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_methods_users_user_id"); + + b.Navigation("FediverseApplication"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany("Members") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_members_users_user_id"); + + b.OwnsOne("System.Collections.Generic.List", "Fields", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("fields"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.OwnsOne("System.Collections.Generic.List", "Names", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("names"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.OwnsOne("System.Collections.Generic.List", "Pronouns", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("pronouns"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.Navigation("Fields") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Pronouns") + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application") + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_applications_application_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_users_user_id"); + + b.Navigation("Application"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("fields"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("names"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("pronouns"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.Navigation("Fields") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Pronouns") + .IsRequired(); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.Navigation("AuthMethods"); + + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.cs b/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.cs new file mode 100644 index 0000000..23671a8 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + /// + public partial class AddPassword : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "password", + table: "users", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "password", + table: "users"); + } + } +} diff --git a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index f90a3c3..d0cb607 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -250,6 +250,10 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("member_title"); + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + b.Property("Role") .HasColumnType("integer") .HasColumnName("role"); diff --git a/Foxnouns.Backend/Database/Models/Application.cs b/Foxnouns.Backend/Database/Models/Application.cs index f4e2ecb..95416f1 100644 --- a/Foxnouns.Backend/Database/Models/Application.cs +++ b/Foxnouns.Backend/Database/Models/Application.cs @@ -15,7 +15,7 @@ public class Application : BaseModel string[] redirectUrls) { var clientId = RandomNumberGenerator.GetHexString(32, true); - var clientSecret = OauthUtils.RandomToken(48); + var clientSecret = OauthUtils.RandomToken(); if (scopes.Except(OauthUtils.ApplicationScopes).Any()) { diff --git a/Foxnouns.Backend/Database/Models/User.cs b/Foxnouns.Backend/Database/Models/User.cs index 1d5f852..238f306 100644 --- a/Foxnouns.Backend/Database/Models/User.cs +++ b/Foxnouns.Backend/Database/Models/User.cs @@ -15,6 +15,7 @@ public class User : BaseModel public List Fields { get; set; } = []; public UserRole Role { get; set; } = UserRole.User; + public string? Password { get; set; } // Password may be null if the user doesn't authenticate with an email address public List Members { get; } = []; public List AuthMethods { get; } = []; diff --git a/Foxnouns.Backend/Database/Snowflake.cs b/Foxnouns.Backend/Database/Snowflake.cs index 5037d89..feaf27b 100644 --- a/Foxnouns.Backend/Database/Snowflake.cs +++ b/Foxnouns.Backend/Database/Snowflake.cs @@ -1,9 +1,11 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Newtonsoft.Json; using NodaTime; namespace Foxnouns.Backend.Database; +[JsonConverter(typeof(JsonConverter))] public readonly struct Snowflake(ulong value) { public const long Epoch = 1_640_995_200_000; // 2022-01-01 at 00:00:00 UTC @@ -63,4 +65,19 @@ public readonly struct Snowflake(ulong value) convertToProviderExpression: x => x, convertFromProviderExpression: x => x ); + + private class JsonConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Snowflake value, JsonSerializer serializer) + { + writer.WriteValue(value.Value.ToString()); + } + + public override Snowflake ReadJson(JsonReader reader, Type objectType, Snowflake existingValue, + bool hasExistingValue, + JsonSerializer serializer) + { + return ulong.Parse((string)reader.Value!); + } + } } \ No newline at end of file diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 886cdf6..84381c4 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -65,7 +65,8 @@ public static class WebApplicationExtensions .AddSingleton(SystemClock.Instance) .AddSnowflakeGenerator() .AddScoped() - .AddScoped(); + .AddScoped() + .AddScoped(); public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services .AddScoped() diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs new file mode 100644 index 0000000..87494ed --- /dev/null +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -0,0 +1,57 @@ +using System.Security.Cryptography; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Utils; +using Microsoft.AspNetCore.Identity; +using NodaTime; + +namespace Foxnouns.Backend.Services; + +public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator) +{ + private readonly PasswordHasher _passwordHasher = new(); + + /// + /// Creates a new user with the given email address and password. + /// This method does not save the resulting user, the caller must still call . + /// + public async Task CreateUserWithPasswordAsync(string username, string email, string password) + { + var user = new User + { + Id = snowflakeGenerator.GenerateSnowflake(), + Username = username, + AuthMethods = { new AuthMethod { Id = snowflakeGenerator.GenerateSnowflake(), AuthType = AuthType.Email, RemoteId = email } } + }; + + db.Add(user); + user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password)); + + return user; + } + + public (string, Token) GenerateToken(User user, Application application, string[] scopes, Instant expires) + { + if (!OauthUtils.ValidateScopes(application, scopes)) + throw new ApiError.BadRequest("Invalid scopes requested for this token"); + + var (token, hash) = GenerateToken(); + return (token, new Token + { + Id = snowflakeGenerator.GenerateSnowflake(), + Hash = hash, + Application = application, + User = user, + ExpiresAt = expires, + Scopes = scopes + }); + } + + private static (string, byte[]) GenerateToken() + { + var token = OauthUtils.RandomToken(48); + var hash = SHA512.HashData(Convert.FromBase64String(token)); + + return (token, hash); + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Utils/OauthUtils.cs b/Foxnouns.Backend/Utils/OauthUtils.cs index 8aab005..4cbc83a 100644 --- a/Foxnouns.Backend/Utils/OauthUtils.cs +++ b/Foxnouns.Backend/Utils/OauthUtils.cs @@ -1,4 +1,5 @@ using System.Security.Cryptography; +using Foxnouns.Backend.Database.Models; namespace Foxnouns.Backend.Utils; @@ -16,13 +17,13 @@ public static class OauthUtils /// /// All scopes endpoints can be secured by. This does *not* include the catch-all token scopes. /// - public static readonly string[] Scopes = ["identify", .. UserScopes, .. MemberScopes]; + public static readonly string[] Scopes = ["identify", ..UserScopes, ..MemberScopes]; /// /// All scopes that can be granted to applications and tokens. Includes the catch-all token scopes, /// except for "*" which is only granted to the frontend. /// - public static readonly string[] ApplicationScopes = [.. Scopes, "user", "member"]; + public static readonly string[] ApplicationScopes = [..Scopes, "user", "member"]; public static string[] ExpandScopes(this string[] scopes) { @@ -34,6 +35,21 @@ public static class OauthUtils return expandedScopes.ToArray(); } + private static string[] ExpandAppScopes(this string[] scopes) + { + var expandedScopes = scopes.ExpandScopes().ToList(); + if (scopes.Contains("user")) expandedScopes.Add("user"); + if (scopes.Contains("member")) expandedScopes.Add("member"); + return expandedScopes.ToArray(); + } + + public static bool ValidateScopes(Application application, string[] scopes) + { + var expandedScopes = scopes.ExpandScopes(); + var appScopes = application.Scopes.ExpandAppScopes(); + return !expandedScopes.Except(appScopes).Any(); + } + public static bool ValidateRedirectUri(string uri) { try diff --git a/Foxnouns.Backend/config.ini b/Foxnouns.Backend/config.ini index b0e71f3..e0e13c4 100644 --- a/Foxnouns.Backend/config.ini +++ b/Foxnouns.Backend/config.ini @@ -17,4 +17,4 @@ Url = "Host=localhost;Database=foxnouns_net;Username=pronouns;Password=pronouns" ; The timeout for opening new connections. Defaults to 5. Timeout = 5 ; The maximum number of open connections. Defaults to 50. -MaxPoolSize = 500 +MaxPoolSize = 50