mirror of https://github.com/ghostfolio/ghostfolio
				
				
			
			
			
				Browse Source
			
			
			
			
				
		* Move device registration to account page * Replace the token login with a WebAuthn prompt if the current device has been registered * Mark the current device in the list of registered auth devicespull/82/head
							committed by
							
								
								Thomas
							
						
					
				
				 32 changed files with 536 additions and 307 deletions
			
			
		@ -0,0 +1,187 @@ | 
				
			|||
export interface AuthenticatorAssertionResponse extends AuthenticatorResponse { | 
				
			|||
    readonly authenticatorData: ArrayBuffer; | 
				
			|||
    readonly signature: ArrayBuffer; | 
				
			|||
    readonly userHandle: ArrayBuffer | null; | 
				
			|||
} | 
				
			|||
export interface AuthenticatorAttestationResponse extends AuthenticatorResponse { | 
				
			|||
    readonly attestationObject: ArrayBuffer; | 
				
			|||
} | 
				
			|||
export interface AuthenticationExtensionsClientInputs { | 
				
			|||
    appid?: string; | 
				
			|||
    appidExclude?: string; | 
				
			|||
    credProps?: boolean; | 
				
			|||
    uvm?: boolean; | 
				
			|||
} | 
				
			|||
export interface AuthenticationExtensionsClientOutputs { | 
				
			|||
    appid?: boolean; | 
				
			|||
    credProps?: CredentialPropertiesOutput; | 
				
			|||
    uvm?: UvmEntries; | 
				
			|||
} | 
				
			|||
export interface AuthenticatorSelectionCriteria { | 
				
			|||
    authenticatorAttachment?: AuthenticatorAttachment; | 
				
			|||
    requireResidentKey?: boolean; | 
				
			|||
    residentKey?: ResidentKeyRequirement; | 
				
			|||
    userVerification?: UserVerificationRequirement; | 
				
			|||
} | 
				
			|||
export interface PublicKeyCredential extends Credential { | 
				
			|||
    readonly rawId: ArrayBuffer; | 
				
			|||
    readonly response: AuthenticatorResponse; | 
				
			|||
    getClientExtensionResults(): AuthenticationExtensionsClientOutputs; | 
				
			|||
} | 
				
			|||
export interface PublicKeyCredentialCreationOptions { | 
				
			|||
    attestation?: AttestationConveyancePreference; | 
				
			|||
    authenticatorSelection?: AuthenticatorSelectionCriteria; | 
				
			|||
    challenge: BufferSource; | 
				
			|||
    excludeCredentials?: PublicKeyCredentialDescriptor[]; | 
				
			|||
    extensions?: AuthenticationExtensionsClientInputs; | 
				
			|||
    pubKeyCredParams: PublicKeyCredentialParameters[]; | 
				
			|||
    rp: PublicKeyCredentialRpEntity; | 
				
			|||
    timeout?: number; | 
				
			|||
    user: PublicKeyCredentialUserEntity; | 
				
			|||
} | 
				
			|||
export interface PublicKeyCredentialDescriptor { | 
				
			|||
    id: BufferSource; | 
				
			|||
    transports?: AuthenticatorTransport[]; | 
				
			|||
    type: PublicKeyCredentialType; | 
				
			|||
} | 
				
			|||
export interface PublicKeyCredentialParameters { | 
				
			|||
    alg: COSEAlgorithmIdentifier; | 
				
			|||
    type: PublicKeyCredentialType; | 
				
			|||
} | 
				
			|||
export interface PublicKeyCredentialRequestOptions { | 
				
			|||
    allowCredentials?: PublicKeyCredentialDescriptor[]; | 
				
			|||
    challenge: BufferSource; | 
				
			|||
    extensions?: AuthenticationExtensionsClientInputs; | 
				
			|||
    rpId?: string; | 
				
			|||
    timeout?: number; | 
				
			|||
    userVerification?: UserVerificationRequirement; | 
				
			|||
} | 
				
			|||
export interface PublicKeyCredentialUserEntity extends PublicKeyCredentialEntity { | 
				
			|||
    displayName: string; | 
				
			|||
    id: BufferSource; | 
				
			|||
} | 
				
			|||
export interface AuthenticatorResponse { | 
				
			|||
    readonly clientDataJSON: ArrayBuffer; | 
				
			|||
} | 
				
			|||
export interface CredentialPropertiesOutput { | 
				
			|||
    rk?: boolean; | 
				
			|||
} | 
				
			|||
export interface Credential { | 
				
			|||
    readonly id: string; | 
				
			|||
    readonly type: string; | 
				
			|||
} | 
				
			|||
export interface PublicKeyCredentialRpEntity extends PublicKeyCredentialEntity { | 
				
			|||
    id?: string; | 
				
			|||
} | 
				
			|||
export interface PublicKeyCredentialEntity { | 
				
			|||
    name: string; | 
				
			|||
} | 
				
			|||
export declare type AttestationConveyancePreference = "direct" | "enterprise" | "indirect" | "none"; | 
				
			|||
export declare type AuthenticatorTransport = "ble" | "internal" | "nfc" | "usb"; | 
				
			|||
export declare type COSEAlgorithmIdentifier = number; | 
				
			|||
export declare type UserVerificationRequirement = "discouraged" | "preferred" | "required"; | 
				
			|||
export declare type UvmEntries = UvmEntry[]; | 
				
			|||
export declare type AuthenticatorAttachment = "cross-platform" | "platform"; | 
				
			|||
export declare type ResidentKeyRequirement = "discouraged" | "preferred" | "required"; | 
				
			|||
export declare type BufferSource = ArrayBufferView | ArrayBuffer; | 
				
			|||
export declare type PublicKeyCredentialType = "public-key"; | 
				
			|||
export declare type UvmEntry = number[]; | 
				
			|||
 | 
				
			|||
export interface PublicKeyCredentialCreationOptionsJSON extends Omit<PublicKeyCredentialCreationOptions, 'challenge' | 'user' | 'excludeCredentials'> { | 
				
			|||
  user: PublicKeyCredentialUserEntityJSON; | 
				
			|||
  challenge: Base64URLString; | 
				
			|||
  excludeCredentials: PublicKeyCredentialDescriptorJSON[]; | 
				
			|||
  extensions?: AuthenticationExtensionsClientInputs; | 
				
			|||
} | 
				
			|||
/** | 
				
			|||
 * A variant of PublicKeyCredentialRequestOptions suitable for JSON transmission to the browser to | 
				
			|||
 * (eventually) get passed into navigator.credentials.get(...) in the browser. | 
				
			|||
 */ | 
				
			|||
export interface PublicKeyCredentialRequestOptionsJSON extends Omit<PublicKeyCredentialRequestOptions, 'challenge' | 'allowCredentials'> { | 
				
			|||
  challenge: Base64URLString; | 
				
			|||
  allowCredentials?: PublicKeyCredentialDescriptorJSON[]; | 
				
			|||
  extensions?: AuthenticationExtensionsClientInputs; | 
				
			|||
} | 
				
			|||
export interface PublicKeyCredentialDescriptorJSON extends Omit<PublicKeyCredentialDescriptor, 'id'> { | 
				
			|||
  id: Base64URLString; | 
				
			|||
} | 
				
			|||
export interface PublicKeyCredentialUserEntityJSON extends Omit<PublicKeyCredentialUserEntity, 'id'> { | 
				
			|||
  id: string; | 
				
			|||
} | 
				
			|||
/** | 
				
			|||
 * The value returned from navigator.credentials.create() | 
				
			|||
 */ | 
				
			|||
export interface AttestationCredential extends PublicKeyCredential { | 
				
			|||
  response: AuthenticatorAttestationResponseFuture; | 
				
			|||
} | 
				
			|||
/** | 
				
			|||
 * A slightly-modified AttestationCredential to simplify working with ArrayBuffers that | 
				
			|||
 * are Base64URL-encoded in the browser so that they can be sent as JSON to the server. | 
				
			|||
 */ | 
				
			|||
export interface AttestationCredentialJSON extends Omit<AttestationCredential, 'response' | 'rawId' | 'getClientExtensionResults'> { | 
				
			|||
  rawId: Base64URLString; | 
				
			|||
  response: AuthenticatorAttestationResponseJSON; | 
				
			|||
  clientExtensionResults: AuthenticationExtensionsClientOutputs; | 
				
			|||
  transports?: AuthenticatorTransport[]; | 
				
			|||
} | 
				
			|||
/** | 
				
			|||
 * The value returned from navigator.credentials.get() | 
				
			|||
 */ | 
				
			|||
export interface AssertionCredential extends PublicKeyCredential { | 
				
			|||
  response: AuthenticatorAssertionResponse; | 
				
			|||
} | 
				
			|||
/** | 
				
			|||
 * A slightly-modified AssertionCredential to simplify working with ArrayBuffers that | 
				
			|||
 * are Base64URL-encoded in the browser so that they can be sent as JSON to the server. | 
				
			|||
 */ | 
				
			|||
export interface AssertionCredentialJSON extends Omit<AssertionCredential, 'response' | 'rawId' | 'getClientExtensionResults'> { | 
				
			|||
  rawId: Base64URLString; | 
				
			|||
  response: AuthenticatorAssertionResponseJSON; | 
				
			|||
  clientExtensionResults: AuthenticationExtensionsClientOutputs; | 
				
			|||
} | 
				
			|||
/** | 
				
			|||
 * A slightly-modified AuthenticatorAttestationResponse to simplify working with ArrayBuffers that | 
				
			|||
 * are Base64URL-encoded in the browser so that they can be sent as JSON to the server. | 
				
			|||
 */ | 
				
			|||
export interface AuthenticatorAttestationResponseJSON extends Omit<AuthenticatorAttestationResponseFuture, 'clientDataJSON' | 'attestationObject'> { | 
				
			|||
  clientDataJSON: Base64URLString; | 
				
			|||
  attestationObject: Base64URLString; | 
				
			|||
} | 
				
			|||
/** | 
				
			|||
 * A slightly-modified AuthenticatorAssertionResponse to simplify working with ArrayBuffers that | 
				
			|||
 * are Base64URL-encoded in the browser so that they can be sent as JSON to the server. | 
				
			|||
 */ | 
				
			|||
export interface AuthenticatorAssertionResponseJSON extends Omit<AuthenticatorAssertionResponse, 'authenticatorData' | 'clientDataJSON' | 'signature' | 'userHandle'> { | 
				
			|||
  authenticatorData: Base64URLString; | 
				
			|||
  clientDataJSON: Base64URLString; | 
				
			|||
  signature: Base64URLString; | 
				
			|||
  userHandle?: string; | 
				
			|||
} | 
				
			|||
/** | 
				
			|||
 * A WebAuthn-compatible device and the information needed to verify assertions by it | 
				
			|||
 */ | 
				
			|||
export declare type AuthenticatorDevice = { | 
				
			|||
  credentialPublicKey: Buffer; | 
				
			|||
  credentialID: Buffer; | 
				
			|||
  counter: number; | 
				
			|||
  transports?: AuthenticatorTransport[]; | 
				
			|||
}; | 
				
			|||
/** | 
				
			|||
 * An attempt to communicate that this isn't just any string, but a Base64URL-encoded string | 
				
			|||
 */ | 
				
			|||
export declare type Base64URLString = string; | 
				
			|||
/** | 
				
			|||
 * AuthenticatorAttestationResponse in TypeScript's DOM lib is outdated (up through v3.9.7). | 
				
			|||
 * Maintain an augmented version here so we can implement additional properties as the WebAuthn | 
				
			|||
 * spec evolves. | 
				
			|||
 * | 
				
			|||
 * See https://www.w3.org/TR/webauthn-2/#iface-authenticatorattestationresponse
 | 
				
			|||
 * | 
				
			|||
 * Properties marked optional are not supported in all browsers. | 
				
			|||
 */ | 
				
			|||
export interface AuthenticatorAttestationResponseFuture extends AuthenticatorAttestationResponse { | 
				
			|||
  getTransports?: () => AuthenticatorTransport[]; | 
				
			|||
  getAuthenticatorData?: () => ArrayBuffer; | 
				
			|||
  getPublicKey?: () => ArrayBuffer; | 
				
			|||
  getPublicKeyAlgorithm?: () => COSEAlgorithmIdentifier[]; | 
				
			|||
} | 
				
			|||
@ -1,14 +0,0 @@ | 
				
			|||
import { NgModule } from '@angular/core'; | 
				
			|||
import { RouterModule, Routes } from '@angular/router'; | 
				
			|||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; | 
				
			|||
import { AuthDevicesPageComponent } from '@ghostfolio/client/pages/auth-devices/auth-devices-page.component'; | 
				
			|||
 | 
				
			|||
const routes: Routes = [ | 
				
			|||
  { path: '', component: AuthDevicesPageComponent, canActivate: [AuthGuard] } | 
				
			|||
]; | 
				
			|||
 | 
				
			|||
@NgModule({ | 
				
			|||
  imports: [RouterModule.forChild(routes)], | 
				
			|||
  exports: [RouterModule] | 
				
			|||
}) | 
				
			|||
export class AuthDevicesPageRoutingModule {} | 
				
			|||
@ -1,46 +0,0 @@ | 
				
			|||
<div class='container'> | 
				
			|||
  <div class='row'> | 
				
			|||
    <div class='col'> | 
				
			|||
      <h3 class='mb-3 text-center' i18n>WebAuthn</h3> | 
				
			|||
      <mat-card class='mb-3'> | 
				
			|||
        <mat-card-content> | 
				
			|||
          <div class='row mb-3'> | 
				
			|||
            <div class='col'> | 
				
			|||
              <gf-auth-device-settings [authDevices]='authDevices$ | async' | 
				
			|||
                                       (authDeviceDeleted)='deleteAuthDevice($event)' | 
				
			|||
                                       (authDeviceToUpdate)='updateAuthDevice($event)' | 
				
			|||
              ></gf-auth-device-settings> | 
				
			|||
            </div> | 
				
			|||
          </div> | 
				
			|||
          <div class='row mb-3'> | 
				
			|||
            <div class='col'> | 
				
			|||
              <button | 
				
			|||
                class='d-inline-block' | 
				
			|||
                color='primary' | 
				
			|||
                i18n | 
				
			|||
                mat-flat-button | 
				
			|||
                (click)='startWebAuthn()' | 
				
			|||
              > | 
				
			|||
                Add this device | 
				
			|||
              </button> | 
				
			|||
            </div> | 
				
			|||
 | 
				
			|||
          </div> | 
				
			|||
          <div class='row'> | 
				
			|||
            <div class='col'> | 
				
			|||
              <button | 
				
			|||
                class='d-inline-block' | 
				
			|||
                color='primary' | 
				
			|||
                i18n | 
				
			|||
                mat-flat-button | 
				
			|||
                (click)='verifyWebAuthn()' | 
				
			|||
              > | 
				
			|||
                DEBUG: verify WebAuthn | 
				
			|||
              </button> | 
				
			|||
            </div> | 
				
			|||
          </div> | 
				
			|||
        </mat-card-content> | 
				
			|||
      </mat-card> | 
				
			|||
    </div> | 
				
			|||
  </div> | 
				
			|||
</div> | 
				
			|||
@ -1,115 +0,0 @@ | 
				
			|||
import { Component, OnDestroy, OnInit } from '@angular/core'; | 
				
			|||
import { startAssertion, startAttestation } from '@simplewebauthn/browser'; | 
				
			|||
import { filter, switchMap, takeUntil } from 'rxjs/operators'; | 
				
			|||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; | 
				
			|||
import { HttpClient } from '@angular/common/http'; | 
				
			|||
import { MatDialog } from '@angular/material/dialog'; | 
				
			|||
import { ReplaySubject, Subject } from 'rxjs'; | 
				
			|||
import { AuthDeviceDto } from '@ghostfolio/api/app/auth-device/auth-device.dto'; | 
				
			|||
import { DataService } from '@ghostfolio/client/services/data.service'; | 
				
			|||
import { AuthDeviceDialog } from '@ghostfolio/client/pages/auth-devices/auth-device-dialog/auth-device-dialog.component'; | 
				
			|||
import { isNonNull } from '@ghostfolio/client/util/rxjs.util'; | 
				
			|||
 | 
				
			|||
@Component({ | 
				
			|||
  selector: 'gf-auth-devices-page', | 
				
			|||
  templateUrl: './auth-devices-page.component.html', | 
				
			|||
  styleUrls: ['./auth-devices-page.component.scss'] | 
				
			|||
}) | 
				
			|||
export class AuthDevicesPageComponent implements OnDestroy, OnInit { | 
				
			|||
 | 
				
			|||
  public authDevices$: ReplaySubject<AuthDeviceDto[]> = new ReplaySubject(1); | 
				
			|||
 | 
				
			|||
  private unsubscribeSubject = new Subject<void>(); | 
				
			|||
 | 
				
			|||
 | 
				
			|||
  constructor( | 
				
			|||
    private dataService: DataService, | 
				
			|||
    private tokenStorageService: TokenStorageService, | 
				
			|||
    private http: HttpClient, | 
				
			|||
    private dialog: MatDialog | 
				
			|||
  ) { | 
				
			|||
    this.fetchAuthDevices(); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public ngOnInit() { | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public ngOnDestroy() { | 
				
			|||
    this.unsubscribeSubject.next(); | 
				
			|||
    this.unsubscribeSubject.complete(); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public startWebAuthn() { | 
				
			|||
    this.http.get<any>(`/api/auth/webauthn/generate-attestation-options`, {}) | 
				
			|||
      .pipe( | 
				
			|||
        switchMap(attOps => { | 
				
			|||
          return startAttestation(attOps); | 
				
			|||
        }), | 
				
			|||
        switchMap(attResp => { | 
				
			|||
          const dialogRef = this.dialog.open(AuthDeviceDialog, { | 
				
			|||
            data: { | 
				
			|||
              authDevice: {} | 
				
			|||
            } | 
				
			|||
          }); | 
				
			|||
          return dialogRef.afterClosed().pipe(switchMap(data => { | 
				
			|||
            const reqBody = { | 
				
			|||
              ...attResp, | 
				
			|||
              deviceName: data.authDevice.name | 
				
			|||
            }; | 
				
			|||
            return this.http.post<any>(`/api/auth/webauthn/verify-attestation`, reqBody); | 
				
			|||
          })); | 
				
			|||
        }) | 
				
			|||
      ) | 
				
			|||
      .subscribe(() => { | 
				
			|||
        this.fetchAuthDevices(); | 
				
			|||
      }); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public verifyWebAuthn() { | 
				
			|||
    this.http.get<any>(`/api/auth/webauthn/generate-assertion-options`, {}) | 
				
			|||
      .pipe( | 
				
			|||
        switchMap(startAssertion), | 
				
			|||
        switchMap(assertionResponse => this.http.post<any>(`/api/auth/webauthn/verify-assertion`, assertionResponse)) | 
				
			|||
      ) | 
				
			|||
      .subscribe(res => { | 
				
			|||
        if (res?.verified) alert('success'); | 
				
			|||
        else alert('fail'); | 
				
			|||
      }); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public deleteAuthDevice(aId: string) { | 
				
			|||
    this.dataService.deleteAuthDevice(aId).subscribe({ | 
				
			|||
      next: () => { | 
				
			|||
        this.fetchAuthDevices(); | 
				
			|||
      } | 
				
			|||
    }); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public updateAuthDevice(aAuthDevice: AuthDeviceDto) { | 
				
			|||
    const dialogRef = this.dialog.open(AuthDeviceDialog, { | 
				
			|||
      data: { | 
				
			|||
        authDevice: aAuthDevice | 
				
			|||
      } | 
				
			|||
    }); | 
				
			|||
 | 
				
			|||
    dialogRef.afterClosed() | 
				
			|||
      .pipe( | 
				
			|||
        filter(isNonNull), | 
				
			|||
        switchMap(data => this.dataService.updateAuthDevice(data.authDevice)) | 
				
			|||
      ) | 
				
			|||
      .subscribe({ | 
				
			|||
        next: () => { | 
				
			|||
          this.fetchAuthDevices(); | 
				
			|||
        } | 
				
			|||
      }); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  private fetchAuthDevices() { | 
				
			|||
    this.dataService | 
				
			|||
      .fetchAuthDevices() | 
				
			|||
      .pipe(takeUntil(this.unsubscribeSubject)) | 
				
			|||
      .subscribe(authDevices => { | 
				
			|||
        this.authDevices$.next(authDevices); | 
				
			|||
      }); | 
				
			|||
  } | 
				
			|||
} | 
				
			|||
@ -1,37 +0,0 @@ | 
				
			|||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; | 
				
			|||
import { CommonModule } from '@angular/common'; | 
				
			|||
import { AuthDevicesPageRoutingModule } from '@ghostfolio/client/pages/auth-devices/auth-devices-page-routing.module'; | 
				
			|||
import { AuthDevicesPageComponent } from '@ghostfolio/client/pages/auth-devices/auth-devices-page.component'; | 
				
			|||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | 
				
			|||
import { GfAuthDeviceSettingsModule } from '@ghostfolio/client/components/auth-device-settings/auth-device-settings.module'; | 
				
			|||
import { MatCardModule } from '@angular/material/card'; | 
				
			|||
import { MatFormFieldModule } from '@angular/material/form-field'; | 
				
			|||
import { MatInputModule } from '@angular/material/input'; | 
				
			|||
import { MatSelectModule } from '@angular/material/select'; | 
				
			|||
import { MatDialogModule } from '@angular/material/dialog'; | 
				
			|||
import { MatButtonModule } from '@angular/material/button'; | 
				
			|||
import { AuthDeviceDialog } from '@ghostfolio/client/pages/auth-devices/auth-device-dialog/auth-device-dialog.component'; | 
				
			|||
 | 
				
			|||
 | 
				
			|||
 | 
				
			|||
@NgModule({ | 
				
			|||
  declarations: [ | 
				
			|||
    AuthDevicesPageComponent, | 
				
			|||
    AuthDeviceDialog, | 
				
			|||
  ], | 
				
			|||
  imports: [ | 
				
			|||
    CommonModule, | 
				
			|||
    AuthDevicesPageRoutingModule, | 
				
			|||
    FormsModule, | 
				
			|||
    GfAuthDeviceSettingsModule, | 
				
			|||
    MatCardModule, | 
				
			|||
    MatFormFieldModule, | 
				
			|||
    MatInputModule, | 
				
			|||
    MatSelectModule, | 
				
			|||
    MatDialogModule, | 
				
			|||
    ReactiveFormsModule, | 
				
			|||
    MatButtonModule | 
				
			|||
  ], | 
				
			|||
  schemas: [CUSTOM_ELEMENTS_SCHEMA], | 
				
			|||
}) | 
				
			|||
export class AuthDevicesPageModule { } | 
				
			|||
@ -0,0 +1,90 @@ | 
				
			|||
import { HttpClient } from '@angular/common/http'; | 
				
			|||
import { Injectable } from '@angular/core'; | 
				
			|||
import { switchMap, tap } from 'rxjs/operators'; | 
				
			|||
import { startAssertion, startAttestation } from '@simplewebauthn/browser'; | 
				
			|||
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service'; | 
				
			|||
import { | 
				
			|||
  PublicKeyCredentialCreationOptionsJSON, | 
				
			|||
  PublicKeyCredentialRequestOptionsJSON | 
				
			|||
} from '@ghostfolio/api/app/auth/interfaces/simplewebauthn'; | 
				
			|||
import { DataService } from '@ghostfolio/client/services/data.service'; | 
				
			|||
import { AuthDeviceDto } from '@ghostfolio/api/app/auth-device/auth-device.dto'; | 
				
			|||
 | 
				
			|||
@Injectable({ | 
				
			|||
  providedIn: 'root' | 
				
			|||
}) | 
				
			|||
export class WebAuthnService { | 
				
			|||
 | 
				
			|||
  private static readonly WEB_AUTH_N_USER_ID = 'WEB_AUTH_N_USER_ID'; | 
				
			|||
  private static readonly WEB_AUTH_N_DEVICE_ID = 'WEB_AUTH_N_DEVICE_ID'; | 
				
			|||
 | 
				
			|||
  public constructor( | 
				
			|||
    private dataService: DataService, | 
				
			|||
    private settingsStorageService: SettingsStorageService, | 
				
			|||
    private http: HttpClient, | 
				
			|||
  ) { | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public startWebAuthn() { | 
				
			|||
    return this.http.get<PublicKeyCredentialCreationOptionsJSON>(`/api/auth/webauthn/generate-attestation-options`, {}) | 
				
			|||
      .pipe( | 
				
			|||
        switchMap(attOps => { | 
				
			|||
          return startAttestation(attOps); | 
				
			|||
        }) | 
				
			|||
      ); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public verifyAttestation(attResp, deviceName) { | 
				
			|||
    return this.http.post<AuthDeviceDto>(`/api/auth/webauthn/verify-attestation`, { | 
				
			|||
      credential: attResp, | 
				
			|||
      deviceName: deviceName, | 
				
			|||
    }).pipe(tap(authDevice => | 
				
			|||
      this.dataService.fetchUser().subscribe((user) => { | 
				
			|||
        this.settingsStorageService.setSetting(WebAuthnService.WEB_AUTH_N_DEVICE_ID, authDevice.id); | 
				
			|||
        this.settingsStorageService.setSetting(WebAuthnService.WEB_AUTH_N_USER_ID, user.id); | 
				
			|||
      }) | 
				
			|||
    )); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public verifyWebAuthn() { | 
				
			|||
    const userId = this.settingsStorageService.getSetting(WebAuthnService.WEB_AUTH_N_USER_ID); | 
				
			|||
    return this.http.post<PublicKeyCredentialRequestOptionsJSON>(`/api/auth/webauthn/generate-assertion-options`, {userId}) | 
				
			|||
      .pipe( | 
				
			|||
        switchMap(startAssertion), | 
				
			|||
        switchMap(assertionResponse => { | 
				
			|||
          return this.http.post<{ authToken: string }>(`/api/auth/webauthn/verify-assertion`, { | 
				
			|||
            credential: assertionResponse, | 
				
			|||
            userId | 
				
			|||
          }) | 
				
			|||
        }) | 
				
			|||
      ); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public getCurrentDeviceId() { | 
				
			|||
    return this.settingsStorageService.getSetting(WebAuthnService.WEB_AUTH_N_DEVICE_ID); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public isEnabled() { | 
				
			|||
    return !!this.settingsStorageService.getSetting(WebAuthnService.WEB_AUTH_N_DEVICE_ID); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public fetchAuthDevices() { | 
				
			|||
    return this.http.get<AuthDeviceDto[]>('/api/auth-device'); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public updateAuthDevice(aAuthDevice: AuthDeviceDto) { | 
				
			|||
    return this.http.put<AuthDeviceDto>(`/api/auth-device/${aAuthDevice.id}`, aAuthDevice); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  public deleteAuthDevice(aId: string) { | 
				
			|||
    return this.http.delete<AuthDeviceDto>(`/api/auth-device/${aId}`) | 
				
			|||
      .pipe( | 
				
			|||
        tap(() => { | 
				
			|||
          if (aId === this.getCurrentDeviceId()) { | 
				
			|||
            this.settingsStorageService.removeSetting(WebAuthnService.WEB_AUTH_N_DEVICE_ID); | 
				
			|||
            this.settingsStorageService.removeSetting(WebAuthnService.WEB_AUTH_N_USER_ID); | 
				
			|||
          } | 
				
			|||
        }) | 
				
			|||
      ); | 
				
			|||
  } | 
				
			|||
} | 
				
			|||
					Loading…
					
					
				
		Reference in new issue