Major update to auth for MAuth

This commit is contained in:
2025-07-29 22:15:48 -07:00
parent e60bf1fc79
commit f64d792e24
23 changed files with 107 additions and 905 deletions
@@ -1,279 +1,51 @@
using Microsoft.AspNetCore.Mvc;
using BoredCareers.Services;
using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities;
using System.Web.Http;
using System.Text.Json;
using System.Text;
namespace BoredCareers.Controllers {
[ApiController]
[Route("api/account/")]
public class AuthenticationController : MistoxControllerBase {
EmailService _emailContext;
public AuthenticationController(DatabaseService db) : base(db) { }
public AuthenticationController(DatabaseService db, EmailService emailContext) : base(db) {
_emailContext = emailContext;
}
[Route("login")]
[HttpPost]
public async Task<ActionResult<Account>> Login([FromForm] string UserName, [FromForm] string PasswordHash, [FromForm] bool StayLoggedIn) {
try {
Account? test = await _databaseService.GetAccount(UserName.ToLower());
if (test != null) {
if (test.EmailVerified == true) {
if (test.FailedPasswordLock) {
if (test.CurrentPasswordAttempts >= test.PasswordAttempts) {
return NotFound("Too many failed password attempts. Please reset your password");
}
}
if (BCrypt.Net.BCrypt.Verify(PasswordHash, test.PasswordHash)) {
test.CurrentPasswordAttempts = 0;
await _databaseService.SetAccount(test);
string jwt = BoredCareersJWT.GenereateJWTToken(test.ID, StayLoggedIn);
BoredCareersJWT.SignIn(Response, StayLoggedIn, jwt);
return Ok(test);
} else {
test.CurrentPasswordAttempts += 1;
await _databaseService.SetAccount(test);
return NotFound("Wrong Password");
}
} else {
await SendVerify(test.UserName);
return NotFound("A new verify email has been sent. \n Note only 1 email send every 5 mintes");
}
}
return NotFound("Account Not Found");
} catch (Exception ex) {
Console.WriteLine("Login Error: " + ex.Message);
return NotFound("An internal server error has occured");
[HttpPost("loginState")]
public ActionResult<Account> LoginState() {
if (isLoggedIn()) {
return Ok(getLoggedInUser());
}
return NotFound("Not logged in");
}
[Route("register")]
[HttpPost]
public async Task<ActionResult<Account>> Register([FromForm] string Email, [FromForm] string UserName, [FromForm] string PasswordHash) {
try {
if (await _databaseService.GetAccount(UserName.ToLower()) == null) {
if (await _databaseService.GetAccount(Email.ToLower()) == null) {
Account created = new Account() {
UserName = UserName.ToLower(),
Email = Email.ToLower(),
EmailVerified = false,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(PasswordHash),
};
await _databaseService.SetAccount(created);
Account? loadedAccount = await _databaseService.GetAccount(Email.ToLower());
if (loadedAccount != null) {
await SendVerify(loadedAccount.UserName);
return Ok(loadedAccount);
}
return NotFound("Unable to create the account");
} else {
return NotFound("Email is already in use");
}
} else {
return NotFound("UserName is taken");
}
} catch (Exception ex) {
Console.WriteLine("Register Error: " + ex.Message);
return NotFound("An internal server error has occured");
}
}
[Route("changepassword")]
[HttpPost]
public async Task<ActionResult> ChangePassword([FromForm] string OldPassword, [FromForm] string NewPassword) {
try {
if (isLoggedIn()) {
Account user = await getLoggedInUser();
if (BCrypt.Net.BCrypt.Verify(OldPassword, user.PasswordHash)) {
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(NewPassword);
user.CurrentPasswordAttempts = 0;
await _databaseService.SetAccount(user);
return Ok();
}
}
return NotFound("Not logged in");
} catch (Exception ex) {
Console.WriteLine("ChangePassword Error: " + ex.Message);
return NotFound("An internal server error has occured");
}
}
[Route("toggleaccountlock")]
[HttpPost]
public async Task<ActionResult<string>> ToggleAccountLock([FromForm] bool AccountLock) {
try {
if (isLoggedIn()) {
Account user = await getLoggedInUser();
user.FailedPasswordLock = AccountLock;
user.CurrentPasswordAttempts = 0;
await _databaseService.SetAccount(user);
[HttpPost("loginticket")]
public async Task<ActionResult> LoginTicket([FromBody] string LoginToken) {
using (HttpClient client = new HttpClient()) {
var payload = new { ticket = LoginToken };
StringContent jsonPayload = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
HttpResponseMessage JWTResponse = await client.PostAsync("https://auth.mistox.com/api/auth/token", jsonPayload);
if (JWTResponse.IsSuccessStatusCode) {
string JWT = await JWTResponse.Content.ReadAsStringAsync();
signIn(JWT);
return Ok();
} else {
string error = await JWTResponse.Content.ReadAsStringAsync();
return NotFound(error);
}
return NotFound("Not logged in");
} catch (Exception ex) {
Console.WriteLine("ToggleAccountLock Error: " + ex.Message);
return NotFound("An internal server error has occured");
}
}
[Route("get")]
[HttpPost]
public async Task<ActionResult<Account>> Get() {
try {
if (isLoggedIn()) {
return Ok(await getLoggedInUser());
}
return NotFound("Not logged in");
} catch (Exception ex) {
Console.WriteLine("Get Error: " + ex);
return NotFound("An internal server error has occured");
}
}
[Route("logout")]
[HttpPost]
[HttpGet("logout")]
public ActionResult Logout() {
if (isLoggedIn()) {
BoredCareersJWT.SignOut(Response);
return Ok();
signOut();
return Redirect("/");
}
return NotFound();
}
[Route("sendverifyemail")]
[HttpPost]
public async Task<ActionResult<string>> SendVerify([FromForm] string UserName) {
try {
string key = "v" + UserName;
// Stop from sending multiple emails quickly
if (_emailContext._SentEmails.ContainsKey(key)) {
DateTime PreviousSentTime = _emailContext._SentEmails.GetValueOrDefault(key);
if (PreviousSentTime.AddMinutes(5) > DateTime.Now) {
return NotFound("Cannot sent another verify email until 5 minutes has elapsed");
}
else {
_emailContext._SentEmails.Remove(key);
}
}
Account? test = await _databaseService.GetAccount(UserName.ToLower());
if (test != null) {
test.EmailToken = Guid.NewGuid().ToString();
await _databaseService.SetAccount(test);
string EmailContents = EmailService.VerifyEmailEmail;
EmailContents = Substitue(EmailContents, "@UserName", UserName);
EmailContents = Substitue(EmailContents, "@UserName", UserName);
EmailContents = Substitue(EmailContents, "@VerifyPassword", test.EmailToken);
string result = _emailContext.Send(test.Email, EmailService.VerifyEmailSubject, EmailContents);
_emailContext._SentEmails.Add(key, DateTime.Now);
return Ok(result);
}
return NotFound("Account not found");
} catch (Exception) {
return NotFound("An internal server error has occured");
}
}
[Route("verifyemail")]
[HttpPost]
public async Task<ActionResult<bool>> VerifyEmail([FromForm] string UserName, [FromForm] string EmailToken) {
try {
Account? test = await _databaseService.GetAccount(UserName.ToLower());
if (test != null) {
if (!string.IsNullOrEmpty(test.EmailToken) && test.EmailToken == EmailToken) {
test.EmailToken = "";
test.EmailVerified = true;
await _databaseService.SetAccount(test);
return Ok(true);
}
}
return NotFound("Account not found or token is invalid");;
} catch {
return NotFound("An internal server error has occured");
}
}
[Route("sendresetpassword")]
[HttpPost]
public async Task<ActionResult<string>> ResetPassword([FromForm] string Email) {
try {
string key = "p" + Email.ToLower();
// Stop from sending multiple emails quickly
if (_emailContext._SentEmails.ContainsKey(key)) {
DateTime PreviousSentTime = _emailContext._SentEmails.GetValueOrDefault(key);
if (PreviousSentTime.AddMinutes(5) > DateTime.Now) {
return NotFound("Cannot sent another reset requests until 5 minutes has elapsed");
}
else {
_emailContext._SentEmails.Remove(key);
}
}
Account? test = await _databaseService.GetAccount(Email.ToLower());
if (test != null) {
test.EmailToken = Guid.NewGuid().ToString();
await _databaseService.SetAccount(test);
string EmailContents = EmailService.ResetPasswordEmail;
EmailContents = Substitue(EmailContents, "@UserName", test.UserName);
EmailContents = Substitue(EmailContents, "@UserName", test.UserName);
EmailContents = Substitue(EmailContents, "@ResetPassWord", test.EmailToken);
string result = _emailContext.Send(test.Email, EmailService.VerifyEmailSubject, EmailContents);
_emailContext._SentEmails.Add(key, DateTime.Now);
return Ok(result);
}
return NotFound("Account Not Found");
} catch (Exception e) {
Console.WriteLine("EmailService Error: " + e.ToString());
return NotFound("An internal server error has occured");
}
}
[Route("resetpassword")]
[HttpPost]
public async Task<ActionResult<bool>> ResetPwdVerify([FromForm] string UserName, [FromForm] string NewPassword, [FromForm] string ResetToken) {
try {
Account? test = await _databaseService.GetAccount(UserName.ToLower());
if (test != null && !string.IsNullOrEmpty(test.EmailToken)) {
if (!string.IsNullOrEmpty(test.EmailToken) && test.EmailToken == ResetToken) {
test.CurrentPasswordAttempts = 0;
test.EmailToken = "";
test.PasswordHash = BCrypt.Net.BCrypt.HashPassword(NewPassword);
await _databaseService.SetAccount(test);
return Ok(true);
}
}
return NotFound("Account not found or reset token is bad");
} catch {
return NotFound("An internal server error has occured");
}
}
[Route("delete")]
[HttpPost]
public async Task<ActionResult> delete([FromForm] string Password) {
try {
if (isLoggedIn()) {
Account user = await getLoggedInUser();
if (BCrypt.Net.BCrypt.Verify(Password, user.PasswordHash)) {
await _databaseService.DeleteAccount(user.ID);
return Ok();
}
}
return NotFound("User is not logged in");
} catch (Exception ex) {
Console.WriteLine("Delete Error: " + ex.Message);
return NotFound("An internal server error has occured");
}
}
}
}
+22 -6
View File
@@ -13,6 +13,19 @@ namespace BoredCareers.Controllers {
_databaseService = databaseService;
}
public void signIn(string JWT) {
Response.Cookies.Append("mistox_session", JWT, new CookieOptions {
Secure = true,
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Expires = DateTime.UtcNow.AddDays(7)
});
}
public void signOut() {
Response.Cookies.Delete("mistox_session");
}
public bool isLoggedIn() {
if (User.Identity != null && User.Identity.IsAuthenticated) {
return true;
@@ -24,13 +37,16 @@ namespace BoredCareers.Controllers {
return Convert.ToInt32(User.FindFirstValue(ClaimTypes.NameIdentifier));
}
public async Task<Account> getLoggedInUser() {
public Account getLoggedInUser() {
try {
Account? test = await _databaseService.GetAccount(getLoggedInUserID());
if (test != null) {
return test;
}
return new Account();
Account building = new Account {
ID = Convert.ToInt32(User.FindFirstValue(ClaimTypes.NameIdentifier)),
UserName = User.FindFirstValue(ClaimTypes.Name)!.ToString(),
Email = User.FindFirstValue(ClaimTypes.Email)!.ToString(),
Role = User.FindFirstValue(ClaimTypes.Role)!.ToString(),
DataServer = User.FindFirstValue(ClaimTypes.UserData)!.ToString()
};
return building;
} catch {
return new Account();
}
+26 -19
View File
@@ -7,7 +7,7 @@ using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using System.Security.Cryptography;
var builder = WebApplication.CreateBuilder(args);
@@ -39,15 +39,6 @@ string dbPass = !string.IsNullOrEmpty(_dbpass) ? _dbpass : "oasv34$8gpv023dd";
DatabaseService databaseService = new DatabaseService(connectionString: "server=" + dbserver + ";user=" + dbUser + ";database=" + dbdatabase + ";password=" + dbPass + ";port=3306;");
builder.Services.Add( new ServiceDescriptor( typeof( DatabaseService ), databaseService ) );
////////////////////////////////
////////// Auth Service ////////
////////////////////////////////
// Address
string? _jwtSecret = Environment.GetEnvironmentVariable("JWTsecret");
string JWTsecret = !string.IsNullOrEmpty(_jwtSecret) ? _jwtSecret : "v0Ftluhdh7Nht8^2b5eaiC^IS^VS1ku0VBs3j*B2";
BoredCareersJWT.TokenSecretKey = JWTsecret;
////////////////////////////////
///////// Email Service ////////
////////////////////////////////
@@ -92,7 +83,26 @@ if (IPayment._PaymentType == PaymentType.StripeIntent) {
IPayment._EndpointSecret = string.IsNullOrEmpty(StripeEndpointKey) ? "" : StripeEndpointKey;
}
// Authentication Service
////////////////////////////////
/////// Auth Service ////////
////////////////////////////////
RsaSecurityKey? PublicKey = null;
using (HttpClient client = new HttpClient()) {
HttpResponseMessage PublicKeyResponse = await client.GetAsync("https://auth.mistox.com/api/auth/publickey");
if (PublicKeyResponse.IsSuccessStatusCode) {
string publicKey = await PublicKeyResponse.Content.ReadAsStringAsync();
RSA rsa = RSA.Create();
rsa.ImportFromPem(publicKey);
PublicKey = new RsaSecurityKey(rsa);
}
}
if (PublicKey == null) {
Console.WriteLine("Unable to load RSA PubKey Shutting Down");
Environment.Exit(100);
}
builder.Services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
@@ -102,14 +112,14 @@ builder.Services.AddAuthentication(options => {
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = BoredCareersJWT.TokenIssuer,
ValidAudience = BoredCareersJWT.TokenAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(BoredCareersJWT.TokenSecretKey)),
ValidIssuer = "https://auth.mistox.com",
ValidAudience = "mistox-llc-auth-token",
IssuerSigningKey = PublicKey,
ClockSkew = TimeSpan.FromMinutes(1)
};
options.Events = new JwtBearerEvents {
OnMessageReceived = context => {
context.Token = context.Request.Cookies[BoredCareersJWT.TokenName];
context.Token = context.Request.Cookies["mistox_session"];
return Task.CompletedTask;
},
OnTokenValidated = context => {
@@ -118,10 +128,7 @@ builder.Services.AddAuthentication(options => {
var exp = jwtToken.ValidTo;
var now = DateTime.UtcNow;
if ((exp - now) < TimeSpan.FromDays(3)) {
int accountID = Convert.ToInt32(context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value);
bool isPersistent = bool.Parse(context.Principal?.FindFirst(ClaimTypes.IsPersistent)?.Value);
var newJWT = BoredCareersJWT.GenereateJWTToken(accountID, isPersistent);
BoredCareersJWT.SignIn(context.HttpContext.Response, isPersistent, newJWT);
// Impliment token refresh
}
}
return Task.CompletedTask;
-57
View File
@@ -1,57 +0,0 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace BoredCareers.Services {
public class BoredCareersJWT {
public static string TokenAudience = "mistox-llc-auth-token";
public static string TokenIssuer = "https://auth.mistox.com";
public static string TokenSecretKey = "";
public static string TokenName = "mistox_session";
public static string GenereateJWTToken(int accountID, bool StayLoggedIn) {
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(TokenSecretKey);
var tokenDiscriptor = new SecurityTokenDescriptor {
Subject = new ClaimsIdentity([
new Claim(ClaimTypes.NameIdentifier, accountID.ToString()),
new Claim(ClaimTypes.IsPersistent, StayLoggedIn.ToString())
]),
Expires = DateTime.UtcNow.AddDays(7),
IssuedAt = DateTime.UtcNow,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256),
Audience = TokenAudience,
Issuer = TokenIssuer
};
var token = tokenHandler.CreateToken(tokenDiscriptor);
return tokenHandler.WriteToken(token);
}
public static void SignIn(HttpResponse Response, bool StayLoggedIn, string jwt) {
if (StayLoggedIn) {
// Stay logged in cookie
Response.Cookies.Append(TokenName, jwt, new CookieOptions {
Secure = true,
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Expires = DateTime.UtcNow.AddDays(7)
});
} else {
// Session cookie
Response.Cookies.Append(TokenName, jwt, new CookieOptions {
Secure = true,
HttpOnly = true,
SameSite = SameSiteMode.Strict,
});
}
}
public static void SignOut(HttpResponse Response) {
Response.Cookies.Delete(TokenName);
}
}
}