feat(auth): add removing authenticator

This commit is contained in:
Karol Sójko
2022-12-29 13:55:08 +01:00
parent 401b78e477
commit de50d76800
13 changed files with 176 additions and 3 deletions
@@ -1,6 +1,6 @@
import { inject } from 'inversify'
import { Request, Response } from 'express'
import { controller, BaseHttpController, httpPost, httpGet } from 'inversify-express-utils'
import { controller, BaseHttpController, httpPost, httpGet, httpDelete } from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
@@ -11,6 +11,16 @@ export class AuthenticatorsController extends BaseHttpController {
super()
}
@httpDelete('/:authenticatorId')
async delete(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
response,
`authenticators/${request.params.authenticatorId}`,
request.body,
)
}
@httpGet('/')
async list(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'authenticators/', request.body)
+5
View File
@@ -221,6 +221,7 @@ import { AuthenticatorsController } from '../Controller/AuthenticatorsController
import { ListAuthenticators } from '../Domain/UseCase/ListAuthenticators/ListAuthenticators'
import { AuthenticatorHttpProjection } from '../Infra/Http/Projection/AuthenticatorHttpProjection'
import { AuthenticatorHttpMapper } from '../Mapping/AuthenticatorHttpMapper'
import { DeleteAuthenticator } from '../Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -592,6 +593,9 @@ export class ContainerConfigLoader {
container
.bind<ListAuthenticators>(TYPES.ListAuthenticators)
.toConstantValue(new ListAuthenticators(container.get(TYPES.AuthenticatorRepository)))
container
.bind<DeleteAuthenticator>(TYPES.DeleteAuthenticator)
.toConstantValue(new DeleteAuthenticator(container.get(TYPES.AuthenticatorRepository)))
container
.bind<CleanupSessionTraces>(TYPES.CleanupSessionTraces)
@@ -657,6 +661,7 @@ export class ContainerConfigLoader {
container.get(TYPES.GenerateAuthenticatorAuthenticationOptions),
container.get(TYPES.VerifyAuthenticatorAuthenticationResponse),
container.get(TYPES.ListAuthenticators),
container.get(TYPES.DeleteAuthenticator),
container.get(TYPES.AuthenticatorHttpMapper),
),
)
+1
View File
@@ -140,6 +140,7 @@ const TYPES = {
GenerateAuthenticatorAuthenticationOptions: Symbol.for('GenerateAuthenticatorAuthenticationOptions'),
VerifyAuthenticatorAuthenticationResponse: Symbol.for('VerifyAuthenticatorAuthenticationResponse'),
ListAuthenticators: Symbol.for('ListAuthenticators'),
DeleteAuthenticator: Symbol.for('DeleteAuthenticator'),
// Handlers
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
@@ -1,6 +1,7 @@
import { HttpStatusCode } from '@standardnotes/api'
import { MapperInterface } from '@standardnotes/domain-core'
import { Authenticator } from '../Domain/Authenticator/Authenticator'
import { DeleteAuthenticator } from '../Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator'
import { GenerateAuthenticatorAuthenticationOptions } from '../Domain/UseCase/GenerateAuthenticatorAuthenticationOptions/GenerateAuthenticatorAuthenticationOptions'
import { GenerateAuthenticatorRegistrationOptions } from '../Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions'
@@ -8,11 +9,13 @@ import { ListAuthenticators } from '../Domain/UseCase/ListAuthenticators/ListAut
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'
@@ -26,6 +29,7 @@ export class AuthenticatorsController {
private generateAuthenticatorAuthenticationOptions: GenerateAuthenticatorAuthenticationOptions,
private verifyAuthenticatorAuthenticationResponse: VerifyAuthenticatorAuthenticationResponse,
private listAuthenticators: ListAuthenticators,
private deleteAuthenticator: DeleteAuthenticator,
private authenticatorHttpMapper: MapperInterface<Authenticator, AuthenticatorHttpProjection>,
) {}
@@ -44,6 +48,20 @@ export class AuthenticatorsController {
}
}
async delete(params: DeleteAuthenticatorRequestParams): Promise<DeleteAuthenticatorResponse> {
const result = await this.deleteAuthenticator.execute({
userUuid: params.userUuid,
authenticatorId: params.authenticatorId,
})
return {
status: HttpStatusCode.Success,
data: {
message: result.getValue(),
},
}
}
async generateRegistrationOptions(
params: GenerateAuthenticatorRegistrationOptionsRequestParams,
): Promise<GenerateAuthenticatorRegistrationOptionsResponse> {
@@ -1,8 +1,11 @@
import { Uuid } from '@standardnotes/domain-core'
import { UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { Authenticator } from './Authenticator'
export interface AuthenticatorRepositoryInterface {
findByUserUuid(userUuid: Uuid): Promise<Authenticator[]>
findById(id: UniqueEntityId): Promise<Authenticator | null>
findByUserUuidAndCredentialId(userUuid: Uuid, credentialId: Buffer): Promise<Authenticator | null>
save(authenticator: Authenticator): Promise<void>
remove(authenticator: Authenticator): Promise<void>
}
@@ -0,0 +1,70 @@
import { Dates, Uuid } from '@standardnotes/domain-core'
import { Authenticator } from '../../Authenticator/Authenticator'
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { DeleteAuthenticator } from './DeleteAuthenticator'
describe('DeleteAuthenticator', () => {
let authenticatorRepository: AuthenticatorRepositoryInterface
let authenticator: Authenticator
const createUseCase = () => new DeleteAuthenticator(authenticatorRepository)
beforeEach(() => {
authenticator = Authenticator.create({
counter: 1,
name: 'my-key',
credentialBackedUp: true,
credentialDeviceType: 'singleDevice',
credentialId: Buffer.from('credentialId'),
credentialPublicKey: Buffer.from('credentialPublicKey'),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
dates: Dates.create(new Date(1), new Date(1)).getValue(),
transports: ['usb'],
}).getValue()
authenticatorRepository = {} as jest.Mocked<AuthenticatorRepositoryInterface>
authenticatorRepository.findById = jest.fn().mockReturnValue(authenticator)
authenticatorRepository.remove = jest.fn()
})
it('should return error if authenticator not found', async () => {
authenticatorRepository.findById = jest.fn().mockReturnValue(null)
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
authenticatorId: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toEqual('Authenticator not found')
})
it('should return error if authenticator does not belong to user', async () => {
authenticatorRepository.findById = jest.fn().mockReturnValue({
...authenticator,
props: {
...authenticator.props,
userUuid: Uuid.create('00000000-0000-0000-0000-00000000a000').getValue(),
},
})
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
authenticatorId: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(true)
expect(result.getError()).toEqual('Authenticator not found')
})
it('should delete authenticator', async () => {
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
authenticatorId: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBe(false)
expect(result.getValue()).toEqual('Authenticator deleted')
expect(authenticatorRepository.remove).toHaveBeenCalled()
})
})
@@ -0,0 +1,17 @@
import { Result, UniqueEntityId, UseCaseInterface } from '@standardnotes/domain-core'
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
import { DeleteAuthenticatorDTO } from './DeleteAuthenticatorDTO'
export class DeleteAuthenticator implements UseCaseInterface<string> {
constructor(private authenticatorRepository: AuthenticatorRepositoryInterface) {}
async execute(dto: DeleteAuthenticatorDTO): Promise<Result<string>> {
const authenticator = await this.authenticatorRepository.findById(new UniqueEntityId(dto.authenticatorId))
if (!authenticator || authenticator.props.userUuid.value !== dto.userUuid) {
return Result.fail('Authenticator not found')
}
await this.authenticatorRepository.remove(authenticator)
return Result.ok('Authenticator deleted')
}
}
@@ -0,0 +1,4 @@
export interface DeleteAuthenticatorDTO {
userUuid: string
authenticatorId: string
}
@@ -0,0 +1,4 @@
export interface DeleteAuthenticatorRequestParams {
userUuid: string
authenticatorId: string
}
@@ -0,0 +1,8 @@
import { HttpErrorResponseBody, HttpResponse } from '@standardnotes/api'
import { Either } from '@standardnotes/common'
import { DeleteAuthenticatorResponseBody } from './DeleteAuthenticatorResponseBody'
export interface DeleteAuthenticatorResponse extends HttpResponse {
data: Either<DeleteAuthenticatorResponseBody, HttpErrorResponseBody>
}
@@ -0,0 +1,3 @@
export interface DeleteAuthenticatorResponseBody {
message: string
}
@@ -3,6 +3,7 @@ import { inject } from 'inversify'
import {
BaseHttpController,
controller,
httpDelete,
httpGet,
httpPost,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -26,6 +27,16 @@ export class InversifyExpressAuthenticatorsController extends BaseHttpController
return this.json(result.data, result.status)
}
@httpDelete('/:authenticatorId')
async delete(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.delete({
userUuid: response.locals.user.uuid,
authenticatorId: request.params.authenticatorId,
})
return this.json(result.data, result.status)
}
@httpGet('/generate-registration-options')
async generateRegistrationOptions(_request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.authenticatorsController.generateRegistrationOptions({
@@ -1,4 +1,4 @@
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
import { MapperInterface, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
import { Repository } from 'typeorm'
import { Authenticator } from '../../Domain/Authenticator/Authenticator'
@@ -11,6 +11,25 @@ export class MySQLAuthenticatorRepository implements AuthenticatorRepositoryInte
private mapper: MapperInterface<Authenticator, TypeORMAuthenticator>,
) {}
async findById(id: UniqueEntityId): Promise<Authenticator | null> {
const persistence = await this.ormRepository
.createQueryBuilder('authenticator')
.where('authenticator.uuid = :id', {
id: id.toString(),
})
.getOne()
if (persistence === null) {
return null
}
return this.mapper.toDomain(persistence)
}
async remove(authenticator: Authenticator): Promise<void> {
await this.ormRepository.remove(this.mapper.toProjection(authenticator))
}
async findByUserUuidAndCredentialId(userUuid: Uuid, credentialId: Buffer): Promise<Authenticator | null> {
const persistence = await this.ormRepository
.createQueryBuilder('authenticator')