add password-based login endpoint

This commit is contained in:
sam 2024-05-20 21:59:30 +02:00
parent 7959b64fe6
commit 656eec81d8
5 changed files with 40 additions and 21 deletions

View file

@ -13,15 +13,13 @@ public class FoxchatError(string message, Exception? inner = null) : Exception(m
public class ApiError(string message, HttpStatusCode? statusCode = null) : FoxchatError(message)
{
public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError;
public class Unauthorized(string message) : ApiError(message, statusCode: HttpStatusCode.Unauthorized);
public class Forbidden(string message, IEnumerable<string>? scopes = null) : ApiError(message, statusCode: HttpStatusCode.Forbidden)
{
public readonly string[] Scopes = scopes?.ToArray() ?? [];
}
public class BadRequest(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
public class NotFound(string message) : ApiError(message, statusCode: HttpStatusCode.NotFound);
public class IncomingFederationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
public class OutgoingFederationError(

View file

@ -1,12 +1,12 @@
using Foxchat.Identity.Middleware;
using Foxchat.Identity.Database;
using Foxchat.Identity.Utils;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Identity;
using Foxchat.Identity.Database.Models;
using Foxchat.Core;
using System.Diagnostics;
using NodaTime;
using Microsoft.EntityFrameworkCore;
namespace Foxchat.Identity.Controllers.Oauth;
@ -36,13 +36,41 @@ public class PasswordAuthController(ILogger logger, IdentityContext db, IClock c
await db.AddAsync(acct);
var hashedPassword = await Task.Run(() => _passwordHasher.HashPassword(acct, req.Password));
acct.Password = hashedPassword;
// TODO: make token expiry configurable
var (tokenStr, token) = Token.Create(acct, app, req.Scopes, clock.GetCurrentInstant() + Duration.FromDays(365));
await db.AddAsync(token);
await db.SaveChangesAsync();
return Ok(new RegisterResponse(acct.Id, acct.Username, acct.Email, tokenStr));
return Ok(new AuthResponse(acct.Id, acct.Username, acct.Email, tokenStr));
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest req)
{
var app = HttpContext.GetApplicationOrThrow();
var appToken = HttpContext.GetToken() ?? throw new UnreachableException();
if (req.Scopes.Except(appToken.Scopes).Any())
throw new ApiError.Forbidden("Cannot request token scopes that are not allowed for this token", req.Scopes.Except(appToken.Scopes));
var acct = await db.Accounts.FirstOrDefaultAsync(a => a.Email == req.Email)
?? throw new ApiError.NotFound("No user with that email found, or password is incorrect");
var pwResult = await Task.Run(() => _passwordHasher.VerifyHashedPassword(acct, acct.Password, req.Password));
if (pwResult == PasswordVerificationResult.Failed)
throw new ApiError.NotFound("No user with that email found, or password is incorrect");
if (pwResult == PasswordVerificationResult.SuccessRehashNeeded)
acct.Password = await Task.Run(() => _passwordHasher.HashPassword(acct, req.Password));
var (tokenStr, token) = Token.Create(acct, app, req.Scopes, clock.GetCurrentInstant() + Duration.FromDays(365));
await db.AddAsync(token);
await db.SaveChangesAsync();
return Ok(new AuthResponse(acct.Id, acct.Username, acct.Email, tokenStr));
}
public record RegisterRequest(string Username, string Password, string Email, string[] Scopes);
public record RegisterResponse(Ulid Id, string Username, string Email, string Token);
public record LoginRequest(string Email, string Password, string[] Scopes);
public record AuthResponse(Ulid Id, string Username, string Email, string Token);
}

View file

@ -39,14 +39,7 @@ public class TokenController(ILogger logger, IdentityContext db, IClock clock) :
{
// TODO: make this configurable
var expiry = clock.GetCurrentInstant() + Duration.FromDays(365);
var (token, hash) = Token.Generate();
var tokenObj = new Token
{
Hash = hash,
Scopes = scopes,
Expires = expiry,
ApplicationId = app.Id
};
var (token, tokenObj) = Token.Create(null, app, scopes, expiry);
await db.AddAsync(tokenObj);
await db.SaveChangesAsync();

View file

@ -71,6 +71,13 @@ public static class HttpContextExtensions
return token as Token;
return null;
}
public static Application GetApplicationOrThrow(this HttpContext context)
{
var token = context.GetToken();
if (token is not { Account: null }) throw new ApiError.Forbidden("This endpoint requires a client token.");
return token.Application;
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]

View file

@ -24,11 +24,4 @@ public static class OauthUtils
return false;
}
}
public static Application GetApplicationOrThrow(this HttpContext context)
{
var token = context.GetToken();
if (token is not { Account: null }) throw new ApiError.Forbidden("This endpoint requires a client token.");
return token.Application;
}
}