feat: initial fediverse registration/login
This commit is contained in:
parent
5a22807410
commit
c4cb08cdc1
16 changed files with 467 additions and 111 deletions
|
@ -50,17 +50,6 @@ public class AuthController(
|
|||
Instant ExpiresAt
|
||||
);
|
||||
|
||||
public record CallbackResponse(
|
||||
bool HasAccount,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Ticket,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
string? RemoteUsername,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
UserRendererService.UserResponse? User,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Token,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? ExpiresAt
|
||||
);
|
||||
|
||||
public record OauthRegisterRequest(string Ticket, string Username);
|
||||
|
||||
public record CallbackRequest(string Code, string State);
|
||||
|
@ -77,3 +66,13 @@ public class AuthController(
|
|||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
public record CallbackResponse(
|
||||
bool HasAccount,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Ticket,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? RemoteUsername,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
UserRendererService.UserResponse? User,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Token,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? ExpiresAt
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ using Foxnouns.Backend.Database;
|
|||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Services.Auth;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -14,20 +15,16 @@ namespace Foxnouns.Backend.Controllers.Authentication;
|
|||
public class DiscordAuthController(
|
||||
[UsedImplicitly] Config config,
|
||||
ILogger logger,
|
||||
IClock clock,
|
||||
DatabaseContext db,
|
||||
KeyCacheService keyCacheService,
|
||||
AuthService authService,
|
||||
RemoteAuthService remoteAuthService,
|
||||
UserRendererService userRenderer
|
||||
RemoteAuthService remoteAuthService
|
||||
) : ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<DiscordAuthController>();
|
||||
|
||||
[HttpPost("callback")]
|
||||
// TODO: duplicating attribute doesn't work, find another way to mark both as possible response
|
||||
// leaving it here for documentation purposes
|
||||
[ProducesResponseType<AuthController.CallbackResponse>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<CallbackResponse>(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> CallbackAsync([FromBody] AuthController.CallbackRequest req)
|
||||
{
|
||||
CheckRequirements();
|
||||
|
@ -36,7 +33,7 @@ public class DiscordAuthController(
|
|||
var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code);
|
||||
var user = await authService.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id);
|
||||
if (user != null)
|
||||
return Ok(await GenerateUserTokenAsync(user));
|
||||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
|
||||
_logger.Debug(
|
||||
"Discord user {Username} ({Id}) authenticated with no local account",
|
||||
|
@ -52,7 +49,7 @@ public class DiscordAuthController(
|
|||
);
|
||||
|
||||
return Ok(
|
||||
new AuthController.CallbackResponse(
|
||||
new CallbackResponse(
|
||||
HasAccount: false,
|
||||
Ticket: ticket,
|
||||
RemoteUsername: remoteUser.Username,
|
||||
|
@ -94,42 +91,7 @@ public class DiscordAuthController(
|
|||
remoteUser.Username
|
||||
);
|
||||
|
||||
return Ok(await GenerateUserTokenAsync(user));
|
||||
}
|
||||
|
||||
private async Task<AuthController.CallbackResponse> GenerateUserTokenAsync(
|
||||
User user,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
var frontendApp = await db.GetFrontendApplicationAsync(ct);
|
||||
_logger.Debug("Logging user {Id} in with Discord", user.Id);
|
||||
|
||||
var (tokenStr, token) = authService.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 AuthController.CallbackResponse(
|
||||
HasAccount: true,
|
||||
Ticket: null,
|
||||
RemoteUsername: null,
|
||||
User: await userRenderer.RenderUserAsync(
|
||||
user,
|
||||
selfUser: user,
|
||||
renderMembers: false,
|
||||
ct: ct
|
||||
),
|
||||
Token: tokenStr,
|
||||
ExpiresAt: token.ExpiresAt
|
||||
);
|
||||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
}
|
||||
|
||||
private void CheckRequirements()
|
||||
|
|
|
@ -3,6 +3,7 @@ using Foxnouns.Backend.Database.Models;
|
|||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Services.Auth;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -84,7 +85,7 @@ public class EmailAuthController(
|
|||
await keyCacheService.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20));
|
||||
|
||||
return Ok(
|
||||
new AuthController.CallbackResponse(
|
||||
new CallbackResponse(
|
||||
HasAccount: false,
|
||||
Ticket: ticket,
|
||||
RemoteUsername: state.Email,
|
||||
|
|
|
@ -1,11 +1,26 @@
|
|||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Services.Auth;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using FediverseAuthService = Foxnouns.Backend.Services.Auth.FediverseAuthService;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Authentication;
|
||||
|
||||
[Route("/api/internal/auth/fediverse")]
|
||||
public class FediverseAuthController(FediverseAuthService fediverseAuthService) : ApiControllerBase
|
||||
public class FediverseAuthController(
|
||||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
FediverseAuthService fediverseAuthService,
|
||||
AuthService authService,
|
||||
KeyCacheService keyCacheService
|
||||
) : ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<FediverseAuthController>();
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType<FediverseUrlResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetFediverseUrlAsync([FromQuery] string instance)
|
||||
|
@ -14,12 +29,88 @@ public class FediverseAuthController(FediverseAuthService fediverseAuthService)
|
|||
return Ok(new FediverseUrlResponse(url));
|
||||
}
|
||||
|
||||
[HttpPost("callback")]
|
||||
[ProducesResponseType<CallbackResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> FediverseCallbackAsync([FromBody] CallbackRequest req)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
var app = await fediverseAuthService.GetApplicationAsync(req.Instance);
|
||||
var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code);
|
||||
|
||||
var user = await authService.AuthenticateUserAsync(
|
||||
AuthType.Fediverse,
|
||||
remoteUser.Id,
|
||||
instance: app
|
||||
);
|
||||
if (user != null)
|
||||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
|
||||
var ticket = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync(
|
||||
$"fediverse:{ticket}",
|
||||
new FediverseTicketData(app.Id, remoteUser),
|
||||
Duration.FromMinutes(20)
|
||||
);
|
||||
|
||||
return Ok(
|
||||
new CallbackResponse(
|
||||
HasAccount: false,
|
||||
Ticket: ticket,
|
||||
RemoteUsername: $"@{remoteUser.Username}@{app.Domain}",
|
||||
User: null,
|
||||
Token: null,
|
||||
ExpiresAt: null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
[ProducesResponseType<AuthController.AuthResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> RegisterAsync(
|
||||
[FromBody] AuthController.OauthRegisterRequest req
|
||||
)
|
||||
{
|
||||
var ticketData = await keyCacheService.GetKeyAsync<FediverseTicketData>(
|
||||
$"fediverse:{req.Ticket}"
|
||||
);
|
||||
if (ticketData == null)
|
||||
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
|
||||
|
||||
var app = await db.FediverseApplications.FindAsync(ticketData.ApplicationId);
|
||||
if (
|
||||
await db.AuthMethods.AnyAsync(a =>
|
||||
a.AuthType == AuthType.Fediverse
|
||||
&& a.RemoteId == ticketData.User.Id
|
||||
&& a.FediverseApplicationId == app.Id
|
||||
)
|
||||
)
|
||||
{
|
||||
_logger.Error(
|
||||
"Fediverse user {Id}/{ApplicationId} ({Username} on {Domain}) has valid ticket but is already linked to an existing account",
|
||||
ticketData.User.Id,
|
||||
ticketData.ApplicationId,
|
||||
ticketData.User.Username,
|
||||
app.Domain
|
||||
);
|
||||
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
|
||||
}
|
||||
|
||||
var user = await authService.CreateUserWithRemoteAuthAsync(
|
||||
req.Username,
|
||||
AuthType.Fediverse,
|
||||
ticketData.User.Id,
|
||||
ticketData.User.Username,
|
||||
instance: app
|
||||
);
|
||||
|
||||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
}
|
||||
|
||||
public record CallbackRequest(string Instance, string Code);
|
||||
|
||||
private record FediverseUrlResponse(string Url);
|
||||
|
||||
private record FediverseTicketData(
|
||||
Snowflake ApplicationId,
|
||||
FediverseAuthService.FediverseUser User
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue