import { Injectable } from '@angular/core'; import { hobaParseChallengeHeader } from '@src/assets/js/hoba.js'; import { signChallenge } from '@src/assets/js/hoba-pgp.js'; import { environment } from '@src/environments/environment'; import { LoggingService } from '@app/_services/logging.service'; import { MutableKeyStore, MutablePgpKeyStore } from '@app/_pgp'; import { ErrorDialogService } from '@app/_services/error-dialog.service'; import { HttpClient } from '@angular/common/http'; import { HttpError, rejectBody } from '@app/_helpers/global-error-handler'; import { Staff } from '@app/_models'; import { BehaviorSubject, Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class AuthService { sessionToken: any; mutableKeyStore: MutableKeyStore; trustedUsers: Array = []; private trustedUsersList: BehaviorSubject> = new BehaviorSubject>( this.trustedUsers ); trustedUsersSubject: Observable> = this.trustedUsersList.asObservable(); constructor( private httpClient: HttpClient, private loggingService: LoggingService, private errorDialogService: ErrorDialogService ) { this.mutableKeyStore = new MutablePgpKeyStore(); } 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'))); } } setState(s): void { document.getElementById('state').innerHTML = s; } getWithToken(): Promise { return new Promise((resolve, reject) => { const headers = { Authorization: 'Bearer ' + this.sessionToken, '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 resolve(true); }); }); } // TODO rename to send signed challenge and set session. Also separate these responsibilities sendResponse(hobaResponseEncoded: any): Promise { return new Promise((resolve, reject) => { const headers = { Authorization: 'HOBA ' + hobaResponseEncoded, '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)); } this.sessionToken = response.headers.get('Token'); sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken); this.setState('Click button to log in'); return resolve(true); }); }); } 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)); } }); }); } 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 }); } } else { try { const o = await this.getChallenge(); const response: boolean = await this.loginResponse(o); return response === true; } 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); } }); } loginView(): void { document.getElementById('one').style.display = 'none'; document.getElementById('two').style.display = 'block'; this.setState('Click button to log in with PGP key ' + this.mutableKeyStore.getPrivateKeyId()); } async setKey(privateKeyArmored): Promise { try { const isValidKeyCheck = await this.mutableKeyStore.isValidKey(privateKeyArmored); if (!isValidKeyCheck) { throw Error('The private key is invalid'); } // TODO leaving this out for now. // const isEncryptedKeyCheck = await this.mutableKeyStore.isEncryptedPrivateKey(privateKeyArmored); // if (!isEncryptedKeyCheck) { // throw Error('The private key doesn\'t have a password!'); // } const key = await this.mutableKeyStore.importPrivateKey(privateKeyArmored); localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored); } catch (err) { this.loggingService.sendErrorLevelMessage( `Failed to set key: ${err.message || err.statusText}`, this, { error: err } ); this.errorDialogService.openDialog({ message: `Failed to set key: ${err.message || err.statusText}`, }); return false; } this.loginView(); return true; } logout(): void { sessionStorage.removeItem(btoa('CICADA_SESSION_TOKEN')); localStorage.removeItem(btoa('CICADA_PRIVATE_KEY')); this.sessionToken = undefined; window.location.reload(); } addTrustedUser(user: Staff): void { const savedIndex = this.trustedUsers.findIndex((staff) => staff.userid === user.userid); if (savedIndex === 0) { return; } if (savedIndex > 0) { this.trustedUsers.splice(savedIndex, 1); } this.trustedUsers.unshift(user); this.trustedUsersList.next(this.trustedUsers); } getTrustedUsers(): void { this.mutableKeyStore.getPublicKeys().forEach((key) => { this.addTrustedUser(key.users[0].userId); }); } async getPublicKeys(): Promise { return new Promise((resolve, reject) => { fetch(environment.publicKeysUrl).then((res) => { if (!res.ok) { // TODO does angular recommend an error interface? return reject(rejectBody(res)); } return resolve(res.text()); }); }); } getPrivateKey(): any { return this.mutableKeyStore.getPrivateKey(); } getPrivateKeyInfo(): any { return this.getPrivateKey().users[0].userId; } }