From de50d76800a4240729763b2df11c4a1718951670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Thu, 29 Dec 2022 13:55:03 +0100 Subject: [PATCH] feat(auth): add removing authenticator --- .../Controller/v1/AuthenticatorsController.ts | 12 +++- packages/auth/src/Bootstrap/Container.ts | 5 ++ packages/auth/src/Bootstrap/Types.ts | 1 + .../Controller/AuthenticatorsController.ts | 18 +++++ .../AuthenticatorRepositoryInterface.ts | 5 +- .../DeleteAuthenticator.spec.ts | 70 +++++++++++++++++++ .../DeleteAuthenticator.ts | 17 +++++ .../DeleteAuthenticatorDTO.ts | 4 ++ .../DeleteAuthenticatorRequestParams.ts | 4 ++ .../Response/DeleteAuthenticatorResponse.ts | 8 +++ .../DeleteAuthenticatorResponseBody.ts | 3 + ...nversifyExpressAuthenticatorsController.ts | 11 +++ .../MySQL/MySQLAuthenticatorRepository.ts | 21 +++++- 13 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.spec.ts create mode 100644 packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.ts create mode 100644 packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticatorDTO.ts create mode 100644 packages/auth/src/Infra/Http/Request/DeleteAuthenticatorRequestParams.ts create mode 100644 packages/auth/src/Infra/Http/Response/DeleteAuthenticatorResponse.ts create mode 100644 packages/auth/src/Infra/Http/Response/DeleteAuthenticatorResponseBody.ts diff --git a/packages/api-gateway/src/Controller/v1/AuthenticatorsController.ts b/packages/api-gateway/src/Controller/v1/AuthenticatorsController.ts index 5c6fc57e2..4478a96b6 100644 --- a/packages/api-gateway/src/Controller/v1/AuthenticatorsController.ts +++ b/packages/api-gateway/src/Controller/v1/AuthenticatorsController.ts @@ -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 { + await this.httpService.callAuthServer( + request, + response, + `authenticators/${request.params.authenticatorId}`, + request.body, + ) + } + @httpGet('/') async list(request: Request, response: Response): Promise { await this.httpService.callAuthServer(request, response, 'authenticators/', request.body) diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index 43692321b..ecbad87b8 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/packages/auth/src/Bootstrap/Container.ts @@ -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(TYPES.ListAuthenticators) .toConstantValue(new ListAuthenticators(container.get(TYPES.AuthenticatorRepository))) + container + .bind(TYPES.DeleteAuthenticator) + .toConstantValue(new DeleteAuthenticator(container.get(TYPES.AuthenticatorRepository))) container .bind(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), ), ) diff --git a/packages/auth/src/Bootstrap/Types.ts b/packages/auth/src/Bootstrap/Types.ts index c5e3ffbc3..eed911728 100644 --- a/packages/auth/src/Bootstrap/Types.ts +++ b/packages/auth/src/Bootstrap/Types.ts @@ -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'), diff --git a/packages/auth/src/Controller/AuthenticatorsController.ts b/packages/auth/src/Controller/AuthenticatorsController.ts index 4c5ed11ae..e819b29e8 100644 --- a/packages/auth/src/Controller/AuthenticatorsController.ts +++ b/packages/auth/src/Controller/AuthenticatorsController.ts @@ -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, ) {} @@ -44,6 +48,20 @@ export class AuthenticatorsController { } } + async delete(params: DeleteAuthenticatorRequestParams): Promise { + 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 { diff --git a/packages/auth/src/Domain/Authenticator/AuthenticatorRepositoryInterface.ts b/packages/auth/src/Domain/Authenticator/AuthenticatorRepositoryInterface.ts index 99e8ac0c7..b622f8c13 100644 --- a/packages/auth/src/Domain/Authenticator/AuthenticatorRepositoryInterface.ts +++ b/packages/auth/src/Domain/Authenticator/AuthenticatorRepositoryInterface.ts @@ -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 + findById(id: UniqueEntityId): Promise findByUserUuidAndCredentialId(userUuid: Uuid, credentialId: Buffer): Promise save(authenticator: Authenticator): Promise + remove(authenticator: Authenticator): Promise } diff --git a/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.spec.ts b/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.spec.ts new file mode 100644 index 000000000..ac540de2c --- /dev/null +++ b/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.spec.ts @@ -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 + 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() + }) +}) diff --git a/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.ts b/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.ts new file mode 100644 index 000000000..9c61fa273 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator.ts @@ -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 { + constructor(private authenticatorRepository: AuthenticatorRepositoryInterface) {} + async execute(dto: DeleteAuthenticatorDTO): Promise> { + 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') + } +} diff --git a/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticatorDTO.ts b/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticatorDTO.ts new file mode 100644 index 000000000..fcb233791 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticatorDTO.ts @@ -0,0 +1,4 @@ +export interface DeleteAuthenticatorDTO { + userUuid: string + authenticatorId: string +} diff --git a/packages/auth/src/Infra/Http/Request/DeleteAuthenticatorRequestParams.ts b/packages/auth/src/Infra/Http/Request/DeleteAuthenticatorRequestParams.ts new file mode 100644 index 000000000..73a22914a --- /dev/null +++ b/packages/auth/src/Infra/Http/Request/DeleteAuthenticatorRequestParams.ts @@ -0,0 +1,4 @@ +export interface DeleteAuthenticatorRequestParams { + userUuid: string + authenticatorId: string +} diff --git a/packages/auth/src/Infra/Http/Response/DeleteAuthenticatorResponse.ts b/packages/auth/src/Infra/Http/Response/DeleteAuthenticatorResponse.ts new file mode 100644 index 000000000..69bf5804f --- /dev/null +++ b/packages/auth/src/Infra/Http/Response/DeleteAuthenticatorResponse.ts @@ -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 +} diff --git a/packages/auth/src/Infra/Http/Response/DeleteAuthenticatorResponseBody.ts b/packages/auth/src/Infra/Http/Response/DeleteAuthenticatorResponseBody.ts new file mode 100644 index 000000000..148baf369 --- /dev/null +++ b/packages/auth/src/Infra/Http/Response/DeleteAuthenticatorResponseBody.ts @@ -0,0 +1,3 @@ +export interface DeleteAuthenticatorResponseBody { + message: string +} diff --git a/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressAuthenticatorsController.ts b/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressAuthenticatorsController.ts index 5b8f1d2e0..f763847e5 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressAuthenticatorsController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressAuthenticatorsController.ts @@ -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 { + 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 { const result = await this.authenticatorsController.generateRegistrationOptions({ diff --git a/packages/auth/src/Infra/MySQL/MySQLAuthenticatorRepository.ts b/packages/auth/src/Infra/MySQL/MySQLAuthenticatorRepository.ts index c813bdcfc..ca5242fe3 100644 --- a/packages/auth/src/Infra/MySQL/MySQLAuthenticatorRepository.ts +++ b/packages/auth/src/Infra/MySQL/MySQLAuthenticatorRepository.ts @@ -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, ) {} + async findById(id: UniqueEntityId): Promise { + 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 { + await this.ormRepository.remove(this.mapper.toProjection(authenticator)) + } + async findByUserUuidAndCredentialId(userUuid: Uuid, credentialId: Buffer): Promise { const persistence = await this.ormRepository .createQueryBuilder('authenticator')