mirror of
https://github.com/standardnotes/server
synced 2026-01-16 20:04:32 -05:00
fix(auth): disallow adding u2f devices if a user does not have 2fa enabled
This commit is contained in:
@@ -573,6 +573,7 @@ export class ContainerConfigLoader {
|
||||
new GenerateAuthenticatorRegistrationOptions(
|
||||
container.get(TYPES.AuthenticatorRepository),
|
||||
container.get(TYPES.AuthenticatorChallengeRepository),
|
||||
container.get(TYPES.SettingService),
|
||||
container.get(TYPES.U2F_RELYING_PARTY_NAME),
|
||||
container.get(TYPES.U2F_RELYING_PARTY_ID),
|
||||
),
|
||||
@@ -583,6 +584,7 @@ export class ContainerConfigLoader {
|
||||
new VerifyAuthenticatorRegistrationResponse(
|
||||
container.get(TYPES.AuthenticatorRepository),
|
||||
container.get(TYPES.AuthenticatorChallengeRepository),
|
||||
container.get(TYPES.SettingService),
|
||||
container.get(TYPES.U2F_RELYING_PARTY_ID),
|
||||
container.get(TYPES.U2F_EXPECTED_ORIGIN),
|
||||
container.get(TYPES.U2F_REQUIRE_USER_VERIFICATION),
|
||||
|
||||
@@ -4,16 +4,20 @@ import { Authenticator } from '../../Authenticator/Authenticator'
|
||||
import { AuthenticatorChallenge } from '../../Authenticator/AuthenticatorChallenge'
|
||||
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
|
||||
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
|
||||
import { Setting } from '../../Setting/Setting'
|
||||
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
|
||||
import { GenerateAuthenticatorRegistrationOptions } from './GenerateAuthenticatorRegistrationOptions'
|
||||
|
||||
describe('GenerateAuthenticatorRegistrationOptions', () => {
|
||||
let authenticatorRepository: AuthenticatorRepositoryInterface
|
||||
let authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface
|
||||
let settingService: SettingServiceInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new GenerateAuthenticatorRegistrationOptions(
|
||||
authenticatorRepository,
|
||||
authenticatorChallengeRepository,
|
||||
settingService,
|
||||
'Standard Notes',
|
||||
'standardnotes.com',
|
||||
)
|
||||
@@ -36,6 +40,11 @@ describe('GenerateAuthenticatorRegistrationOptions', () => {
|
||||
|
||||
authenticatorChallengeRepository = {} as jest.Mocked<AuthenticatorChallengeRepositoryInterface>
|
||||
authenticatorChallengeRepository.save = jest.fn()
|
||||
|
||||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: 'secret',
|
||||
} as jest.Mocked<Setting>)
|
||||
})
|
||||
|
||||
it('should return error if userUuid is invalid', async () => {
|
||||
@@ -52,6 +61,40 @@ describe('GenerateAuthenticatorRegistrationOptions', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should return error if user does not have 2FA enabled', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: null,
|
||||
} as jest.Mocked<Setting>)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
username: 'username',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe(
|
||||
'Could not verify authenticator registration response: Fallback 2FA not enabled for user.',
|
||||
)
|
||||
})
|
||||
|
||||
it('should return error if user has 2FA disabled', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
username: 'username',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe(
|
||||
'Could not verify authenticator registration response: Fallback 2FA not enabled for user.',
|
||||
)
|
||||
})
|
||||
|
||||
it('should return error if username is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
|
||||
@@ -5,11 +5,14 @@ import { GenerateAuthenticatorRegistrationOptionsDTO } from './GenerateAuthentic
|
||||
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
|
||||
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
|
||||
import { AuthenticatorChallenge } from '../../Authenticator/AuthenticatorChallenge'
|
||||
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
|
||||
export class GenerateAuthenticatorRegistrationOptions implements UseCaseInterface<Record<string, unknown>> {
|
||||
constructor(
|
||||
private authenticatorRepository: AuthenticatorRepositoryInterface,
|
||||
private authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface,
|
||||
private settingService: SettingServiceInterface,
|
||||
private relyingPartyName: string,
|
||||
private relyingPartyId: string,
|
||||
) {}
|
||||
@@ -21,6 +24,15 @@ export class GenerateAuthenticatorRegistrationOptions implements UseCaseInterfac
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const mfaSecret = await this.settingService.findSettingWithDecryptedValue({
|
||||
userUuid: userUuid.value,
|
||||
settingName: SettingName.MfaSecret,
|
||||
})
|
||||
const twoFactorEnabled = mfaSecret !== null && mfaSecret.value !== null
|
||||
if (!twoFactorEnabled) {
|
||||
return Result.fail('Could not verify authenticator registration response: Fallback 2FA not enabled for user.')
|
||||
}
|
||||
|
||||
const usernameOrError = Username.create(dto.username)
|
||||
if (usernameOrError.isFailed()) {
|
||||
return Result.fail(`Could not generate authenticator registration options: ${usernameOrError.getError()}`)
|
||||
|
||||
@@ -6,16 +6,20 @@ import { Authenticator } from '../../Authenticator/Authenticator'
|
||||
import { AuthenticatorChallenge } from '../../Authenticator/AuthenticatorChallenge'
|
||||
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
|
||||
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
|
||||
import { Setting } from '../../Setting/Setting'
|
||||
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
|
||||
import { VerifyAuthenticatorRegistrationResponse } from './VerifyAuthenticatorRegistrationResponse'
|
||||
|
||||
describe('VerifyAuthenticatorRegistrationResponse', () => {
|
||||
let authenticatorRepository: AuthenticatorRepositoryInterface
|
||||
let authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface
|
||||
let settingService: SettingServiceInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new VerifyAuthenticatorRegistrationResponse(
|
||||
authenticatorRepository,
|
||||
authenticatorChallengeRepository,
|
||||
settingService,
|
||||
'standardnotes.com',
|
||||
['localhost', 'https://app.standardnotes.com'],
|
||||
true,
|
||||
@@ -31,6 +35,11 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
|
||||
challenge: Buffer.from('challenge'),
|
||||
},
|
||||
} as jest.Mocked<AuthenticatorChallenge>)
|
||||
|
||||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: 'secret',
|
||||
} as jest.Mocked<Setting>)
|
||||
})
|
||||
|
||||
it('should return error if user uuid is invalid', async () => {
|
||||
@@ -56,6 +65,58 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should return error if user does not have 2FA enabled', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
name: 'name',
|
||||
attestationResponse: {
|
||||
id: Buffer.from('id'),
|
||||
rawId: Buffer.from('rawId'),
|
||||
response: {
|
||||
attestationObject: Buffer.from('attestationObject'),
|
||||
clientDataJSON: Buffer.from('clientDataJSON'),
|
||||
},
|
||||
type: 'type',
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual(
|
||||
'Could not verify authenticator registration response: Fallback 2FA not enabled for user.',
|
||||
)
|
||||
})
|
||||
|
||||
it('should return error if user has 2FA disabled', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: null,
|
||||
} as jest.Mocked<Setting>)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
name: 'name',
|
||||
attestationResponse: {
|
||||
id: Buffer.from('id'),
|
||||
rawId: Buffer.from('rawId'),
|
||||
response: {
|
||||
attestationObject: Buffer.from('attestationObject'),
|
||||
clientDataJSON: Buffer.from('clientDataJSON'),
|
||||
},
|
||||
type: 'type',
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(result.getError()).toEqual(
|
||||
'Could not verify authenticator registration response: Fallback 2FA not enabled for user.',
|
||||
)
|
||||
})
|
||||
|
||||
it('should return error if name is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
|
||||
@@ -5,11 +5,14 @@ import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/A
|
||||
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
|
||||
import { Authenticator } from '../../Authenticator/Authenticator'
|
||||
import { VerifyAuthenticatorRegistrationResponseDTO } from './VerifyAuthenticatorRegistrationResponseDTO'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
|
||||
|
||||
export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface<boolean> {
|
||||
constructor(
|
||||
private authenticatorRepository: AuthenticatorRepositoryInterface,
|
||||
private authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface,
|
||||
private settingService: SettingServiceInterface,
|
||||
private relyingPartyId: string,
|
||||
private expectedOrigin: string[],
|
||||
private requireUserVerification: boolean,
|
||||
@@ -22,6 +25,15 @@ export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const mfaSecret = await this.settingService.findSettingWithDecryptedValue({
|
||||
userUuid: userUuid.value,
|
||||
settingName: SettingName.MfaSecret,
|
||||
})
|
||||
const twoFactorEnabled = mfaSecret !== null && mfaSecret.value !== null
|
||||
if (!twoFactorEnabled) {
|
||||
return Result.fail('Could not verify authenticator registration response: Fallback 2FA not enabled for user.')
|
||||
}
|
||||
|
||||
const nameValidation = Validator.isNotEmpty(dto.name)
|
||||
if (nameValidation.isFailed()) {
|
||||
return Result.fail(`Could not verify authenticator registration response: ${nameValidation.getError()}`)
|
||||
|
||||
Reference in New Issue
Block a user