Merge branch 'bvander/not-authorized-flow' into 'master'
blocking access on 401/403 See merge request grassrootseconomics/cic-staff-client!9
This commit is contained in:
commit
d3285a5478
@ -22,6 +22,9 @@ Run `ng generate module module-name --route module-name --module app.module` to
|
||||
|
||||
## Build
|
||||
|
||||
set you environment variables - set these via environment variables as found in set-env.ts
|
||||
// TODO create a .env file so people don't have to set these one-by-one
|
||||
|
||||
Run `npm run build:dev` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `build:prod` script for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
@ -1,6 +1,7 @@
|
||||
// @ts-ignore
|
||||
import * as registry from '@src/assets/js/block-sync/data/Registry.json';
|
||||
import {environment} from '@src/environments/environment';
|
||||
import {LoggingService} from '@app/_services/logging.service';
|
||||
const Web3 = require('web3');
|
||||
|
||||
const web3 = new Web3(environment.web3Provider);
|
||||
@ -27,6 +28,12 @@ export class Registry {
|
||||
|
||||
public async addressOf(identifier: string): Promise<string> {
|
||||
const id = '0x' + web3.utils.padRight(new Buffer(identifier).toString('hex'), 64);
|
||||
return await this.contract.methods.addressOf(id).call();
|
||||
try {
|
||||
return await this.contract.methods.addressOf(id).call();
|
||||
} catch (error) {
|
||||
// TODO logger service
|
||||
// this.loggingService.sendInfoLevelMessage
|
||||
console.log('Unable to fetch addressOf', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,10 +12,9 @@ export class AuthGuard implements CanActivate {
|
||||
canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||
if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) {
|
||||
if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.router.navigate(['/auth']);
|
||||
return false;
|
||||
}
|
||||
|
@ -3,6 +3,16 @@ import {LoggingService} from '@app/_services/logging.service';
|
||||
import {HttpErrorResponse} from '@angular/common/http';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
// A generalized http repsonse error
|
||||
export class HttpError extends Error {
|
||||
public status: number
|
||||
constructor(message, status) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.name = 'HttpError';
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GlobalErrorHandler extends ErrorHandler {
|
||||
private sentencesForWarningLogging: string[] = [];
|
||||
@ -14,13 +24,13 @@ export class GlobalErrorHandler extends ErrorHandler {
|
||||
super();
|
||||
}
|
||||
|
||||
handleError(error: any): void {
|
||||
handleError(error: Error): void {
|
||||
this.logError(error);
|
||||
const message = error.message ? error.message : error.toString();
|
||||
|
||||
if (error.status) {
|
||||
error = new Error(message);
|
||||
}
|
||||
// if (error.status) {
|
||||
// error = new Error(message);
|
||||
// }
|
||||
|
||||
const errorTraceString = `Error message:\n${message}.\nStack trace: ${error.stack}`;
|
||||
|
||||
|
@ -20,29 +20,30 @@ export class ErrorInterceptor implements HttpInterceptor {
|
||||
) {}
|
||||
|
||||
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||
return next.handle(request).pipe(
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
let errorMessage;
|
||||
if (err.error instanceof ErrorEvent) {
|
||||
// A client-side or network error occurred. Handle it accordingly.
|
||||
errorMessage = `An error occurred: ${err.error.message}`;
|
||||
} else {
|
||||
// The backend returned an unsuccessful response code.
|
||||
// The response body may contain clues as to what went wrong.
|
||||
errorMessage = `Backend returned code ${err.status}, body was: ${JSON.stringify(err.error)}`;
|
||||
}
|
||||
this.loggingService.sendErrorLevelMessage(errorMessage, this, {error: err});
|
||||
switch (err.status) {
|
||||
case 401: // unauthorized
|
||||
this.router.navigateByUrl('/auth').then();
|
||||
break;
|
||||
case 403: // forbidden
|
||||
location.reload(true);
|
||||
break;
|
||||
}
|
||||
// Return an observable with a user-facing error message.
|
||||
return throwError(err);
|
||||
})
|
||||
);
|
||||
return next.handle(request).pipe(
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
let errorMessage;
|
||||
if (err.error instanceof ErrorEvent) {
|
||||
// A client-side or network error occurred. Handle it accordingly.
|
||||
errorMessage = `An error occurred: ${err.error.message}`;
|
||||
} else {
|
||||
// The backend returned an unsuccessful response code.
|
||||
// The response body may contain clues as to what went wrong.
|
||||
errorMessage = `Backend returned code ${err.status}, body was: ${JSON.stringify(err.error)}`;
|
||||
}
|
||||
this.loggingService.sendErrorLevelMessage(errorMessage, this, {error: err});
|
||||
switch (err.status) {
|
||||
case 401: // unauthorized
|
||||
this.router.navigateByUrl('/auth').then();
|
||||
break;
|
||||
case 403: // forbidden
|
||||
location.reload(true);
|
||||
break;
|
||||
}
|
||||
// Return an observable with a user-facing error message.
|
||||
return throwError(err);
|
||||
})
|
||||
);
|
||||
return next.handle(request);
|
||||
}
|
||||
}
|
||||
|
@ -18,20 +18,21 @@ export class LoggingInterceptor implements HttpInterceptor {
|
||||
) {}
|
||||
|
||||
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||
this.loggingService.sendInfoLevelMessage(request);
|
||||
const startTime = Date.now();
|
||||
let status: string;
|
||||
return next.handle(request);
|
||||
// this.loggingService.sendInfoLevelMessage(request);
|
||||
// const startTime = Date.now();
|
||||
// let status: string;
|
||||
|
||||
return next.handle(request).pipe(tap(event => {
|
||||
status = '';
|
||||
if (event instanceof HttpResponse) {
|
||||
status = 'succeeded';
|
||||
}
|
||||
}, error => status = 'failed'),
|
||||
finalize(() => {
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
const message = `${request.method} request for ${request.urlWithParams} ${status} in ${elapsedTime} ms`;
|
||||
this.loggingService.sendInfoLevelMessage(message);
|
||||
}));
|
||||
// return next.handle(request).pipe(tap(event => {
|
||||
// status = '';
|
||||
// if (event instanceof HttpResponse) {
|
||||
// status = 'succeeded';
|
||||
// }
|
||||
// }, error => status = 'failed'),
|
||||
// finalize(() => {
|
||||
// const elapsedTime = Date.now() - startTime;
|
||||
// const message = `${request.method} request for ${request.urlWithParams} ${status} in ${elapsedTime} ms`;
|
||||
// this.loggingService.sendInfoLevelMessage(message);
|
||||
// }));
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
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 { 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 {Observable } from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
import { HttpError } from '@app/_helpers/global-error-handler';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -14,20 +15,25 @@ import {Observable } from 'rxjs';
|
||||
export class AuthService {
|
||||
sessionToken: any;
|
||||
sessionLoginCount = 0;
|
||||
privateKey: any;
|
||||
mutableKeyStore: MutableKeyStore = new MutablePgpKeyStore();
|
||||
mutableKeyStore: MutableKeyStore;
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private loggingService: LoggingService,
|
||||
private errorDialogService: ErrorDialogService
|
||||
) {
|
||||
this.mutableKeyStore = new MutablePgpKeyStore()
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.mutableKeyStore.loadKeyring();
|
||||
// TODO setting these together shoulds be atomic
|
||||
if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) {
|
||||
this.sessionToken = sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'));
|
||||
}
|
||||
if (localStorage.getItem(btoa('CICADA_PRIVATE_KEY'))) {
|
||||
this.privateKey = localStorage.getItem(btoa('CICADA_PRIVATE_KEY'));
|
||||
this.mutableKeyStore.importPrivateKey(localStorage.getItem(btoa('CICADA_PRIVATE_KEY')))
|
||||
// this.privateKey = localStorage.getItem(btoa('CICADA_PRIVATE_KEY'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,24 +59,28 @@ export class AuthService {
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
sendResponse(hobaResponseEncoded): void {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'text';
|
||||
xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1));
|
||||
xhr.setRequestHeader('Authorization', 'HOBA ' + hobaResponseEncoded);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.setRequestHeader('x-cic-automerge', 'none');
|
||||
xhr.addEventListener('load', (e) => {
|
||||
if (xhr.status === 401) {
|
||||
throw new Error('login rejected');
|
||||
}
|
||||
this.sessionToken = xhr.getResponseHeader('Token');
|
||||
sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken);
|
||||
this.sessionLoginCount++;
|
||||
this.setState('Click button to log in');
|
||||
return;
|
||||
});
|
||||
xhr.send();
|
||||
//TODO renmae to send signed challenge and set session. Also seperate these responsibilities
|
||||
sendResponse(hobaResponseEncoded): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'text';
|
||||
xhr.open('GET', environment.cicMetaUrl + window.location.search.substring(1));
|
||||
xhr.setRequestHeader('Authorization', 'HOBA ' + hobaResponseEncoded);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.setRequestHeader('x-cic-automerge', 'none');
|
||||
xhr.addEventListener('load', (e) => {
|
||||
if (xhr.status !== 200) {
|
||||
const error = new HttpError(xhr.statusText, xhr.status);
|
||||
return reject(error);
|
||||
}
|
||||
this.sessionToken = xhr.getResponseHeader('Token');
|
||||
sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken);
|
||||
this.sessionLoginCount++;
|
||||
this.setState('Click button to log in');
|
||||
return resolve(true);
|
||||
});
|
||||
xhr.send();
|
||||
})
|
||||
}
|
||||
|
||||
getChallenge(): void {
|
||||
@ -81,39 +91,38 @@ export class AuthService {
|
||||
if (xhr.status === 401) {
|
||||
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
||||
const o = hobaParseChallengeHeader(authHeader);
|
||||
await this.loginResponse(o);
|
||||
this.loginResponse(o);
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
|
||||
login(): boolean {
|
||||
if (this.sessionToken !== undefined) {
|
||||
try {
|
||||
this.getWithToken();
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.loggingService.sendErrorLevelMessage('Login token failed', this, {error: e});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
this.getChallenge();
|
||||
} catch (e) {
|
||||
this.loggingService.sendErrorLevelMessage('Login challenge failed', this, {error: e});
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
async loginResponse(o): Promise<any> {
|
||||
try {
|
||||
const r = await signChallenge(o.challenge, o.realm, environment.cicMetaUrl, this.mutableKeyStore);
|
||||
this.sendResponse(r);
|
||||
} catch (error) {
|
||||
this.errorDialogService.openDialog({message: 'Incorrect key passphrase.'});
|
||||
}
|
||||
return new Promise(async(resolve, reject) => {
|
||||
try {
|
||||
const r = await signChallenge(o.challenge,
|
||||
o.realm,
|
||||
environment.cicMetaUrl,
|
||||
this.mutableKeyStore);
|
||||
const sessionTokenResult = await this.sendResponse(r);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpError) {
|
||||
if (error.status === 403) {
|
||||
this.errorDialogService.openDialog({ message: 'You are not authorized to use this system' })
|
||||
}
|
||||
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.' })
|
||||
|
||||
}
|
||||
}
|
||||
// TODO define this error
|
||||
this.errorDialogService.openDialog({message: 'Incorrect key passphrase.'});
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loginView(): void {
|
||||
@ -128,10 +137,11 @@ export class AuthService {
|
||||
if (!isValidKeyCheck) {
|
||||
throw Error('The private key is invalid');
|
||||
}
|
||||
const isEncryptedKeyCheck = await this.mutableKeyStore.isEncryptedPrivateKey(privateKeyArmored);
|
||||
if (!isEncryptedKeyCheck) {
|
||||
throw Error('The private key doesn\'t have a password!');
|
||||
}
|
||||
// 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) {
|
||||
@ -157,13 +167,19 @@ export class AuthService {
|
||||
return trustedUsers;
|
||||
}
|
||||
|
||||
getPublicKeys(): Observable<any> {
|
||||
return this.httpClient.get(`${environment.publicKeysUrl}`, {responseType: 'text'});
|
||||
async getPublicKeys(): Promise<any> {
|
||||
const data = await fetch(environment.publicKeysUrl)
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
//TODO does angular recommend an error interface?
|
||||
throw Error(`${res.statusText} - ${res.status}`);
|
||||
}
|
||||
return res.text();
|
||||
})
|
||||
return data;
|
||||
}
|
||||
|
||||
async getPrivateKeys(): Promise<void> {
|
||||
if (this.privateKey !== undefined) {
|
||||
await this.mutableKeyStore.importPrivateKey(this.privateKey);
|
||||
}
|
||||
getPrivateKey(): any {
|
||||
return this.mutableKeyStore.getPrivateKey();
|
||||
}
|
||||
}
|
||||
|
@ -21,12 +21,20 @@ export class AppComponent {
|
||||
private errorDialogService: ErrorDialogService
|
||||
) {
|
||||
(async () => {
|
||||
await this.authService.mutableKeyStore.loadKeyring();
|
||||
this.authService.getPublicKeys()
|
||||
.pipe(catchError(async (error) => {
|
||||
this.loggingService.sendErrorLevelMessage('Unable to load trusted public keys.', this, {error});
|
||||
this.errorDialogService.openDialog({message: 'Trusted keys endpoint can\'t be reached. Please try again later.'});
|
||||
})).subscribe(this.authService.mutableKeyStore.importPublicKey);
|
||||
try {
|
||||
await this.authService.mutableKeyStore.loadKeyring();
|
||||
// this.authService.getPublicKeys()
|
||||
// .pipe(catchError(async (error) => {
|
||||
// this.loggingService.sendErrorLevelMessage('Unable to load trusted public keys.', this, {error});
|
||||
// this.errorDialogService.openDialog({message: 'Trusted keys endpoint can\'t be reached. Please try again later.'});
|
||||
// })).subscribe(this.authService.mutableKeyStore.importPublicKey);
|
||||
const publicKeys = await this.authService.getPublicKeys()
|
||||
await this.authService.mutableKeyStore.importPublicKey(publicKeys);
|
||||
} catch(error) {
|
||||
this.errorDialogService.openDialog({message: 'Trusted keys endpoint can\'t be reached. Please try again later.'});
|
||||
// TODO do something to halt user progress...show a sad cicada page 🦗?
|
||||
}
|
||||
|
||||
})();
|
||||
this.mediaQuery.addListener(this.onResize);
|
||||
this.onResize(this.mediaQuery);
|
||||
|
@ -26,12 +26,11 @@ export class AuthComponent implements OnInit {
|
||||
this.keyForm = this.formBuilder.group({
|
||||
key: ['', Validators.required],
|
||||
});
|
||||
if (this.authService.privateKey !== undefined) {
|
||||
const setKey = await this.authService.setKey(this.authService.privateKey);
|
||||
if (setKey && this.authService.sessionToken !== undefined) {
|
||||
this.authService.setState('Click button to log in');
|
||||
}
|
||||
}
|
||||
await this.authService.init();
|
||||
//if (this.authService.privateKey !== undefined) {
|
||||
// const setKey = await this.authService.setKey(this.authService.privateKey);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
get keyFormStub(): any { return this.keyForm.controls; }
|
||||
@ -47,8 +46,12 @@ export class AuthComponent implements OnInit {
|
||||
}
|
||||
|
||||
login(): void {
|
||||
const loginStatus = this.authService.login();
|
||||
if (loginStatus) {
|
||||
// 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()) {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
}
|
||||
|
@ -27,9 +27,11 @@ export class AccountsComponent implements OnInit {
|
||||
private userService: UserService,
|
||||
private loggingService: LoggingService,
|
||||
private router: Router
|
||||
) {
|
||||
)
|
||||
{
|
||||
(async () => {
|
||||
try {
|
||||
// TODO it feels like this shuold be in the onInit handler
|
||||
await this.userService.loadAccounts(100);
|
||||
} catch (error) {
|
||||
this.loggingService.sendErrorLevelMessage('Failed to load accounts', this, {error});
|
||||
|
Loading…
Reference in New Issue
Block a user