chore: generate mfa secret in backend (#2930) [skip e2e]

* chore: get mfa secret from backend

* chore: remove unused code
This commit is contained in:
Antonella Sgarlatta
2025-09-12 14:34:51 -03:00
committed by GitHub
parent d6840ba41c
commit 2338449425
9 changed files with 52 additions and 32 deletions

View File

@@ -1,7 +1,6 @@
export interface MfaServiceInterface { export interface MfaServiceInterface {
isMfaActivated(): Promise<boolean> isMfaActivated(): Promise<boolean>
generateMfaSecret(): Promise<string> generateMfaSecret(): Promise<string>
getOtpToken(secret: string): Promise<string>
enableMfa(secret: string, otpToken: string): Promise<void> enableMfa(secret: string, otpToken: string): Promise<void>
disableMfa(): Promise<void> disableMfa(): Promise<void>
} }

View File

@@ -1229,7 +1229,6 @@ export class Dependencies {
this.factory.set(TYPES.MfaService, () => { this.factory.set(TYPES.MfaService, () => {
return new MfaService( return new MfaService(
this.get<SettingsService>(TYPES.SettingsService), this.get<SettingsService>(TYPES.SettingsService),
this.get<PureCryptoInterface>(TYPES.Crypto),
this.get<FeaturesService>(TYPES.FeaturesService), this.get<FeaturesService>(TYPES.FeaturesService),
this.get<ProtectionsClientInterface>(TYPES.ProtectionService), this.get<ProtectionsClientInterface>(TYPES.ProtectionService),
this.get<EncryptionService>(TYPES.EncryptionService), this.get<EncryptionService>(TYPES.EncryptionService),

View File

@@ -77,7 +77,7 @@ import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { Paths } from './Paths' import { Paths } from './Paths'
import { DiskStorageService } from '../Storage/DiskStorageService' import { DiskStorageService } from '../Storage/DiskStorageService'
import { UuidString } from '../../Types/UuidString' import { UuidString } from '../../Types/UuidString'
import { SettingsServerInterface } from '../Settings/SettingsServerInterface' import { SettingsServerInterface, MfaSecretResponse } from '../Settings/SettingsServerInterface'
import { Strings } from '@Lib/Strings' import { Strings } from '@Lib/Strings'
import { AnyFeatureDescription } from '@standardnotes/features' import { AnyFeatureDescription } from '@standardnotes/features'
@@ -563,11 +563,13 @@ export class LegacyApiService
settingName: string, settingName: string,
settingValue: string | null, settingValue: string | null,
sensitive: boolean, sensitive: boolean,
totpToken?: string,
): Promise<HttpResponse<UpdateSettingResponse>> { ): Promise<HttpResponse<UpdateSettingResponse>> {
const params = { const params = {
name: settingName, name: settingName,
value: settingValue, value: settingValue,
sensitive: sensitive, sensitive: sensitive,
...(totpToken && { totpToken }),
} }
return this.tokenRefreshableRequest<UpdateSettingResponse>({ return this.tokenRefreshableRequest<UpdateSettingResponse>({
verb: HttpVerb.Put, verb: HttpVerb.Put,
@@ -637,6 +639,15 @@ export class LegacyApiService
}) })
} }
async getMfaSecret(userUuid: UuidString): Promise<HttpResponse<MfaSecretResponse>> {
return this.tokenRefreshableRequest<MfaSecretResponse>({
verb: HttpVerb.Get,
url: joinPaths(this.host, Paths.v1.mfaSecret(userUuid)),
authentication: this.getSessionAccessToken(),
fallbackErrorMessage: 'Failed to get MFA secret.',
})
}
public downloadFeatureUrl(url: string): Promise<HttpResponse> { public downloadFeatureUrl(url: string): Promise<HttpResponse> {
return this.request({ return this.request({
verb: HttpVerb.Get, verb: HttpVerb.Get,

View File

@@ -40,6 +40,7 @@ const ItemsPaths = {
const SettingsPaths = { const SettingsPaths = {
settings: (userUuid: string) => `/v1/users/${userUuid}/settings`, settings: (userUuid: string) => `/v1/users/${userUuid}/settings`,
setting: (userUuid: string, settingName: string) => `/v1/users/${userUuid}/settings/${settingName}`, setting: (userUuid: string, settingName: string) => `/v1/users/${userUuid}/settings/${settingName}`,
mfaSecret: (userUuid: string) => `/v1/users/${userUuid}/mfa-secret`,
subscriptionSetting: (userUuid: string, settingName: string) => subscriptionSetting: (userUuid: string, settingName: string) =>
`/v1/users/${userUuid}/subscription-settings/${settingName}`, `/v1/users/${userUuid}/subscription-settings/${settingName}`,
subscriptionSettings: (userUuid: string) => `/v1/users/${userUuid}/subscription-settings`, subscriptionSettings: (userUuid: string) => `/v1/users/${userUuid}/subscription-settings`,

View File

@@ -1,5 +1,4 @@
import { SettingsService } from '../Settings' import { SettingsService } from '../Settings'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { FeaturesService } from '../Features/FeaturesService' import { FeaturesService } from '../Features/FeaturesService'
import { import {
AbstractService, AbstractService,
@@ -7,7 +6,6 @@ import {
MfaServiceInterface, MfaServiceInterface,
ProtectionsClientInterface, ProtectionsClientInterface,
EncryptionService, EncryptionService,
SignInStrings,
ChallengeValidation, ChallengeValidation,
} from '@standardnotes/services' } from '@standardnotes/services'
import { SettingName } from '@standardnotes/domain-core' import { SettingName } from '@standardnotes/domain-core'
@@ -16,7 +14,6 @@ import { SNRootKeyParams } from '@standardnotes/encryption'
export class MfaService extends AbstractService implements MfaServiceInterface { export class MfaService extends AbstractService implements MfaServiceInterface {
constructor( constructor(
private settingsService: SettingsService, private settingsService: SettingsService,
private crypto: PureCryptoInterface,
private featuresService: FeaturesService, private featuresService: FeaturesService,
private protections: ProtectionsClientInterface, private protections: ProtectionsClientInterface,
private encryption: EncryptionService, private encryption: EncryptionService,
@@ -25,14 +22,6 @@ export class MfaService extends AbstractService implements MfaServiceInterface {
super(internalEventBus) super(internalEventBus)
} }
private async saveMfaSetting(secret: string): Promise<void> {
return await this.settingsService.updateSetting(
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
secret,
true,
)
}
async isMfaActivated(): Promise<boolean> { async isMfaActivated(): Promise<boolean> {
const mfaSetting = await this.settingsService.getDoesSensitiveSettingExist( const mfaSetting = await this.settingsService.getDoesSensitiveSettingExist(
SettingName.create(SettingName.NAMES.MfaSecret).getValue(), SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
@@ -41,21 +30,11 @@ export class MfaService extends AbstractService implements MfaServiceInterface {
} }
async generateMfaSecret(): Promise<string> { async generateMfaSecret(): Promise<string> {
return this.crypto.generateOtpSecret() return this.settingsService.generateMfaSecret()
}
async getOtpToken(secret: string): Promise<string> {
return this.crypto.totpToken(secret, Date.now(), 6, 30)
} }
async enableMfa(secret: string, otpToken: string): Promise<void> { async enableMfa(secret: string, otpToken: string): Promise<void> {
const otpTokenValid = otpToken != undefined && otpToken === (await this.getOtpToken(secret)) return this.settingsService.updateMfaSetting(secret, otpToken)
if (!otpTokenValid) {
throw new Error(SignInStrings.IncorrectMfa)
}
return this.saveMfaSetting(secret)
} }
async disableMfa(): Promise<void> { async disableMfa(): Promise<void> {
@@ -80,7 +59,6 @@ export class MfaService extends AbstractService implements MfaServiceInterface {
override deinit(): void { override deinit(): void {
;(this.settingsService as unknown) = undefined ;(this.settingsService as unknown) = undefined
;(this.crypto as unknown) = undefined
;(this.featuresService as unknown) = undefined ;(this.featuresService as unknown) = undefined
super.deinit() super.deinit()
} }

View File

@@ -42,8 +42,8 @@ export class SettingsService extends AbstractService implements SettingsClientIn
return this.provider.updateSubscriptionSetting(name, payload, sensitive) return this.provider.updateSubscriptionSetting(name, payload, sensitive)
} }
async updateSetting(name: SettingName, payload: string, sensitive = false) { async updateSetting(name: SettingName, payload: string, sensitive = false, totpToken?: string) {
return this.provider.updateSetting(name, payload, sensitive) return this.provider.updateSetting(name, payload, sensitive, totpToken)
} }
async getDoesSensitiveSettingExist(name: SettingName) { async getDoesSensitiveSettingExist(name: SettingName) {
@@ -54,6 +54,19 @@ export class SettingsService extends AbstractService implements SettingsClientIn
return this.provider.deleteSetting(name, serverPassword) return this.provider.deleteSetting(name, serverPassword)
} }
async generateMfaSecret(): Promise<string> {
return this.provider.getMfaSecret()
}
async updateMfaSetting(secret: string, totpToken: string): Promise<void> {
return this.provider.updateSetting(
SettingName.create(SettingName.NAMES.MfaSecret).getValue(),
secret,
true,
totpToken,
)
}
getEmailBackupFrequencyOptionLabel(frequency: EmailBackupFrequency): string { getEmailBackupFrequencyOptionLabel(frequency: EmailBackupFrequency): string {
return this.frequencyOptionsLabels[frequency] return this.frequencyOptionsLabels[frequency]
} }

View File

@@ -9,9 +9,13 @@ export interface SettingsClientInterface {
getDoesSensitiveSettingExist(name: SettingName): Promise<boolean> getDoesSensitiveSettingExist(name: SettingName): Promise<boolean>
updateSetting(name: SettingName, payload: string, sensitive?: boolean): Promise<void> updateSetting(name: SettingName, payload: string, sensitive?: boolean, totpToken?: string): Promise<void>
deleteSetting(name: SettingName, serverPassword?: string): Promise<void> deleteSetting(name: SettingName, serverPassword?: string): Promise<void>
generateMfaSecret(): Promise<string>
updateMfaSetting(secret: string, totpToken: string): Promise<void>
getEmailBackupFrequencyOptionLabel(frequency: EmailBackupFrequency): string getEmailBackupFrequencyOptionLabel(frequency: EmailBackupFrequency): string
} }

View File

@@ -102,8 +102,8 @@ export class SettingsGateway {
return response.data?.success ?? false return response.data?.success ?? false
} }
async updateSetting(name: SettingName, payload: string, sensitive: boolean): Promise<void> { async updateSetting(name: SettingName, payload: string, sensitive: boolean, totpToken?: string): Promise<void> {
const response = await this.settingsApi.updateSetting(this.userUuid, name.value, payload, sensitive) const response = await this.settingsApi.updateSetting(this.userUuid, name.value, payload, sensitive, totpToken)
if (isErrorResponse(response)) { if (isErrorResponse(response)) {
throw new Error(getErrorFromErrorResponse(response).message) throw new Error(getErrorFromErrorResponse(response).message)
} }
@@ -116,6 +116,14 @@ export class SettingsGateway {
} }
} }
async getMfaSecret(): Promise<string> {
const response = await this.settingsApi.getMfaSecret(this.userUuid)
if (isErrorResponse(response)) {
throw new Error(getErrorFromErrorResponse(response).message)
}
return response.data.secret
}
deinit() { deinit() {
;(this.settingsApi as unknown) = undefined ;(this.settingsApi as unknown) = undefined
;(this.userProvider as unknown) = undefined ;(this.userProvider as unknown) = undefined

View File

@@ -7,6 +7,10 @@ import {
} from '@standardnotes/responses' } from '@standardnotes/responses'
import { UuidString } from '@Lib/Types/UuidString' import { UuidString } from '@Lib/Types/UuidString'
export interface MfaSecretResponse {
secret: string
}
export interface SettingsServerInterface { export interface SettingsServerInterface {
listSettings(userUuid: UuidString): Promise<HttpResponse<ListSettingsResponse>> listSettings(userUuid: UuidString): Promise<HttpResponse<ListSettingsResponse>>
@@ -15,6 +19,7 @@ export interface SettingsServerInterface {
settingName: string, settingName: string,
settingValue: string, settingValue: string,
sensitive: boolean, sensitive: boolean,
totpToken?: string,
): Promise<HttpResponse<UpdateSettingResponse>> ): Promise<HttpResponse<UpdateSettingResponse>>
getSetting( getSetting(
@@ -32,6 +37,8 @@ export interface SettingsServerInterface {
sensitive: boolean, sensitive: boolean,
): Promise<HttpResponse<UpdateSettingResponse>> ): Promise<HttpResponse<UpdateSettingResponse>>
getMfaSecret(userUuid: UuidString): Promise<HttpResponse<MfaSecretResponse>>
deleteSetting( deleteSetting(
userUuid: UuidString, userUuid: UuidString,
settingName: string, settingName: string,