feat(auth): add generating recovery codes

This commit is contained in:
Karol Sójko
2023-01-04 15:29:15 +01:00
parent 6ccc6ee42f
commit a360231fd0
6 changed files with 127 additions and 0 deletions
+10
View File
@@ -222,6 +222,7 @@ import { ListAuthenticators } from '../Domain/UseCase/ListAuthenticators/ListAut
import { AuthenticatorHttpProjection } from '../Infra/Http/Projection/AuthenticatorHttpProjection'
import { AuthenticatorHttpMapper } from '../Mapping/AuthenticatorHttpMapper'
import { DeleteAuthenticator } from '../Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator'
import { GenerateRecoveryCodes } from '../Domain/UseCase/GenerateRecoveryCodes/GenerateRecoveryCodes'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -596,6 +597,15 @@ export class ContainerConfigLoader {
container
.bind<DeleteAuthenticator>(TYPES.DeleteAuthenticator)
.toConstantValue(new DeleteAuthenticator(container.get(TYPES.AuthenticatorRepository)))
container
.bind<GenerateRecoveryCodes>(TYPES.GenerateRecoveryCodes)
.toConstantValue(
new GenerateRecoveryCodes(
container.get(TYPES.UserRepository),
container.get(TYPES.SettingService),
container.get(TYPES.CryptoNode),
),
)
container
.bind<CleanupSessionTraces>(TYPES.CleanupSessionTraces)
+1
View File
@@ -141,6 +141,7 @@ const TYPES = {
VerifyAuthenticatorAuthenticationResponse: Symbol.for('VerifyAuthenticatorAuthenticationResponse'),
ListAuthenticators: Symbol.for('ListAuthenticators'),
DeleteAuthenticator: Symbol.for('DeleteAuthenticator'),
GenerateRecoveryCodes: Symbol.for('GenerateRecoveryCodes'),
// Handlers
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
@@ -0,0 +1,65 @@
import { CryptoNode } from '@standardnotes/sncrypto-node'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { GenerateRecoveryCodes } from './GenerateRecoveryCodes'
describe('GenerateRecoveryCodes', () => {
let userRepository: UserRepositoryInterface
let settingService: SettingServiceInterface
let cryptoNode: CryptoNode
const createUseCase = () => new GenerateRecoveryCodes(userRepository, settingService, cryptoNode)
beforeEach(() => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUuid = jest.fn().mockReturnValue({} as jest.Mocked<User>)
settingService = {} as jest.Mocked<SettingServiceInterface>
settingService.createOrReplace = jest.fn()
cryptoNode = {} as jest.Mocked<CryptoNode>
cryptoNode.generateRandomKey = jest.fn().mockReturnValue('randomKey123')
})
it('should generate recovery codes', async () => {
const useCase = createUseCase()
const result = await useCase.execute({ userUuid: '2221101c-1da9-4d2b-9b32-b8be2a8d1c82' })
expect(settingService.createOrReplace).toHaveBeenCalled()
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual('RAND OMKE Y123')
})
it('should return error if empty random string', async () => {
cryptoNode.generateRandomKey = jest.fn().mockReturnValue('')
const useCase = createUseCase()
const result = await useCase.execute({ userUuid: '2221101c-1da9-4d2b-9b32-b8be2a8d1c82' })
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not generate recovery codes: random key is invalid')
})
it('should return error if user not found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
const useCase = createUseCase()
const result = await useCase.execute({ userUuid: '2221101c-1da9-4d2b-9b32-b8be2a8d1c82' })
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not generate recovery codes: user not found')
})
it('should return error if user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({ userUuid: 'invalid' })
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not generate recovery codes: Given value is not a valid uuid: invalid')
})
})
@@ -0,0 +1,47 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { SettingName } from '@standardnotes/settings'
import { CryptoNode } from '@standardnotes/sncrypto-node'
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { GenerateRecoveryCodesDTO } from './GenerateRecoveryCodesDTO'
export class GenerateRecoveryCodes implements UseCaseInterface<string> {
constructor(
private userRepository: UserRepositoryInterface,
private settingService: SettingServiceInterface,
private cryptoNode: CryptoNode,
) {}
async execute(dto: GenerateRecoveryCodesDTO): Promise<Result<string>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(`Could not generate recovery codes: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid.value)
if (user === null) {
return Result.fail('Could not generate recovery codes: user not found')
}
const randomKey = await this.cryptoNode.generateRandomKey(160)
const recoveryCodesSplit = randomKey.toUpperCase().match(/.{1,4}/g)
if (!recoveryCodesSplit) {
return Result.fail('Could not generate recovery codes: random key is invalid')
}
const recoveryCodes = recoveryCodesSplit.join(' ')
await this.settingService.createOrReplace({
user,
props: {
name: SettingName.RecoveryCodes,
unencryptedValue: recoveryCodes,
serverEncryptionVersion: EncryptionVersion.Default,
sensitive: true,
},
})
return Result.ok(recoveryCodes)
}
}
@@ -0,0 +1,3 @@
export interface GenerateRecoveryCodesDTO {
userUuid: string
}
@@ -14,4 +14,5 @@ export enum SettingName {
MuteMarketingEmails = 'MUTE_MARKETING_EMAILS',
ListedAuthorSecrets = 'LISTED_AUTHOR_SECRETS',
LogSessionUserAgent = 'LOG_SESSION_USER_AGENT',
RecoveryCodes = 'RECOVERY_CODES',
}