From f64d792e24485523ea98e9f6ad5e60ee2234c654 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Tue, 29 Jul 2025 22:15:48 -0700 Subject: [PATCH] Major update to auth for MAuth --- src/Client/src/app/app.html | 4 +- src/Client/src/app/app.routes.ts | 16 - src/Client/src/app/app.ts | 31 +- src/Client/src/app/models/Account.ts | 2 +- .../forgotpassword.component.html | 23 -- .../forgotpassword.component.ts | 55 ---- .../pages/account/login/login.component.html | 35 --- .../pages/account/login/login.component.ts | 55 ---- .../account/logout/logout.component.html | 0 .../pages/account/logout/logout.component.ts | 30 -- .../account/register/register.component.html | 35 --- .../account/register/register.component.ts | 80 ----- .../resetpassword.component.html | 29 -- .../resetpassword/resetpassword.component.ts | 69 ----- .../account/settings/settings.component.html | 0 .../account/settings/settings.component.ts | 29 -- .../verifyemail/verifyemail.component.html | 8 - .../verifyemail/verifyemail.component.ts | 54 ---- src/Client/src/app/services/Authentication.ts | 51 +--- .../Controllers/AuthenticationController.cs | 276 ++---------------- .../Controllers/MistoxControllerBase.cs | 28 +- src/Server/Program.cs | 45 +-- src/Server/Services/jwt.cs | 57 ---- 23 files changed, 107 insertions(+), 905 deletions(-) delete mode 100644 src/Client/src/app/pages/account/forgotpassword/forgotpassword.component.html delete mode 100644 src/Client/src/app/pages/account/forgotpassword/forgotpassword.component.ts delete mode 100644 src/Client/src/app/pages/account/login/login.component.html delete mode 100644 src/Client/src/app/pages/account/login/login.component.ts delete mode 100644 src/Client/src/app/pages/account/logout/logout.component.html delete mode 100644 src/Client/src/app/pages/account/logout/logout.component.ts delete mode 100644 src/Client/src/app/pages/account/register/register.component.html delete mode 100644 src/Client/src/app/pages/account/register/register.component.ts delete mode 100644 src/Client/src/app/pages/account/resetpassword/resetpassword.component.html delete mode 100644 src/Client/src/app/pages/account/resetpassword/resetpassword.component.ts delete mode 100644 src/Client/src/app/pages/account/settings/settings.component.html delete mode 100644 src/Client/src/app/pages/account/settings/settings.component.ts delete mode 100644 src/Client/src/app/pages/account/verifyemail/verifyemail.component.html delete mode 100644 src/Client/src/app/pages/account/verifyemail/verifyemail.component.ts delete mode 100644 src/Server/Services/jwt.cs diff --git a/src/Client/src/app/app.html b/src/Client/src/app/app.html index c142ed6..70d5048 100644 --- a/src/Client/src/app/app.html +++ b/src/Client/src/app/app.html @@ -9,10 +9,10 @@
- +
- +
diff --git a/src/Client/src/app/app.routes.ts b/src/Client/src/app/app.routes.ts index 9a9b176..1a884a2 100644 --- a/src/Client/src/app/app.routes.ts +++ b/src/Client/src/app/app.routes.ts @@ -1,12 +1,5 @@ import { Routes } from '@angular/router'; -import { ForgotPasswordComponent } from './pages/account/forgotpassword/forgotpassword.component'; -import { LoginComponent } from './pages/account/login/login.component'; -import { RegisterComponent } from './pages/account/register/register.component'; import { AboutComponent } from './pages/legal/about/about.component'; -import { SettingsComponent } from './pages/account/settings/settings.component'; -import { LogoutComponent } from './pages/account/logout/logout.component'; -import { ResetPasswordComponent } from './pages/account/resetpassword/resetpassword.component'; -import { VerifyEmailComponent } from './pages/account/verifyemail/verifyemail.component'; import { HomeComponent } from './pages/main/home/home.component'; import { ContactComponent } from './pages/legal/contact/contact.component'; import { PrivacyComponent } from './pages/legal/privacy/privacy.component'; @@ -30,15 +23,6 @@ export const routes: Routes = [ // Company { path: "company/connect", component: CompanyConnectComponent }, - // Account stuff - { path: "account/forgotpassword", component: ForgotPasswordComponent }, - { path: "account/resetpassword", component: ResetPasswordComponent }, - { path: "account/verifyemail", component: VerifyEmailComponent }, - { path: "account/login", component: LoginComponent }, - { path: "account/logout", component: LogoutComponent }, - { path: "account/register", component: RegisterComponent }, - { path: "account/settings", component: SettingsComponent }, - // Legal { path: "about", component: AboutComponent }, { path: "contact", component: ContactComponent }, diff --git a/src/Client/src/app/app.ts b/src/Client/src/app/app.ts index 596a131..bcba189 100644 --- a/src/Client/src/app/app.ts +++ b/src/Client/src/app/app.ts @@ -1,7 +1,8 @@ import { Component, ElementRef, ViewChild } from '@angular/core'; -import { Router, RouterModule, RouterOutlet } from '@angular/router'; +import { Router, RouterModule, RouterOutlet, ActivatedRoute } from '@angular/router'; import { Authentication } from './services/Authentication'; -import { CommonModule } from '@angular/common'; +import { CommonModule, Location } from '@angular/common'; +import { HttpClient } from '@angular/common/http'; @Component({ selector: 'app-root', @@ -15,7 +16,31 @@ export class App { @ViewChild('jobsLink') jobLink!: ElementRef; @ViewChild('resumesLink') resumeLink!: ElementRef; - constructor(public auth: Authentication, private router: Router){} + constructor( private http: HttpClient, public auth: Authentication, private router: Router, private route: ActivatedRoute, private location: Location){ + this.route.queryParams.subscribe(params => { + + const loginToken = params['LoginToken']; + console.log("LoginToken : " + loginToken); + + if (loginToken){ + this.http.post( "api/account/loginticket", JSON.stringify(loginToken), { headers: {'Content-Type': 'application/json'} } ).subscribe({ + next: data => { + auth.getLoginState(); + const pathWithoutQuery = this.location.path().split('?')[0]; + this.location.replaceState(pathWithoutQuery); + }, + error: err => { + auth.getLoginState(); + const pathWithoutQuery = this.location.path().split('?')[0]; + this.location.replaceState(pathWithoutQuery); + } + }) + }else{ + auth.getLoginState(); + } + + }); + } ngAfterViewInit(){ let ViewLinks = [ this.homeLink, this.resumeLink, this.jobLink ]; diff --git a/src/Client/src/app/models/Account.ts b/src/Client/src/app/models/Account.ts index 99c9acf..12f75e0 100644 --- a/src/Client/src/app/models/Account.ts +++ b/src/Client/src/app/models/Account.ts @@ -1,5 +1,5 @@ export class Account { - public id: number = 0; + public id: number = -1; public userName: string = ""; public email: string = ""; public emailVerified: boolean = false; diff --git a/src/Client/src/app/pages/account/forgotpassword/forgotpassword.component.html b/src/Client/src/app/pages/account/forgotpassword/forgotpassword.component.html deleted file mode 100644 index c0e6404..0000000 --- a/src/Client/src/app/pages/account/forgotpassword/forgotpassword.component.html +++ /dev/null @@ -1,23 +0,0 @@ -
-
- -

Forgot Password

- -
- - -
- -
-
-
- -
-
-
- -
    -
  • {{ msg }}
  • -
-
-
\ No newline at end of file diff --git a/src/Client/src/app/pages/account/forgotpassword/forgotpassword.component.ts b/src/Client/src/app/pages/account/forgotpassword/forgotpassword.component.ts deleted file mode 100644 index 1a546a8..0000000 --- a/src/Client/src/app/pages/account/forgotpassword/forgotpassword.component.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Component } from '@angular/core'; -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; -import { FormsModule } from '@angular/forms'; -import { Router, ActivatedRoute } from '@angular/router'; -import { Title } from '@angular/platform-browser'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'account-forgot', - templateUrl: './forgotpassword.component.html', - imports: [ FormsModule, CommonModule ] -}) -export class ForgotPasswordComponent { - email: string = ""; - errorMsgs: string[] = []; - returnURL: string = '/'; - - constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title) { - this.title.setTitle("Forgot Password | Mistox"); - this.route.queryParams.subscribe(params => { - this.returnURL = params['returnURL'] || '/'; - }); - } - - sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - onSubmit() { - // Clear errors - this.errorMsgs = []; - - // Send to server and wait for response - this.errorMsgs.push("Waiting for response from server"); - const body = new HttpParams() - .set("Email", this.email) - const headers = new HttpHeaders({ - 'Content-Type': 'application/x-www-form-urlencoded', - }); - this.http.post( "api/account/sendresetpassword", body, { headers, responseType: "text" } ).subscribe({ - next: async (data) => { - if (data.trim() == "Success"){ - this.errorMsgs = ["Reset-password sent"]; - await this.sleep(3000); - this.router.navigate([this.returnURL]); - }else{ - this.errorMsgs = [data]; - } - }, - error: err => { - console.log("HTTP Error Signing In: ", err); - } - }); - } -} \ No newline at end of file diff --git a/src/Client/src/app/pages/account/login/login.component.html b/src/Client/src/app/pages/account/login/login.component.html deleted file mode 100644 index 52431bb..0000000 --- a/src/Client/src/app/pages/account/login/login.component.html +++ /dev/null @@ -1,35 +0,0 @@ -
-
-

Login

- -
- - -
- -
- - -
- -
-
- -
- -
-
- Stay Logged In - -
- -
-
- -
    -
  • {{ msg }}
  • -
-
-
\ No newline at end of file diff --git a/src/Client/src/app/pages/account/login/login.component.ts b/src/Client/src/app/pages/account/login/login.component.ts deleted file mode 100644 index 751f5ed..0000000 --- a/src/Client/src/app/pages/account/login/login.component.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Component } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { FormsModule } from '@angular/forms'; -import { Router, ActivatedRoute } from '@angular/router'; -import { Title } from '@angular/platform-browser'; -import { CommonModule } from '@angular/common'; -import { Authentication, SessionType } from '../../../services/Authentication'; - -@Component({ - selector: 'account-login', - templateUrl: './login.component.html', - imports: [ FormsModule, CommonModule ], - standalone: true -}) -export class LoginComponent { - UserName: string = ""; - Password: string = ""; - StayLoggedIn: boolean = false; - errorMsgs: string[] = []; - returnURL: string = '/'; - - constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) { - this.title.setTitle("Login | Mistox"); - this.route.queryParams.subscribe(params => { - this.returnURL = params['returnURL'] || '/'; - }); - } - - onSubmit() { - this.errorMsgs = []; - - if (!this.UserName) { - this.errorMsgs.push("The 'username' field is required"); - } - if (!this.Password) { - this.errorMsgs.push("The 'password' field is required"); - } - if (this.Password.length < 6) { - this.errorMsgs.push("Password must be at least 6 Characters long"); - } - if (this.errorMsgs.length > 0) { - return; - } - - this.errorMsgs.push("Waiting for response from server"); - this.auth.Login(this.UserName, this.Password, this.StayLoggedIn).subscribe({ - next: data => { - this.router.navigate([this.returnURL]); - }, - error: err => { - this.errorMsgs = [ err.error ]; - } - }) - } -} \ No newline at end of file diff --git a/src/Client/src/app/pages/account/logout/logout.component.html b/src/Client/src/app/pages/account/logout/logout.component.html deleted file mode 100644 index e69de29..0000000 diff --git a/src/Client/src/app/pages/account/logout/logout.component.ts b/src/Client/src/app/pages/account/logout/logout.component.ts deleted file mode 100644 index 130ee39..0000000 --- a/src/Client/src/app/pages/account/logout/logout.component.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Component } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { FormsModule } from '@angular/forms'; -import { Router, ActivatedRoute } from '@angular/router'; -import { Title } from '@angular/platform-browser'; -import { CommonModule } from '@angular/common'; -import { Authentication } from '../../../services/Authentication'; - -@Component({ - selector: 'account-logout', - templateUrl: './logout.component.html', - imports: [ FormsModule, CommonModule ], - standalone: true -}) -export class LogoutComponent { - errorMsgs: string[] = []; - returnURL: string = '/'; - - constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) { - this.title.setTitle("Logout | Mistox"); - } - - ngAfterViewInit(){ - this.auth.Logout().subscribe({ - next: data => { - window.location.href = ""; - } - }); - } -} \ No newline at end of file diff --git a/src/Client/src/app/pages/account/register/register.component.html b/src/Client/src/app/pages/account/register/register.component.html deleted file mode 100644 index e324cff..0000000 --- a/src/Client/src/app/pages/account/register/register.component.html +++ /dev/null @@ -1,35 +0,0 @@ -
-
-

Register

- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
-
- -
-
- -
    -
  • {{ msg }}
  • -
-
-
\ No newline at end of file diff --git a/src/Client/src/app/pages/account/register/register.component.ts b/src/Client/src/app/pages/account/register/register.component.ts deleted file mode 100644 index 321c552..0000000 --- a/src/Client/src/app/pages/account/register/register.component.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Component } from '@angular/core'; -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; -import { FormsModule } from '@angular/forms'; -import { Router, ActivatedRoute } from '@angular/router'; -import { Account } from '../../../models/Account'; -import { Title } from '@angular/platform-browser'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'account-register', - templateUrl: './register.component.html', - imports: [ FormsModule, CommonModule ] -}) -export class RegisterComponent { - userName: string = "" - email: string = ""; - passwordHash: string = ""; - passwordHash2: string = ""; - error: string = ""; - - errorMsgs: string[] = []; - returnURL: string = '/'; - - constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) { - this.title.setTitle("Register | Mistox"); - this.route.queryParams.subscribe(params => { - this.returnURL = params['returnURL'] || '/'; - }); - } - - sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - onSubmit() { - // Clear errors - this.errorMsgs = []; - - // Validate data - const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; - if (!regex.test(this.email)){ - this.errorMsgs.push("A valid email is required"); - } - if (!this.userName) { - this.errorMsgs.push("The 'username' field is required"); - } - if (!this.passwordHash) { - this.errorMsgs.push("The 'password' field is required"); - } - if (this.passwordHash.length < 6) { - this.errorMsgs.push("Password must be at least 6 Characters long"); - } - if (this.passwordHash !== this.passwordHash2){ - this.errorMsgs.push("Passwords don't match"); - } - if (this.errorMsgs.length > 0) { - return; - } - - // Send to server and wait for response - this.errorMsgs.push("Waiting for response from server"); - const body = new HttpParams() - .set("Email", this.email) - .set("UserName", this.userName) - .set("PasswordHash", this.passwordHash); - const headers = new HttpHeaders({ - 'Content-Type': 'application/x-www-form-urlencoded' - }); - this.http.post( "api/account/register", body, { headers } ).subscribe({ - next: async (data) => { - this.errorMsgs = ["Account Created"]; - await this.sleep(3000); - this.router.navigate([this.returnURL]); - }, - error: err => { - this.errorMsgs = [ err.error ] - } - }); - } -} \ No newline at end of file diff --git a/src/Client/src/app/pages/account/resetpassword/resetpassword.component.html b/src/Client/src/app/pages/account/resetpassword/resetpassword.component.html deleted file mode 100644 index 3c3a393..0000000 --- a/src/Client/src/app/pages/account/resetpassword/resetpassword.component.html +++ /dev/null @@ -1,29 +0,0 @@ -
-
- -

Reset Password

-

User: {{ UserName }}

- -
- - -
- -
- - -
- -
-
-
- -
-
-
- -
    -
  • {{ msg }}
  • -
-
-
\ No newline at end of file diff --git a/src/Client/src/app/pages/account/resetpassword/resetpassword.component.ts b/src/Client/src/app/pages/account/resetpassword/resetpassword.component.ts deleted file mode 100644 index 7cb2467..0000000 --- a/src/Client/src/app/pages/account/resetpassword/resetpassword.component.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Component } from '@angular/core'; -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; -import { FormsModule } from '@angular/forms'; -import { Router, ActivatedRoute } from '@angular/router'; -import { Title } from '@angular/platform-browser'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'account-reset', - templateUrl: './resetpassword.component.html', - imports: [ FormsModule, CommonModule ] -}) -export class ResetPasswordComponent { - - UserName: string = ""; - ResetPwd: string = ""; - Password: string = ""; - PassworR: string = ""; - - errorMsgs: string[] = []; - - constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) { - this.title.setTitle("Reset Password | Mistox"); - this.route.queryParams.subscribe(params => { - this.UserName = params['UserName'] || ''; - this.ResetPwd = params['ResetPwd'] || ''; - }); - } - - sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - onSubmit() { - if (this.Password != this.PassworR){ - this.errorMsgs.push("Passwords must match"); - } - if (this.Password.length < 6){ - this.errorMsgs.push("Password must be at least 6 Characters long"); - } - if (this.errorMsgs.length == 0){ - // Send to server and wait for response - this.errorMsgs.push("Waiting for response from server"); - const body = new HttpParams() - .set("UserName", this.UserName) - .set("NewPassword", this.Password) - .set("ResetToken", this.ResetPwd); - const headers = new HttpHeaders({ - 'Content-Type': 'application/x-www-form-urlencoded' - }); - this.http.post( "api/account/resetpassword", body, { headers } ).subscribe({ - next: async (data) => { - if (data == true){ - this.errorMsgs = ["Password reset successfully"]; - await this.sleep(3000); - this.router.navigate(["/account/login"]); - }else{ - this.errorMsgs = ["An error has ocurred"]; - await this.sleep(3000); - this.router.navigate(["/account/sendresetpassword"]); - } - }, - error: err => { - console.log("HTTP Error Signing In: ", err); - } - }); - } - } -} \ No newline at end of file diff --git a/src/Client/src/app/pages/account/settings/settings.component.html b/src/Client/src/app/pages/account/settings/settings.component.html deleted file mode 100644 index e69de29..0000000 diff --git a/src/Client/src/app/pages/account/settings/settings.component.ts b/src/Client/src/app/pages/account/settings/settings.component.ts deleted file mode 100644 index de3b951..0000000 --- a/src/Client/src/app/pages/account/settings/settings.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Component } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { FormsModule } from '@angular/forms'; -import { Router, ActivatedRoute } from '@angular/router'; -import { Account } from '../../../models/Account'; -import { Title } from '@angular/platform-browser'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'account-settings', - templateUrl: './settings.component.html', - imports: [ FormsModule, CommonModule ] -}) -export class SettingsComponent { - user!: Account; - errorMsgs: string[] = []; - returnURL: string = '/'; - - constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) { - this.title.setTitle("Settings | Mistox"); - this.route.queryParams.subscribe(params => { - this.returnURL = params['returnURL'] || '/'; - }); - } - - onSubmit() { - - } -} \ No newline at end of file diff --git a/src/Client/src/app/pages/account/verifyemail/verifyemail.component.html b/src/Client/src/app/pages/account/verifyemail/verifyemail.component.html deleted file mode 100644 index a8a8ddc..0000000 --- a/src/Client/src/app/pages/account/verifyemail/verifyemail.component.html +++ /dev/null @@ -1,8 +0,0 @@ -
-
- -

Verifying Email

-

{{ Result }}

- -
-
\ No newline at end of file diff --git a/src/Client/src/app/pages/account/verifyemail/verifyemail.component.ts b/src/Client/src/app/pages/account/verifyemail/verifyemail.component.ts deleted file mode 100644 index 044a57c..0000000 --- a/src/Client/src/app/pages/account/verifyemail/verifyemail.component.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Component } from '@angular/core'; -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; -import { FormsModule } from '@angular/forms'; -import { Router, ActivatedRoute } from '@angular/router'; -import { Title } from '@angular/platform-browser'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'account-verifyemail', - templateUrl: './verifyemail.component.html', - imports: [ FormsModule, CommonModule ] -}) -export class VerifyEmailComponent { - - UserName: string = ""; - Guid: string = ""; - Result: string = ""; - - constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) { - this.title.setTitle("Verify Email | Mistox"); - this.route.queryParams.subscribe(params => { - this.UserName = params['UserName'] || ''; - this.Guid = params['Guid'] || ''; - }); - } - - sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - async onSubmit() { - // Send to server and wait for response - const body = new HttpParams() - .set("UserName", this.UserName) - .set("EmailToken", this.Guid); - const headers = new HttpHeaders({ - 'Content-Type': 'application/x-www-form-urlencoded' - }); - this.http.post( "api/account/verifyemail", body, { headers } ).subscribe({ - next: async (data) => { - if (data == true){ - this.Result = "Verified Email Successfully"; - }else{ - this.Result = "Email was not able to be verified please resend email"; - } - await this.sleep(3000); - this.router.navigate(["/"]); - }, - error: err => { - console.log("HTTP Error Signing In: ", err); - } - }); - } -} \ No newline at end of file diff --git a/src/Client/src/app/services/Authentication.ts b/src/Client/src/app/services/Authentication.ts index 60803ba..f653b1b 100644 --- a/src/Client/src/app/services/Authentication.ts +++ b/src/Client/src/app/services/Authentication.ts @@ -6,30 +6,20 @@ import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http"; @Injectable({ providedIn: 'root' }) export class Authentication{ - private _user = new BehaviorSubject(this.getUserFromStorage()); + private _user = new BehaviorSubject( new Account ); user$ = this._user.asObservable(); constructor( private http: HttpClient){ } - Login(UserName: string, Password: string, StayLoggedIn: boolean): Observable { - - const body = new HttpParams() - .set("UserName", UserName) - .set("PasswordHash", Password) - .set("StayLoggedIn", StayLoggedIn ); - const headers = new HttpHeaders({ - 'Content-Type': 'application/x-www-form-urlencoded' - }); - - let sub = this.http.post( "api/account/login", body, { headers } ); + getLoginState(): Observable { + let sub = this.http.post( "api/account/loginState", {}, {} ); sub.subscribe({ next: data => { data.passwordHash = ""; this._user.next(data); - this.setUserToStorage(data, StayLoggedIn == true ? SessionType.Forever : SessionType.Session); }, error: err => { - console.log("HTTP Error Signing In: ", err.error); + console.log("No login state found: ", err.error); } }); return sub; @@ -37,7 +27,6 @@ export class Authentication{ Logout(){ this._user.next( new Account ); - this.delUserFromStorage(); return this.http.post( "api/account/logout", {}, { responseType: 'json' } ); } @@ -48,36 +37,4 @@ export class Authentication{ get loggedInUser(): Account { return this._user.value; } - - private getUserFromStorage(): Account { - const foreverUser = localStorage.getItem('user'); - const sessionUser = sessionStorage.getItem('user'); - let user = null; - if (foreverUser != null){ - user = JSON.parse(foreverUser) - } else if (sessionUser != null){ - user = JSON.parse(sessionUser) - } else { - user = new Account(); - user.id = -1; - } - return user; - } - private setUserToStorage(user: Account, session: SessionType): void { - if (session == SessionType.Forever){ - localStorage.setItem('user', JSON.stringify(user)); - }else if(session == SessionType.Session){ - sessionStorage.setItem('user', JSON.stringify(user)); - } - } - private delUserFromStorage(): void { - localStorage.removeItem('user'); - sessionStorage.removeItem('user'); - } - -} - -export enum SessionType { - Forever, - Session } \ No newline at end of file diff --git a/src/Server/Controllers/AuthenticationController.cs b/src/Server/Controllers/AuthenticationController.cs index 81fcf06..2a55b40 100755 --- a/src/Server/Controllers/AuthenticationController.cs +++ b/src/Server/Controllers/AuthenticationController.cs @@ -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> 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 LoginState() { + if (isLoggedIn()) { + return Ok(getLoggedInUser()); } + return NotFound("Not logged in"); } - [Route("register")] - [HttpPost] - public async Task> 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 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> 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 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> 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> 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> 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> 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> 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 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"); - } - } - } + } diff --git a/src/Server/Controllers/MistoxControllerBase.cs b/src/Server/Controllers/MistoxControllerBase.cs index eb502ca..9ea558b 100644 --- a/src/Server/Controllers/MistoxControllerBase.cs +++ b/src/Server/Controllers/MistoxControllerBase.cs @@ -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 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(); } diff --git a/src/Server/Program.cs b/src/Server/Program.cs index d5beb5c..1ec73c9 100755 --- a/src/Server/Program.cs +++ b/src/Server/Program.cs @@ -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; diff --git a/src/Server/Services/jwt.cs b/src/Server/Services/jwt.cs deleted file mode 100644 index 21dcf33..0000000 --- a/src/Server/Services/jwt.cs +++ /dev/null @@ -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); - } - } -} \ No newline at end of file -- 2.52.0