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>
This commit is contained in:
parent
698d5ec3b7
commit
6c1119caec
@ -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/),
|
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).
|
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
|
## 1.15.0 - 14.06.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -57,8 +57,9 @@ export class WebAuthService {
|
|||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
attestationType: 'indirect',
|
attestationType: 'indirect',
|
||||||
authenticatorSelection: {
|
authenticatorSelection: {
|
||||||
userVerification: 'preferred',
|
authenticatorAttachment: 'platform',
|
||||||
requireResidentKey: false
|
requireResidentKey: false,
|
||||||
|
userVerification: 'required'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -143,7 +144,7 @@ export class WebAuthService {
|
|||||||
{
|
{
|
||||||
id: device.credentialId,
|
id: device.credentialId,
|
||||||
type: 'public-key',
|
type: 'public-key',
|
||||||
transports: ['usb', 'ble', 'nfc', 'internal']
|
transports: ['internal']
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
userVerification: 'preferred',
|
userVerification: 'preferred',
|
||||||
|
@ -92,6 +92,13 @@ const routes: Routes = [
|
|||||||
(m) => m.TransactionsPageModule
|
(m) => m.TransactionsPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'webauthn',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/webauthn/webauthn-page.module').then(
|
||||||
|
(m) => m.WebauthnPageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'zen',
|
path: 'zen',
|
||||||
loadChildren: () =>
|
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 { 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 { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.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 { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
||||||
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||||
@ -43,8 +47,8 @@ export class HeaderComponent implements OnChanges {
|
|||||||
private dialog: MatDialog,
|
private dialog: MatDialog,
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private tokenStorageService: TokenStorageService,
|
private settingsStorageService: SettingsStorageService,
|
||||||
private webAuthnService: WebAuthnService
|
private tokenStorageService: TokenStorageService
|
||||||
) {
|
) {
|
||||||
this.impersonationStorageService
|
this.impersonationStorageService
|
||||||
.onChangeHasImpersonation()
|
.onChangeHasImpersonation()
|
||||||
@ -108,14 +112,17 @@ export class HeaderComponent implements OnChanges {
|
|||||||
takeUntil(this.unsubscribeSubject)
|
takeUntil(this.unsubscribeSubject)
|
||||||
)
|
)
|
||||||
.subscribe(({ authToken }) => {
|
.subscribe(({ authToken }) => {
|
||||||
this.setToken(authToken, data.staySignedIn);
|
this.setToken(authToken);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public setToken(aToken: string, staySignedIn: boolean) {
|
public setToken(aToken: string) {
|
||||||
this.tokenStorageService.saveToken(aToken, staySignedIn);
|
this.tokenStorageService.saveToken(
|
||||||
|
aToken,
|
||||||
|
this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true'
|
||||||
|
);
|
||||||
|
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||||
|
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import {
|
||||||
|
STAY_SIGNED_IN,
|
||||||
|
SettingsStorageService
|
||||||
|
} from '@ghostfolio/client/services/settings-storage.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-login-with-access-token-dialog',
|
selector: 'gf-login-with-access-token-dialog',
|
||||||
@ -9,13 +14,21 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
|||||||
})
|
})
|
||||||
export class LoginWithAccessTokenDialog {
|
export class LoginWithAccessTokenDialog {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||||
public dialogRef: MatDialogRef<LoginWithAccessTokenDialog>,
|
public dialogRef: MatDialogRef<LoginWithAccessTokenDialog>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: any
|
private settingsStorageService: SettingsStorageService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {}
|
ngOnInit() {}
|
||||||
|
|
||||||
public onClose(): void {
|
public onChangeStaySignedIn(aValue: MatCheckboxChange) {
|
||||||
|
this.settingsStorageService.setSetting(
|
||||||
|
STAY_SIGNED_IN,
|
||||||
|
aValue.checked?.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onClose() {
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div mat-dialog-actions>
|
<div mat-dialog-actions>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<mat-checkbox i18n [(ngModel)]="data.staySignedIn"
|
<mat-checkbox i18n (change)="onChangeStaySignedIn($event)"
|
||||||
>Stay signed in</mat-checkbox
|
>Stay signed in</mat-checkbox
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -79,10 +79,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
|||||||
}
|
}
|
||||||
} else if (error.status === StatusCodes.UNAUTHORIZED) {
|
} else if (error.status === StatusCodes.UNAUTHORIZED) {
|
||||||
if (this.webAuthnService.isEnabled()) {
|
if (this.webAuthnService.isEnabled()) {
|
||||||
this.webAuthnService.login().subscribe(({ authToken }) => {
|
this.router.navigate(['/webauthn']);
|
||||||
this.tokenStorageService.saveToken(authToken, false);
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.tokenStorageService.signOut();
|
this.tokenStorageService.signOut();
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
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';
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -14,6 +18,7 @@ export class AuthPageComponent implements OnInit {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
private settingsStorageService: SettingsStorageService,
|
||||||
private tokenStorageService: TokenStorageService
|
private tokenStorageService: TokenStorageService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -23,7 +28,10 @@ export class AuthPageComponent implements OnInit {
|
|||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.route.params.subscribe((params) => {
|
this.route.params.subscribe((params) => {
|
||||||
const jwt = params['jwt'];
|
const jwt = params['jwt'];
|
||||||
this.tokenStorageService.saveToken(jwt);
|
this.tokenStorageService.saveToken(
|
||||||
|
jwt,
|
||||||
|
this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true'
|
||||||
|
);
|
||||||
|
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
});
|
});
|
||||||
|
@ -26,8 +26,7 @@ export class LandingPageComponent implements OnDestroy, OnInit {
|
|||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private tokenStorageService: TokenStorageService,
|
private tokenStorageService: TokenStorageService
|
||||||
private webAuthnService: WebAuthnService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -257,7 +256,7 @@ export class LandingPageComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setToken(aToken: string) {
|
public setToken(aToken: string) {
|
||||||
this.tokenStorageService.saveToken(aToken);
|
this.tokenStorageService.saveToken(aToken, true);
|
||||||
|
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
}
|
}
|
||||||
|
@ -78,19 +78,13 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
dialogRef.afterClosed().subscribe((data) => {
|
dialogRef.afterClosed().subscribe((data) => {
|
||||||
if (data?.authToken) {
|
if (data?.authToken) {
|
||||||
this.tokenStorageService.saveToken(authToken);
|
this.tokenStorageService.saveToken(authToken, true);
|
||||||
|
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public setToken(aToken: string) {
|
|
||||||
this.tokenStorageService.saveToken(aToken);
|
|
||||||
|
|
||||||
this.router.navigate(['/']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
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();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
27
apps/client/src/app/pages/webauthn/webauthn-page.html
Normal file
27
apps/client/src/app/pages/webauthn/webauthn-page.html
Normal file
@ -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>
|
20
apps/client/src/app/pages/webauthn/webauthn-page.module.ts
Normal file
20
apps/client/src/app/pages/webauthn/webauthn-page.module.ts
Normal file
@ -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';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
export const RANGE = 'range';
|
export const RANGE = 'range';
|
||||||
|
export const STAY_SIGNED_IN = 'staySignedIn';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
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) {
|
if (staySignedIn) {
|
||||||
window.localStorage.setItem(TOKEN_KEY, token);
|
window.localStorage.setItem(TOKEN_KEY, token);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user