Browse Source

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 4 years ago
committed by GitHub
parent
commit
6c1119caec
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 7
      apps/api/src/app/auth/web-auth.service.ts
  3. 7
      apps/client/src/app/app-routing.module.ts
  4. 17
      apps/client/src/app/components/header/header.component.ts
  5. 17
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts
  6. 2
      apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html
  7. 5
      apps/client/src/app/core/http-response.interceptor.ts
  8. 10
      apps/client/src/app/pages/auth/auth-page.component.ts
  9. 5
      apps/client/src/app/pages/landing/landing-page.component.ts
  10. 8
      apps/client/src/app/pages/register/register-page.component.ts
  11. 11
      apps/client/src/app/pages/webauthn/webauthn-page-routing.module.ts
  12. 46
      apps/client/src/app/pages/webauthn/webauthn-page.component.ts
  13. 27
      apps/client/src/app/pages/webauthn/webauthn-page.html
  14. 20
      apps/client/src/app/pages/webauthn/webauthn-page.module.ts
  15. 0
      apps/client/src/app/pages/webauthn/webauthn-page.scss
  16. 1
      apps/client/src/app/services/settings-storage.service.ts
  17. 2
      apps/client/src/app/services/token-storage.service.ts

6
CHANGELOG.md

@ -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

7
apps/api/src/app/auth/web-auth.service.ts

@ -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',

7
apps/client/src/app/app-routing.module.ts

@ -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: () =>

17
apps/client/src/app/components/header/header.component.ts

@ -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(['/']);
} }

17
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.component.ts

@ -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();
} }
} }

2
apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html

@ -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>

5
apps/client/src/app/core/http-response.interceptor.ts

@ -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();
} }

10
apps/client/src/app/pages/auth/auth-page.component.ts

@ -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(['/']);
}); });

5
apps/client/src/app/pages/landing/landing-page.component.ts

@ -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(['/']);
} }

8
apps/client/src/app/pages/register/register-page.component.ts

@ -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();

11
apps/client/src/app/pages/webauthn/webauthn-page-routing.module.ts

@ -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 {}

46
apps/client/src/app/pages/webauthn/webauthn-page.component.ts

@ -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

@ -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

@ -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 {}

0
apps/client/src/app/pages/webauthn/webauthn-page.scss

1
apps/client/src/app/services/settings-storage.service.ts

@ -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'

2
apps/client/src/app/services/token-storage.service.ts

@ -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…
Cancel
Save