refactor: generating authentication options

This commit is contained in:
Karol Sójko
2023-01-25 13:24:02 +01:00
parent b6eadfcebc
commit ef997be219
8 changed files with 97 additions and 69 deletions
@@ -4,14 +4,22 @@ import { Authenticator } from '../../Authenticator/Authenticator'
import { AuthenticatorChallenge } from '../../Authenticator/AuthenticatorChallenge'
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { GenerateAuthenticatorAuthenticationOptions } from './GenerateAuthenticatorAuthenticationOptions'
describe('GenerateAuthenticatorAuthenticationOptions', () => {
let userRepository: UserRepositoryInterface
let authenticatorRepository: AuthenticatorRepositoryInterface
let authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface
const createUseCase = () =>
new GenerateAuthenticatorAuthenticationOptions(authenticatorRepository, authenticatorChallengeRepository)
new GenerateAuthenticatorAuthenticationOptions(
userRepository,
authenticatorRepository,
authenticatorChallengeRepository,
'pseudo-key-params-key',
)
beforeEach(() => {
const authenticator = Authenticator.create({
@@ -31,13 +39,33 @@ describe('GenerateAuthenticatorAuthenticationOptions', () => {
authenticatorChallengeRepository = {} as jest.Mocked<AuthenticatorChallengeRepositoryInterface>
authenticatorChallengeRepository.save = jest.fn()
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByEmail = jest.fn().mockReturnValue({
uuid: '00000000-0000-0000-0000-000000000000',
} as jest.Mocked<User>)
})
it('should return error if userUuid is invalid', async () => {
it('should return error if username is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid',
username: '',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Could not generate authenticator registration options: Username cannot be empty')
})
it('should return error if user uuid is not valid', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue({
uuid: 'invalid',
} as jest.Mocked<User>)
const useCase = createUseCase()
const result = await useCase.execute({
username: '[email protected]',
})
expect(result.isFailed()).toBe(true)
@@ -46,6 +74,18 @@ describe('GenerateAuthenticatorAuthenticationOptions', () => {
)
})
it('should return pseudo options if user is not found', async () => {
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
const useCase = createUseCase()
const result = await useCase.execute({
username: '[email protected]',
})
expect(result.isFailed()).toBe(false)
})
it('should return error if authenticator challenge is invalid', async () => {
const mock = jest.spyOn(AuthenticatorChallenge, 'create')
mock.mockReturnValue(Result.fail('Oops'))
@@ -53,7 +93,7 @@ describe('GenerateAuthenticatorAuthenticationOptions', () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
username: '[email protected]',
})
expect(result.isFailed()).toBe(true)
@@ -66,7 +106,7 @@ describe('GenerateAuthenticatorAuthenticationOptions', () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
username: '[email protected]',
})
expect(result.isFailed()).toBe(false)
@@ -1,19 +1,50 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import * as crypto from 'crypto'
import { Result, UseCaseInterface, Username, Uuid } from '@standardnotes/domain-core'
import { generateAuthenticationOptions } from '@simplewebauthn/server'
import { GenerateAuthenticatorAuthenticationOptionsDTO } from './GenerateAuthenticatorAuthenticationOptionsDTO'
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
import { AuthenticatorChallenge } from '../../Authenticator/AuthenticatorChallenge'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
export class GenerateAuthenticatorAuthenticationOptions implements UseCaseInterface<Record<string, unknown>> {
constructor(
private userRepository: UserRepositoryInterface,
private authenticatorRepository: AuthenticatorRepositoryInterface,
private authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface,
private pseudoKeyParamsKey: string,
) {}
async execute(dto: GenerateAuthenticatorAuthenticationOptionsDTO): Promise<Result<Record<string, unknown>>> {
const userUuidOrError = Uuid.create(dto.userUuid)
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 user = await this.userRepository.findOneByEmail(username.value)
if (user === null) {
const credentialIdHash = crypto
.createHash('sha256')
.update(`u2f-selector-${dto.username}${this.pseudoKeyParamsKey}`)
.digest('base64url')
const options = generateAuthenticationOptions({
allowCredentials: [
{
id: Buffer.from(credentialIdHash),
type: 'public-key',
transports: [],
},
],
userVerification: 'preferred',
})
return Result.ok(options)
}
const userUuidOrError = Uuid.create(user.uuid)
if (userUuidOrError.isFailed()) {
return Result.fail(`Could not generate authenticator registration options: ${userUuidOrError.getError()}`)
}
@@ -1,3 +1,3 @@
export interface GenerateAuthenticatorAuthenticationOptionsDTO {
userUuid: string
username: string
}