Add auth guard connection for dashboard routes.

This commit is contained in:
Spencer Ofwiti 2021-02-17 21:22:06 +03:00
parent 0e07e4ccdc
commit 3286c1df82
13 changed files with 56 additions and 79 deletions

View File

@ -12,11 +12,11 @@ export class AuthGuard implements CanActivate {
canActivate( canActivate(
route: ActivatedRouteSnapshot, route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
if (localStorage.getItem(atob('CICADA_USER'))) { if (sessionStorage.getItem(btoa('CICADA_SESSION_TOKEN'))) {
return true; return true;
} }
this.router.navigate(['/auth'], { queryParams: { returnUrl: state.url }}); this.router.navigate(['/auth']);
return false; return false;
} }

View File

@ -19,7 +19,7 @@ interface Signer {
fingerprint(): string; fingerprint(): string;
prepare(material: Signable): boolean; prepare(material: Signable): boolean;
verify(digest: string, signature: Signature): void; verify(digest: string, signature: Signature): void;
sign(digest: string): void; sign(digest: string): Promise<void>;
} }
class PGPSigner implements Signer { class PGPSigner implements Signer {
@ -72,9 +72,13 @@ class PGPSigner implements Signer {
}); });
} }
public sign(digest: string): void { public async sign(digest: string): Promise<void> {
const m = openpgp.cleartext.fromText(digest); const m = openpgp.cleartext.fromText(digest);
const pk = this.keyStore.getPrivateKey(); const pk = this.keyStore.getPrivateKey();
if (!pk.isDecrypted()) {
const password = window.prompt('password');
await pk.decrypt(password);
}
const opts = { const opts = {
message: m, message: m,
privateKeys: [pk], privateKeys: [pk],

View File

@ -5,8 +5,6 @@ import { signChallenge } from '@src/assets/js/hoba-pgp.js';
import {environment} from '@src/environments/environment'; import {environment} from '@src/environments/environment';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
const origin = 'http://localhost:4444';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
@ -34,7 +32,7 @@ export class AuthService {
getWithToken(): void { getWithToken(): void {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.responseType = 'text'; xhr.responseType = 'text';
xhr.open('GET', origin + window.location.search.substring(1)); xhr.open('GET', environment.cicAuthUrl + window.location.search.substring(1));
xhr.setRequestHeader('Authorization', 'Bearer ' + this.sessionToken); xhr.setRequestHeader('Authorization', 'Bearer ' + this.sessionToken);
xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('x-cic-automerge', 'none'); xhr.setRequestHeader('x-cic-automerge', 'none');
@ -44,7 +42,6 @@ export class AuthService {
} }
this.sessionLoginCount++; this.sessionLoginCount++;
this.setState('click to perform login ' + this.sessionLoginCount + ' with token ' + this.sessionToken); this.setState('click to perform login ' + this.sessionLoginCount + ' with token ' + this.sessionToken);
console.log('received', xhr.responseText);
return; return;
}); });
xhr.send(); xhr.send();
@ -53,7 +50,7 @@ export class AuthService {
sendResponse(hobaResponseEncoded): void { sendResponse(hobaResponseEncoded): void {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.responseType = 'text'; xhr.responseType = 'text';
xhr.open('GET', origin + window.location.search.substring(1)); xhr.open('GET', environment.cicAuthUrl + window.location.search.substring(1));
xhr.setRequestHeader('Authorization', 'HOBA ' + hobaResponseEncoded); xhr.setRequestHeader('Authorization', 'HOBA ' + hobaResponseEncoded);
xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('x-cic-automerge', 'none'); xhr.setRequestHeader('x-cic-automerge', 'none');
@ -65,7 +62,6 @@ export class AuthService {
sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken); sessionStorage.setItem(btoa('CICADA_SESSION_TOKEN'), this.sessionToken);
this.sessionLoginCount++; this.sessionLoginCount++;
this.setState('click to perform login ' + this.sessionLoginCount + ' with token ' + this.sessionToken); this.setState('click to perform login ' + this.sessionLoginCount + ' with token ' + this.sessionToken);
console.log('received', xhr.responseText);
return; return;
}); });
xhr.send(); xhr.send();
@ -74,7 +70,7 @@ export class AuthService {
getChallenge(): void { getChallenge(): void {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.responseType = 'arraybuffer'; xhr.responseType = 'arraybuffer';
xhr.open('GET', origin + window.location.search.substring(1)); xhr.open('GET', environment.cicAuthUrl + window.location.search.substring(1));
xhr.onload = (e) => { xhr.onload = (e) => {
if (xhr.status === 401) { if (xhr.status === 401) {
const authHeader = xhr.getResponseHeader('WWW-Authenticate'); const authHeader = xhr.getResponseHeader('WWW-Authenticate');
@ -107,7 +103,7 @@ export class AuthService {
async loginResponse(o): Promise<any> { async loginResponse(o): Promise<any> {
const r = await signChallenge(o.challenge, o.realm, origin, this.mutableKeyStore); const r = await signChallenge(o.challenge, o.realm, environment.cicAuthUrl, this.mutableKeyStore);
this.sendResponse(r); this.sendResponse(r);
} }
@ -118,7 +114,6 @@ export class AuthService {
} }
async setKey(privateKeyArmored): Promise<boolean> { async setKey(privateKeyArmored): Promise<boolean> {
console.log('settings pk' + privateKeyArmored);
try { try {
await this.mutableKeyStore.importPrivateKey(privateKeyArmored); await this.mutableKeyStore.importPrivateKey(privateKeyArmored);
localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored); localStorage.setItem(btoa('CICADA_PRIVATE_KEY'), privateKeyArmored);

View File

@ -1,11 +1,10 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {first, map} from 'rxjs/operators'; import {map} from 'rxjs/operators';
import {BehaviorSubject, Observable} from 'rxjs'; import {BehaviorSubject, Observable} from 'rxjs';
import {environment} from '@src/environments/environment'; import {environment} from '@src/environments/environment';
import {User} from 'cic-client-meta'; import {User} from 'cic-client-meta';
import {UserService} from '@app/_services/user.service'; import {UserService} from '@app/_services/user.service';
import {parse} from '@src/assets/js/parse-vcard';
import { AccountIndex } from '@app/_helpers'; import { AccountIndex } from '@app/_helpers';
@Injectable({ @Injectable({
@ -75,13 +74,7 @@ export class TransactionService {
} }
async getUser(address: string): Promise<void> { async getUser(address: string): Promise<void> {
this.userService.getUser(await User.toKey(address)).pipe(first()).subscribe(res => { this.userInfo = this.userService.getUser(await User.toKey(address));
const vcard = parse(atob(res.vcard));
res.vcard = vcard;
this.userInfo = res;
}, error => {
console.log(error);
});
} }
transferRequest(tokenAddress: string, senderAddress: string, recipientAddress: string, value: number): Observable<any> { transferRequest(tokenAddress: string, senderAddress: string, recipientAddress: string, value: number): Observable<any> {

View File

@ -2,9 +2,10 @@ import { Injectable } from '@angular/core';
import {BehaviorSubject, Observable} from 'rxjs'; import {BehaviorSubject, Observable} from 'rxjs';
import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http';
import {environment} from '@src/environments/environment'; import {environment} from '@src/environments/environment';
import {first, map} from 'rxjs/operators'; import {first} from 'rxjs/operators';
import { ArgPair, Envelope, Syncable } from '@src/assets/js/cic-meta/sync.js'; import { ArgPair, Envelope, Syncable } from '@src/assets/js/cic-meta/sync.js';
import {MutableKeyStore, MutablePgpKeyStore, PGPSigner, Signer} from '@app/_helpers'; import {MutableKeyStore, MutablePgpKeyStore, PGPSigner, Signer} from '@app/_helpers';
import {User} from 'cic-client-meta';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -70,30 +71,29 @@ export class UserService {
location, location,
referrer referrer
}; };
let addressStub = address.slice(2); const accountKey = await User.toKey(address);
addressStub = '0000000000000000000000000000000000000000000000000000000000000000';
const headers = new HttpHeaders({'x-cic-automerge': 'client'}); const headers = new HttpHeaders({'x-cic-automerge': 'client'});
this.http.get(`${environment.cicMetaUrl}/${addressStub}`, { headers }).pipe(first()).subscribe(async res => { this.http.get(`${environment.cicMetaUrl}/${accountKey}`, { headers }).pipe(first()).subscribe(async res => {
this.syncableAccount = Envelope.fromJSON(res).unwrap(); this.syncableAccount = Envelope.fromJSON(res).unwrap();
let update = []; let update = [];
for (const prop in accountDetails) { for (const prop in accountDetails) {
update.push(new ArgPair(prop, accountDetails[prop])); update.push(new ArgPair(prop, accountDetails[prop]));
} }
this.syncableAccount.update(update, 'client-branch'); this.syncableAccount.update(update, 'client-branch');
await this.updateMeta(addressStub, headers); await this.updateMeta(accountKey, headers);
}, async error => { }, async error => {
console.error('There is an error!', error); console.error('There is an error!', error);
const refName = addressStub + ':cic-person'; const refName = accountKey + ':cic.person';
this.syncableAccount = new Syncable(refName, accountDetails); this.syncableAccount = new Syncable(refName, accountDetails);
await this.updateMeta(addressStub, headers); await this.updateMeta(accountKey, headers);
}); });
return addressStub; return accountKey;
} }
async updateMeta(addressStub: string, headers: HttpHeaders): Promise<any> { async updateMeta(accountKey: string, headers: HttpHeaders): Promise<any> {
const envelope = await this.wrap(this.syncableAccount , this.signer); const envelope = await this.wrap(this.syncableAccount , this.signer);
const reqBody = envelope.toJSON(); const reqBody = envelope.toJSON();
this.http.put(`${environment.cicMetaUrl}/${addressStub}`, { data: reqBody }, { headers }).pipe(first()).subscribe(res => { this.http.put(`${environment.cicMetaUrl}/${accountKey}`, { data: reqBody }, { headers }).pipe(first()).subscribe(res => {
console.log(res); console.log(res);
}); });
} }
@ -146,16 +146,16 @@ export class UserService {
return this.http.post(`${environment.cicCacheUrl}/staff/${id}`, {accountType: type}); return this.http.post(`${environment.cicCacheUrl}/staff/${id}`, {accountType: type});
} }
getUser(userKey: string): Observable<any> { getUser(userKey: string): any {
const params = new HttpParams().set('userKey', '0970c6e9cdad650ba9006e5a1caf090a13da312792389a147263c98ac78cd037'); const headers = new HttpHeaders({'x-cic-automerge': 'client'});
return this.http.get(`${environment.cicScriptsUrl}`, { params }) this.http.get(`${environment.cicMetaUrl}/${userKey}`, { headers }).pipe(first()).subscribe(async res => {
.pipe(map(response => { const fetchedAccount = Envelope.fromJSON(res).unwrap();
return response; return fetchedAccount.m.data;
})); });
} }
wrap(syncable: Syncable, signer: Signer): Promise<Envelope> { wrap(syncable: Syncable, signer: Signer): Promise<Envelope> {
return new Promise<Envelope>((whohoo, doh) => { return new Promise<Envelope>(async (whohoo, doh) => {
syncable.setSigner(signer); syncable.setSigner(signer);
syncable.onwrap = async (env) => { syncable.onwrap = async (env) => {
if (env === undefined) { if (env === undefined) {
@ -164,7 +164,7 @@ export class UserService {
} }
whohoo(env); whohoo(env);
}; };
syncable.sign(); await syncable.sign();
}); });
} }
} }

View File

@ -1,9 +1,10 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import {AuthGuard} from '@app/_guards';
const routes: Routes = [ const routes: Routes = [
{ path: 'auth', loadChildren: () => import('@app/auth/auth.module').then(m => m.AuthModule) }, { path: 'auth', loadChildren: () => import('@app/auth/auth.module').then(m => m.AuthModule) },
{ path: '', loadChildren: () => import('@pages/pages.module').then(m => m.PagesModule) }, { path: '', loadChildren: () => import('@pages/pages.module').then(m => m.PagesModule), canActivate: [AuthGuard] },
{ path: '**', redirectTo: '', pathMatch: 'full' } { path: '**', redirectTo: '', pathMatch: 'full' }
]; ];

View File

@ -4,11 +4,12 @@ import { NgModule } from '@angular/core';
import { AppRoutingModule } from '@app/app-routing.module'; import { AppRoutingModule } from '@app/app-routing.module';
import { AppComponent } from '@app/app.component'; import { AppComponent } from '@app/app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {HttpClientModule} from '@angular/common/http'; import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {MutablePgpKeyStore, MockBackendProvider} from '@app/_helpers'; import {MutablePgpKeyStore, MockBackendProvider, ErrorInterceptor} from '@app/_helpers';
import {DataTablesModule} from 'angular-datatables'; import {DataTablesModule} from 'angular-datatables';
import {SharedModule} from '@app/shared/shared.module'; import {SharedModule} from '@app/shared/shared.module';
import {MatTableModule} from '@angular/material/table'; import {MatTableModule} from '@angular/material/table';
import {AuthGuard} from '@app/_guards';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -24,8 +25,10 @@ import {MatTableModule} from '@angular/material/table';
MatTableModule MatTableModule
], ],
providers: [ providers: [
AuthGuard,
MutablePgpKeyStore, MutablePgpKeyStore,
MockBackendProvider MockBackendProvider,
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View File

@ -37,20 +37,15 @@
<h4 class="text-dark-50 text-center font-weight-bold">Enter Passphrase</h4> <h4 class="text-dark-50 text-center font-weight-bold">Enter Passphrase</h4>
</div> </div>
<form [formGroup]="passphraseForm" (ngSubmit)="login()"> <form [formGroup]="stateForm" (ngSubmit)="login()">
<mat-form-field appearance="outline" class="full-width"> <mat-form-field appearance="outline" class="full-width">
<mat-label>Passphrase</mat-label> <mat-label>Login State</mat-label>
<input matInput name="state" id="state" formControlName="passphrase" <input matInput name="state" id="state" formControlName="state" readonly disabled>
placeholder="Enter your passphrase..." [errorStateMatcher]="matcher">
<div *ngIf="submitted && passphraseFormStub.passphrase.errors" class="invalid-feedback">
<mat-error *ngIf="passphraseFormStub.passphrase.errors.required">Passphrase is required.</mat-error>
</div>
</mat-form-field> </mat-form-field>
<div class="form-group mb-0 text-center"> <div class="form-group mb-0 text-center">
<button mat-raised-button matRipple color="primary" type="submit"> <button mat-raised-button matRipple color="primary" type="submit">
<!-- <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>-->
Login Login
</button> </button>
</div> </div>

View File

@ -1,16 +1,17 @@
import {AfterViewInit, Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms'; import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {CustomErrorStateMatcher} from '@app/_helpers'; import {CustomErrorStateMatcher} from '@app/_helpers';
import {AuthService} from '@app/_services'; import {AuthService} from '@app/_services';
import {Router} from '@angular/router';
@Component({ @Component({
selector: 'app-auth', selector: 'app-auth',
templateUrl: './auth.component.html', templateUrl: './auth.component.html',
styleUrls: ['./auth.component.scss'] styleUrls: ['./auth.component.scss']
}) })
export class AuthComponent implements OnInit, AfterViewInit { export class AuthComponent implements OnInit {
keyForm: FormGroup; keyForm: FormGroup;
passphraseForm: FormGroup; stateForm: FormGroup;
submitted: boolean = false; submitted: boolean = false;
loading: boolean = false; loading: boolean = false;
matcher = new CustomErrorStateMatcher(); matcher = new CustomErrorStateMatcher();
@ -18,14 +19,15 @@ export class AuthComponent implements OnInit, AfterViewInit {
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private router: Router
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.keyForm = this.formBuilder.group({ this.keyForm = this.formBuilder.group({
key: ['', Validators.required], key: ['', Validators.required],
}); });
this.passphraseForm = this.formBuilder.group({ this.stateForm = this.formBuilder.group({
passphrase: ['', Validators.required], state: '',
}); });
if (this.authService.privateKey !== undefined ) { if (this.authService.privateKey !== undefined ) {
this.authService.setKey(this.authService.privateKey).then(r => { this.authService.setKey(this.authService.privateKey).then(r => {
@ -37,23 +39,7 @@ export class AuthComponent implements OnInit, AfterViewInit {
} }
} }
ngAfterViewInit(): void {
console.log(window.location);
const xhr = new XMLHttpRequest();
xhr.responseType = 'text';
xhr.open('GET', window.location.origin + '/privatekey.asc');
xhr.onload = (e) => {
if (xhr.status !== 200) {
console.warn('failed to autoload private key ciphertext');
return;
}
(document.getElementById('privateKeyAsc') as HTMLInputElement).value = xhr.responseText;
};
xhr.send();
}
get keyFormStub(): any { return this.keyForm.controls; } get keyFormStub(): any { return this.keyForm.controls; }
get passphraseFormStub(): any { return this.passphraseForm.controls; }
onSubmit(): void { onSubmit(): void {
this.submitted = true; this.submitted = true;
@ -66,8 +52,10 @@ export class AuthComponent implements OnInit, AfterViewInit {
} }
login(): void { login(): void {
// if (this.passphraseForm.invalid) { return; } const loginStatus = this.authService.login();
this.authService.login(); if (loginStatus) {
this.router.navigate(['/home']);
}
} }
switchWindows(): void { switchWindows(): void {

View File

@ -13,12 +13,10 @@ export async function signChallenge(challenge, realm, origin, keyStore) {
const a_nonce = btoa(String.fromCharCode.apply(null, nonce_array)); const a_nonce = btoa(String.fromCharCode.apply(null, nonce_array));
const a_challenge = btoa(challenge); const a_challenge = btoa(challenge);
const message = hobaToSign(a_nonce, a_kid, a_challenge, realm, origin, alg); const message = hobaToSign(a_nonce, a_kid, a_challenge, realm, origin, alg);
console.debug('message to sign', challenge, realm, origin, message);
const signature = await keyStore.sign(message); const signature = await keyStore.sign(message);
const a_signature = btoa(signature); const a_signature = btoa(signature);
const result = hobaResult(a_nonce, a_kid, a_challenge, a_signature); const result = hobaResult(a_nonce, a_kid, a_challenge, a_signature);
console.debug('result', result);
return result; return result;
} }

View File

@ -19,9 +19,7 @@ export function hobaParseChallengeHeader(s) {
var auth_kv = auth_pairs[i].split(/^([^=]+)="(.+)"/); var auth_kv = auth_pairs[i].split(/^([^=]+)="(.+)"/);
auth_values[auth_kv[1]] = auth_kv[2]; auth_values[auth_kv[1]] = auth_kv[2];
} }
console.debug('challenge b64', auth_values['challenge']);
const challenge_bytes = atob(auth_values['challenge']); const challenge_bytes = atob(auth_values['challenge']);
console.debug('challenge bytes', challenge_bytes);
return { return {
challenge: challenge_bytes, challenge: challenge_bytes,

View File

@ -1,5 +1,6 @@
export const environment = { export const environment = {
production: true, production: true,
cicAuthUrl: 'http://localhost:4444',
cicMetaUrl: 'http://localhost:63380', cicMetaUrl: 'http://localhost:63380',
publicKeysUrl: 'http://localhost:8000', publicKeysUrl: 'http://localhost:8000',
cicCacheUrl: 'http://localhost:63313', cicCacheUrl: 'http://localhost:63313',

View File

@ -4,6 +4,7 @@
export const environment = { export const environment = {
production: false, production: false,
cicAuthUrl: 'http://localhost:4444',
cicMetaUrl: 'http://localhost:63380', cicMetaUrl: 'http://localhost:63380',
publicKeysUrl: 'http://localhost:8000', publicKeysUrl: 'http://localhost:8000',
cicCacheUrl: 'http://localhost:63313', cicCacheUrl: 'http://localhost:63313',