From aca83fa1ef0909c0ff329731c1fdb45a91fb0c84 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 19 May 2024 17:20:45 +0200 Subject: [PATCH] add a bunch of authentication stuff --- Foxchat.Chat/Database/ChatContext.cs | 11 +- Foxchat.Chat/Foxchat.Chat.csproj | 1 + Foxchat.Chat/Program.cs | 2 +- Foxchat.Core/CoreConfig.cs | 1 + .../RequestSigningService.Client.cs | 6 +- .../Federation/RequestSigningService.cs | 10 +- Foxchat.Core/Foxchat.Core.csproj | 1 + Foxchat.Core/ServiceCollectionExtensions.cs | 12 +- Foxchat.Core/Utils/ConvertUtils.cs | 23 ++ .../Authorization/AuthenticationHandler.cs | 43 ++- Foxchat.Identity/Database/BaseModel.cs | 6 + Foxchat.Identity/Database/IdentityContext.cs | 14 +- Foxchat.Identity/Database/Models/Account.cs | 3 +- .../Database/Models/Application.cs | 46 +++ .../Database/Models/ChatInstance.cs | 3 +- Foxchat.Identity/Database/Models/Token.cs | 24 +- Foxchat.Identity/Foxchat.Identity.csproj | 3 +- ...20240519151928_AddApplications.Designer.cs | 320 ++++++++++++++++++ .../20240519151928_AddApplications.cs | 109 ++++++ .../IdentityContextModelSnapshot.cs | 67 ++++ Foxchat.Identity/Program.cs | 2 +- Foxchat.Identity/identity.ini | 2 + 22 files changed, 681 insertions(+), 28 deletions(-) create mode 100644 Foxchat.Core/Utils/ConvertUtils.cs create mode 100644 Foxchat.Identity/Database/BaseModel.cs create mode 100644 Foxchat.Identity/Database/Models/Application.cs create mode 100644 Foxchat.Identity/Migrations/20240519151928_AddApplications.Designer.cs create mode 100644 Foxchat.Identity/Migrations/20240519151928_AddApplications.cs diff --git a/Foxchat.Chat/Database/ChatContext.cs b/Foxchat.Chat/Database/ChatContext.cs index 22ee6f2..09e0180 100644 --- a/Foxchat.Chat/Database/ChatContext.cs +++ b/Foxchat.Chat/Database/ChatContext.cs @@ -2,27 +2,32 @@ using Foxchat.Core; using Foxchat.Core.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; +using Npgsql; namespace Foxchat.Chat.Database; public class ChatContext : IDatabaseContext { - private readonly string _connString; + private readonly NpgsqlDataSource _dataSource; public override DbSet Instance { get; set; } public ChatContext(InstanceConfig config) { - _connString = new Npgsql.NpgsqlConnectionStringBuilder(config.Database.Url) + var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) { Timeout = config.Database.Timeout ?? 5, MaxPoolSize = config.Database.MaxPoolSize ?? 50, }.ConnectionString; + + var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); + dataSourceBuilder.UseNodaTime(); + _dataSource = dataSourceBuilder.Build(); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .UseNpgsql(_connString) + .UseNpgsql(_dataSource, o => o.UseNodaTime()) .UseSnakeCaseNamingConvention(); protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) diff --git a/Foxchat.Chat/Foxchat.Chat.csproj b/Foxchat.Chat/Foxchat.Chat.csproj index 9f5ee81..b363222 100644 --- a/Foxchat.Chat/Foxchat.Chat.csproj +++ b/Foxchat.Chat/Foxchat.Chat.csproj @@ -16,6 +16,7 @@ all + diff --git a/Foxchat.Chat/Program.cs b/Foxchat.Chat/Program.cs index a7f939e..68e4115 100644 --- a/Foxchat.Chat/Program.cs +++ b/Foxchat.Chat/Program.cs @@ -8,7 +8,7 @@ var builder = WebApplication.CreateBuilder(args); var config = builder.AddConfiguration("chat.ini"); -builder.Services.AddSerilog(config.LogEventLevel); +builder.AddSerilog(config.LogEventLevel); await BuildInfo.ReadBuildInfo(); Log.Information("Starting Foxchat.Chat {Version} ({Hash})", BuildInfo.Version, BuildInfo.Hash); diff --git a/Foxchat.Core/CoreConfig.cs b/Foxchat.Core/CoreConfig.cs index 39e784c..40a97b4 100644 --- a/Foxchat.Core/CoreConfig.cs +++ b/Foxchat.Core/CoreConfig.cs @@ -12,6 +12,7 @@ public class CoreConfig public string Address => $"{(Secure ? "https" : "http")}://{Host}:{Port}"; public LogEventLevel LogEventLevel { get; set; } = LogEventLevel.Debug; + public string? SeqLogUrl { get; set; } public DatabaseConfig Database { get; set; } = new(); diff --git a/Foxchat.Core/Federation/RequestSigningService.Client.cs b/Foxchat.Core/Federation/RequestSigningService.Client.cs index 49370e0..eb84fb3 100644 --- a/Foxchat.Core/Federation/RequestSigningService.Client.cs +++ b/Foxchat.Core/Federation/RequestSigningService.Client.cs @@ -30,12 +30,12 @@ public partial class RequestSigningService if (!resp.IsSuccessStatusCode) { var error = await resp.Content.ReadAsStringAsync(); - throw new FoxchatError.OutgoingFederationError($"Request to {domain}/{requestPath} returned an error", DeserializeObject(error)); + throw new FoxchatError.OutgoingFederationError($"Request to {domain}{requestPath} returned an error", DeserializeObject(error)); } var bodyString = await resp.Content.ReadAsStringAsync(); return DeserializeObject(bodyString) - ?? throw new FoxchatError.OutgoingFederationError($"Request to {domain}/{requestPath} returned invalid response body"); + ?? throw new FoxchatError.OutgoingFederationError($"Request to {domain}{requestPath} returned invalid response body"); } private HttpRequestMessage BuildHttpRequest(HttpMethod method, string domain, string requestPath, string? userId = null, object? bodyData = null) @@ -55,9 +55,7 @@ public partial class RequestSigningService if (userId != null) request.Headers.Add(USER_HEADER, userId); if (body != null) - { request.Content = new StringContent(body, new MediaTypeHeaderValue("application/json", "utf-8")); - } return request; } diff --git a/Foxchat.Core/Federation/RequestSigningService.cs b/Foxchat.Core/Federation/RequestSigningService.cs index 87f69b7..6c1e8ed 100644 --- a/Foxchat.Core/Federation/RequestSigningService.cs +++ b/Foxchat.Core/Federation/RequestSigningService.cs @@ -2,7 +2,7 @@ using System.Globalization; using System.Security.Cryptography; using System.Text; using Foxchat.Core.Database; -using Microsoft.AspNetCore.WebUtilities; +using Foxchat.Core.Utils; using NodaTime; using NodaTime.Text; using Serilog; @@ -28,7 +28,7 @@ public partial class RequestSigningService(ILogger logger, IClock clock, IDataba var signature = formatter.CreateSignature(hash); _logger.Debug("Generated signature for {Host} {RequestPath}", data.Host, data.RequestPath); - return WebEncoders.Base64UrlEncode(signature); + return Convert.ToBase64String(signature); } public bool VerifySignature( @@ -51,7 +51,11 @@ public partial class RequestSigningService(ILogger logger, IClock clock, IDataba var plaintext = GeneratePlaintext(new SignatureData(time, host, requestPath, contentLength, userId)); var plaintextBytes = Encoding.UTF8.GetBytes(plaintext); var hash = SHA256.HashData(plaintextBytes); - var signature = WebEncoders.Base64UrlDecode(encodedSignature); + + if (!CryptoUtils.TryFromBase64String(encodedSignature, out var signature)) + { + throw new FoxchatError.IncomingFederationError("Invalid base64 signature"); + } var deformatter = new RSAPKCS1SignatureDeformatter(rsa); deformatter.SetHashAlgorithm(nameof(SHA256)); diff --git a/Foxchat.Core/Foxchat.Core.csproj b/Foxchat.Core/Foxchat.Core.csproj index bbf8d72..e22365b 100644 --- a/Foxchat.Core/Foxchat.Core.csproj +++ b/Foxchat.Core/Foxchat.Core.csproj @@ -14,6 +14,7 @@ + diff --git a/Foxchat.Core/ServiceCollectionExtensions.cs b/Foxchat.Core/ServiceCollectionExtensions.cs index 4f0dd8a..f5d4893 100644 --- a/Foxchat.Core/ServiceCollectionExtensions.cs +++ b/Foxchat.Core/ServiceCollectionExtensions.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.DependencyInjection; using NodaTime; using Serilog; using Serilog.Events; -using Serilog.Sinks.SystemConsole.Themes; namespace Foxchat.Core; @@ -15,8 +14,10 @@ public static class ServiceCollectionExtensions /// /// Adds Serilog to this service collection. This method also initializes Serilog so it should be called as early as possible, before any log calls. /// - public static IServiceCollection AddSerilog(this IServiceCollection services, LogEventLevel level) + public static void AddSerilog(this WebApplicationBuilder builder, LogEventLevel level) { + var config = builder.Configuration.Get() ?? new(); + var logCfg = new LoggerConfiguration() .Enrich.FromLogContext() .MinimumLevel.Is(level) @@ -26,12 +27,15 @@ public static class ServiceCollectionExtensions .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) - .WriteTo.Console(theme: AnsiConsoleTheme.Code); + .WriteTo.Console(); + + if (config.SeqLogUrl != null) + logCfg.WriteTo.Seq(config.SeqLogUrl, restrictedToMinimumLevel: LogEventLevel.Verbose); Log.Logger = logCfg.CreateLogger(); // AddSerilog doesn't seem to add an ILogger to the service collection, so add that manually. - return services.AddSerilog().AddSingleton(Log.Logger); + builder.Services.AddSerilog().AddSingleton(Log.Logger); } /// diff --git a/Foxchat.Core/Utils/ConvertUtils.cs b/Foxchat.Core/Utils/ConvertUtils.cs new file mode 100644 index 0000000..bd06053 --- /dev/null +++ b/Foxchat.Core/Utils/ConvertUtils.cs @@ -0,0 +1,23 @@ +using System.Security.Cryptography; + +namespace Foxchat.Core.Utils; + +public static class CryptoUtils +{ + public static bool TryFromBase64String(string b64, out byte[] bytes) + { + try + { + bytes = Convert.FromBase64String(b64); + return true; + } + catch + { + bytes = []; + return false; + } + } + + public static string RandomToken(int bytes = 48) => + Convert.ToBase64String(RandomNumberGenerator.GetBytes(bytes)).Trim('='); +} diff --git a/Foxchat.Identity/Authorization/AuthenticationHandler.cs b/Foxchat.Identity/Authorization/AuthenticationHandler.cs index 92f2112..78223a2 100644 --- a/Foxchat.Identity/Authorization/AuthenticationHandler.cs +++ b/Foxchat.Identity/Authorization/AuthenticationHandler.cs @@ -1,3 +1,14 @@ +using System.Security.Cryptography; +using System.Text.Encodings.Web; +using Foxchat.Core; +using Foxchat.Core.Utils; +using Foxchat.Identity.Database; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.BearerToken; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using NodaTime; + namespace Foxchat.Identity.Authorization; public static class AuthenticationHandlerExtensions @@ -6,4 +17,34 @@ public static class AuthenticationHandlerExtensions { } -} \ No newline at end of file +} + +public class FoxchatAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + IdentityContext context, + IClock clock +) : AuthenticationHandler(options, logger, encoder) +{ + protected override async Task HandleAuthenticateAsync() + { + var header = Request.Headers.Authorization.ToString(); + if (!header.StartsWith("bearer ", StringComparison.InvariantCultureIgnoreCase)) + return AuthenticateResult.NoResult(); + var token = header[7..]; + + if (!CryptoUtils.TryFromBase64String(token, out var rawToken)) + return AuthenticateResult.Fail(new FoxchatError.BadRequest("Invalid token format")); + + var hash = SHA512.HashData(rawToken); + var oauthToken = await context.Tokens + .Include(t => t.Account) + .Include(t => t.Application) + .FirstOrDefaultAsync(t => t.Hash == hash && t.Expires > clock.GetCurrentInstant()); + if (oauthToken == null) + return AuthenticateResult.NoResult(); + + throw new NotImplementedException(); + } +} diff --git a/Foxchat.Identity/Database/BaseModel.cs b/Foxchat.Identity/Database/BaseModel.cs new file mode 100644 index 0000000..b8e2444 --- /dev/null +++ b/Foxchat.Identity/Database/BaseModel.cs @@ -0,0 +1,6 @@ +namespace Foxchat.Identity.Database; + +public abstract class BaseModel +{ + public Ulid Id { get; init; } = Ulid.NewUlid(); +} diff --git a/Foxchat.Identity/Database/IdentityContext.cs b/Foxchat.Identity/Database/IdentityContext.cs index c276029..b8c7bca 100644 --- a/Foxchat.Identity/Database/IdentityContext.cs +++ b/Foxchat.Identity/Database/IdentityContext.cs @@ -3,31 +3,37 @@ using Foxchat.Core.Database; using Foxchat.Identity.Database.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; +using Npgsql; namespace Foxchat.Identity.Database; public class IdentityContext : IDatabaseContext { - private readonly string _connString; + private readonly NpgsqlDataSource _dataSource; public DbSet Accounts { get; set; } public DbSet ChatInstances { get; set; } public override DbSet Instance { get; set; } + public DbSet Applications { get; set; } public DbSet Tokens { get; set; } public DbSet GuildAccounts { get; set; } public IdentityContext(InstanceConfig config) { - _connString = new Npgsql.NpgsqlConnectionStringBuilder(config.Database.Url) + var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) { Timeout = config.Database.Timeout ?? 5, MaxPoolSize = config.Database.MaxPoolSize ?? 50, }.ConnectionString; + + var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); + dataSourceBuilder.UseNodaTime(); + _dataSource = dataSourceBuilder.Build(); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .UseNpgsql(_connString) + .UseNpgsql(_dataSource, o => o.UseNodaTime()) .UseSnakeCaseNamingConvention(); protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) @@ -44,6 +50,8 @@ public class IdentityContext : IDatabaseContext modelBuilder.Entity().HasIndex(i => i.Domain).IsUnique(); modelBuilder.Entity().HasKey(g => new { g.ChatInstanceId, g.GuildId, g.AccountId }); + + modelBuilder.Entity().HasIndex(a => a.ClientId).IsUnique(); } } diff --git a/Foxchat.Identity/Database/Models/Account.cs b/Foxchat.Identity/Database/Models/Account.cs index d639e27..4273358 100644 --- a/Foxchat.Identity/Database/Models/Account.cs +++ b/Foxchat.Identity/Database/Models/Account.cs @@ -1,8 +1,7 @@ namespace Foxchat.Identity.Database.Models; -public class Account +public class Account : BaseModel { - public Ulid Id { get; init; } = Ulid.NewUlid(); public string Username { get; set; } = null!; public string Email { get; set; } = null!; public string Password { get; set; } = null!; diff --git a/Foxchat.Identity/Database/Models/Application.cs b/Foxchat.Identity/Database/Models/Application.cs new file mode 100644 index 0000000..1976725 --- /dev/null +++ b/Foxchat.Identity/Database/Models/Application.cs @@ -0,0 +1,46 @@ +using System.Security.Cryptography; +using Microsoft.AspNetCore.WebUtilities; + +namespace Foxchat.Identity.Database.Models; + +public class Application : BaseModel +{ + public required string ClientId { get; init; } + public required string ClientSecret { get; init; } + public required string Name { get; init; } + public required string[] Scopes { get; init; } + + public static Application Create(string name, string[] scopes) + { + var clientId = RandomNumberGenerator.GetHexString(16, true); + var clientSecretBytes = RandomNumberGenerator.GetBytes(48); + var clientSecret = WebEncoders.Base64UrlEncode(clientSecretBytes); + + if (!scopes.All(s => Scope.ValidScopes.Contains(s))) + { + throw new ArgumentException("Invalid scopes passed to Application.Create", nameof(scopes)); + } + + return new Application + { + ClientId = clientId, + ClientSecret = clientSecret, + Name = name, + Scopes = scopes, + }; + } +} + +public static class Scope +{ + /// + /// OAuth scope for identifying a user and nothing else. + /// + public const string Identity = "identity"; + /// + /// OAuth scope for a full chat client. This grants *full access* to an account. + /// + public const string ChatClient = "chat_client"; + + public static readonly string[] ValidScopes = [Identity, ChatClient]; +} diff --git a/Foxchat.Identity/Database/Models/ChatInstance.cs b/Foxchat.Identity/Database/Models/ChatInstance.cs index 12e572f..9c21082 100644 --- a/Foxchat.Identity/Database/Models/ChatInstance.cs +++ b/Foxchat.Identity/Database/Models/ChatInstance.cs @@ -1,8 +1,7 @@ namespace Foxchat.Identity.Database.Models; -public class ChatInstance +public class ChatInstance : BaseModel { - public Ulid Id { get; init; } = Ulid.NewUlid(); public string Domain { get; init; } = null!; public string BaseUrl { get; set; } = null!; public string PublicKey { get; set; } = null!; diff --git a/Foxchat.Identity/Database/Models/Token.cs b/Foxchat.Identity/Database/Models/Token.cs index c32678e..bf35136 100644 --- a/Foxchat.Identity/Database/Models/Token.cs +++ b/Foxchat.Identity/Database/Models/Token.cs @@ -1,8 +1,26 @@ +using System.Security.Cryptography; +using Foxchat.Core.Utils; +using NodaTime; + namespace Foxchat.Identity.Database.Models; -public class Token +public class Token : BaseModel { - public Ulid Id { get; init; } = Ulid.NewUlid(); + public byte[] Hash { get; set; } = null!; + public string[] Scopes { get; set; } = []; + public Instant Expires { get; set; } + public Ulid AccountId { get; set; } public Account Account { get; set; } = null!; -} \ No newline at end of file + + public Ulid ApplicationId { get; set; } + public Application Application { get; set; } = null!; + + public static (string, byte[]) Generate() + { + var token = CryptoUtils.RandomToken(48); + var hash = SHA512.HashData(Convert.FromBase64String(token)); + + return (token, hash); + } +} diff --git a/Foxchat.Identity/Foxchat.Identity.csproj b/Foxchat.Identity/Foxchat.Identity.csproj index 96ce4a8..93900fd 100644 --- a/Foxchat.Identity/Foxchat.Identity.csproj +++ b/Foxchat.Identity/Foxchat.Identity.csproj @@ -16,7 +16,8 @@ all - + + diff --git a/Foxchat.Identity/Migrations/20240519151928_AddApplications.Designer.cs b/Foxchat.Identity/Migrations/20240519151928_AddApplications.Designer.cs new file mode 100644 index 0000000..8d77921 --- /dev/null +++ b/Foxchat.Identity/Migrations/20240519151928_AddApplications.Designer.cs @@ -0,0 +1,320 @@ +// +using System; +using Foxchat.Identity.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 Foxchat.Identity.Migrations +{ + [DbContext(typeof(IdentityContext))] + [Migration("20240519151928_AddApplications")] + partial class AddApplications + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AccountChatInstance", b => + { + b.Property("AccountsId") + .HasColumnType("uuid") + .HasColumnName("accounts_id"); + + b.Property("ChatInstancesId") + .HasColumnType("uuid") + .HasColumnName("chat_instances_id"); + + b.HasKey("AccountsId", "ChatInstancesId") + .HasName("pk_account_chat_instance"); + + b.HasIndex("ChatInstancesId") + .HasDatabaseName("ix_account_chat_instance_chat_instances_id"); + + b.ToTable("account_chat_instance", (string)null); + }); + + modelBuilder.Entity("Foxchat.Core.Database.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PrivateKey") + .IsRequired() + .HasColumnType("text") + .HasColumnName("private_key"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("text") + .HasColumnName("public_key"); + + b.HasKey("Id") + .HasName("pk_instance"); + + b.ToTable("instance", (string)null); + }); + + modelBuilder.Entity("Foxchat.Identity.Database.Models.Account", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_accounts_email"); + + b.HasIndex("Username") + .IsUnique() + .HasDatabaseName("ix_accounts_username"); + + b.ToTable("accounts", (string)null); + }); + + modelBuilder.Entity("Foxchat.Identity.Database.Models.Application", b => + { + b.Property("Id") + .HasColumnType("uuid") + .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("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.HasKey("Id") + .HasName("pk_applications"); + + b.HasIndex("ClientId") + .IsUnique() + .HasDatabaseName("ix_applications_client_id"); + + b.ToTable("applications", (string)null); + }); + + modelBuilder.Entity("Foxchat.Identity.Database.Models.ChatInstance", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("BaseUrl") + .IsRequired() + .HasColumnType("text") + .HasColumnName("base_url"); + + b.Property("Domain") + .IsRequired() + .HasColumnType("text") + .HasColumnName("domain"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("text") + .HasColumnName("public_key"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_chat_instances"); + + b.HasIndex("Domain") + .IsUnique() + .HasDatabaseName("ix_chat_instances_domain"); + + b.ToTable("chat_instances", (string)null); + }); + + modelBuilder.Entity("Foxchat.Identity.Database.Models.GuildAccount", b => + { + b.Property("ChatInstanceId") + .HasColumnType("uuid") + .HasColumnName("chat_instance_id"); + + b.Property("GuildId") + .HasColumnType("text") + .HasColumnName("guild_id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.HasKey("ChatInstanceId", "GuildId", "AccountId") + .HasName("pk_guild_accounts"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_guild_accounts_account_id"); + + b.ToTable("guild_accounts", (string)null); + }); + + modelBuilder.Entity("Foxchat.Identity.Database.Models.Token", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hash"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.HasKey("Id") + .HasName("pk_tokens"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_tokens_account_id"); + + b.HasIndex("ApplicationId") + .HasDatabaseName("ix_tokens_application_id"); + + b.ToTable("tokens", (string)null); + }); + + modelBuilder.Entity("AccountChatInstance", b => + { + b.HasOne("Foxchat.Identity.Database.Models.Account", null) + .WithMany() + .HasForeignKey("AccountsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_chat_instance_accounts_accounts_id"); + + b.HasOne("Foxchat.Identity.Database.Models.ChatInstance", null) + .WithMany() + .HasForeignKey("ChatInstancesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_chat_instance_chat_instances_chat_instances_id"); + }); + + modelBuilder.Entity("Foxchat.Identity.Database.Models.GuildAccount", b => + { + b.HasOne("Foxchat.Identity.Database.Models.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_guild_accounts_accounts_account_id"); + + b.HasOne("Foxchat.Identity.Database.Models.ChatInstance", "ChatInstance") + .WithMany() + .HasForeignKey("ChatInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_guild_accounts_chat_instances_chat_instance_id"); + + b.Navigation("Account"); + + b.Navigation("ChatInstance"); + }); + + modelBuilder.Entity("Foxchat.Identity.Database.Models.Token", b => + { + b.HasOne("Foxchat.Identity.Database.Models.Account", "Account") + .WithMany("Tokens") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_accounts_account_id"); + + b.HasOne("Foxchat.Identity.Database.Models.Application", "Application") + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_applications_application_id"); + + b.Navigation("Account"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Foxchat.Identity.Database.Models.Account", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Foxchat.Identity/Migrations/20240519151928_AddApplications.cs b/Foxchat.Identity/Migrations/20240519151928_AddApplications.cs new file mode 100644 index 0000000..a961f56 --- /dev/null +++ b/Foxchat.Identity/Migrations/20240519151928_AddApplications.cs @@ -0,0 +1,109 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Foxchat.Identity.Migrations +{ + /// + public partial class AddApplications : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "application_id", + table: "tokens", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "expires", + table: "tokens", + type: "timestamp with time zone", + nullable: false, + defaultValue: NodaTime.Instant.FromUnixTimeTicks(0L)); + + migrationBuilder.AddColumn( + name: "hash", + table: "tokens", + type: "bytea", + nullable: false, + defaultValue: new byte[0]); + + migrationBuilder.AddColumn( + name: "scopes", + table: "tokens", + type: "text[]", + nullable: false, + defaultValue: new string[0]); + + migrationBuilder.CreateTable( + name: "applications", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + client_id = table.Column(type: "text", nullable: false), + client_secret = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: false), + scopes = table.Column(type: "text[]", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_applications", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "ix_tokens_application_id", + table: "tokens", + column: "application_id"); + + migrationBuilder.CreateIndex( + name: "ix_applications_client_id", + table: "applications", + column: "client_id", + unique: true); + + migrationBuilder.AddForeignKey( + name: "fk_tokens_applications_application_id", + table: "tokens", + column: "application_id", + principalTable: "applications", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_tokens_applications_application_id", + table: "tokens"); + + migrationBuilder.DropTable( + name: "applications"); + + migrationBuilder.DropIndex( + name: "ix_tokens_application_id", + table: "tokens"); + + migrationBuilder.DropColumn( + name: "application_id", + table: "tokens"); + + migrationBuilder.DropColumn( + name: "expires", + table: "tokens"); + + migrationBuilder.DropColumn( + name: "hash", + table: "tokens"); + + migrationBuilder.DropColumn( + name: "scopes", + table: "tokens"); + } + } +} diff --git a/Foxchat.Identity/Migrations/IdentityContextModelSnapshot.cs b/Foxchat.Identity/Migrations/IdentityContextModelSnapshot.cs index 4d7184d..8467785 100644 --- a/Foxchat.Identity/Migrations/IdentityContextModelSnapshot.cs +++ b/Foxchat.Identity/Migrations/IdentityContextModelSnapshot.cs @@ -4,6 +4,7 @@ using Foxchat.Identity.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable @@ -109,6 +110,42 @@ namespace Foxchat.Identity.Migrations b.ToTable("accounts", (string)null); }); + modelBuilder.Entity("Foxchat.Identity.Database.Models.Application", b => + { + b.Property("Id") + .HasColumnType("uuid") + .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("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.HasKey("Id") + .HasName("pk_applications"); + + b.HasIndex("ClientId") + .IsUnique() + .HasDatabaseName("ix_applications_client_id"); + + b.ToTable("applications", (string)null); + }); + modelBuilder.Entity("Foxchat.Identity.Database.Models.ChatInstance", b => { b.Property("Id") @@ -181,12 +218,33 @@ namespace Foxchat.Identity.Migrations .HasColumnType("uuid") .HasColumnName("account_id"); + b.Property("ApplicationId") + .HasColumnType("uuid") + .HasColumnName("application_id"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hash"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + b.HasKey("Id") .HasName("pk_tokens"); b.HasIndex("AccountId") .HasDatabaseName("ix_tokens_account_id"); + b.HasIndex("ApplicationId") + .HasDatabaseName("ix_tokens_application_id"); + b.ToTable("tokens", (string)null); }); @@ -237,7 +295,16 @@ namespace Foxchat.Identity.Migrations .IsRequired() .HasConstraintName("fk_tokens_accounts_account_id"); + b.HasOne("Foxchat.Identity.Database.Models.Application", "Application") + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_applications_application_id"); + b.Navigation("Account"); + + b.Navigation("Application"); }); modelBuilder.Entity("Foxchat.Identity.Database.Models.Account", b => diff --git a/Foxchat.Identity/Program.cs b/Foxchat.Identity/Program.cs index 53acc5f..79d1837 100644 --- a/Foxchat.Identity/Program.cs +++ b/Foxchat.Identity/Program.cs @@ -9,7 +9,7 @@ var builder = WebApplication.CreateBuilder(args); var config = builder.AddConfiguration("identity.ini"); -builder.Services.AddSerilog(config.LogEventLevel); +builder.AddSerilog(config.LogEventLevel); await BuildInfo.ReadBuildInfo(); Log.Information("Starting Foxchat.Identity {Version} ({Hash})", BuildInfo.Version, BuildInfo.Hash); diff --git a/Foxchat.Identity/identity.ini b/Foxchat.Identity/identity.ini index 7e6ba08..d4b3c40 100644 --- a/Foxchat.Identity/identity.ini +++ b/Foxchat.Identity/identity.ini @@ -4,6 +4,8 @@ Domain = id.fox.localhost ; The level to log things at. Valid settings: Verbose, Debug, Information, Warning, Error, Fatal LogEventLevel = Debug +; Optional logging to Seq +SeqLogUrl = http://localhost:5341 [Database] ; The database URL in ADO.NET format.