fix(auth): disallow adding u2f devices if a user does not have 2fa enabled

This commit is contained in:
Karol Sójko
2023-02-02 13:57:20 +01:00
parent 2f6d19dc91
commit 11bcd318ab
5 changed files with 130 additions and 0 deletions

View File

@@ -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),

View File

@@ -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()

View File

@@ -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()}`)

View File

@@ -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()

View File

@@ -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()}`)