mirror of
https://github.com/standardnotes/server
synced 2026-07-14 00:01:54 -04:00
fix(auth): add cleanup of authenticator devices upon sign in with recovery codes
This commit is contained in:
@@ -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>
|
||||
}
|
||||
|
||||
+26
-1
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
+13
-1
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user