feat: initial fediverse registration/login
This commit is contained in:
parent
5a22807410
commit
c4cb08cdc1
16 changed files with 467 additions and 111 deletions
|
@ -1,4 +1,5 @@
|
|||
using System.Security.Cryptography;
|
||||
using Foxnouns.Backend.Controllers.Authentication;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Utils;
|
||||
|
@ -6,10 +7,17 @@ using Microsoft.AspNetCore.Identity;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Services;
|
||||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
||||
public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator)
|
||||
public class AuthService(
|
||||
IClock clock,
|
||||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
UserRendererService userRenderer
|
||||
)
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<AuthService>();
|
||||
private readonly PasswordHasher<User> _passwordHasher = new();
|
||||
|
||||
/// <summary>
|
||||
|
@ -256,6 +264,45 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s
|
|||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a token for the given user and adds it to the database, returning a fully formed auth response for the user.
|
||||
/// This method is always called at the end of an endpoint method, so the resulting token
|
||||
/// (and user, if this is a registration request) is also saved to the database.
|
||||
/// </summary>
|
||||
public async Task<CallbackResponse> GenerateUserTokenAsync(
|
||||
User user,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
var frontendApp = await db.GetFrontendApplicationAsync(ct);
|
||||
|
||||
var (tokenStr, token) = GenerateToken(
|
||||
user,
|
||||
frontendApp,
|
||||
["*"],
|
||||
clock.GetCurrentInstant() + Duration.FromDays(365)
|
||||
);
|
||||
db.Add(token);
|
||||
|
||||
_logger.Debug("Generated token {TokenId} for {UserId}", user.Id, token.Id);
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return new CallbackResponse(
|
||||
HasAccount: true,
|
||||
Ticket: null,
|
||||
RemoteUsername: null,
|
||||
User: await userRenderer.RenderUserAsync(
|
||||
user,
|
||||
selfUser: user,
|
||||
renderMembers: false,
|
||||
ct: ct
|
||||
),
|
||||
Token: tokenStr,
|
||||
ExpiresAt: token.ExpiresAt
|
||||
);
|
||||
}
|
||||
|
||||
private static (string, byte[]) GenerateToken()
|
||||
{
|
||||
var token = AuthUtils.RandomToken();
|
|
@ -5,12 +5,12 @@ using Foxnouns.Backend.Database.Models;
|
|||
using Duration = NodaTime.Duration;
|
||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||
|
||||
namespace Foxnouns.Backend.Services;
|
||||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
||||
public partial class FediverseAuthService
|
||||
{
|
||||
private string MastodonRedirectUri(string instance) =>
|
||||
$"{_config.BaseUrl}/auth/login/mastodon/{instance}";
|
||||
$"{_config.BaseUrl}/auth/callback/mastodon/{instance}";
|
||||
|
||||
private async Task<FediverseApplication> CreateMastodonApplicationAsync(
|
||||
string instance,
|
|
@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
|||
using NodaTime;
|
||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||
|
||||
namespace Foxnouns.Backend.Services;
|
||||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
||||
public partial class FediverseAuthService
|
||||
{
|
||||
|
@ -43,12 +43,6 @@ public partial class FediverseAuthService
|
|||
return await GenerateAuthUrlAsync(app);
|
||||
}
|
||||
|
||||
public async Task<FediverseUser> GetRemoteFediverseUserAsync(string instance, string code)
|
||||
{
|
||||
var app = await GetApplicationAsync(instance);
|
||||
return await GetRemoteUserAsync(app, code);
|
||||
}
|
||||
|
||||
// thank you, gargron and syuilo, for agreeing on a name for *once* in your lives,
|
||||
// and having both mastodon and misskey use "username" in the self user response
|
||||
public record FediverseUser(
|
||||
|
@ -56,7 +50,7 @@ public partial class FediverseAuthService
|
|||
[property: J("username")] string Username
|
||||
);
|
||||
|
||||
private async Task<FediverseApplication> GetApplicationAsync(string instance)
|
||||
public async Task<FediverseApplication> GetApplicationAsync(string instance)
|
||||
{
|
||||
var app = await _db.FediverseApplications.FirstOrDefaultAsync(a => a.Domain == instance);
|
||||
if (app != null)
|
||||
|
@ -110,7 +104,10 @@ public partial class FediverseAuthService
|
|||
_ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null),
|
||||
};
|
||||
|
||||
private async Task<FediverseUser> GetRemoteUserAsync(FediverseApplication app, string code) =>
|
||||
public async Task<FediverseUser> GetRemoteFediverseUserAsync(
|
||||
FediverseApplication app,
|
||||
string code
|
||||
) =>
|
||||
app.InstanceType switch
|
||||
{
|
||||
FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code),
|
|
@ -97,7 +97,7 @@ public class UserRendererService(
|
|||
? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp"
|
||||
: null;
|
||||
|
||||
public string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp";
|
||||
private string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp";
|
||||
|
||||
public record UserResponse(
|
||||
Snowflake Id,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue