add password-based login endpoint
This commit is contained in:
parent
7959b64fe6
commit
656eec81d8
5 changed files with 40 additions and 21 deletions
|
@ -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 class ApiError(string message, HttpStatusCode? statusCode = null) : FoxchatError(message)
|
||||||
{
|
{
|
||||||
public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError;
|
public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError;
|
||||||
|
|
||||||
public class Unauthorized(string message) : ApiError(message, statusCode: HttpStatusCode.Unauthorized);
|
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 class Forbidden(string message, IEnumerable<string>? scopes = null) : ApiError(message, statusCode: HttpStatusCode.Forbidden)
|
||||||
{
|
{
|
||||||
public readonly string[] Scopes = scopes?.ToArray() ?? [];
|
public readonly string[] Scopes = scopes?.ToArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BadRequest(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
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 IncomingFederationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
public class OutgoingFederationError(
|
public class OutgoingFederationError(
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
using Foxchat.Identity.Middleware;
|
using Foxchat.Identity.Middleware;
|
||||||
using Foxchat.Identity.Database;
|
using Foxchat.Identity.Database;
|
||||||
using Foxchat.Identity.Utils;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Foxchat.Identity.Database.Models;
|
using Foxchat.Identity.Database.Models;
|
||||||
using Foxchat.Core;
|
using Foxchat.Core;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Foxchat.Identity.Controllers.Oauth;
|
namespace Foxchat.Identity.Controllers.Oauth;
|
||||||
|
|
||||||
|
@ -36,13 +36,41 @@ public class PasswordAuthController(ILogger logger, IdentityContext db, IClock c
|
||||||
await db.AddAsync(acct);
|
await db.AddAsync(acct);
|
||||||
var hashedPassword = await Task.Run(() => _passwordHasher.HashPassword(acct, req.Password));
|
var hashedPassword = await Task.Run(() => _passwordHasher.HashPassword(acct, req.Password));
|
||||||
acct.Password = hashedPassword;
|
acct.Password = hashedPassword;
|
||||||
|
// TODO: make token expiry configurable
|
||||||
var (tokenStr, token) = Token.Create(acct, app, req.Scopes, clock.GetCurrentInstant() + Duration.FromDays(365));
|
var (tokenStr, token) = Token.Create(acct, app, req.Scopes, clock.GetCurrentInstant() + Duration.FromDays(365));
|
||||||
await db.AddAsync(token);
|
await db.AddAsync(token);
|
||||||
await db.SaveChangesAsync();
|
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 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);
|
||||||
}
|
}
|
|
@ -39,14 +39,7 @@ public class TokenController(ILogger logger, IdentityContext db, IClock clock) :
|
||||||
{
|
{
|
||||||
// TODO: make this configurable
|
// TODO: make this configurable
|
||||||
var expiry = clock.GetCurrentInstant() + Duration.FromDays(365);
|
var expiry = clock.GetCurrentInstant() + Duration.FromDays(365);
|
||||||
var (token, hash) = Token.Generate();
|
var (token, tokenObj) = Token.Create(null, app, scopes, expiry);
|
||||||
var tokenObj = new Token
|
|
||||||
{
|
|
||||||
Hash = hash,
|
|
||||||
Scopes = scopes,
|
|
||||||
Expires = expiry,
|
|
||||||
ApplicationId = app.Id
|
|
||||||
};
|
|
||||||
|
|
||||||
await db.AddAsync(tokenObj);
|
await db.AddAsync(tokenObj);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
|
@ -71,6 +71,13 @@ public static class HttpContextExtensions
|
||||||
return token as Token;
|
return token as Token;
|
||||||
return null;
|
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)]
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||||
|
|
|
@ -24,11 +24,4 @@ public static class OauthUtils
|
||||||
return false;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue