From 7faef2e97643a99380325b9606f1067856ad2af8 Mon Sep 17 00:00:00 2001 From: Zeeshaun Date: Sat, 11 Jul 2026 12:39:02 -0500 Subject: [PATCH] Harden public account registration --- .../AuthApi/Controllers/AuthController.cs | 33 +++++++++++++++---- microservices/AuthApi/DOCUMENTS.md | 7 ++-- microservices/AuthApi/README.md | 2 +- microservices/AuthApi/Services/UserService.cs | 4 +-- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/microservices/AuthApi/Controllers/AuthController.cs b/microservices/AuthApi/Controllers/AuthController.cs index d91fd07..3f94fac 100644 --- a/microservices/AuthApi/Controllers/AuthController.cs +++ b/microservices/AuthApi/Controllers/AuthController.cs @@ -3,9 +3,12 @@ using AuthApi.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; +using MongoDB.Driver; using System.IdentityModel.Tokens.Jwt; +using System.Net.Mail; using System.Security.Claims; using System.Text; +using System.Text.RegularExpressions; namespace AuthApi.Controllers; @@ -25,15 +28,31 @@ public class AuthController : ControllerBase [HttpPost("register")] public async Task Register([FromBody] RegisterRequest req) { - if (string.IsNullOrWhiteSpace(req.Username) || string.IsNullOrWhiteSpace(req.Password)) - return BadRequest("Username and password required"); + var username = req.Username?.Trim() ?? ""; + var password = req.Password ?? ""; + var email = req.Email?.Trim() ?? ""; - if (await _users.GetByUsernameAsync(req.Username) != null) - return BadRequest("User already exists"); + if (!Regex.IsMatch(username, "^[A-Za-z0-9_]{3,24}$")) + return BadRequest("Username must be 3-24 characters using only letters, numbers, or underscores"); + if (password.Length < 8) + return BadRequest("Password must be at least 8 characters"); + var emailDomain = email.Split('@').LastOrDefault() ?? ""; + if (string.IsNullOrWhiteSpace(email) || !MailAddress.TryCreate(email, out _) || !emailDomain.Contains('.')) + return BadRequest("A valid email address is required"); - var hash = BCrypt.Net.BCrypt.HashPassword(req.Password); - var user = new User { Username = req.Username, PasswordHash = hash, Role = "USER", Email = req.Email }; - await _users.CreateAsync(user); + if (await _users.GetByUsernameAsync(username) != null) + return Conflict("Username already exists"); + + var hash = BCrypt.Net.BCrypt.HashPassword(password); + var user = new User { Username = username, PasswordHash = hash, Role = "USER", Email = email }; + try + { + await _users.CreateAsync(user); + } + catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + return Conflict("Username already exists"); + } return Ok("User created"); } diff --git a/microservices/AuthApi/DOCUMENTS.md b/microservices/AuthApi/DOCUMENTS.md index 7aa9a40..7dbf715 100644 --- a/microservices/AuthApi/DOCUMENTS.md +++ b/microservices/AuthApi/DOCUMENTS.md @@ -9,9 +9,12 @@ Inbound JSON documents { "username": "string", "password": "string", - "email": "string (optional)" + "email": "string" } ``` + Usernames must be 3-24 characters containing only letters, numbers, or + underscores. Passwords must contain at least 8 characters, and email is + required with a valid address format. - LoginRequest (`POST /api/auth/login`) ```json { @@ -42,7 +45,7 @@ Stored documents (MongoDB) "username": "string", "passwordHash": "string", "role": "USER | SUPER", - "email": "string (optional)", + "email": "string", "refreshToken": "string (optional)", "refreshTokenExpiry": "string (optional, ISO-8601 datetime)" } diff --git a/microservices/AuthApi/README.md b/microservices/AuthApi/README.md index 68cb79b..e880a0c 100644 --- a/microservices/AuthApi/README.md +++ b/microservices/AuthApi/README.md @@ -4,7 +4,7 @@ See `DOCUMENTS.md` for request payloads and stored document shapes. ## Endpoints -- `POST /api/auth/register` Register a new user. +- `POST /api/auth/register` Register a USER account with a username, email, and password. - `POST /api/auth/login` Issue access and refresh tokens. - `POST /api/auth/refresh` Refresh an access token. - `POST /api/auth/logout` Revoke the current access token. diff --git a/microservices/AuthApi/Services/UserService.cs b/microservices/AuthApi/Services/UserService.cs index 9bda631..9bfd504 100644 --- a/microservices/AuthApi/Services/UserService.cs +++ b/microservices/AuthApi/Services/UserService.cs @@ -19,8 +19,8 @@ public class UserService _col.Indexes.CreateOne(new CreateIndexModel(keys, new CreateIndexOptions { Unique = true })); } - public Task GetByUsernameAsync(string username) => - _col.Find(u => u.Username == username).FirstOrDefaultAsync(); + public async Task GetByUsernameAsync(string username) => + await _col.Find(u => u.Username == username).FirstOrDefaultAsync(); public Task CreateAsync(User user) => _col.InsertOneAsync(user);