fix(auth): add cleanup of authenticator devices upon sign in with recovery codes

This commit is contained in:
Karol Sójko
2023-01-24 11:17:51 +01:00
parent 8559948a5a
commit f1d3117518
5 changed files with 49 additions and 2 deletions
+1
View File
@@ -665,6 +665,7 @@ export class ContainerConfigLoader {
container.get(TYPES.IncreaseLoginAttempts),
container.get(TYPES.ClearLoginAttempts),
container.get(TYPES.DeleteSetting),
container.get(TYPES.AuthenticatorRepository),
),
)
container.bind<DeleteAccount>(TYPES.DeleteAccount).to(DeleteAccount)
@@ -8,4 +8,5 @@ export interface AuthenticatorRepositoryInterface {
findByUserUuidAndCredentialId(userUuid: Uuid, credentialId: Buffer): Promise<Authenticator | null>
save(authenticator: Authenticator): Promise<void>
remove(authenticator: Authenticator): Promise<void>
removeByUserUuid(userUuid: Uuid): Promise<void>
}
@@ -2,6 +2,7 @@ import { Result } from '@standardnotes/domain-core'
import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115'
import { AuthResponseFactory20200115 } from '../../Auth/AuthResponseFactory20200115'
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { CrypterInterface } from '../../Encryption/CrypterInterface'
import { Setting } from '../../Setting/Setting'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
@@ -24,6 +25,7 @@ describe('SignInWithRecoveryCodes', () => {
let increaseLoginAttempts: IncreaseLoginAttempts
let clearLoginAttempts: ClearLoginAttempts
let deleteSetting: DeleteSetting
let authenticatorRepository: AuthenticatorRepositoryInterface
const createUseCase = () =>
new SignInWithRecoveryCodes(
@@ -36,12 +38,13 @@ describe('SignInWithRecoveryCodes', () => {
increaseLoginAttempts,
clearLoginAttempts,
deleteSetting,
authenticatorRepository,
)
beforeEach(() => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByEmail = jest.fn().mockReturnValue({
uuid: '1-2-3',
uuid: '00000000-0000-0000-0000-000000000000',
encryptedPassword: '$2a$11$K3g6XoTau8VmLJcai1bB0eD9/YvBSBRtBhMprJOaVZ0U3SgasZH3a',
} as jest.Mocked<User>)
@@ -69,6 +72,9 @@ describe('SignInWithRecoveryCodes', () => {
deleteSetting = {} as jest.Mocked<DeleteSetting>
deleteSetting.execute = jest.fn()
authenticatorRepository = {} as jest.Mocked<AuthenticatorRepositoryInterface>
authenticatorRepository.removeByUserUuid = jest.fn()
})
it('should return error if password is not provided', async () => {
@@ -209,6 +215,24 @@ describe('SignInWithRecoveryCodes', () => {
expect(result.getError()).toBe('Could not sign in with recovery codes: Oops')
})
it('should return error if user has an invalid uuid', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue({
uuid: '1-2-3',
encryptedPassword: '$2a$11$K3g6XoTau8VmLJcai1bB0eD9/YvBSBRtBhMprJOaVZ0U3SgasZH3a',
} as jest.Mocked<User>)
const result = await createUseCase().execute({
userAgent: 'user-agent',
username: '[email protected]',
password: 'qweqwe123123',
codeVerifier: 'code-verifier',
recoveryCodes: 'foo',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Invalid user uuid')
})
it('should return auth response', async () => {
const result = await createUseCase().execute({
userAgent: 'user-agent',
@@ -220,6 +244,7 @@ describe('SignInWithRecoveryCodes', () => {
expect(clearLoginAttempts.execute).toHaveBeenCalled()
expect(deleteSetting.execute).toHaveBeenCalled()
expect(authenticatorRepository.removeByUserUuid).toHaveBeenCalled()
expect(result.isFailed()).toBe(false)
})
})
@@ -1,5 +1,5 @@
import * as bcrypt from 'bcryptjs'
import { Result, UseCaseInterface, Username, Validator } from '@standardnotes/domain-core'
import { Result, UseCaseInterface, Username, Uuid, Validator } from '@standardnotes/domain-core'
import { SettingName } from '@standardnotes/settings'
import { ApiVersion } from '@standardnotes/api'
@@ -15,6 +15,7 @@ import { AuthResponseFactory20200115 } from '../../Auth/AuthResponseFactory20200
import { IncreaseLoginAttempts } from '../IncreaseLoginAttempts'
import { ClearLoginAttempts } from '../ClearLoginAttempts'
import { DeleteSetting } from '../DeleteSetting/DeleteSetting'
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse20200115> {
constructor(
@@ -27,6 +28,7 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse202
private increaseLoginAttempts: IncreaseLoginAttempts,
private clearLoginAttempts: ClearLoginAttempts,
private deleteSetting: DeleteSetting,
private authenticatorRepository: AuthenticatorRepositoryInterface,
) {}
async execute(dto: SignInWithRecoveryCodesDTO): Promise<Result<AuthResponse20200115>> {
@@ -65,6 +67,14 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse202
return Result.fail('Could not find user')
}
const userUuidOrError = Uuid.create(user.uuid)
if (userUuidOrError.isFailed()) {
await this.increaseLoginAttempts.execute({ email: username.value })
return Result.fail('Invalid user uuid')
}
const userUuid = userUuidOrError.getValue()
const passwordMatches = await bcrypt.compare(dto.password, user.encryptedPassword)
if (!passwordMatches) {
await this.increaseLoginAttempts.execute({ email: username.value })
@@ -110,6 +120,8 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse202
userUuid: user.uuid,
})
await this.authenticatorRepository.removeByUserUuid(userUuid)
await this.clearLoginAttempts.execute({ email: username.value })
return Result.ok(authResponse as AuthResponse20200115)
@@ -11,6 +11,14 @@ export class MySQLAuthenticatorRepository implements AuthenticatorRepositoryInte
private mapper: MapperInterface<Authenticator, TypeORMAuthenticator>,
) {}
async removeByUserUuid(userUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.delete()
.where('user_uuid = :userUuid', { userUuid: userUuid.value })
.execute()
}
async findById(id: UniqueEntityId): Promise<Authenticator | null> {
const persistence = await this.ormRepository
.createQueryBuilder('authenticator')