feat(snjs): add authenticator use cases (#2145)
* feat(snjs): add authenticator use case * feat(snjs): add use cases for listing, deleting and verifying authenticators * fix(snjs): spec for deleting authenticator
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import { AuthenticatorClientInterface } from '@standardnotes/services'
|
||||
import { AddAuthenticator } from './AddAuthenticator'
|
||||
|
||||
describe('AddAuthenticator', () => {
|
||||
let authenticatorClient: AuthenticatorClientInterface
|
||||
let authenticatorRegistrationPromptFunction: (
|
||||
registrationOptions: Record<string, unknown>,
|
||||
) => Promise<Record<string, unknown>>
|
||||
|
||||
|
||||
const createUseCase = () => new AddAuthenticator(authenticatorClient, authenticatorRegistrationPromptFunction)
|
||||
|
||||
beforeEach(() => {
|
||||
authenticatorClient = {} as jest.Mocked<AuthenticatorClientInterface>
|
||||
authenticatorClient.generateRegistrationOptions = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
authenticatorClient.verifyRegistrationResponse = jest.fn().mockReturnValue(true)
|
||||
|
||||
authenticatorRegistrationPromptFunction = jest.fn()
|
||||
})
|
||||
|
||||
it('should return error if userUuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'invalid',
|
||||
username: 'username',
|
||||
authenticatorName: 'authenticatorName',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('Could not generate authenticator registration options: Given value is not a valid uuid: invalid')
|
||||
})
|
||||
|
||||
it('should return error if username is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
username: '',
|
||||
authenticatorName: 'authenticatorName',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('Could not generate authenticator registration options: Username cannot be empty')
|
||||
})
|
||||
|
||||
it('should return error if authenticatorName is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
username: 'username',
|
||||
authenticatorName: '',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('Could not generate authenticator registration options: Given value is empty: ')
|
||||
})
|
||||
|
||||
it('should return error if registration options are null', async () => {
|
||||
authenticatorClient.generateRegistrationOptions = jest.fn().mockReturnValue(null)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
username: 'username',
|
||||
authenticatorName: 'authenticator',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('Could not generate authenticator registration options')
|
||||
})
|
||||
|
||||
it('should return error if authenticatorRegistrationPromptFunction throws', async () => {
|
||||
authenticatorRegistrationPromptFunction = jest.fn().mockRejectedValue(new Error('error'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
username: 'username',
|
||||
authenticatorName: 'authenticator',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('Could not generate authenticator registration options: error')
|
||||
})
|
||||
|
||||
it('should return error if authenticatorRegistrationPromptFunction throws InvalidStateError', async () => {
|
||||
const error = new Error('error')
|
||||
error.name = 'InvalidStateError'
|
||||
authenticatorRegistrationPromptFunction = jest.fn().mockRejectedValue(error)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
username: 'username',
|
||||
authenticatorName: 'authenticator',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('Authenticator was probably already registered by user')
|
||||
})
|
||||
|
||||
it('should return error if registration response verification returns false', async () => {
|
||||
authenticatorClient.verifyRegistrationResponse = jest.fn().mockReturnValue(false)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
username: 'username',
|
||||
authenticatorName: 'authenticator',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('Could not verify authenticator registration response')
|
||||
})
|
||||
|
||||
it('should register an authenticator', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
username: 'username',
|
||||
authenticatorName: 'authenticator',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(false)
|
||||
})
|
||||
|
||||
it('should return error if authenticator registration prompt function is not passed', async () => {
|
||||
const useCase = new AddAuthenticator(authenticatorClient)
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
username: 'username',
|
||||
authenticatorName: 'authenticator',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('Could not generate authenticator registration options: No authenticator registration prompt function provided')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
import { AuthenticatorClientInterface } from '@standardnotes/services'
|
||||
import { Result, UseCaseInterface, Username, Uuid, Validator } from '@standardnotes/domain-core'
|
||||
|
||||
import { AddAuthenticatorDTO } from './AddAuthenticatorDTO'
|
||||
|
||||
export class AddAuthenticator implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private authenticatorClient: AuthenticatorClientInterface,
|
||||
private authenticatorRegistrationPromptFunction?: (
|
||||
registrationOptions: Record<string, unknown>,
|
||||
) => Promise<Record<string, unknown>>,
|
||||
) {}
|
||||
|
||||
async execute(dto: AddAuthenticatorDTO): Promise<Result<void>> {
|
||||
if (!this.authenticatorRegistrationPromptFunction) {
|
||||
return Result.fail(
|
||||
'Could not generate authenticator registration options: No authenticator registration prompt function provided',
|
||||
)
|
||||
}
|
||||
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(`Could not generate authenticator registration options: ${userUuidOrError.getError()}`)
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const usernameOrError = Username.create(dto.username)
|
||||
if (usernameOrError.isFailed()) {
|
||||
return Result.fail(`Could not generate authenticator registration options: ${usernameOrError.getError()}`)
|
||||
}
|
||||
const username = usernameOrError.getValue()
|
||||
|
||||
const authenticatorNameValidatorResult = Validator.isNotEmpty(dto.authenticatorName)
|
||||
if (authenticatorNameValidatorResult.isFailed()) {
|
||||
return Result.fail(
|
||||
`Could not generate authenticator registration options: ${authenticatorNameValidatorResult.getError()}`,
|
||||
)
|
||||
}
|
||||
|
||||
const registrationOptions = await this.authenticatorClient.generateRegistrationOptions(userUuid, username)
|
||||
if (registrationOptions === null) {
|
||||
return Result.fail('Could not generate authenticator registration options')
|
||||
}
|
||||
|
||||
let authenticatorResponse
|
||||
try {
|
||||
authenticatorResponse = await this.authenticatorRegistrationPromptFunction(registrationOptions)
|
||||
} catch (error) {
|
||||
if ((error as Error).name === 'InvalidStateError') {
|
||||
return Result.fail('Authenticator was probably already registered by user')
|
||||
} else {
|
||||
return Result.fail(`Could not generate authenticator registration options: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const verificationResponse = await this.authenticatorClient.verifyRegistrationResponse(
|
||||
userUuid,
|
||||
dto.authenticatorName,
|
||||
authenticatorResponse,
|
||||
)
|
||||
if (!verificationResponse) {
|
||||
return Result.fail('Could not verify authenticator registration response')
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface AddAuthenticatorDTO {
|
||||
userUuid: string
|
||||
username: string
|
||||
authenticatorName: string
|
||||
}
|
||||
Reference in New Issue
Block a user