refactor: generating authentication options

This commit is contained in:
Karol Sójko 2023-01-25 13:23:56 +01:00
parent b6eadfcebc
commit ef997be219
No known key found for this signature in database
GPG key ID: A50543BF560BDEB0
8 changed files with 97 additions and 69 deletions

View file

@ -5,13 +5,13 @@ import { controller, BaseHttpController, httpPost, httpGet, httpDelete } from 'i
import TYPES from '../../Bootstrap/Types'
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
@controller('/v1/authenticators', TYPES.AuthMiddleware)
@controller('/v1/authenticators')
export class AuthenticatorsController extends BaseHttpController {
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
super()
}
@httpDelete('/:authenticatorId')
@httpDelete('/:authenticatorId', TYPES.AuthMiddleware)
async delete(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@ -21,12 +21,12 @@ export class AuthenticatorsController extends BaseHttpController {
)
}
@httpGet('/')
@httpGet('/', TYPES.AuthMiddleware)
async list(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'authenticators/', request.body)
}
@httpGet('/generate-registration-options')
@httpGet('/generate-registration-options', TYPES.AuthMiddleware)
async generateRegistrationOptions(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@ -36,7 +36,7 @@ export class AuthenticatorsController extends BaseHttpController {
)
}
@httpGet('/generate-authentication-options')
@httpPost('/generate-authentication-options')
async generateAuthenticationOptions(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
@ -46,13 +46,8 @@ export class AuthenticatorsController extends BaseHttpController {
)
}
@httpPost('/verify-registration')
@httpPost('/verify-registration', TYPES.AuthMiddleware)
async verifyRegistration(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'authenticators/verify-registration', request.body)
}
@httpPost('/verify-authentication')
async verifyAuthentication(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'authenticators/verify-authentication', request.body)
}
}

View file

@ -589,8 +589,10 @@ export class ContainerConfigLoader {
.bind<GenerateAuthenticatorAuthenticationOptions>(TYPES.GenerateAuthenticatorAuthenticationOptions)
.toConstantValue(
new GenerateAuthenticatorAuthenticationOptions(
container.get(TYPES.UserRepository),
container.get(TYPES.AuthenticatorRepository),
container.get(TYPES.AuthenticatorChallengeRepository),
container.get(TYPES.PSEUDO_KEY_PARAMS_KEY),
),
)
container
@ -708,7 +710,6 @@ export class ContainerConfigLoader {
container.get(TYPES.GenerateAuthenticatorRegistrationOptions),
container.get(TYPES.VerifyAuthenticatorRegistrationResponse),
container.get(TYPES.GenerateAuthenticatorAuthenticationOptions),
container.get(TYPES.VerifyAuthenticatorAuthenticationResponse),
container.get(TYPES.ListAuthenticators),
container.get(TYPES.DeleteAuthenticator),
container.get(TYPES.AuthenticatorHttpMapper),

View file

@ -6,20 +6,17 @@ import { DeleteAuthenticator } from '../Domain/UseCase/DeleteAuthenticator/Delet
import { GenerateAuthenticatorAuthenticationOptions } from '../Domain/UseCase/GenerateAuthenticatorAuthenticationOptions/GenerateAuthenticatorAuthenticationOptions'
import { GenerateAuthenticatorRegistrationOptions } from '../Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions'
import { ListAuthenticators } from '../Domain/UseCase/ListAuthenticators/ListAuthenticators'
import { VerifyAuthenticatorAuthenticationResponse } from '../Domain/UseCase/VerifyAuthenticatorAuthenticationResponse/VerifyAuthenticatorAuthenticationResponse'
import { VerifyAuthenticatorRegistrationResponse } from '../Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse'
import { AuthenticatorHttpProjection } from '../Infra/Http/Projection/AuthenticatorHttpProjection'
import { DeleteAuthenticatorRequestParams } from '../Infra/Http/Request/DeleteAuthenticatorRequestParams'
import { GenerateAuthenticatorAuthenticationOptionsRequestParams } from '../Infra/Http/Request/GenerateAuthenticatorAuthenticationOptionsRequestParams'
import { GenerateAuthenticatorRegistrationOptionsRequestParams } from '../Infra/Http/Request/GenerateAuthenticatorRegistrationOptionsRequestParams'
import { ListAuthenticatorsRequestParams } from '../Infra/Http/Request/ListAuthenticatorsRequestParams'
import { VerifyAuthenticatorAuthenticationResponseRequestParams } from '../Infra/Http/Request/VerifyAuthenticatorAuthenticationResponseRequestParams'
import { VerifyAuthenticatorRegistrationResponseRequestParams } from '../Infra/Http/Request/VerifyAuthenticatorRegistrationResponseRequestParams'
import { DeleteAuthenticatorResponse } from '../Infra/Http/Response/DeleteAuthenticatorResponse'
import { GenerateAuthenticatorAuthenticationOptionsResponse } from '../Infra/Http/Response/GenerateAuthenticatorAuthenticationOptionsResponse'
import { GenerateAuthenticatorRegistrationOptionsResponse } from '../Infra/Http/Response/GenerateAuthenticatorRegistrationOptionsResponse'
import { ListAuthenticatorsResponse } from '../Infra/Http/Response/ListAuthenticatorsResponse'
import { VerifyAuthenticatorAuthenticationResponseResponse } from '../Infra/Http/Response/VerifyAuthenticatorAuthenticationResponseResponse'
import { VerifyAuthenticatorRegistrationResponseResponse } from '../Infra/Http/Response/VerifyAuthenticatorRegistrationResponseResponse'
export class AuthenticatorsController {
@ -27,7 +24,6 @@ export class AuthenticatorsController {
private generateAuthenticatorRegistrationOptions: GenerateAuthenticatorRegistrationOptions,
private verifyAuthenticatorRegistrationResponse: VerifyAuthenticatorRegistrationResponse,
private generateAuthenticatorAuthenticationOptions: GenerateAuthenticatorAuthenticationOptions,
private verifyAuthenticatorAuthenticationResponse: VerifyAuthenticatorAuthenticationResponse,
private listAuthenticators: ListAuthenticators,
private deleteAuthenticator: DeleteAuthenticator,
private authenticatorHttpMapper: MapperInterface<Authenticator, AuthenticatorHttpProjection>,
@ -117,7 +113,7 @@ export class AuthenticatorsController {
params: GenerateAuthenticatorAuthenticationOptionsRequestParams,
): Promise<GenerateAuthenticatorAuthenticationOptionsResponse> {
const result = await this.generateAuthenticatorAuthenticationOptions.execute({
userUuid: params.userUuid,
username: params.username,
})
if (result.isFailed()) {
@ -136,29 +132,4 @@ export class AuthenticatorsController {
data: { options: result.getValue() },
}
}
async verifyAuthenticationResponse(
params: VerifyAuthenticatorAuthenticationResponseRequestParams,
): Promise<VerifyAuthenticatorAuthenticationResponseResponse> {
const result = await this.verifyAuthenticatorAuthenticationResponse.execute({
userUuid: params.userUuid,
authenticatorResponse: params.authenticatorResponse,
})
if (result.isFailed()) {
return {
status: HttpStatusCode.Unauthorized,
data: {
error: {
message: result.getError(),
},
},
}
}
return {
status: HttpStatusCode.Success,
data: { success: result.getValue() },
}
}
}

View file

@ -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: 'test@test.te',
})
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: 'test@test.te',
})
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: 'test@test.te',
})
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: 'test@test.te',
})
expect(result.isFailed()).toBe(false)

View file

@ -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()}`)
}

View file

@ -1,3 +1,3 @@
export interface GenerateAuthenticatorAuthenticationOptionsDTO {
userUuid: string
username: string
}

View file

@ -1,3 +1,3 @@
export interface GenerateAuthenticatorAuthenticationOptionsRequestParams {
userUuid: string
username: string
}

View file

@ -12,13 +12,13 @@ import {
import TYPES from '../../Bootstrap/Types'
import { AuthenticatorsController } from '../../Controller/AuthenticatorsController'
@controller('/authenticators', TYPES.ApiGatewayAuthMiddleware)
@controller('/authenticators')
export class InversifyExpressAuthenticatorsController extends BaseHttpController {
constructor(@inject(TYPES.AuthenticatorsController) private authenticatorsController: AuthenticatorsController) {
super()
}
@httpGet('/')
@httpGet('/', TYPES.ApiGatewayAuthMiddleware)
async list(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.list({
userUuid: response.locals.user.uuid,
@ -27,7 +27,7 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
return this.json(result.data, result.status)
}
@httpDelete('/:authenticatorId')
@httpDelete('/:authenticatorId', TYPES.ApiGatewayAuthMiddleware)
async delete(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.delete({
userUuid: response.locals.user.uuid,
@ -37,7 +37,7 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
return this.json(result.data, result.status)
}
@httpGet('/generate-registration-options')
@httpGet('/generate-registration-options', TYPES.ApiGatewayAuthMiddleware)
async generateRegistrationOptions(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.generateRegistrationOptions({
username: response.locals.user.email,
@ -47,7 +47,7 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
return this.json(result.data, result.status)
}
@httpPost('/verify-registration')
@httpPost('/verify-registration', TYPES.ApiGatewayAuthMiddleware)
async verifyRegistration(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.verifyRegistrationResponse({
userUuid: response.locals.user.uuid,
@ -58,20 +58,10 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
return this.json(result.data, result.status)
}
@httpGet('/generate-authentication-options')
async generateAuthenticationOptions(_request: Request, response: Response): Promise<results.JsonResult> {
@httpPost('/generate-authentication-options')
async generateAuthenticationOptions(request: Request): Promise<results.JsonResult> {
const result = await this.authenticatorsController.generateAuthenticationOptions({
userUuid: response.locals.user.uuid,
})
return this.json(result.data, result.status)
}
@httpPost('/verify-authentication')
async verifyAuthentication(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.verifyAuthenticationResponse({
userUuid: response.locals.user.uuid,
authenticatorResponse: request.body,
username: request.body.username,
})
return this.json(result.data, result.status)