feat(auth): add verifying authenticator registration response

This commit is contained in:
Karol Sójko
2022-12-28 15:50:48 +01:00
parent 0a420ce30e
commit f5683cfd94
11 changed files with 319 additions and 6 deletions
@@ -0,0 +1,209 @@
import * as simeplWebAuthnServer from '@simplewebauthn/server'
import { VerifiedRegistrationResponse } from '@simplewebauthn/server'
import { Result } from '@standardnotes/domain-core'
import { Authenticator } from '../../Authenticator/Authenticator'
import { AuthenticatorChallenge } from '../../Authenticator/AuthenticatorChallenge'
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { VerifyAuthenticatorRegistrationResponse } from './VerifyAuthenticatorRegistrationResponse'
describe('VerifyAuthenticatorRegistrationResponse', () => {
let authenticatorRepository: AuthenticatorRepositoryInterface
let authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface
const createUseCase = () =>
new VerifyAuthenticatorRegistrationResponse(authenticatorRepository, authenticatorChallengeRepository)
beforeEach(() => {
authenticatorRepository = {} as jest.Mocked<AuthenticatorRepositoryInterface>
authenticatorRepository.save = jest.fn()
authenticatorChallengeRepository = {} as jest.Mocked<AuthenticatorChallengeRepositoryInterface>
authenticatorChallengeRepository.findByUserUuidAndChallenge = jest.fn().mockReturnValue({
props: {
challenge: Buffer.from('challenge'),
},
} as jest.Mocked<AuthenticatorChallenge>)
})
it('should return error if user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid',
challenge: Buffer.from('challenge'),
registrationCredential: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
response: {
attestationObject: Buffer.from('attestationObject'),
clientDataJSON: Buffer.from('clientDataJSON'),
},
type: 'type',
},
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual(
'Could not verify authenticator registration response: Given value is not a valid uuid: invalid',
)
})
it('should return error if challenge is not found', async () => {
authenticatorChallengeRepository.findByUserUuidAndChallenge = jest.fn().mockReturnValue(null)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
challenge: Buffer.from('challenge'),
registrationCredential: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
response: {
attestationObject: Buffer.from('attestationObject'),
clientDataJSON: Buffer.from('clientDataJSON'),
},
type: 'type',
},
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not verify authenticator registration response: challenge not found')
})
it('should return error if verification could not verify', async () => {
authenticatorChallengeRepository.findByUserUuidAndChallenge = jest.fn().mockReturnValue({
props: {
challenge: Buffer.from('challenge'),
},
} as jest.Mocked<AuthenticatorChallenge>)
const useCase = createUseCase()
const mock = jest.spyOn(simeplWebAuthnServer, 'verifyRegistrationResponse')
mock.mockImplementation(() => {
return Promise.resolve({
verified: false,
registrationInfo: {
counter: 1,
credentialBackedUp: true,
credentialDeviceType: 'singleDevice',
credentialID: Buffer.from('test'),
credentialPublicKey: Buffer.from('test'),
},
} as jest.Mocked<VerifiedRegistrationResponse>)
})
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
challenge: Buffer.from('invalid'),
registrationCredential: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
response: {
attestationObject: Buffer.from('attestationObject'),
clientDataJSON: Buffer.from('clientDataJSON'),
},
type: 'type',
},
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not verify authenticator registration response: verification failed')
mock.mockRestore()
})
it('should return error if authenticator could not be created', async () => {
authenticatorChallengeRepository.findByUserUuidAndChallenge = jest.fn().mockReturnValue({
props: {
challenge: Buffer.from('challenge'),
},
} as jest.Mocked<AuthenticatorChallenge>)
const useCase = createUseCase()
const mock = jest.spyOn(simeplWebAuthnServer, 'verifyRegistrationResponse')
mock.mockImplementation(() => {
return Promise.resolve({
verified: true,
registrationInfo: {
counter: 1,
credentialBackedUp: true,
credentialDeviceType: 'singleDevice',
credentialID: Buffer.from('test'),
credentialPublicKey: Buffer.from('test'),
},
} as jest.Mocked<VerifiedRegistrationResponse>)
})
const mockAuthenticator = jest.spyOn(Authenticator, 'create')
mockAuthenticator.mockImplementation(() => {
return Result.fail('Oops')
})
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
challenge: Buffer.from('invalid'),
registrationCredential: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
response: {
attestationObject: Buffer.from('attestationObject'),
clientDataJSON: Buffer.from('clientDataJSON'),
},
type: 'type',
},
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Could not verify authenticator registration response: Oops')
mock.mockRestore()
mockAuthenticator.mockRestore()
})
it('should verify authenticator registration response', async () => {
authenticatorChallengeRepository.findByUserUuidAndChallenge = jest.fn().mockReturnValue({
props: {
challenge: Buffer.from('challenge'),
},
} as jest.Mocked<AuthenticatorChallenge>)
const useCase = createUseCase()
const mock = jest.spyOn(simeplWebAuthnServer, 'verifyRegistrationResponse')
mock.mockImplementation(() => {
return Promise.resolve({
verified: true,
registrationInfo: {
counter: 1,
credentialBackedUp: true,
credentialDeviceType: 'singleDevice',
credentialID: Buffer.from('test'),
credentialPublicKey: Buffer.from('test'),
},
} as jest.Mocked<VerifiedRegistrationResponse>)
})
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
challenge: Buffer.from('invalid'),
registrationCredential: {
id: Buffer.from('id'),
rawId: Buffer.from('rawId'),
response: {
attestationObject: Buffer.from('attestationObject'),
clientDataJSON: Buffer.from('clientDataJSON'),
},
type: 'type',
},
})
expect(result.isFailed()).toBeFalsy()
mock.mockRestore()
})
})
@@ -0,0 +1,61 @@
import { Dates, Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { verifyRegistrationResponse } from '@simplewebauthn/server'
import { AuthenticatorChallengeRepositoryInterface } from '../../Authenticator/AuthenticatorChallengeRepositoryInterface'
import { RelyingParty } from '../../Authenticator/RelyingParty'
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { Authenticator } from '../../Authenticator/Authenticator'
import { VerifyAuthenticatorRegistrationResponseDTO } from './VerifyAuthenticatorRegistrationResponseDTO'
export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface<boolean> {
constructor(
private authenticatorRepository: AuthenticatorRepositoryInterface,
private authenticatorChallengeRepository: AuthenticatorChallengeRepositoryInterface,
) {}
async execute(dto: VerifyAuthenticatorRegistrationResponseDTO): Promise<Result<boolean>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(`Could not verify authenticator registration response: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
const authenticatorChallenge = await this.authenticatorChallengeRepository.findByUserUuidAndChallenge(
userUuid,
dto.challenge,
)
if (!authenticatorChallenge) {
return Result.fail('Could not verify authenticator registration response: challenge not found')
}
const verification = await verifyRegistrationResponse({
credential: dto.registrationCredential,
expectedChallenge: authenticatorChallenge.props.challenge.toString(),
expectedOrigin: `https://${RelyingParty.RP_ID}`,
expectedRPID: RelyingParty.RP_ID,
})
if (!verification.verified) {
return Result.fail('Could not verify authenticator registration response: verification failed')
}
const authenticatorOrError = Authenticator.create({
userUuid,
counter: verification.registrationInfo?.counter as number,
credentialBackedUp: verification.registrationInfo?.credentialBackedUp as boolean,
credentialDeviceType: verification.registrationInfo?.credentialDeviceType,
credentialId: verification.registrationInfo?.credentialID as Buffer,
credentialPublicKey: verification.registrationInfo?.credentialPublicKey as Buffer,
dates: Dates.create(new Date(), new Date()).getValue(),
})
if (authenticatorOrError.isFailed()) {
return Result.fail(`Could not verify authenticator registration response: ${authenticatorOrError.getError()}`)
}
const authenticator = authenticatorOrError.getValue()
await this.authenticatorRepository.save(authenticator)
return Result.ok(true)
}
}
@@ -0,0 +1,5 @@
export interface VerifyAuthenticatorRegistrationResponseDTO {
userUuid: string
challenge: Buffer
registrationCredential: Record<string, unknown>
}