浏览代码

feat(auth): add http endpoints for authenticators

Karol Sójko 2 年之前
父节点
当前提交
b6fda901ef
共有 28 个文件被更改,包括 328 次插入46 次删除
  1. 1 0
      packages/api-gateway/bin/server.ts
  2. 43 0
      packages/api-gateway/src/Controller/v1/AuthenticatorsController.ts
  3. 1 0
      packages/auth/bin/server.ts
  4. 15 0
      packages/auth/migrations/1672307975117-remove_compound_index.ts
  5. 16 5
      packages/auth/src/Bootstrap/Container.ts
  6. 1 0
      packages/auth/src/Bootstrap/Types.ts
  7. 122 0
      packages/auth/src/Controller/AuthenticatorsController.ts
  8. 0 1
      packages/auth/src/Domain/Authenticator/AuthenticatorChallengeRepositoryInterface.ts
  9. 3 3
      packages/auth/src/Domain/UseCase/VerifyAuthenticatorAuthenticationResponse/VerifyAuthenticatorAuthenticationResponse.ts
  10. 1 1
      packages/auth/src/Domain/UseCase/VerifyAuthenticatorAuthenticationResponse/VerifyAuthenticatorAuthenticationResponseDTO.ts
  11. 7 14
      packages/auth/src/Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse.spec.ts
  12. 1 4
      packages/auth/src/Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse.ts
  13. 0 1
      packages/auth/src/Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponseDTO.ts
  14. 3 0
      packages/auth/src/Infra/Http/Request/GenerateAuthenticatorAuthenticationOptionsRequestParams.ts
  15. 4 0
      packages/auth/src/Infra/Http/Request/GenerateAuthenticatorRegistrationOptionsRequestParams.ts
  16. 4 0
      packages/auth/src/Infra/Http/Request/VerifyAuthenticatorAuthenticationResponseRequestParams.ts
  17. 4 0
      packages/auth/src/Infra/Http/Request/VerifyAuthenticatorRegistrationResponseRequestParams.ts
  18. 8 0
      packages/auth/src/Infra/Http/Response/GenerateAuthenticatorAuthenticationOptionsResponse.ts
  19. 3 0
      packages/auth/src/Infra/Http/Response/GenerateAuthenticatorAuthenticationOptionsResponseBody.ts
  20. 8 0
      packages/auth/src/Infra/Http/Response/GenerateAuthenticatorRegistrationOptionsResponse.ts
  21. 3 0
      packages/auth/src/Infra/Http/Response/GenerateAuthenticatorRegistrationOptionsResponseBody.ts
  22. 8 0
      packages/auth/src/Infra/Http/Response/VerifyAuthenticatorAuthenticationResponseResponse.ts
  23. 3 0
      packages/auth/src/Infra/Http/Response/VerifyAuthenticatorAuthenticationResponseResponseBody.ts
  24. 8 0
      packages/auth/src/Infra/Http/Response/VerifyAuthenticatorRegistrationResponseResponse.ts
  25. 3 0
      packages/auth/src/Infra/Http/Response/VerifyAuthenticatorRegistrationResponseResponseBody.ts
  26. 58 0
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressAuthenticatorsController.ts
  27. 0 16
      packages/auth/src/Infra/MySQL/MySQLAuthenticatorChallengeRepository.ts
  28. 0 1
      packages/auth/src/Infra/TypeORM/TypeORMAuthenticatorChallenge.ts

+ 1 - 0
packages/api-gateway/bin/server.ts

@@ -21,6 +21,7 @@ import '../src/Controller/v1/FilesController'
 import '../src/Controller/v1/SubscriptionInvitesController'
 import '../src/Controller/v1/SubscriptionInvitesController'
 import '../src/Controller/v1/WorkspacesController'
 import '../src/Controller/v1/WorkspacesController'
 import '../src/Controller/v1/InvitesController'
 import '../src/Controller/v1/InvitesController'
+import '../src/Controller/v1/AuthenticatorsController'
 
 
 import '../src/Controller/v2/PaymentsControllerV2'
 import '../src/Controller/v2/PaymentsControllerV2'
 import '../src/Controller/v2/ActionsControllerV2'
 import '../src/Controller/v2/ActionsControllerV2'

+ 43 - 0
packages/api-gateway/src/Controller/v1/AuthenticatorsController.ts

@@ -0,0 +1,43 @@
+import { inject } from 'inversify'
+import { Request, Response } from 'express'
+import { controller, BaseHttpController, httpPost, httpGet } from 'inversify-express-utils'
+
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+
+@controller('/v1/authenticators', TYPES.AuthMiddleware)
+export class AuthenticatorsController extends BaseHttpController {
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+  }
+
+  @httpGet('/generate-registration-options')
+  async generateRegistrationOptions(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(
+      request,
+      response,
+      'authenticators/generate-registration-options',
+      request.body,
+    )
+  }
+
+  @httpGet('/generate-authentication-options')
+  async generateAuthenticationOptions(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(
+      request,
+      response,
+      'authenticators/generate-authentication-options',
+      request.body,
+    )
+  }
+
+  @httpPost('/verify-registration')
+  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)
+  }
+}

+ 1 - 0
packages/auth/bin/server.ts

@@ -21,6 +21,7 @@ import '../src/Controller/ListedController'
 import '../src/Controller/SubscriptionSettingsController'
 import '../src/Controller/SubscriptionSettingsController'
 
 
 import '../src/Infra/InversifyExpressUtils/InversifyExpressAuthController'
 import '../src/Infra/InversifyExpressUtils/InversifyExpressAuthController'
+import '../src/Infra/InversifyExpressUtils/InversifyExpressAuthenticatorsController'
 import '../src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController'
 import '../src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController'
 import '../src/Infra/InversifyExpressUtils/InversifyExpressUserRequestsController'
 import '../src/Infra/InversifyExpressUtils/InversifyExpressUserRequestsController'
 import '../src/Infra/InversifyExpressUtils/InversifyExpressWebSocketsController'
 import '../src/Infra/InversifyExpressUtils/InversifyExpressWebSocketsController'

+ 15 - 0
packages/auth/migrations/1672307975117-remove_compound_index.ts

@@ -0,0 +1,15 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class removeCompoundIndex1672307975117 implements MigrationInterface {
+  name = 'removeCompoundIndex1672307975117'
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX `user_uuid_and_challenge` ON `authenticator_challenges`')
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(
+      'CREATE INDEX `user_uuid_and_challenge` ON `authenticator_challenges` (`user_uuid`, `challenge`)',
+    )
+  }
+}

+ 16 - 5
packages/auth/src/Bootstrap/Container.ts

@@ -217,6 +217,7 @@ import { GenerateAuthenticatorRegistrationOptions } from '../Domain/UseCase/Gene
 import { VerifyAuthenticatorRegistrationResponse } from '../Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse'
 import { VerifyAuthenticatorRegistrationResponse } from '../Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse'
 import { GenerateAuthenticatorAuthenticationOptions } from '../Domain/UseCase/GenerateAuthenticatorAuthenticationOptions/GenerateAuthenticatorAuthenticationOptions'
 import { GenerateAuthenticatorAuthenticationOptions } from '../Domain/UseCase/GenerateAuthenticatorAuthenticationOptions/GenerateAuthenticatorAuthenticationOptions'
 import { VerifyAuthenticatorAuthenticationResponse } from '../Domain/UseCase/VerifyAuthenticatorAuthenticationResponse/VerifyAuthenticatorAuthenticationResponse'
 import { VerifyAuthenticatorAuthenticationResponse } from '../Domain/UseCase/VerifyAuthenticatorAuthenticationResponse/VerifyAuthenticatorAuthenticationResponse'
+import { AuthenticatorsController } from '../Controller/AuthenticatorsController'
 
 
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 const newrelicFormatter = require('@newrelic/winston-enricher')
 const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -303,11 +304,6 @@ export class ContainerConfigLoader {
       )
       )
       .toConstantValue(new AuthenticatorChallengePersistenceMapper())
       .toConstantValue(new AuthenticatorChallengePersistenceMapper())
 
 
-    // Controller
-    container.bind<AuthController>(TYPES.AuthController).to(AuthController)
-    container.bind<SubscriptionInvitesController>(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController)
-    container.bind<UserRequestsController>(TYPES.UserRequestsController).to(UserRequestsController)
-
     // ORM
     // ORM
     container
     container
       .bind<Repository<OfflineSetting>>(TYPES.ORMOfflineSettingRepository)
       .bind<Repository<OfflineSetting>>(TYPES.ORMOfflineSettingRepository)
@@ -641,6 +637,21 @@ export class ContainerConfigLoader {
     container.bind<CreateCrossServiceToken>(TYPES.CreateCrossServiceToken).to(CreateCrossServiceToken)
     container.bind<CreateCrossServiceToken>(TYPES.CreateCrossServiceToken).to(CreateCrossServiceToken)
     container.bind<ProcessUserRequest>(TYPES.ProcessUserRequest).to(ProcessUserRequest)
     container.bind<ProcessUserRequest>(TYPES.ProcessUserRequest).to(ProcessUserRequest)
 
 
+    // Controller
+    container.bind<AuthController>(TYPES.AuthController).to(AuthController)
+    container
+      .bind<AuthenticatorsController>(TYPES.AuthenticatorsController)
+      .toConstantValue(
+        new AuthenticatorsController(
+          container.get(TYPES.GenerateAuthenticatorRegistrationOptions),
+          container.get(TYPES.VerifyAuthenticatorRegistrationResponse),
+          container.get(TYPES.GenerateAuthenticatorAuthenticationOptions),
+          container.get(TYPES.VerifyAuthenticatorAuthenticationResponse),
+        ),
+      )
+    container.bind<SubscriptionInvitesController>(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController)
+    container.bind<UserRequestsController>(TYPES.UserRequestsController).to(UserRequestsController)
+
     // Handlers
     // Handlers
     container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
     container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
     container
     container

+ 1 - 0
packages/auth/src/Bootstrap/Types.ts

@@ -9,6 +9,7 @@ const TYPES = {
   AuthenticatorPersistenceMapper: Symbol.for('AuthenticatorPersistenceMapper'),
   AuthenticatorPersistenceMapper: Symbol.for('AuthenticatorPersistenceMapper'),
   // Controller
   // Controller
   AuthController: Symbol.for('AuthController'),
   AuthController: Symbol.for('AuthController'),
+  AuthenticatorsController: Symbol.for('AuthenticatorsController'),
   SubscriptionInvitesController: Symbol.for('SubscriptionInvitesController'),
   SubscriptionInvitesController: Symbol.for('SubscriptionInvitesController'),
   UserRequestsController: Symbol.for('UserRequestsController'),
   UserRequestsController: Symbol.for('UserRequestsController'),
   // Repositories
   // Repositories

+ 122 - 0
packages/auth/src/Controller/AuthenticatorsController.ts

@@ -0,0 +1,122 @@
+import { HttpStatusCode } from '@standardnotes/api'
+
+import { GenerateAuthenticatorAuthenticationOptions } from '../Domain/UseCase/GenerateAuthenticatorAuthenticationOptions/GenerateAuthenticatorAuthenticationOptions'
+import { GenerateAuthenticatorRegistrationOptions } from '../Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions'
+import { VerifyAuthenticatorAuthenticationResponse } from '../Domain/UseCase/VerifyAuthenticatorAuthenticationResponse/VerifyAuthenticatorAuthenticationResponse'
+import { VerifyAuthenticatorRegistrationResponse } from '../Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse'
+import { GenerateAuthenticatorAuthenticationOptionsRequestParams } from '../Infra/Http/Request/GenerateAuthenticatorAuthenticationOptionsRequestParams'
+import { GenerateAuthenticatorRegistrationOptionsRequestParams } from '../Infra/Http/Request/GenerateAuthenticatorRegistrationOptionsRequestParams'
+import { VerifyAuthenticatorAuthenticationResponseRequestParams } from '../Infra/Http/Request/VerifyAuthenticatorAuthenticationResponseRequestParams'
+import { VerifyAuthenticatorRegistrationResponseRequestParams } from '../Infra/Http/Request/VerifyAuthenticatorRegistrationResponseRequestParams'
+import { GenerateAuthenticatorAuthenticationOptionsResponse } from '../Infra/Http/Response/GenerateAuthenticatorAuthenticationOptionsResponse'
+import { GenerateAuthenticatorRegistrationOptionsResponse } from '../Infra/Http/Response/GenerateAuthenticatorRegistrationOptionsResponse'
+import { VerifyAuthenticatorAuthenticationResponseResponse } from '../Infra/Http/Response/VerifyAuthenticatorAuthenticationResponseResponse'
+import { VerifyAuthenticatorRegistrationResponseResponse } from '../Infra/Http/Response/VerifyAuthenticatorRegistrationResponseResponse'
+
+export class AuthenticatorsController {
+  constructor(
+    private generateAuthenticatorRegistrationOptions: GenerateAuthenticatorRegistrationOptions,
+    private verifyAuthenticatorRegistrationResponse: VerifyAuthenticatorRegistrationResponse,
+    private generateAuthenticatorAuthenticationOptions: GenerateAuthenticatorAuthenticationOptions,
+    private verifyAuthenticatorAuthenticationResponse: VerifyAuthenticatorAuthenticationResponse,
+  ) {}
+
+  async generateRegistrationOptions(
+    params: GenerateAuthenticatorRegistrationOptionsRequestParams,
+  ): Promise<GenerateAuthenticatorRegistrationOptionsResponse> {
+    const result = await this.generateAuthenticatorRegistrationOptions.execute({
+      userUuid: params.userUuid,
+      username: params.username,
+    })
+
+    if (result.isFailed()) {
+      return {
+        status: HttpStatusCode.BadRequest,
+        data: {
+          error: {
+            message: result.getError(),
+          },
+        },
+      }
+    }
+
+    return {
+      status: HttpStatusCode.Success,
+      data: { options: result.getValue() },
+    }
+  }
+
+  async verifyRegistrationResponse(
+    params: VerifyAuthenticatorRegistrationResponseRequestParams,
+  ): Promise<VerifyAuthenticatorRegistrationResponseResponse> {
+    const result = await this.verifyAuthenticatorRegistrationResponse.execute({
+      userUuid: params.userUuid,
+      registrationCredential: params.registrationCredential,
+    })
+
+    if (result.isFailed()) {
+      return {
+        status: HttpStatusCode.Unauthorized,
+        data: {
+          error: {
+            message: result.getError(),
+          },
+        },
+      }
+    }
+
+    return {
+      status: HttpStatusCode.Success,
+      data: { success: result.getValue() },
+    }
+  }
+
+  async generateAuthenticationOptions(
+    params: GenerateAuthenticatorAuthenticationOptionsRequestParams,
+  ): Promise<GenerateAuthenticatorAuthenticationOptionsResponse> {
+    const result = await this.generateAuthenticatorAuthenticationOptions.execute({
+      userUuid: params.userUuid,
+    })
+
+    if (result.isFailed()) {
+      return {
+        status: HttpStatusCode.BadRequest,
+        data: {
+          error: {
+            message: result.getError(),
+          },
+        },
+      }
+    }
+
+    return {
+      status: HttpStatusCode.Success,
+      data: { options: result.getValue() },
+    }
+  }
+
+  async verifyAuthenticationResponse(
+    params: VerifyAuthenticatorAuthenticationResponseRequestParams,
+  ): Promise<VerifyAuthenticatorAuthenticationResponseResponse> {
+    const result = await this.verifyAuthenticatorAuthenticationResponse.execute({
+      userUuid: params.userUuid,
+      authenticationCredential: params.authenticationCredential,
+    })
+
+    if (result.isFailed()) {
+      return {
+        status: HttpStatusCode.Unauthorized,
+        data: {
+          error: {
+            message: result.getError(),
+          },
+        },
+      }
+    }
+
+    return {
+      status: HttpStatusCode.Success,
+      data: { success: result.getValue() },
+    }
+  }
+}

+ 0 - 1
packages/auth/src/Domain/Authenticator/AuthenticatorChallengeRepositoryInterface.ts

@@ -3,7 +3,6 @@ import { Uuid } from '@standardnotes/domain-core'
 import { AuthenticatorChallenge } from './AuthenticatorChallenge'
 import { AuthenticatorChallenge } from './AuthenticatorChallenge'
 
 
 export interface AuthenticatorChallengeRepositoryInterface {
 export interface AuthenticatorChallengeRepositoryInterface {
-  findByUserUuidAndChallenge(userUuid: Uuid, challenge: Buffer): Promise<AuthenticatorChallenge | null>
   findByUserUuid(userUuid: Uuid): Promise<AuthenticatorChallenge | null>
   findByUserUuid(userUuid: Uuid): Promise<AuthenticatorChallenge | null>
   save(authenticatorChallenge: AuthenticatorChallenge): Promise<void>
   save(authenticatorChallenge: AuthenticatorChallenge): Promise<void>
 }
 }

+ 3 - 3
packages/auth/src/Domain/UseCase/VerifyAuthenticatorAuthenticationResponse/VerifyAuthenticatorAuthenticationResponse.ts

@@ -27,18 +27,18 @@ export class VerifyAuthenticatorAuthenticationResponse implements UseCaseInterfa
 
 
     const authenticator = await this.authenticatorRepository.findByUserUuidAndCredentialId(
     const authenticator = await this.authenticatorRepository.findByUserUuidAndCredentialId(
       userUuid,
       userUuid,
-      Buffer.from(dto.registrationCredential.id as string),
+      Buffer.from(dto.authenticationCredential.id as string),
     )
     )
     if (!authenticator) {
     if (!authenticator) {
       return Result.fail(
       return Result.fail(
-        `Could not verify authenticator authentication response: authenticator ${dto.registrationCredential.id} not found`,
+        `Could not verify authenticator authentication response: authenticator ${dto.authenticationCredential.id} not found`,
       )
       )
     }
     }
 
 
     let verification: VerifiedAuthenticationResponse
     let verification: VerifiedAuthenticationResponse
     try {
     try {
       verification = await verifyAuthenticationResponse({
       verification = await verifyAuthenticationResponse({
-        credential: dto.registrationCredential,
+        credential: dto.authenticationCredential,
         expectedChallenge: authenticatorChallenge.props.challenge.toString(),
         expectedChallenge: authenticatorChallenge.props.challenge.toString(),
         expectedOrigin: `https://${RelyingParty.RP_ID}`,
         expectedOrigin: `https://${RelyingParty.RP_ID}`,
         expectedRPID: RelyingParty.RP_ID,
         expectedRPID: RelyingParty.RP_ID,

+ 1 - 1
packages/auth/src/Domain/UseCase/VerifyAuthenticatorAuthenticationResponse/VerifyAuthenticatorAuthenticationResponseDTO.ts

@@ -1,4 +1,4 @@
 export interface VerifyAuthenticatorAuthenticationResponseDTO {
 export interface VerifyAuthenticatorAuthenticationResponseDTO {
   userUuid: string
   userUuid: string
-  registrationCredential: Record<string, unknown>
+  authenticationCredential: Record<string, unknown>
 }
 }

+ 7 - 14
packages/auth/src/Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse.spec.ts

@@ -20,7 +20,7 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
     authenticatorRepository.save = jest.fn()
     authenticatorRepository.save = jest.fn()
 
 
     authenticatorChallengeRepository = {} as jest.Mocked<AuthenticatorChallengeRepositoryInterface>
     authenticatorChallengeRepository = {} as jest.Mocked<AuthenticatorChallengeRepositoryInterface>
-    authenticatorChallengeRepository.findByUserUuidAndChallenge = jest.fn().mockReturnValue({
+    authenticatorChallengeRepository.findByUserUuid = jest.fn().mockReturnValue({
       props: {
       props: {
         challenge: Buffer.from('challenge'),
         challenge: Buffer.from('challenge'),
       },
       },
@@ -32,7 +32,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
 
 
     const result = await useCase.execute({
     const result = await useCase.execute({
       userUuid: 'invalid',
       userUuid: 'invalid',
-      challenge: Buffer.from('challenge'),
       registrationCredential: {
       registrationCredential: {
         id: Buffer.from('id'),
         id: Buffer.from('id'),
         rawId: Buffer.from('rawId'),
         rawId: Buffer.from('rawId'),
@@ -51,13 +50,12 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
   })
   })
 
 
   it('should return error if challenge is not found', async () => {
   it('should return error if challenge is not found', async () => {
-    authenticatorChallengeRepository.findByUserUuidAndChallenge = jest.fn().mockReturnValue(null)
+    authenticatorChallengeRepository.findByUserUuid = jest.fn().mockReturnValue(null)
 
 
     const useCase = createUseCase()
     const useCase = createUseCase()
 
 
     const result = await useCase.execute({
     const result = await useCase.execute({
       userUuid: '00000000-0000-0000-0000-000000000000',
       userUuid: '00000000-0000-0000-0000-000000000000',
-      challenge: Buffer.from('challenge'),
       registrationCredential: {
       registrationCredential: {
         id: Buffer.from('id'),
         id: Buffer.from('id'),
         rawId: Buffer.from('rawId'),
         rawId: Buffer.from('rawId'),
@@ -74,7 +72,7 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
   })
   })
 
 
   it('should return error if verification could not verify', async () => {
   it('should return error if verification could not verify', async () => {
-    authenticatorChallengeRepository.findByUserUuidAndChallenge = jest.fn().mockReturnValue({
+    authenticatorChallengeRepository.findByUserUuid = jest.fn().mockReturnValue({
       props: {
       props: {
         challenge: Buffer.from('challenge'),
         challenge: Buffer.from('challenge'),
       },
       },
@@ -98,7 +96,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
 
 
     const result = await useCase.execute({
     const result = await useCase.execute({
       userUuid: '00000000-0000-0000-0000-000000000000',
       userUuid: '00000000-0000-0000-0000-000000000000',
-      challenge: Buffer.from('invalid'),
       registrationCredential: {
       registrationCredential: {
         id: Buffer.from('id'),
         id: Buffer.from('id'),
         rawId: Buffer.from('rawId'),
         rawId: Buffer.from('rawId'),
@@ -117,7 +114,7 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
   })
   })
 
 
   it('should return error if verification throws error', async () => {
   it('should return error if verification throws error', async () => {
-    authenticatorChallengeRepository.findByUserUuidAndChallenge = jest.fn().mockReturnValue({
+    authenticatorChallengeRepository.findByUserUuid = jest.fn().mockReturnValue({
       props: {
       props: {
         challenge: Buffer.from('challenge'),
         challenge: Buffer.from('challenge'),
       },
       },
@@ -132,7 +129,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
 
 
     const result = await useCase.execute({
     const result = await useCase.execute({
       userUuid: '00000000-0000-0000-0000-000000000000',
       userUuid: '00000000-0000-0000-0000-000000000000',
-      challenge: Buffer.from('invalid'),
       registrationCredential: {
       registrationCredential: {
         id: Buffer.from('id'),
         id: Buffer.from('id'),
         rawId: Buffer.from('rawId'),
         rawId: Buffer.from('rawId'),
@@ -151,7 +147,7 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
   })
   })
 
 
   it('should return error if verification is missing registration info', async () => {
   it('should return error if verification is missing registration info', async () => {
-    authenticatorChallengeRepository.findByUserUuidAndChallenge = jest.fn().mockReturnValue({
+    authenticatorChallengeRepository.findByUserUuid = jest.fn().mockReturnValue({
       props: {
       props: {
         challenge: Buffer.from('challenge'),
         challenge: Buffer.from('challenge'),
       },
       },
@@ -168,7 +164,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
 
 
     const result = await useCase.execute({
     const result = await useCase.execute({
       userUuid: '00000000-0000-0000-0000-000000000000',
       userUuid: '00000000-0000-0000-0000-000000000000',
-      challenge: Buffer.from('invalid'),
       registrationCredential: {
       registrationCredential: {
         id: Buffer.from('id'),
         id: Buffer.from('id'),
         rawId: Buffer.from('rawId'),
         rawId: Buffer.from('rawId'),
@@ -189,7 +184,7 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
   })
   })
 
 
   it('should return error if authenticator could not be created', async () => {
   it('should return error if authenticator could not be created', async () => {
-    authenticatorChallengeRepository.findByUserUuidAndChallenge = jest.fn().mockReturnValue({
+    authenticatorChallengeRepository.findByUserUuid = jest.fn().mockReturnValue({
       props: {
       props: {
         challenge: Buffer.from('challenge'),
         challenge: Buffer.from('challenge'),
       },
       },
@@ -218,7 +213,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
 
 
     const result = await useCase.execute({
     const result = await useCase.execute({
       userUuid: '00000000-0000-0000-0000-000000000000',
       userUuid: '00000000-0000-0000-0000-000000000000',
-      challenge: Buffer.from('invalid'),
       registrationCredential: {
       registrationCredential: {
         id: Buffer.from('id'),
         id: Buffer.from('id'),
         rawId: Buffer.from('rawId'),
         rawId: Buffer.from('rawId'),
@@ -238,7 +232,7 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
   })
   })
 
 
   it('should verify authenticator registration response', async () => {
   it('should verify authenticator registration response', async () => {
-    authenticatorChallengeRepository.findByUserUuidAndChallenge = jest.fn().mockReturnValue({
+    authenticatorChallengeRepository.findByUserUuid = jest.fn().mockReturnValue({
       props: {
       props: {
         challenge: Buffer.from('challenge'),
         challenge: Buffer.from('challenge'),
       },
       },
@@ -262,7 +256,6 @@ describe('VerifyAuthenticatorRegistrationResponse', () => {
 
 
     const result = await useCase.execute({
     const result = await useCase.execute({
       userUuid: '00000000-0000-0000-0000-000000000000',
       userUuid: '00000000-0000-0000-0000-000000000000',
-      challenge: Buffer.from('invalid'),
       registrationCredential: {
       registrationCredential: {
         id: Buffer.from('id'),
         id: Buffer.from('id'),
         rawId: Buffer.from('rawId'),
         rawId: Buffer.from('rawId'),

+ 1 - 4
packages/auth/src/Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse.ts

@@ -20,10 +20,7 @@ export class VerifyAuthenticatorRegistrationResponse implements UseCaseInterface
     }
     }
     const userUuid = userUuidOrError.getValue()
     const userUuid = userUuidOrError.getValue()
 
 
-    const authenticatorChallenge = await this.authenticatorChallengeRepository.findByUserUuidAndChallenge(
-      userUuid,
-      dto.challenge,
-    )
+    const authenticatorChallenge = await this.authenticatorChallengeRepository.findByUserUuid(userUuid)
     if (!authenticatorChallenge) {
     if (!authenticatorChallenge) {
       return Result.fail('Could not verify authenticator registration response: challenge not found')
       return Result.fail('Could not verify authenticator registration response: challenge not found')
     }
     }

+ 0 - 1
packages/auth/src/Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponseDTO.ts

@@ -1,5 +1,4 @@
 export interface VerifyAuthenticatorRegistrationResponseDTO {
 export interface VerifyAuthenticatorRegistrationResponseDTO {
   userUuid: string
   userUuid: string
-  challenge: Buffer
   registrationCredential: Record<string, unknown>
   registrationCredential: Record<string, unknown>
 }
 }

+ 3 - 0
packages/auth/src/Infra/Http/Request/GenerateAuthenticatorAuthenticationOptionsRequestParams.ts

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

+ 4 - 0
packages/auth/src/Infra/Http/Request/GenerateAuthenticatorRegistrationOptionsRequestParams.ts

@@ -0,0 +1,4 @@
+export interface GenerateAuthenticatorRegistrationOptionsRequestParams {
+  userUuid: string
+  username: string
+}

+ 4 - 0
packages/auth/src/Infra/Http/Request/VerifyAuthenticatorAuthenticationResponseRequestParams.ts

@@ -0,0 +1,4 @@
+export interface VerifyAuthenticatorAuthenticationResponseRequestParams {
+  userUuid: string
+  authenticationCredential: Record<string, unknown>
+}

+ 4 - 0
packages/auth/src/Infra/Http/Request/VerifyAuthenticatorRegistrationResponseRequestParams.ts

@@ -0,0 +1,4 @@
+export interface VerifyAuthenticatorRegistrationResponseRequestParams {
+  userUuid: string
+  registrationCredential: Record<string, unknown>
+}

+ 8 - 0
packages/auth/src/Infra/Http/Response/GenerateAuthenticatorAuthenticationOptionsResponse.ts

@@ -0,0 +1,8 @@
+import { HttpErrorResponseBody, HttpResponse } from '@standardnotes/api'
+import { Either } from '@standardnotes/common'
+
+import { GenerateAuthenticatorAuthenticationOptionsResponseBody } from './GenerateAuthenticatorAuthenticationOptionsResponseBody'
+
+export interface GenerateAuthenticatorAuthenticationOptionsResponse extends HttpResponse {
+  data: Either<GenerateAuthenticatorAuthenticationOptionsResponseBody, HttpErrorResponseBody>
+}

+ 3 - 0
packages/auth/src/Infra/Http/Response/GenerateAuthenticatorAuthenticationOptionsResponseBody.ts

@@ -0,0 +1,3 @@
+export interface GenerateAuthenticatorAuthenticationOptionsResponseBody {
+  options: Record<string, unknown>
+}

+ 8 - 0
packages/auth/src/Infra/Http/Response/GenerateAuthenticatorRegistrationOptionsResponse.ts

@@ -0,0 +1,8 @@
+import { HttpErrorResponseBody, HttpResponse } from '@standardnotes/api'
+import { Either } from '@standardnotes/common'
+
+import { GenerateAuthenticatorRegistrationOptionsResponseBody } from './GenerateAuthenticatorRegistrationOptionsResponseBody'
+
+export interface GenerateAuthenticatorRegistrationOptionsResponse extends HttpResponse {
+  data: Either<GenerateAuthenticatorRegistrationOptionsResponseBody, HttpErrorResponseBody>
+}

+ 3 - 0
packages/auth/src/Infra/Http/Response/GenerateAuthenticatorRegistrationOptionsResponseBody.ts

@@ -0,0 +1,3 @@
+export interface GenerateAuthenticatorRegistrationOptionsResponseBody {
+  options: Record<string, unknown>
+}

+ 8 - 0
packages/auth/src/Infra/Http/Response/VerifyAuthenticatorAuthenticationResponseResponse.ts

@@ -0,0 +1,8 @@
+import { HttpErrorResponseBody, HttpResponse } from '@standardnotes/api'
+import { Either } from '@standardnotes/common'
+
+import { VerifyAuthenticatorAuthenticationResponseResponseBody } from './VerifyAuthenticatorAuthenticationResponseResponseBody'
+
+export interface VerifyAuthenticatorAuthenticationResponseResponse extends HttpResponse {
+  data: Either<VerifyAuthenticatorAuthenticationResponseResponseBody, HttpErrorResponseBody>
+}

+ 3 - 0
packages/auth/src/Infra/Http/Response/VerifyAuthenticatorAuthenticationResponseResponseBody.ts

@@ -0,0 +1,3 @@
+export interface VerifyAuthenticatorAuthenticationResponseResponseBody {
+  success: boolean
+}

+ 8 - 0
packages/auth/src/Infra/Http/Response/VerifyAuthenticatorRegistrationResponseResponse.ts

@@ -0,0 +1,8 @@
+import { HttpErrorResponseBody, HttpResponse } from '@standardnotes/api'
+import { Either } from '@standardnotes/common'
+
+import { VerifyAuthenticatorRegistrationResponseResponseBody } from './VerifyAuthenticatorRegistrationResponseResponseBody'
+
+export interface VerifyAuthenticatorRegistrationResponseResponse extends HttpResponse {
+  data: Either<VerifyAuthenticatorRegistrationResponseResponseBody, HttpErrorResponseBody>
+}

+ 3 - 0
packages/auth/src/Infra/Http/Response/VerifyAuthenticatorRegistrationResponseResponseBody.ts

@@ -0,0 +1,3 @@
+export interface VerifyAuthenticatorRegistrationResponseResponseBody {
+  success: boolean
+}

+ 58 - 0
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressAuthenticatorsController.ts

@@ -0,0 +1,58 @@
+import { Request, Response } from 'express'
+import { inject } from 'inversify'
+import {
+  BaseHttpController,
+  controller,
+  httpGet,
+  httpPost,
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  results,
+} from 'inversify-express-utils'
+import TYPES from '../../Bootstrap/Types'
+import { AuthenticatorsController } from '../../Controller/AuthenticatorsController'
+
+@controller('/authenticators', TYPES.ApiGatewayAuthMiddleware)
+export class InversifyExpressAuthenticatorsController extends BaseHttpController {
+  constructor(@inject(TYPES.AuthenticatorsController) private authenticatorsController: AuthenticatorsController) {
+    super()
+  }
+
+  @httpGet('/generate-registration-options')
+  async generateRegistrationOptions(_request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.authenticatorsController.generateRegistrationOptions({
+      username: response.locals.user.email,
+      userUuid: response.locals.user.uuid,
+    })
+
+    return this.json(result.data, result.status)
+  }
+
+  @httpPost('/verify-registration')
+  async verifyRegistration(request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.authenticatorsController.verifyRegistrationResponse({
+      userUuid: response.locals.user.uuid,
+      registrationCredential: request.body,
+    })
+
+    return this.json(result.data, result.status)
+  }
+
+  @httpGet('/generate-authentication-options')
+  async generateAuthenticationOptions(_request: Request, response: Response): 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,
+      authenticationCredential: request.body,
+    })
+
+    return this.json(result.data, result.status)
+  }
+}

+ 0 - 16
packages/auth/src/Infra/MySQL/MySQLAuthenticatorChallengeRepository.ts

@@ -12,22 +12,6 @@ export class MySQLAuthenticatorChallengeRepository implements AuthenticatorChall
     private mapper: MapperInterface<AuthenticatorChallenge, TypeORMAuthenticatorChallenge>,
     private mapper: MapperInterface<AuthenticatorChallenge, TypeORMAuthenticatorChallenge>,
   ) {}
   ) {}
 
 
-  async findByUserUuidAndChallenge(userUuid: Uuid, challenge: Buffer): Promise<AuthenticatorChallenge | null> {
-    const typeOrm = await this.ormRepository
-      .createQueryBuilder('challenge')
-      .where('challenge.user_uuid = :userUuid and challenge.challenge = :challenge', {
-        userUuid: userUuid.value,
-        challenge,
-      })
-      .getOne()
-
-    if (typeOrm === null) {
-      return null
-    }
-
-    return this.mapper.toDomain(typeOrm)
-  }
-
   async save(authenticatorChallenge: AuthenticatorChallenge): Promise<void> {
   async save(authenticatorChallenge: AuthenticatorChallenge): Promise<void> {
     let persistence = this.mapper.toProjection(authenticatorChallenge)
     let persistence = this.mapper.toProjection(authenticatorChallenge)
 
 

+ 0 - 1
packages/auth/src/Infra/TypeORM/TypeORMAuthenticatorChallenge.ts

@@ -1,7 +1,6 @@
 import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
 import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
 
 
 @Entity({ name: 'authenticator_challenges' })
 @Entity({ name: 'authenticator_challenges' })
-@Index('user_uuid_and_challenge', ['userUuid', 'challenge'])
 export class TypeORMAuthenticatorChallenge {
 export class TypeORMAuthenticatorChallenge {
   @PrimaryGeneratedColumn('uuid')
   @PrimaryGeneratedColumn('uuid')
   declare uuid: string
   declare uuid: string