Major update to auth for MAuth
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user