mirror of
https://github.com/standardnotes/server
synced 2026-10-16 18:13:09 -04:00
refactor: generating authentication options
This commit is contained in:
+45
-5
@@ -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)
|
||||
|
||||
+33
-2
@@ -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
-1
@@ -1,3 +1,3 @@
|
||||
export interface GenerateAuthenticatorAuthenticationOptionsDTO {
|
||||
userUuid: string
|
||||
username: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user