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