using System.ComponentModel.DataAnnotations; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using VoidCat.Database; using VoidCat.Model; using VoidCat.Services.Abstractions; using VoidCat.Services.Users; namespace VoidCat.Controllers; [Route("auth")] public class AuthController : Controller { private readonly UserManager _manager; private readonly VoidSettings _settings; private readonly ICaptchaVerifier _captchaVerifier; private readonly IApiKeyStore _apiKeyStore; private readonly IUserStore _userStore; public AuthController(UserManager userManager, VoidSettings settings, ICaptchaVerifier captchaVerifier, IApiKeyStore apiKeyStore, IUserStore userStore) { _manager = userManager; _settings = settings; _captchaVerifier = captchaVerifier; _apiKeyStore = apiKeyStore; _userStore = userStore; } /// /// Login to a user account /// /// /// [HttpPost] [Route("login")] public async Task Login([FromBody] LoginRequest req) { try { if (!TryValidateModel(req)) { var error = ControllerContext.ModelState.FirstOrDefault().Value?.Errors.FirstOrDefault()?.ErrorMessage; return new(null, error); } // check captcha if (!await _captchaVerifier.Verify(req.Captcha)) { return new(null, "Captcha verification failed"); } var user = await _manager.Login(req.Username, req.Password); var token = CreateToken(user, DateTime.UtcNow.AddHours(12)); var tokenWriter = new JwtSecurityTokenHandler(); return new(tokenWriter.WriteToken(token), Profile: user.ToApiUser(true)); } catch (Exception ex) { return new(null, ex.Message); } } /// /// Register a new account /// /// /// [HttpPost] [Route("register")] public async Task Register([FromBody] LoginRequest req) { try { if (!TryValidateModel(req)) { var error = ControllerContext.ModelState.FirstOrDefault().Value?.Errors.FirstOrDefault()?.ErrorMessage; return new(null, error); } // check captcha if (!await _captchaVerifier.Verify(req.Captcha)) { return new(null, "Captcha verification failed"); } var newUser = await _manager.Register(req.Username, req.Password); var token = CreateToken(newUser, DateTime.UtcNow.AddHours(12)); var tokenWriter = new JwtSecurityTokenHandler(); return new(tokenWriter.WriteToken(token), Profile: newUser.ToApiUser(true)); } catch (Exception ex) { return new(null, ex.Message); } } /// /// Start OAuth2 authorize flow /// /// OAuth provider /// [HttpGet] [Route("{provider}")] public IActionResult Authorize([FromRoute] string provider) { return Redirect(_manager.Authorize(provider).ToString()); } /// /// Authorize user from OAuth2 code grant /// /// Code used to generate access token /// OAuth provider /// [HttpGet] [Route("{provider}/token")] public async Task Token([FromRoute] string provider, [FromQuery] string code) { var newUser = await _manager.LoginOrRegister(code, provider); var token = CreateToken(newUser, DateTime.UtcNow.AddHours(12)); var tokenWriter = new JwtSecurityTokenHandler(); return Redirect($"/login#{tokenWriter.WriteToken(token)}"); } /// /// List api keys for the user /// /// /// [HttpGet] [Route("api-key")] public async Task ListApiKeys() { var uid = HttpContext.GetUserId(); if (uid == default) return Unauthorized(); return Json(await _apiKeyStore.ListKeys(uid.Value)); } /// /// Create a new API key for the logged in user /// /// /// /// [HttpPost] [Route("api-key")] public async Task CreateApiKey([FromBody] CreateApiKeyRequest request) { var uid = HttpContext.GetUserId(); if (uid == default) return Unauthorized(); var user = await _userStore.Get(uid.Value); if (user == default) return Unauthorized(); var expiry = DateTime.SpecifyKind(request.Expiry, DateTimeKind.Utc); if (expiry > DateTime.UtcNow.AddYears(1)) { return BadRequest(); } var key = new ApiKey() { Id = Guid.NewGuid(), UserId = user.Id, Token = new JwtSecurityTokenHandler().WriteToken(CreateToken(user, expiry)), Expiry = expiry }; await _apiKeyStore.Add(key.Id, key); return Json(key); } private JwtSecurityToken CreateToken(User user, DateTime expiry) { var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.JwtSettings.Key)); var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); var claims = new List() { new(ClaimTypes.NameIdentifier, user.Id.ToString()), new(JwtRegisteredClaimNames.Exp, new DateTimeOffset(expiry).ToUnixTimeSeconds().ToString()), new(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()) }; claims.AddRange(user.Roles.Select(a => new Claim(ClaimTypes.Role, a.Role))); return new JwtSecurityToken(_settings.JwtSettings.Issuer, claims: claims, signingCredentials: credentials); } public sealed class LoginRequest { public LoginRequest(string username, string password) { Username = username; Password = password; } [Required] [EmailAddress] public string Username { get; } [Required] [MinLength(6)] public string Password { get; } public string? Captcha { get; init; } } public sealed record LoginResponse(string? Jwt, string? Error = null, ApiUser? Profile = null); public sealed record CreateApiKeyRequest(DateTime Expiry); }