From a9763f9109cd77dc5abdaf5f90edc208fbb7d42b Mon Sep 17 00:00:00 2001 From: Blair Vanderlugt Date: Tue, 8 Jun 2021 17:59:01 -0700 Subject: [PATCH 1/3] auth flow fixes --- src/app/_services/auth.service.ts | 180 +++++++++++-------- src/app/_services/user.service.ts | 8 +- src/app/auth/auth.component.ts | 8 +- src/app/pages/accounts/accounts.component.ts | 15 +- 4 files changed, 117 insertions(+), 94 deletions(-) diff --git a/src/app/_services/auth.service.ts b/src/app/_services/auth.service.ts index d29bc43..7256c5d 100644 --- a/src/app/_services/auth.service.ts +++ b/src/app/_services/auth.service.ts @@ -12,7 +12,7 @@ import { HttpError, rejectBody } from '@app/_helpers/global-error-handler'; providedIn: 'root', }) export class AuthService { - sessionToken: any; + //sessionToken: any; mutableKeyStore: MutableKeyStore; constructor( @@ -26,40 +26,47 @@ export class AuthService { async init(): Promise { await this.mutableKeyStore.loadKeyring(); // TODO setting these together should be atomic - if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) { - this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN')); - } + //if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) { + // this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN')); + //} if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) { await this.mutableKeyStore.importPrivateKey(localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))); } } + + getSessionToken(): string { + return sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN')); + } + + setSessionToken(token): void { + console.log('Setting sessiong token! ', token) + sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), token); + } setState(s): void { document.getElementById('state').innerHTML = s; } getWithToken(): Promise { - return new Promise((resolve, reject) => { const headers = { - Authorization: 'Bearer ' + this.sessionToken, + Authorization: 'Bearer ' + this.getSessionToken, 'Content-Type': 'application/json;charset=utf-8', 'x-cic-automerge': 'none', }; const options = { headers, }; - fetch(environment.cicMetaUrl, options).then((response) => { - if (response.status === 401) { - return reject(rejectBody(response)); + return fetch(environment.cicMetaUrl, options).then((response) => { + if (!response.ok) { + console.log("failed to getWithToken...maybe try clearing the token and try again?") + return false; } - return resolve(true); + return true; }); - }); } // TODO rename to send signed challenge and set session. Also separate these responsibilities - sendResponse(hobaResponseEncoded: any): Promise { - return new Promise((resolve, reject) => { + sendSignedChallenge(hobaResponseEncoded: any): Promise { const headers = { Authorization: 'HOBA ' + hobaResponseEncoded, 'Content-Type': 'application/json;charset=utf-8', @@ -68,85 +75,105 @@ export class AuthService { const options = { headers, }; - fetch(environment.cicMetaUrl, options).then((response) => { - if (response.status === 401) { - return reject(rejectBody(response)); - } - this.sessionToken = response.headers.get('Token'); - sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken); - this.setState('Click button to log in'); - return resolve(true); - }); - }); + return fetch(environment.cicMetaUrl, options) } getChallenge(): Promise { - return new Promise((resolve, reject) => { - fetch(environment.cicMetaUrl).then(async (response) => { - if (response.status === 401) { - const authHeader: string = response.headers.get('WWW-Authenticate'); - return resolve(hobaParseChallengeHeader(authHeader)); - } - if (!response.ok) { - return reject(rejectBody(response)); - } - }); - }); + return fetch(environment.cicMetaUrl) + .then(response => { + if (response.status === 401) { + const authHeader: string = response.headers.get('WWW-Authenticate'); + return hobaParseChallengeHeader(authHeader); + } + }); } async login(): Promise { - if (this.sessionToken !== undefined) { - try { - const response: boolean = await this.getWithToken(); - return response === true; - } catch (e) { - this.loggingService.sendErrorLevelMessage('Login token failed', this, { error: e }); - } + if (this.getSessionToken()) { + sessionStorage.removeItem(btoa('CICADA_SESSION_TOKEN')); + //try { + // // TODO do we need to do this? is it just a test of the token? + // const response: boolean = await this.getWithToken(); + // return response + //} catch (e) { + // this.loggingService.sendErrorLevelMessage('Login token failed', this, { error: e }); + //} } else { try { const o = await this.getChallenge(); - const response: boolean = await this.loginResponse(o); - return response === true; + + const r = await signChallenge( + o.challenge, + o.realm, + environment.cicMetaUrl, + this.mutableKeyStore + ); + + const tokenResponse = await this.sendSignedChallenge(r) + .then(response => { + const token = response.headers.get('Token') + if (token) { + return token + } + if (response.status === 401) { + this.errorDialogService.openDialog({ + message: 'You are not authorized to use this system', + }); + return + } + if (response.status === 403) { + console.log('Getting back a 403 but I think the server should send 200') + //this.errorDialogService.openDialog({ + // message: 'You are not authorized to use this system', + //}); + // return + } + if (!response.ok) { + console.log("Failed to get a login token with signed challenge 😭", response.statusText) + return + } + }) + + if (tokenResponse) { + this.setSessionToken(tokenResponse); + this.setState('Click button to log in'); + return true + } + return false } catch (e) { this.loggingService.sendErrorLevelMessage('Login challenge failed', this, { error: e }); } } - return false; } async loginResponse(o: { challenge: string; realm: any }): Promise { - return new Promise(async (resolve, reject) => { - try { - const r = await signChallenge( - o.challenge, - o.realm, - environment.cicMetaUrl, - this.mutableKeyStore - ); - const response: boolean = await this.sendResponse(r); - resolve(response); - } catch (error) { - if (error instanceof HttpError) { - if (error.status === 403) { - this.errorDialogService.openDialog({ - message: 'You are not authorized to use this system', - }); - } else if (error.status === 401) { - this.errorDialogService.openDialog({ - message: - 'Unable to authenticate with the service. ' + - 'Please speak with the staff at Grassroots ' + - 'Economics for requesting access ' + - 'staff@grassrootseconomics.net.', - }); - } - } else { - // TODO define this error - this.errorDialogService.openDialog({ message: 'Incorrect key passphrase.' }); - } - resolve(false); - } - }); + const r = await signChallenge( + o.challenge, + o.realm, + environment.cicMetaUrl, + this.mutableKeyStore + ); + + return this.sendSignedChallenge(r); + // if (error instanceof HttpError) { + // if (error.status === 403) { + // this.errorDialogService.openDialog({ + // message: 'You are not authorized to use this system', + // }); + // } else if (error.status === 401) { + // this.errorDialogService.openDialog({ + // message: + // 'Unable to authenticate with the service. ' + + // 'Please speak with the staff at Grassroots ' + + // 'Economics for requesting access ' + + // 'staff@grassrootseconomics.net.', + // }); + // } + // } else { + // // TODO define this error + // this.errorDialogService.openDialog({ message: 'Incorrect key passphrase.' }); + // } + // resolve(false); } loginView(): void { @@ -186,7 +213,6 @@ export class AuthService { logout(): void { sessionStorage.removeItem(btoa('CICADA_SESSION_TOKEN')); localStorage.removeItem(btoa('CICADA_PRIVATE_KEY')); - this.sessionToken = undefined; window.location.reload(); } diff --git a/src/app/_services/user.service.ts b/src/app/_services/user.service.ts index 2ee24ca..721b120 100644 --- a/src/app/_services/user.service.ts +++ b/src/app/_services/user.service.ts @@ -42,10 +42,10 @@ export class UserService { private registryService: RegistryService, private authService: AuthService ) { - this.authService.init().then(() => { - this.keystore = authService.mutableKeyStore; - this.signer = new PGPSigner(this.keystore); - }); + //this.authService.init().then(() => { + // this.keystore = authService.mutableKeyStore; + // this.signer = new PGPSigner(this.keystore); + //}); this.registry = registryService.getRegistry(); this.registry.load(); } diff --git a/src/app/auth/auth.component.ts b/src/app/auth/auth.component.ts index f9cab02..7c33f4c 100644 --- a/src/app/auth/auth.component.ts +++ b/src/app/auth/auth.component.ts @@ -26,7 +26,7 @@ export class AuthComponent implements OnInit { this.keyForm = this.formBuilder.group({ key: ['', Validators.required], }); - await this.authService.init(); + //await this.authService.init(); // if (this.authService.privateKey !== undefined) { // const setKey = await this.authService.setKey(this.authService.privateKey); // } @@ -49,19 +49,19 @@ export class AuthComponent implements OnInit { this.loading = false; } - login(): void { + async login(): Promise { // TODO check if we have privatekey // Send us to home if we have a private key // talk to meta somehow // in the error interceptor if 401/403 handle it // if 200 go /home - if (this.authService.getPrivateKey()) { + const loginResult = await this.authService.login() + if (loginResult) { this.router.navigate(['/home']); } } switchWindows(): void { - this.authService.sessionToken = undefined; const divOne: HTMLElement = document.getElementById('one'); const divTwo: HTMLElement = document.getElementById('two'); this.toggleDisplay(divOne); diff --git a/src/app/pages/accounts/accounts.component.ts b/src/app/pages/accounts/accounts.component.ts index 853997a..0433ba6 100644 --- a/src/app/pages/accounts/accounts.component.ts +++ b/src/app/pages/accounts/accounts.component.ts @@ -33,21 +33,18 @@ export class AccountsComponent implements OnInit { private loggingService: LoggingService, private router: Router ) { - (async () => { - try { - // TODO it feels like this should be in the onInit handler + } + + async ngOnInit(): Promise { + try { // TODO it feels like this should be in the onInit handler await this.userService.loadAccounts(100); - } catch (error) { + } catch (error) { this.loggingService.sendErrorLevelMessage('Failed to load accounts', this, { error }); - } - })(); + } this.userService .getAccountTypes() .pipe(first()) .subscribe((res) => (this.accountTypes = res)); - } - - ngOnInit(): void { this.userService.accountsSubject.subscribe((accounts) => { this.dataSource = new MatTableDataSource(accounts); this.dataSource.paginator = this.paginator; From 849f60307e98f9bcaf18355ce8fe250c83df5ebb Mon Sep 17 00:00:00 2001 From: Blair Vanderlugt Date: Wed, 9 Jun 2021 15:16:55 -0700 Subject: [PATCH 2/3] log error --- src/app/_services/auth.service.ts | 68 +++++++++++++------------------ 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/src/app/_services/auth.service.ts b/src/app/_services/auth.service.ts index 5c02f96..c7771d9 100644 --- a/src/app/_services/auth.service.ts +++ b/src/app/_services/auth.service.ts @@ -92,6 +92,7 @@ export class AuthService { const authHeader: string = response.headers.get('WWW-Authenticate'); return hobaParseChallengeHeader(authHeader); } + console.log('DEBUG: expected a 401 and www-authenticate header!') }); } @@ -106,7 +107,6 @@ export class AuthService { // this.loggingService.sendErrorLevelMessage('Login token failed', this, { error: e }); //} } else { - try { const o = await this.getChallenge(); const r = await signChallenge( @@ -128,13 +128,6 @@ export class AuthService { }); return } - if (response.status === 403) { - console.log('Getting back a 403 but I think the server should send 200') - //this.errorDialogService.openDialog({ - // message: 'You are not authorized to use this system', - //}); - // return - } if (!response.ok) { console.log("Failed to get a login token with signed challenge 😭", response.statusText) return @@ -147,41 +140,38 @@ export class AuthService { return true } return false - } catch (e) { - this.loggingService.sendErrorLevelMessage('Login challenge failed', this, { error: e }); - } } } - async loginResponse(o: { challenge: string; realm: any }): Promise { - const r = await signChallenge( - o.challenge, - o.realm, - environment.cicMetaUrl, - this.mutableKeyStore - ); + //async loginResponse(o: { challenge: string; realm: any }): Promise { + // const r = await signChallenge( + // o.challenge, + // o.realm, + // environment.cicMetaUrl, + // this.mutableKeyStore + // ); - return this.sendSignedChallenge(r); - // if (error instanceof HttpError) { - // if (error.status === 403) { - // this.errorDialogService.openDialog({ - // message: 'You are not authorized to use this system', - // }); - // } else if (error.status === 401) { - // this.errorDialogService.openDialog({ - // message: - // 'Unable to authenticate with the service. ' + - // 'Please speak with the staff at Grassroots ' + - // 'Economics for requesting access ' + - // 'staff@grassrootseconomics.net.', - // }); - // } - // } else { - // // TODO define this error - // this.errorDialogService.openDialog({ message: 'Incorrect key passphrase.' }); - // } - // resolve(false); - } + // return this.sendSignedChallenge(r); + // // if (error instanceof HttpError) { + // // if (error.status === 403) { + // // this.errorDialogService.openDialog({ + // // message: 'You are not authorized to use this system', + // // }); + // // } else if (error.status === 401) { + // // this.errorDialogService.openDialog({ + // // message: + // // 'Unable to authenticate with the service. ' + + // // 'Please speak with the staff at Grassroots ' + + // // 'Economics for requesting access ' + + // // 'staff@grassrootseconomics.net.', + // // }); + // // } + // // } else { + // // // TODO define this error + // // this.errorDialogService.openDialog({ message: 'Incorrect key passphrase.' }); + // // } + // // resolve(false); + //} loginView(): void { document.getElementById('one').style.display = 'none'; From 90c0836eeeb2e442876ad9fce91e4a1348d42584 Mon Sep 17 00:00:00 2001 From: Blair Vanderlugt Date: Wed, 9 Jun 2021 17:07:25 -0700 Subject: [PATCH 3/3] auth component logging and error handling --- src/app/_services/auth.service.ts | 63 +++++-------------------------- src/app/auth/auth.component.ts | 27 +++++++------ 2 files changed, 23 insertions(+), 67 deletions(-) diff --git a/src/app/_services/auth.service.ts b/src/app/_services/auth.service.ts index c7771d9..937f2ce 100644 --- a/src/app/_services/auth.service.ts +++ b/src/app/_services/auth.service.ts @@ -14,7 +14,6 @@ import { BehaviorSubject, Observable } from 'rxjs'; providedIn: 'root', }) export class AuthService { - //sessionToken: any; mutableKeyStore: MutableKeyStore; trustedUsers: Array = []; private trustedUsersList: BehaviorSubject> = new BehaviorSubject>( @@ -32,22 +31,17 @@ export class AuthService { async init(): Promise { await this.mutableKeyStore.loadKeyring(); - // TODO setting these together should be atomic - //if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) { - // this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN')); - //} if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) { await this.mutableKeyStore.importPrivateKey(localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))); } } getSessionToken(): string { - return sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN')); + return sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN')); } setSessionToken(token): void { - console.log('Setting sessiong token! ', token) - sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), token); + sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), token); } setState(s): void { @@ -65,7 +59,10 @@ export class AuthService { }; return fetch(environment.cicMetaUrl, options).then((response) => { if (!response.ok) { - console.log("failed to getWithToken...maybe try clearing the token and try again?") + this.loggingService.sendErrorLevelMessage('failed to get with auth token.', + this, + { error: "" }); + return false; } return true; @@ -92,20 +89,12 @@ export class AuthService { const authHeader: string = response.headers.get('WWW-Authenticate'); return hobaParseChallengeHeader(authHeader); } - console.log('DEBUG: expected a 401 and www-authenticate header!') }); } async login(): Promise { if (this.getSessionToken()) { sessionStorage.removeItem(btoa('CICADA_SESSION_TOKEN')); - //try { - // // TODO do we need to do this? is it just a test of the token? - // const response: boolean = await this.getWithToken(); - // return response - //} catch (e) { - // this.loggingService.sendErrorLevelMessage('Login token failed', this, { error: e }); - //} } else { const o = await this.getChallenge(); @@ -123,14 +112,12 @@ export class AuthService { return token } if (response.status === 401) { - this.errorDialogService.openDialog({ - message: 'You are not authorized to use this system', - }); - return + let e = new HttpError("You are not authorized to use this system", response.status) + throw e } if (!response.ok) { - console.log("Failed to get a login token with signed challenge 😭", response.statusText) - return + let e = new HttpError("Unknown error from authentication server", response.status) + throw e } }) @@ -143,36 +130,6 @@ export class AuthService { } } - //async loginResponse(o: { challenge: string; realm: any }): Promise { - // const r = await signChallenge( - // o.challenge, - // o.realm, - // environment.cicMetaUrl, - // this.mutableKeyStore - // ); - - // return this.sendSignedChallenge(r); - // // if (error instanceof HttpError) { - // // if (error.status === 403) { - // // this.errorDialogService.openDialog({ - // // message: 'You are not authorized to use this system', - // // }); - // // } else if (error.status === 401) { - // // this.errorDialogService.openDialog({ - // // message: - // // 'Unable to authenticate with the service. ' + - // // 'Please speak with the staff at Grassroots ' + - // // 'Economics for requesting access ' + - // // 'staff@grassrootseconomics.net.', - // // }); - // // } - // // } else { - // // // TODO define this error - // // this.errorDialogService.openDialog({ message: 'Incorrect key passphrase.' }); - // // } - // // resolve(false); - //} - loginView(): void { document.getElementById('one').style.display = 'none'; document.getElementById('two').style.display = 'block'; diff --git a/src/app/auth/auth.component.ts b/src/app/auth/auth.component.ts index 7c33f4c..328997f 100644 --- a/src/app/auth/auth.component.ts +++ b/src/app/auth/auth.component.ts @@ -2,6 +2,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { CustomErrorStateMatcher } from '@app/_helpers'; import { AuthService } from '@app/_services'; +import { ErrorDialogService } from '@app/_services/error-dialog.service'; +import { LoggingService } from '@app/_services/logging.service'; import { Router } from '@angular/router'; @Component({ @@ -19,18 +21,14 @@ export class AuthComponent implements OnInit { constructor( private authService: AuthService, private formBuilder: FormBuilder, - private router: Router + private router: Router, + private errorDialogService: ErrorDialogService, ) {} async ngOnInit(): Promise { this.keyForm = this.formBuilder.group({ key: ['', Validators.required], }); - //await this.authService.init(); - // if (this.authService.privateKey !== undefined) { - // const setKey = await this.authService.setKey(this.authService.privateKey); - // } - // } } get keyFormStub(): any { @@ -50,14 +48,15 @@ export class AuthComponent implements OnInit { } async login(): Promise { - // TODO check if we have privatekey - // Send us to home if we have a private key - // talk to meta somehow - // in the error interceptor if 401/403 handle it - // if 200 go /home - const loginResult = await this.authService.login() - if (loginResult) { - this.router.navigate(['/home']); + try { + const loginResult = await this.authService.login() + if (loginResult) { + this.router.navigate(['/home']); + } + } catch (HttpError) { + this.errorDialogService.openDialog({ + message: HttpError.message, + }); } }