Restrict webauthn to fingerprint only and improve UX (#161)

* Restrict webauthn to fingerprint only

* Move webauthn login to separate page /webauthn

* Stay signed in with social login

* Update changelog

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
pull/162/head
Matthias Frey 3 years ago committed by GitHub
parent 698d5ec3b7
commit 6c1119caec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Improved the sign in with fingerprint
## 1.15.0 - 14.06.2021
### Added

@ -57,8 +57,9 @@ export class WebAuthService {
timeout: 60000,
attestationType: 'indirect',
authenticatorSelection: {
userVerification: 'preferred',
requireResidentKey: false
authenticatorAttachment: 'platform',
requireResidentKey: false,
userVerification: 'required'
}
};
@ -143,7 +144,7 @@ export class WebAuthService {
{
id: device.credentialId,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
transports: ['internal']
}
],
userVerification: 'preferred',

@ -92,6 +92,13 @@ const routes: Routes = [
(m) => m.TransactionsPageModule
)
},
{
path: 'webauthn',
loadChildren: () =>
import('./pages/webauthn/webauthn-page.module').then(
(m) => m.WebauthnPageModule
)
},
{
path: 'zen',
loadChildren: () =>

@ -11,6 +11,10 @@ import { Router } from '@angular/router';
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import {
STAY_SIGNED_IN,
SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { InfoItem, User } from '@ghostfolio/common/interfaces';
@ -43,8 +47,8 @@ export class HeaderComponent implements OnChanges {
private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService,
private router: Router,
private tokenStorageService: TokenStorageService,
private webAuthnService: WebAuthnService
private settingsStorageService: SettingsStorageService,
private tokenStorageService: TokenStorageService
) {
this.impersonationStorageService
.onChangeHasImpersonation()
@ -108,14 +112,17 @@ export class HeaderComponent implements OnChanges {
takeUntil(this.unsubscribeSubject)
)
.subscribe(({ authToken }) => {
this.setToken(authToken, data.staySignedIn);
this.setToken(authToken);
});
}
});
}
public setToken(aToken: string, staySignedIn: boolean) {
this.tokenStorageService.saveToken(aToken, staySignedIn);
public setToken(aToken: string) {
this.tokenStorageService.saveToken(
aToken,
this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true'
);
this.router.navigate(['/']);
}

@ -1,5 +1,10 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import {
STAY_SIGNED_IN,
SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service';
@Component({
selector: 'gf-login-with-access-token-dialog',
@ -9,13 +14,21 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
})
export class LoginWithAccessTokenDialog {
public constructor(
@Inject(MAT_DIALOG_DATA) public data: any,
public dialogRef: MatDialogRef<LoginWithAccessTokenDialog>,
@Inject(MAT_DIALOG_DATA) public data: any
private settingsStorageService: SettingsStorageService
) {}
ngOnInit() {}
public onClose(): void {
public onChangeStaySignedIn(aValue: MatCheckboxChange) {
this.settingsStorageService.setSetting(
STAY_SIGNED_IN,
aValue.checked?.toString()
);
}
public onClose() {
this.dialogRef.close();
}
}

@ -28,7 +28,7 @@
</div>
<div mat-dialog-actions>
<div class="flex-grow-1">
<mat-checkbox i18n [(ngModel)]="data.staySignedIn"
<mat-checkbox i18n (change)="onChangeStaySignedIn($event)"
>Stay signed in</mat-checkbox
>
</div>

@ -79,10 +79,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
}
} else if (error.status === StatusCodes.UNAUTHORIZED) {
if (this.webAuthnService.isEnabled()) {
this.webAuthnService.login().subscribe(({ authToken }) => {
this.tokenStorageService.saveToken(authToken, false);
window.location.reload();
});
this.router.navigate(['/webauthn']);
} else {
this.tokenStorageService.signOut();
}

@ -1,5 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import {
STAY_SIGNED_IN,
SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
@Component({
@ -14,6 +18,7 @@ export class AuthPageComponent implements OnInit {
public constructor(
private route: ActivatedRoute,
private router: Router,
private settingsStorageService: SettingsStorageService,
private tokenStorageService: TokenStorageService
) {}
@ -23,7 +28,10 @@ export class AuthPageComponent implements OnInit {
public ngOnInit() {
this.route.params.subscribe((params) => {
const jwt = params['jwt'];
this.tokenStorageService.saveToken(jwt);
this.tokenStorageService.saveToken(
jwt,
this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true'
);
this.router.navigate(['/']);
});

@ -26,8 +26,7 @@ export class LandingPageComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private router: Router,
private tokenStorageService: TokenStorageService,
private webAuthnService: WebAuthnService
private tokenStorageService: TokenStorageService
) {}
/**
@ -257,7 +256,7 @@ export class LandingPageComponent implements OnDestroy, OnInit {
}
public setToken(aToken: string) {
this.tokenStorageService.saveToken(aToken);
this.tokenStorageService.saveToken(aToken, true);
this.router.navigate(['/']);
}

@ -78,19 +78,13 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
dialogRef.afterClosed().subscribe((data) => {
if (data?.authToken) {
this.tokenStorageService.saveToken(authToken);
this.tokenStorageService.saveToken(authToken, true);
this.router.navigate(['/']);
}
});
}
public setToken(aToken: string) {
this.tokenStorageService.saveToken(aToken);
this.router.navigate(['/']);
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();

@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
const routes: Routes = [{ path: '', component: WebauthnPageComponent }];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class WebauthnPageRoutingModule {}

@ -0,0 +1,46 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
@Component({
selector: 'gf-webauthn-page',
templateUrl: './webauthn-page.html',
styleUrls: ['./webauthn-page.scss']
})
export class WebauthnPageComponent implements OnInit {
public hasError = false;
constructor(
private changeDetectorRef: ChangeDetectorRef,
private router: Router,
private tokenStorageService: TokenStorageService,
private webAuthnService: WebAuthnService
) {}
public ngOnInit() {
this.signIn();
}
public deregisterDevice() {
this.webAuthnService.deregister().subscribe(() => {
this.router.navigate(['/']);
});
}
public signIn() {
this.hasError = false;
this.webAuthnService.login().subscribe(
({ authToken }) => {
this.tokenStorageService.saveToken(authToken, false);
this.router.navigate(['/']);
},
(error) => {
console.error(error);
this.hasError = true;
this.changeDetectorRef.markForCheck();
}
);
}
}

@ -0,0 +1,27 @@
<div class="container">
<div class="row">
<div *ngIf="!hasError" class="col d-flex justify-content-center">
<mat-spinner [diameter]="20"></mat-spinner>
</div>
<div
*ngIf="hasError"
class="align-items-center col d-flex flex-column justify-content-center"
>
<h3 class="d-flex justify-content-center" i18n>
Oops, authentication failed
</h3>
<button
(click)="signIn()"
class="my-4"
color="primary"
i18n
mat-flat-button
>
Try again
</button>
<button (click)="deregisterDevice()" i18n mat-flat-button>
Go back to Home Page
</button>
</div>
</div>
</div>

@ -0,0 +1,20 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
import { WebauthnPageRoutingModule } from './webauthn-page-routing.module';
@NgModule({
declarations: [WebauthnPageComponent],
exports: [],
imports: [
CommonModule,
MatButtonModule,
MatProgressSpinnerModule,
WebauthnPageRoutingModule
],
providers: []
})
export class WebauthnPageModule {}

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
export const RANGE = 'range';
export const STAY_SIGNED_IN = 'staySignedIn';
@Injectable({
providedIn: 'root'

@ -21,7 +21,7 @@ export class TokenStorageService {
);
}
public saveToken(token: string, staySignedIn: boolean = false): void {
public saveToken(token: string, staySignedIn = false): void {
if (staySignedIn) {
window.localStorage.setItem(TOKEN_KEY, token);
}

Loading…
Cancel
Save