feat(dev): add u2f ui for managing devices and signing in (#2182)
* feat: add u2f ui for managing devices and signing in * refactor: change unnecessary useState to derived constant * fix: modal refactor * fix(web): hide u2f under feature trunk * fix(web): jest setup --------- Co-authored-by: Aman Harwara <amanharwara@protonmail.com>
This commit is contained in:
@@ -232,6 +232,7 @@ export class SNApiService
|
||||
email: string
|
||||
mfaKeyPath?: string
|
||||
mfaCode?: string
|
||||
authenticatorResponse?: Record<string, unknown>
|
||||
}): Promise<Responses.KeyParamsResponse | Responses.HttpResponse> {
|
||||
const codeVerifier = this.crypto.generateRandomKey(256)
|
||||
this.inMemoryStore.setValue(StorageKey.CodeVerifier, codeVerifier)
|
||||
@@ -247,6 +248,10 @@ export class SNApiService
|
||||
params[dto.mfaKeyPath] = dto.mfaCode
|
||||
}
|
||||
|
||||
if (dto.authenticatorResponse) {
|
||||
params.authenticator_response = dto.authenticatorResponse
|
||||
}
|
||||
|
||||
return this.request({
|
||||
verb: HttpVerb.Post,
|
||||
url: joinPaths(this.host, Paths.v2.keyParams),
|
||||
|
||||
@@ -93,6 +93,8 @@ export class ChallengeService extends AbstractService implements ChallengeServic
|
||||
return this.protocolService.validateAccountPassword(value.value as string)
|
||||
case ChallengeValidation.Biometric:
|
||||
return { valid: value.value === true }
|
||||
case ChallengeValidation.Authenticator:
|
||||
return { valid: 'id' in (value.value as Record<string, unknown>) }
|
||||
case ChallengeValidation.ProtectionSessionDuration:
|
||||
return { valid: isValidProtectionSessionLength(value.value) }
|
||||
default:
|
||||
|
||||
@@ -48,6 +48,7 @@ import { ChallengeService } from '../Challenge'
|
||||
import {
|
||||
ApiCallError,
|
||||
ErrorMessage,
|
||||
ErrorTag,
|
||||
HttpErrorResponseBody,
|
||||
HttpServiceInterface,
|
||||
UserApiServiceInterface,
|
||||
@@ -284,6 +285,35 @@ export class SNSessionManager
|
||||
return (response as Responses.GetAvailableSubscriptionsResponse).data!
|
||||
}
|
||||
|
||||
private async promptForU2FVerification(username: string): Promise<Record<string, unknown> | undefined> {
|
||||
const challenge = new Challenge(
|
||||
[
|
||||
new ChallengePrompt(
|
||||
ChallengeValidation.Authenticator,
|
||||
ChallengePromptTitle.U2F,
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
username,
|
||||
},
|
||||
),
|
||||
],
|
||||
ChallengeReason.Custom,
|
||||
true,
|
||||
SessionStrings.InputU2FDevice,
|
||||
)
|
||||
|
||||
const response = await this.challengeService.promptForChallengeResponse(challenge)
|
||||
|
||||
if (!response) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return response.values[0].value as Record<string, unknown>
|
||||
}
|
||||
|
||||
private async promptForMfaValue(): Promise<string | undefined> {
|
||||
const challenge = new Challenge(
|
||||
[
|
||||
@@ -344,31 +374,28 @@ export class SNSessionManager
|
||||
return registerResponse.data
|
||||
}
|
||||
|
||||
private async retrieveKeyParams(
|
||||
email: string,
|
||||
mfaKeyPath?: string,
|
||||
mfaCode?: string,
|
||||
): Promise<{
|
||||
private async retrieveKeyParams(dto: {
|
||||
email: string
|
||||
mfaKeyPath?: string
|
||||
mfaCode?: string
|
||||
authenticatorResponse?: Record<string, unknown>
|
||||
}): Promise<{
|
||||
keyParams?: SNRootKeyParams
|
||||
response: Responses.KeyParamsResponse | Responses.HttpResponse
|
||||
mfaKeyPath?: string
|
||||
mfaCode?: string
|
||||
}> {
|
||||
const response = await this.apiService.getAccountKeyParams({
|
||||
email,
|
||||
mfaKeyPath,
|
||||
mfaCode,
|
||||
})
|
||||
const response = await this.apiService.getAccountKeyParams(dto)
|
||||
|
||||
if (response.error || isNullOrUndefined(response.data)) {
|
||||
if (mfaCode) {
|
||||
if (dto.mfaCode) {
|
||||
await this.alertService.alert(SignInStrings.IncorrectMfa)
|
||||
}
|
||||
if (response.error?.payload?.mfa_key) {
|
||||
/** Prompt for MFA code and try again */
|
||||
const inputtedCode = await this.promptForMfaValue()
|
||||
if (!inputtedCode) {
|
||||
/** User dismissed window without input */
|
||||
|
||||
if ([ErrorTag.U2FRequired, ErrorTag.MfaRequired].includes(response.error?.tag as ErrorTag)) {
|
||||
const isU2FRequired = response.error?.tag === ErrorTag.U2FRequired
|
||||
const result = isU2FRequired ? await this.promptForU2FVerification(dto.email) : await this.promptForMfaValue()
|
||||
if (!result) {
|
||||
return {
|
||||
response: this.apiService.createErrorResponse(
|
||||
SignInStrings.SignInCanceledMissingMfa,
|
||||
@@ -376,19 +403,25 @@ export class SNSessionManager
|
||||
),
|
||||
}
|
||||
}
|
||||
return this.retrieveKeyParams(email, response.error.payload.mfa_key, inputtedCode)
|
||||
|
||||
return this.retrieveKeyParams({
|
||||
email: dto.email,
|
||||
mfaKeyPath: isU2FRequired ? undefined : response.error?.payload?.mfa_key,
|
||||
mfaCode: isU2FRequired ? undefined : (result as string),
|
||||
authenticatorResponse: isU2FRequired ? (result as Record<string, unknown>) : undefined,
|
||||
})
|
||||
} else {
|
||||
return { response }
|
||||
}
|
||||
}
|
||||
/** Make sure to use client value for identifier/email */
|
||||
const keyParams = KeyParamsFromApiResponse(response as Responses.KeyParamsResponse, email)
|
||||
const keyParams = KeyParamsFromApiResponse(response as Responses.KeyParamsResponse, dto.email)
|
||||
if (!keyParams || !keyParams.version) {
|
||||
return {
|
||||
response: this.apiService.createErrorResponse(API_MESSAGE_FALLBACK_LOGIN_FAIL),
|
||||
}
|
||||
}
|
||||
return { keyParams, response, mfaKeyPath, mfaCode }
|
||||
return { keyParams, response, mfaKeyPath: dto.mfaKeyPath, mfaCode: dto.mfaCode }
|
||||
}
|
||||
|
||||
public async signIn(
|
||||
@@ -425,7 +458,9 @@ export class SNSessionManager
|
||||
ephemeral = false,
|
||||
minAllowedVersion?: Common.ProtocolVersion,
|
||||
): Promise<SessionManagerResponse> {
|
||||
const paramsResult = await this.retrieveKeyParams(email)
|
||||
const paramsResult = await this.retrieveKeyParams({
|
||||
email,
|
||||
})
|
||||
if (paramsResult.response.error) {
|
||||
return {
|
||||
response: paramsResult.response,
|
||||
|
||||
Reference in New Issue
Block a user