From 9a417d1e6cab28f7f0f4ed79b1418dfbfea217b4 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 26 Aug 2021 04:04:21 -0400 Subject: [PATCH 1/3] Add new use cases to authorize user, register SPs, and get id tokens --- .../authenticate-user-errors.ts | 11 +-- .../authorize-user-controller.test.unit.ts | 80 ++++++++++++++++ .../authorize-user-controller.ts | 53 +++++++++++ .../authorize-user/authorize-user-dto.ts | 33 +++++++ .../authorize-user/authorize-user-errors.ts | 12 +++ .../authorize-user/authorize-user-use-case.ts | 76 +++++++++++++++ .../use-cases/create-user/create-user-dto.ts | 18 ++-- .../create-user/create-user-errors.ts | 6 -- .../create-user/create-user-use-case.ts | 50 ++++------ .../__tests__/discover-sp.test.unit.ts | 63 +++++++++++++ .../discover-sp/discover-sp-controller.ts | 50 ++++++++++ .../use-cases/discover-sp/discover-sp-dto.ts | 11 +++ .../discover-sp/discover-sp-errors.ts | 8 ++ .../discover-sp/discover-sp-use-case.ts | 65 +++++++++++++ .../get-token-controller.test.unit.ts | 62 +++++++++++++ .../get-token/get-token-controller.ts | 50 ++++++++++ .../use-cases/get-token/get-token-dto.ts | 31 +++++++ .../use-cases/get-token/get-token-errors.ts | 8 ++ .../use-cases/get-token/get-token-use-case.ts | 93 +++++++++++++++++++ .../use-cases/get-user/get-user-errors.ts | 11 +-- .../login-user/login-user-controller.ts | 17 ++-- .../use-cases/login-user/login-user-dto.ts | 17 +--- .../use-cases/login-user/login-user-errors.ts | 10 +- .../login-user/login-user-use-case.ts | 52 +++++------ .../protected-user-controller.test.unit.ts | 56 ----------- .../protected-user-controller.ts | 44 --------- .../protected-user/protected-user-dto.ts | 10 -- .../protected-user/protected-user-use-case.ts | 28 ------ .../__tests__/auth-code.test.unit.ts | 7 +- .../domain/entities/auth-code/auth-code.ts | 12 ++- .../entities/auth-secret/auth-secret.ts | 16 +++- .../entities/user/__tests__/user.test.unit.ts | 23 ----- .../users/domain/entities/user/user.ts | 25 +---- .../__tests__/authCode.test.unit.ts | 7 +- .../{auth-code.ts => auth-code-string.ts} | 10 +- .../repos/auth-code-repo/auth-code-repo.ts | 2 +- .../implementations/mock-auth-code-repo.ts | 2 +- .../implementations/redis-auth-code-repo.ts | 12 ++- .../auth-secret-repo/auth-secret-repo.ts | 9 +- .../implementations/mikro-auth-secret-repo.ts | 21 ++++- .../implementations/mock-auth-secret-repo.ts | 17 +++- .../modules/users/mappers/auth-code-map.ts | 7 +- .../modules/users/mappers/auth-secret-map.ts | 7 +- server/src/setup/application/controllers.ts | 8 +- server/src/setup/application/types.ts | 18 +++- server/src/setup/application/use-cases.ts | 12 ++- server/src/shared/app/base-controller.ts | 4 +- .../implementations/mock-user-auth-handler.ts | 12 +-- .../passport-user-auth-handler.ts | 59 +++--------- server/src/shared/auth/user-auth-handler.ts | 16 +--- .../infra/cache/entities/auth-code-entity.ts | 8 +- .../shared/infra/cache/redis-repository.ts | 9 +- .../infra/db/entities/auth-secret.entity.ts | 6 ++ .../shared/infra/db/entities/user.entity.ts | 6 -- server/src/shared/infra/db/errors/errors.ts | 35 ++++--- .../src/test-utils/mocks/application/index.ts | 4 +- .../mocks/application/mock-authorize-user.ts | 20 ++++ .../mocks/application/mock-discover-sp.ts | 14 +++ .../mocks/application/mock-get-token.ts | 20 ++++ .../mocks/application/mock-protected-user.ts | 11 --- 60 files changed, 1010 insertions(+), 454 deletions(-) create mode 100644 server/src/modules/users/application/use-cases/authorize-user/__tests__/authorize-user-controller.test.unit.ts create mode 100644 server/src/modules/users/application/use-cases/authorize-user/authorize-user-controller.ts create mode 100644 server/src/modules/users/application/use-cases/authorize-user/authorize-user-dto.ts create mode 100644 server/src/modules/users/application/use-cases/authorize-user/authorize-user-errors.ts create mode 100644 server/src/modules/users/application/use-cases/authorize-user/authorize-user-use-case.ts create mode 100644 server/src/modules/users/application/use-cases/discover-sp/__tests__/discover-sp.test.unit.ts create mode 100644 server/src/modules/users/application/use-cases/discover-sp/discover-sp-controller.ts create mode 100644 server/src/modules/users/application/use-cases/discover-sp/discover-sp-dto.ts create mode 100644 server/src/modules/users/application/use-cases/discover-sp/discover-sp-errors.ts create mode 100644 server/src/modules/users/application/use-cases/discover-sp/discover-sp-use-case.ts create mode 100644 server/src/modules/users/application/use-cases/get-token/__tests__/get-token-controller.test.unit.ts create mode 100644 server/src/modules/users/application/use-cases/get-token/get-token-controller.ts create mode 100644 server/src/modules/users/application/use-cases/get-token/get-token-dto.ts create mode 100644 server/src/modules/users/application/use-cases/get-token/get-token-errors.ts create mode 100644 server/src/modules/users/application/use-cases/get-token/get-token-use-case.ts delete mode 100644 server/src/modules/users/application/use-cases/protected-user/__tests__/protected-user-controller.test.unit.ts delete mode 100644 server/src/modules/users/application/use-cases/protected-user/protected-user-controller.ts delete mode 100644 server/src/modules/users/application/use-cases/protected-user/protected-user-dto.ts delete mode 100644 server/src/modules/users/application/use-cases/protected-user/protected-user-use-case.ts rename server/src/modules/users/domain/value-objects/{auth-code.ts => auth-code-string.ts} (74%) create mode 100644 server/src/test-utils/mocks/application/mock-authorize-user.ts create mode 100644 server/src/test-utils/mocks/application/mock-discover-sp.ts create mode 100644 server/src/test-utils/mocks/application/mock-get-token.ts delete mode 100644 server/src/test-utils/mocks/application/mock-protected-user.ts diff --git a/server/src/modules/users/application/use-cases/authenticate-user/authenticate-user-errors.ts b/server/src/modules/users/application/use-cases/authenticate-user/authenticate-user-errors.ts index aa9641e..2da9230 100644 --- a/server/src/modules/users/application/use-cases/authenticate-user/authenticate-user-errors.ts +++ b/server/src/modules/users/application/use-cases/authenticate-user/authenticate-user-errors.ts @@ -1,9 +1,8 @@ export namespace AuthenticateUserErrors { - export class AuthenticationFailedError { - public message: string - public constructor(email: string, message: string) { - this.message = `Authentication for user with ${email} failed: ${message}` - } + export class AuthenticationFailedError extends Error { + public constructor(email: string, message: string) { + super() + this.message = `Authentication for user with ${email} failed: ${message}` } } - \ No newline at end of file +} diff --git a/server/src/modules/users/application/use-cases/authorize-user/__tests__/authorize-user-controller.test.unit.ts b/server/src/modules/users/application/use-cases/authorize-user/__tests__/authorize-user-controller.test.unit.ts new file mode 100644 index 0000000..5c29676 --- /dev/null +++ b/server/src/modules/users/application/use-cases/authorize-user/__tests__/authorize-user-controller.test.unit.ts @@ -0,0 +1,80 @@ +import express from 'express' +import httpMocks from 'node-mocks-http' +import { AppError } from '../../../../../../shared/core/app-error' +import { Result } from '../../../../../../shared/core/result' +import { AuthorizeUserDTO } from '../authorize-user-dto' +import { AuthorizeUserErrors } from '../authorize-user-errors' +import { AuthorizeUserUseCase } from '../authorize-user-use-case' +import { AuthorizeUserController } from '../authorize-user-controller' +import { mocks } from '../../../../../../test-utils' +import { ParamList, ParamPair } from '../../../../../../shared/app/param-list' + +jest.mock('../authorize-user-use-case') +jest.mock('../authorize-user-use-case') + +describe('AuthorizeUserController', () => { + let authorizeUserDTO: AuthorizeUserDTO + let authorizeUserController: AuthorizeUserController + let mockResponse: express.Response + + beforeAll(async () => { + const authorizeUser = await mocks.mockAuthorizeUser() + authorizeUserController = authorizeUser.authorizeUserController + mockResponse = httpMocks.createResponse() + authorizeUserDTO = { + req: httpMocks.createRequest(), + params: { + client_id: 'i291u92jksdn', + response_type: 'code', + redirect_uri: 'www.loolabs.com', + scope: 'openid', + }, + } + }) + + test('When the AuthorizeUserUseCase returns Ok, the AuthorizeUserController returns 302 Redirect', async () => { + const useCaseResolvedValue = { + redirectParams: new ParamList([new ParamPair('type', 'test')]), + redirectUrl: 'test@loolabs.com', + } + jest + .spyOn(AuthorizeUserUseCase.prototype, 'execute') + .mockResolvedValue(Result.ok(useCaseResolvedValue)) + + const result = await authorizeUserController.executeImpl(authorizeUserDTO, mockResponse) + expect(result.statusCode).toBe(302) + }) + + test('When the AuthorizeUserUseCase returns AuthorizeUserErrors.InvalidRequestParameters, AuthorizeUserController returns 400 Bad Request', async () => { + jest + .spyOn(AuthorizeUserUseCase.prototype, 'execute') + .mockResolvedValue(Result.err(new AuthorizeUserErrors.InvalidRequestParameters())) + + const result = await authorizeUserController.executeImpl(authorizeUserDTO, mockResponse) + + expect(result.statusCode).toBe(400) + }) + + test('When the AuthorizeUserUseCase returns AuthorizeUserErrors.UserNotAuthenticated, AuthorizeUserController returns 302 Redirect', async () => { + const useCaseErrorValue = { + redirectParams: new ParamList([new ParamPair('type', 'test')]), + redirectUrl: 'test@loolabs.com', + } + jest + .spyOn(AuthorizeUserUseCase.prototype, 'execute') + .mockResolvedValue(Result.err(useCaseErrorValue)) + + const result = await authorizeUserController.executeImpl(authorizeUserDTO, mockResponse) + expect(result.statusCode).toBe(302) + }) + + test('When the AuthorizeUserUseCase returns AppError.UnexpectedError, AuthorizeUserController returns 500 Internal Server Error', async () => { + jest + .spyOn(AuthorizeUserUseCase.prototype, 'execute') + .mockResolvedValue(Result.err(new AppError.UnexpectedError('Unexpected error'))) + + const result = await authorizeUserController.executeImpl(authorizeUserDTO, mockResponse) + + expect(result.statusCode).toBe(500) + }) +}) diff --git a/server/src/modules/users/application/use-cases/authorize-user/authorize-user-controller.ts b/server/src/modules/users/application/use-cases/authorize-user/authorize-user-controller.ts new file mode 100644 index 0000000..b0dabf2 --- /dev/null +++ b/server/src/modules/users/application/use-cases/authorize-user/authorize-user-controller.ts @@ -0,0 +1,53 @@ +import express from 'express' +import { ControllerWithDTO } from '../../../../../shared/app/controller-with-dto' +import { AuthorizeUserUseCase } from './authorize-user-use-case' +import { AuthorizeUserDTO, AuthorizeUserDTOSchema } from './authorize-user-dto' +import { AuthorizeUserErrors } from './authorize-user-errors' +import { Result } from '../../../../../shared/core/result' +import { ValidationError } from 'joi' + +export class AuthorizeUserController extends ControllerWithDTO { + constructor(useCase: AuthorizeUserUseCase) { + super(useCase) + } + + buildDTO(req: express.Request): Result> { + let params: any = req.params + const errs: Array = [] + const compiledRequest = { + req, + params, + } + const bodyResult = this.validate(compiledRequest, AuthorizeUserDTOSchema) + if (bodyResult.isOk()) { + const body = bodyResult.value + return Result.ok(body) + } else { + errs.push(bodyResult.error) + return Result.err(errs) + } + } + + async executeImpl(dto: AuthorizeUserDTO, res: Res): Promise { + try { + const result = await this.useCase.execute(dto) + + if (result.isOk()) { + return this.redirect(res, result.value.redirectUrl, result.value.redirectParams) + } else { + const error = result.error + if ('redirectParams' in error) { + return this.redirect(res, error.redirectUrl, error.redirectParams) + } + switch (error.constructor) { + case AuthorizeUserErrors.InvalidRequestParameters: + return this.clientError(res, error.message) + default: + return this.fail(res, error.message) + } + } + } catch (err) { + return this.fail(res, err) + } + } +} diff --git a/server/src/modules/users/application/use-cases/authorize-user/authorize-user-dto.ts b/server/src/modules/users/application/use-cases/authorize-user/authorize-user-dto.ts new file mode 100644 index 0000000..91bd263 --- /dev/null +++ b/server/src/modules/users/application/use-cases/authorize-user/authorize-user-dto.ts @@ -0,0 +1,33 @@ +import Joi from 'joi' +import express from 'express' + +export const SUPPORTED_OPEN_ID_RESPONSE_TYPES = ['code'] +export const SUPPORTED_OPEN_ID_SCOPE = ['openid'] + +export interface AuthorizeUserDTOParams { + client_id: string + scope: string + response_type: string + redirect_uri: string +} + +export interface AuthorizeUserDTO { + req: express.Request + params: AuthorizeUserDTOParams +} + +export const AuthorizeUserDTOParamsSchema = Joi.object({ + client_id: Joi.string().required(), + scope: Joi.string() + .valid(...SUPPORTED_OPEN_ID_SCOPE) + .required(), + response_type: Joi.string() + .valid(...SUPPORTED_OPEN_ID_RESPONSE_TYPES) + .required(), + redirect_uri: Joi.string().uri().required(), +}).options({ abortEarly: false }) + +export const AuthorizeUserDTOSchema = Joi.object({ + req: Joi.object().required(), + params: AuthorizeUserDTOParamsSchema.optional(), // this ensures that all of the necessary request params for client authentication are present, not just an insufficient subset +}).options({ abortEarly: false }) diff --git a/server/src/modules/users/application/use-cases/authorize-user/authorize-user-errors.ts b/server/src/modules/users/application/use-cases/authorize-user/authorize-user-errors.ts new file mode 100644 index 0000000..db94363 --- /dev/null +++ b/server/src/modules/users/application/use-cases/authorize-user/authorize-user-errors.ts @@ -0,0 +1,12 @@ +export namespace AuthorizeUserErrors { + export class InvalidRequestParameters extends Error { + public constructor() { + super(`Invalid openid request parameters supplied.`) + } + } + export class UserNotAuthenticated extends Error { + public constructor(email: string) { + super(`The user with email ${email} is not authenticated.`) + } + } +} diff --git a/server/src/modules/users/application/use-cases/authorize-user/authorize-user-use-case.ts b/server/src/modules/users/application/use-cases/authorize-user/authorize-user-use-case.ts new file mode 100644 index 0000000..8ad7bc6 --- /dev/null +++ b/server/src/modules/users/application/use-cases/authorize-user/authorize-user-use-case.ts @@ -0,0 +1,76 @@ +import { UseCaseWithDTO } from '../../../../../shared/app/use-case-with-dto' +import { AppError } from '../../../../../shared/core/app-error' +import { Result } from '../../../../../shared/core/result' +import { AuthorizeUserDTO } from './authorize-user-dto' +import { AuthorizeUserErrors } from './authorize-user-errors' +import { ParamList, ParamPair } from '../../../../../shared/app/param-list' +import { AuthCodeRepo } from '../../../infra/repos/auth-code-repo/auth-code-repo' +import { AuthSecretRepo } from '../../../infra/repos/auth-secret-repo/auth-secret-repo' +import { AuthCode } from '../../../domain/entities/auth-code' +import { AuthCodeString } from '../../../domain/value-objects/auth-code-string' +import { User } from '../../../domain/entities/user' + +export type AuthorizeUserUseCaseClientError = + | AuthorizeUserErrors.InvalidRequestParameters + | AppError.UnexpectedError + +export type AuthorizeUserUseCaseRedirectError = { + redirectParams: ParamList + redirectUrl: string +} + +export type AuthorizeUserUseCaseError = + | AuthorizeUserUseCaseClientError + | AuthorizeUserUseCaseRedirectError + +export interface AuthorizeUserSuccess { + redirectParams: ParamList + redirectUrl: string +} + +export type AuthorizeUserUseCaseResponse = Result + +export class AuthorizeUserUseCase + implements UseCaseWithDTO +{ + constructor(private authCodeRepo: AuthCodeRepo, private authSecretRepo: AuthSecretRepo) {} + + async execute(dto: AuthorizeUserDTO): Promise { + const params = dto.params + const decodedUri = decodeURI(params.redirect_uri) + const authSecretExists = await this.authSecretRepo.exists(params.client_id, decodedUri) + if (authSecretExists.isErr() || authSecretExists.value === false) { + return Result.err(new AuthorizeUserErrors.InvalidRequestParameters()) + } + const user = dto.req.user as User + if (user === undefined) { + const redirectParams = new ParamList( + Object.entries(params).map((paramPair) => new ParamPair(paramPair[0], paramPair[1])) + ) + return Result.err({ + redirectParams, + redirectUrl: `${process.env.PUBLIC_HOST}/login`, + }) + } + const authCode = AuthCode.create({ + clientId: params.client_id, + userId: user.userId.id.toString(), + userEmail: user.email.value, + userEmailVerified: user.isEmailVerified || false, + authCodeString: new AuthCodeString(), + }) + if (authCode.isErr()) { + return Result.err(new AppError.UnexpectedError('Authcode creation failed')) + } + await this.authCodeRepo.save(authCode.value) + const redirectParams = new ParamList([ + new ParamPair('code', authCode.value.authCodeString.getValue()), + ]) + const AuthorizeUserSuccessResponse: AuthorizeUserSuccess = { + redirectParams: redirectParams, + redirectUrl: params.redirect_uri, + } + + return Result.ok(AuthorizeUserSuccessResponse) + } +} diff --git a/server/src/modules/users/application/use-cases/create-user/create-user-dto.ts b/server/src/modules/users/application/use-cases/create-user/create-user-dto.ts index 202a987..fc771dc 100644 --- a/server/src/modules/users/application/use-cases/create-user/create-user-dto.ts +++ b/server/src/modules/users/application/use-cases/create-user/create-user-dto.ts @@ -1,5 +1,8 @@ import Joi from 'joi' +export const SUPPORTED_OPEN_ID_RESPONSE_TYPES = ['code'] +export const SUPPORTED_OPEN_ID_SCOPE = ['openid'] + export interface CreateUserDTOBody { email: string password: string @@ -8,12 +11,12 @@ export interface CreateUserDTOBody { export interface CreateUserDTOParams { client_id: string scope: string - response_type: string, + response_type: string redirect_uri: string } -export interface CreateUserDTO { - body: CreateUserDTOBody, +export interface CreateUserDTO { + body: CreateUserDTOBody params?: CreateUserDTOParams } @@ -22,14 +25,7 @@ export const createUserDTOBodySchema = Joi.object({ password: Joi.string().required(), }).options({ abortEarly: false }) -export const createUserDTOParamsSchema = Joi.object({ - client_id: Joi.string().required(), - scope: Joi.string().required(), - response_type: Joi.string().required(), - redirect_uri: Joi.string().required() -}).options({ abortEarly: false }) - export const createUserDTOSchema = Joi.object({ body: createUserDTOBodySchema.required(), - params: createUserDTOParamsSchema.optional() // this ensures that all of the necessary request params for client authentication are present, not just an insufficient subset + params: Joi.object().optional(), }).options({ abortEarly: false }) diff --git a/server/src/modules/users/application/use-cases/create-user/create-user-errors.ts b/server/src/modules/users/application/use-cases/create-user/create-user-errors.ts index b5e7e9c..24b9e4d 100644 --- a/server/src/modules/users/application/use-cases/create-user/create-user-errors.ts +++ b/server/src/modules/users/application/use-cases/create-user/create-user-errors.ts @@ -4,10 +4,4 @@ export namespace CreateUserErrors { super(`An account with the email ${email} already exists`) } } - - export class InvalidOpenIDParamsError extends Error { - public constructor() { - super(`Invalid OpenID parameters were provided.`) - } - } } diff --git a/server/src/modules/users/application/use-cases/create-user/create-user-use-case.ts b/server/src/modules/users/application/use-cases/create-user/create-user-use-case.ts index 5f98b9d..b2d620a 100644 --- a/server/src/modules/users/application/use-cases/create-user/create-user-use-case.ts +++ b/server/src/modules/users/application/use-cases/create-user/create-user-use-case.ts @@ -17,17 +17,16 @@ export type CreateUserUseCaseError = | UserValueObjectErrors.InvalidEmail | UserValueObjectErrors.InvalidSecretValue | CreateUserErrors.EmailAlreadyExistsError - | CreateUserErrors.InvalidOpenIDParamsError | AppError.UnexpectedError -export interface CreateUserClientRequestSuccess { - redirectParams: ParamList, +export interface CreateUserClientRequestSuccess { + redirectParams: ParamList redirectUrl: string -} +} export interface CreateUserNonClientRequestSuccess { user: UserDTO -} +} // TODO: perhaps better to decouple these into separate use-cases or further subclasses export type CreateUserSuccess = CreateUserClientRequestSuccess | CreateUserNonClientRequestSuccess @@ -38,11 +37,6 @@ export class CreateUserUseCase implements UseCaseWithDTO { - if(dto.params && dto.params.scope){ - if(dto.params.scope !== 'openid' || dto.params.response_type !== 'code'){ - return Result.err(new CreateUserErrors.InvalidOpenIDParamsError()) - } - } const emailResult = UserEmail.create(dto.body.email) const passwordResult = UserPassword.create({ value: dto.body.password, @@ -60,47 +54,41 @@ export class CreateUserUseCase implements UseCaseWithDTO { + let discoverSPDTO: DiscoverSPDTO + let discoverSPController: DiscoverSPController + let mockResponse: express.Response + + beforeAll(async () => { + const discoverSP = await mocks.mockDiscoverSP() + discoverSPController = discoverSP.discoverSPController + mockResponse = httpMocks.createResponse() + discoverSPDTO = { + client_name: 'testclient', + redirect_uri: 'loolabs.com/cb', + } + }) + + test('When the DiscoverSPUseCase returns Ok, the DiscoverSPController returns 200 OK', async () => { + const useCaseResolvedValue = { + clientId: '232039sdkljkasldj', + clientSecret: '65039sdasd123kljkasldj', + } + jest + .spyOn(DiscoverSPUseCase.prototype, 'execute') + .mockResolvedValue(Result.ok(useCaseResolvedValue)) + + const result = await discoverSPController.executeImpl(discoverSPDTO, mockResponse) + + expect(result.statusCode).toBe(200) + }) + + test('When the DiscoverSPUseCase returns DiscoverSPErrors, DiscoverSPController returns 400 Bad Request', async () => { + jest + .spyOn(DiscoverSPUseCase.prototype, 'execute') + .mockResolvedValue( + Result.err(new DiscoverSPErrors.ClientNameAlreadyInUse(discoverSPDTO.client_name)) + ) + + const result = await discoverSPController.executeImpl(discoverSPDTO, mockResponse) + + expect(result.statusCode).toBe(400) + }) + + test('When the DiscoverSPUseCase returns AppError.UnexpectedError, DiscoverSPController returns 500 Internal Server Error', async () => { + jest + .spyOn(DiscoverSPUseCase.prototype, 'execute') + .mockResolvedValue(Result.err(new AppError.UnexpectedError('Unexpected error'))) + + const result = await discoverSPController.executeImpl(discoverSPDTO, mockResponse) + + expect(result.statusCode).toBe(500) + }) +}) diff --git a/server/src/modules/users/application/use-cases/discover-sp/discover-sp-controller.ts b/server/src/modules/users/application/use-cases/discover-sp/discover-sp-controller.ts new file mode 100644 index 0000000..ad5ba85 --- /dev/null +++ b/server/src/modules/users/application/use-cases/discover-sp/discover-sp-controller.ts @@ -0,0 +1,50 @@ +import express from 'express' +import { DiscoverSPUseCase } from './discover-sp-use-case' +import { DiscoverSPDTO, discoverSPDTOSchema } from './discover-sp-dto' +import { DiscoverSPErrors } from './discover-sp-errors' +import { ControllerWithDTO } from '../../../../../shared/app/controller-with-dto' +import { Result } from '../../../../../shared/core/result' +import { ValidationError } from 'joi' + +export class DiscoverSPController extends ControllerWithDTO { + constructor(useCase: DiscoverSPUseCase) { + super(useCase) + } + + buildDTO(req: express.Request): Result> { + const errs: Array = [] + const compiledValidationBody = { + authHeader: req.headers.authorization, + params: req.params, + } + const bodyResult = this.validate(compiledValidationBody, discoverSPDTOSchema) + if (bodyResult.isOk()) { + const body: DiscoverSPDTO = bodyResult.value + return Result.ok(body) + } else { + errs.push(bodyResult.error) + return Result.err(errs) + } + } + + async executeImpl(dto: DiscoverSPDTO, res: express.Response): Promise { + try { + const result = await this.useCase.execute(dto) + + if (result.isOk()) { + return this.ok(res, result.value) + } else { + const error = result.error + + switch (error.constructor) { + case DiscoverSPErrors.ClientNameAlreadyInUse: + return this.clientError(res, error.message) + default: + return this.fail(res, error.message) + } + } + } catch (err) { + return this.fail(res, err) + } + } +} diff --git a/server/src/modules/users/application/use-cases/discover-sp/discover-sp-dto.ts b/server/src/modules/users/application/use-cases/discover-sp/discover-sp-dto.ts new file mode 100644 index 0000000..0558dc0 --- /dev/null +++ b/server/src/modules/users/application/use-cases/discover-sp/discover-sp-dto.ts @@ -0,0 +1,11 @@ +import Joi from 'joi' + +export interface DiscoverSPDTO { + client_name: string + redirect_uri: string +} + +export const discoverSPDTOSchema = Joi.object({ + client_name: Joi.string().required(), + redirect_uri: Joi.string().uri().required(), +}).options({ abortEarly: false }) diff --git a/server/src/modules/users/application/use-cases/discover-sp/discover-sp-errors.ts b/server/src/modules/users/application/use-cases/discover-sp/discover-sp-errors.ts new file mode 100644 index 0000000..49e40a5 --- /dev/null +++ b/server/src/modules/users/application/use-cases/discover-sp/discover-sp-errors.ts @@ -0,0 +1,8 @@ +export namespace DiscoverSPErrors { + export class ClientNameAlreadyInUse extends Error { + public constructor(clientName: string) { + super() + this.message = `The provided client name ${clientName} is already in use.` + } + } +} diff --git a/server/src/modules/users/application/use-cases/discover-sp/discover-sp-use-case.ts b/server/src/modules/users/application/use-cases/discover-sp/discover-sp-use-case.ts new file mode 100644 index 0000000..98859c7 --- /dev/null +++ b/server/src/modules/users/application/use-cases/discover-sp/discover-sp-use-case.ts @@ -0,0 +1,65 @@ +import { UseCaseWithDTO } from '../../../../../shared/app/use-case-with-dto' +import '../../../../../shared/auth/user-auth-handler' +import { DiscoverSPDTO } from './discover-sp-dto' +import { AppError } from '../../../../../shared/core/app-error' +import { DiscoverSPErrors } from './discover-sp-errors' +import { Result } from '../../../../../shared/core/result' +import { AuthSecretRepo } from '../../../infra/repos/auth-secret-repo/auth-secret-repo' +import { EncryptedClientSecret } from '../../../domain/value-objects/encrypted-client-secret' +import { AuthSecret } from '../../../domain/entities/auth-secret' +import crypto from 'crypto' + +export type DiscoverSPUseCaseError = + | DiscoverSPErrors.ClientNameAlreadyInUse + | AppError.UnexpectedError + +export interface DiscoverSPSuccess { + clientId: string + clientSecret: string +} + +export type DiscoverSPUseCaseResponse = Result + +export class DiscoverSPUseCase implements UseCaseWithDTO { + private authSecretRepo + + constructor(authSecretRepo: AuthSecretRepo) { + this.authSecretRepo = authSecretRepo + } + + async execute(dto: DiscoverSPDTO): Promise { + const authSecretExists = await this.authSecretRepo.clientNameExists(dto.client_name) + if (authSecretExists.isErr()) { + return Result.err( + new AppError.UnexpectedError('Unexpected error when validating client name.') + ) + } + if (authSecretExists.value) { + return Result.err(new DiscoverSPErrors.ClientNameAlreadyInUse(dto.client_name)) + } + const encryptedClientSecret = EncryptedClientSecret.create({ + value: crypto.randomBytes(32).toString('hex'), + hashed: false, + }) + if (encryptedClientSecret.isErr()) { + return Result.err( + new AppError.UnexpectedError('Unexpected error when creating client secret.') + ) + } + const authSecret = AuthSecret.create({ + clientName: dto.client_name, + encryptedClientSecret: encryptedClientSecret.value, + decodedRedirectUri: decodeURI(dto.redirect_uri), + isVerified: false, + clientId: crypto.randomBytes(32).toString('hex'), + }) + if (authSecret.isErr()) { + return Result.err(new AppError.UnexpectedError('Unexpected error when saving client secret.')) + } + await this.authSecretRepo.save(authSecret.value) + return Result.ok({ + clientId: authSecret.value.clientId, + clientSecret: encryptedClientSecret.value.value, + }) + } +} diff --git a/server/src/modules/users/application/use-cases/get-token/__tests__/get-token-controller.test.unit.ts b/server/src/modules/users/application/use-cases/get-token/__tests__/get-token-controller.test.unit.ts new file mode 100644 index 0000000..2b47773 --- /dev/null +++ b/server/src/modules/users/application/use-cases/get-token/__tests__/get-token-controller.test.unit.ts @@ -0,0 +1,62 @@ +import express from 'express' +import httpMocks from 'node-mocks-http' +import { AppError } from '../../../../../../shared/core/app-error' +import { Result } from '../../../../../../shared/core/result' +import { GetTokenDTO } from '../get-token-dto' +import { GetTokenErrors } from '../get-token-errors' +import { GetTokenUseCase } from '../get-token-use-case' +import { GetTokenController } from '../get-token-controller' +import { mocks } from '../../../../../../test-utils' + +jest.mock('../get-token-use-case') + +describe('GetTokenController', () => { + let getTokenDTO: GetTokenDTO + let getTokenController: GetTokenController + let mockResponse: express.Response + + beforeAll(async () => { + const getToken = await mocks.mockGetToken() + getTokenController = getToken.getTokenController + mockResponse = httpMocks.createResponse() + getTokenDTO = { + authHeader: 'asdklasdoladoassald', + params: { + code: 'sd', + grant_type: 'code', + response_type: 'id', + }, + } + }) + + test('When the GetTokenUseCase returns Ok, the GetTokenController returns 200 OK', async () => { + const useCaseResolvedValue = 'asdklasdhnjkjkewhf' + jest + .spyOn(GetTokenUseCase.prototype, 'execute') + .mockResolvedValue(Result.ok(useCaseResolvedValue)) + + const result = await getTokenController.executeImpl(getTokenDTO, mockResponse) + + expect(result.statusCode).toBe(200) + }) + + test('When the GetTokenUseCase returns GetTokenErrors.InvalidCredentials, GetTokenController returns 400 Bad Request', async () => { + jest + .spyOn(GetTokenUseCase.prototype, 'execute') + .mockResolvedValue(Result.err(new GetTokenErrors.InvalidCredentials())) + + const result = await getTokenController.executeImpl(getTokenDTO, mockResponse) + + expect(result.statusCode).toBe(400) + }) + + test('When the GetTokenUseCase returns AppError.UnexpectedError, GetTokenController returns 500 Internal Server Error', async () => { + jest + .spyOn(GetTokenUseCase.prototype, 'execute') + .mockResolvedValue(Result.err(new AppError.UnexpectedError('Unexpected error'))) + + const result = await getTokenController.executeImpl(getTokenDTO, mockResponse) + + expect(result.statusCode).toBe(500) + }) +}) diff --git a/server/src/modules/users/application/use-cases/get-token/get-token-controller.ts b/server/src/modules/users/application/use-cases/get-token/get-token-controller.ts new file mode 100644 index 0000000..aff87a3 --- /dev/null +++ b/server/src/modules/users/application/use-cases/get-token/get-token-controller.ts @@ -0,0 +1,50 @@ +import express from 'express' +import { GetTokenUseCase } from './get-token-use-case' +import { GetTokenDTO, getTokenDTOSchema } from './get-token-dto' +import { GetTokenErrors } from './get-token-errors' +import { ControllerWithDTO } from '../../../../../shared/app/controller-with-dto' +import { Result } from '../../../../../shared/core/result' +import { ValidationError } from 'joi' + +export class GetTokenController extends ControllerWithDTO { + constructor(useCase: GetTokenUseCase) { + super(useCase) + } + + buildDTO(req: express.Request): Result> { + const errs: Array = [] + const compiledValidationBody = { + authHeader: req.headers.authorization, + params: req.params, + } + const bodyResult = this.validate(compiledValidationBody, getTokenDTOSchema) + if (bodyResult.isOk()) { + const body: GetTokenDTO = bodyResult.value + return Result.ok(body) + } else { + errs.push(bodyResult.error) + return Result.err(errs) + } + } + + async executeImpl(dto: GetTokenDTO, res: express.Response): Promise { + try { + const result = await this.useCase.execute(dto) + + if (result.isOk()) { + return this.ok(res, result.value) + } else { + const error = result.error + + switch (error.constructor) { + case GetTokenErrors.InvalidCredentials: + return this.clientError(res, error.message) + default: + return this.fail(res, error.message) + } + } + } catch (err) { + return this.fail(res, err) + } + } +} diff --git a/server/src/modules/users/application/use-cases/get-token/get-token-dto.ts b/server/src/modules/users/application/use-cases/get-token/get-token-dto.ts new file mode 100644 index 0000000..0234b93 --- /dev/null +++ b/server/src/modules/users/application/use-cases/get-token/get-token-dto.ts @@ -0,0 +1,31 @@ +import Joi from 'joi' + +export const SUPPORTED_OPEN_ID_GRANT_TYPES = ['authorization_code'] +export const SUPPORTED_OPEN_ID_RESPONSE_TYPES = ['id'] + +export interface GetTokenDTOParams { + code: string + grant_type: string + response_type: string +} + +export interface GetTokenDTO { + authHeader: string + params: GetTokenDTOParams +} + +export const getTokenDTOParamsSchema = Joi.object({ + code: Joi.string().required(), + grant_type: Joi.string() + .valid(...SUPPORTED_OPEN_ID_GRANT_TYPES) + .required(), + response_type: Joi.string() + .valid(...SUPPORTED_OPEN_ID_RESPONSE_TYPES) + .required(), +}).options({ abortEarly: false }) + +export const getTokenDTOSchema = Joi.object({ + //Example: Authorization: Basic 3904238orfiefiekfhjri3u24r789 + authHeader: Joi.string().pattern(new RegExp('^Basic .+$')).required(), + params: getTokenDTOParamsSchema.required(), +}).options({ abortEarly: false }) diff --git a/server/src/modules/users/application/use-cases/get-token/get-token-errors.ts b/server/src/modules/users/application/use-cases/get-token/get-token-errors.ts new file mode 100644 index 0000000..d74abe4 --- /dev/null +++ b/server/src/modules/users/application/use-cases/get-token/get-token-errors.ts @@ -0,0 +1,8 @@ +export namespace GetTokenErrors { + export class InvalidCredentials extends Error { + public constructor() { + super() + this.message = `Incorrect authentication credentials provided.` + } + } +} diff --git a/server/src/modules/users/application/use-cases/get-token/get-token-use-case.ts b/server/src/modules/users/application/use-cases/get-token/get-token-use-case.ts new file mode 100644 index 0000000..739b563 --- /dev/null +++ b/server/src/modules/users/application/use-cases/get-token/get-token-use-case.ts @@ -0,0 +1,93 @@ +import { UseCaseWithDTO } from '../../../../../shared/app/use-case-with-dto' +import '../../../../../shared/auth/user-auth-handler' +import { GetTokenDTO } from './get-token-dto' +import { AppError } from '../../../../../shared/core/app-error' +import { GetTokenErrors } from './get-token-errors' +import { Result } from '../../../../../shared/core/result' +import { AuthCodeRepo } from '../../../infra/repos/auth-code-repo/auth-code-repo' +import { AuthCodeString } from '../../../domain/value-objects/auth-code-string' +import { DBError } from '../../../../../shared/infra/db/errors/errors' +import { AuthSecretRepo } from '../../../infra/repos/auth-secret-repo/auth-secret-repo' +import { EncryptedClientSecret } from '../../../domain/value-objects/encrypted-client-secret' +import { AuthCode } from '../../../domain/entities/auth-code' +import jwt from 'jsonwebtoken' + +export type GetTokenUseCaseError = GetTokenErrors.InvalidCredentials | AppError.UnexpectedError + +export type GetTokenIdToken = string + +export type GetTokenSupportedResponseType = GetTokenIdToken + +export type GetTokenUseCaseResponse = Result + +export const ID_TOKEN_EXPIRY_TIME_SECONDS = 300 //5 minutes + +export class GetTokenUseCase implements UseCaseWithDTO { + private authCodeRepo + private authSecretRepo + + constructor(authCodeRepo: AuthCodeRepo, authSecretRepo: AuthSecretRepo) { + this.authCodeRepo = authCodeRepo + this.authSecretRepo = authSecretRepo + } + + async execute(dto: GetTokenDTO): Promise { + const authHeader = dto.authHeader + const params = dto.params + + const authCodeString = new AuthCodeString(params.code) + + const authCodeResult = await this.authCodeRepo.getAuthCodeFromAuthCodeString(authCodeString) + + if (authCodeResult.isErr()) { + if (authCodeResult.error.constructor === DBError.AuthCodeNotFoundError) { + return Result.err(new GetTokenErrors.InvalidCredentials()) + } else { + return Result.err(authCodeResult.error) + } + } else { + const authHeaderValue = authHeader.split(' ')[1] + const authHeaderValueDecoded = Buffer.from(authHeaderValue, 'base64').toString('utf-8') + const serviceProviderCredentials = authHeaderValueDecoded.split(':') + const clientId = serviceProviderCredentials[0] + const clientSecret = serviceProviderCredentials[1] + const encryptedClientSecret = EncryptedClientSecret.create({ + value: clientSecret, + hashed: false, + }) + if (encryptedClientSecret.isErr() || clientId != authCodeResult.value.clientId) { + return Result.err(new GetTokenErrors.InvalidCredentials()) + } + const authSecretResult = await this.authSecretRepo.getAuthSecretByClientIdandSecret( + clientId, + encryptedClientSecret.value + ) + if (authSecretResult.isErr()) { + return Result.err(new GetTokenErrors.InvalidCredentials()) + } + this.authCodeRepo.delete(authCodeResult.value) + const successResponse: GetTokenSupportedResponseType = this.generateIdToken( + authCodeResult.value, + encryptedClientSecret.value + ) + + return Result.ok(successResponse) + } + } + + private generateIdToken(authCode: AuthCode, authSecret: EncryptedClientSecret): GetTokenIdToken { + //currently using HMAC symmetric signing via the client secret + return jwt.sign( + { + iss: `${process.env.PUBLIC_HOST}`, + sub: authCode.userId, + aud: authCode.clientId, + iat: new Date().getTime() / 1000, + exp: (new Date().getTime() + ID_TOKEN_EXPIRY_TIME_SECONDS * 1000) / 1000, + email: authCode.userEmail, + email_verified: authCode.userEmailVerified, + }, + authSecret.value + ) + } +} diff --git a/server/src/modules/users/application/use-cases/get-user/get-user-errors.ts b/server/src/modules/users/application/use-cases/get-user/get-user-errors.ts index 8cf2098..514bf9b 100644 --- a/server/src/modules/users/application/use-cases/get-user/get-user-errors.ts +++ b/server/src/modules/users/application/use-cases/get-user/get-user-errors.ts @@ -1,9 +1,8 @@ export namespace GetUserErrors { - export class GetUserByIdFailedError { - public message: string - public constructor(id: string) { - this.message = `No account with the id ${id} exists` - } + export class GetUserByIdFailedError extends Error { + public constructor(id: string) { + super() + this.message = `No account with the id ${id} exists` } } - \ No newline at end of file +} diff --git a/server/src/modules/users/application/use-cases/login-user/login-user-controller.ts b/server/src/modules/users/application/use-cases/login-user/login-user-controller.ts index 7118cd9..98a286e 100644 --- a/server/src/modules/users/application/use-cases/login-user/login-user-controller.ts +++ b/server/src/modules/users/application/use-cases/login-user/login-user-controller.ts @@ -8,23 +8,28 @@ import { Result } from '../../../../../shared/core/result' import { ValidationError } from 'joi' export class LoginUserController extends ControllerWithDTO { - constructor(useCase: LoginUserUseCase) { super(useCase) } - buildDTO(req: express.Request, res: express.Response): Result> { + buildDTO( + req: express.Request, + res: express.Response + ): Result> { const errs: Array = [] let params: any = req.params - if(Object.keys(req.params).length === 0){ + if (Object.keys(req.params).length === 0) { params = undefined } const compiledValidationBody = { - req, res, body: req.body, params + req, + res, + body: req.body, + params, } const bodyResult = this.validate(compiledValidationBody, loginUserDTOSchema) if (bodyResult.isOk()) { - const body: LoginUserDTO = compiledValidationBody + const body: LoginUserDTO = bodyResult.value return Result.ok(body) } else { errs.push(bodyResult.error) @@ -37,7 +42,7 @@ export class LoginUserController extends ControllerWithDTO { const result = await this.useCase.execute(dto) if (result.isOk()) { - if('user' in result.value){ + if ('user' in result.value) { return this.ok(res, result.value) } else { return this.redirect(res, result.value.redirectUrl, result.value.redirectParams) diff --git a/server/src/modules/users/application/use-cases/login-user/login-user-dto.ts b/server/src/modules/users/application/use-cases/login-user/login-user-dto.ts index 005c56c..39e088c 100644 --- a/server/src/modules/users/application/use-cases/login-user/login-user-dto.ts +++ b/server/src/modules/users/application/use-cases/login-user/login-user-dto.ts @@ -9,14 +9,14 @@ export interface LoginUserDTOBody { export interface LoginUserDTOParams { client_id: string scope: string - response_type: string, + response_type: string redirect_uri: string } export interface LoginUserDTO { - req: express.Request, - res: express.Response, - body: LoginUserDTOBody, + req: express.Request + res: express.Response + body: LoginUserDTOBody params?: LoginUserDTOParams } @@ -25,16 +25,9 @@ export const loginUserDTOBodySchema = Joi.object({ password: Joi.string().required(), }).options({ abortEarly: false }) -export const loginUserDTOParamsSchema = Joi.object({ - client_id: Joi.string().required(), - scope: Joi.string().required(), - response_type: Joi.string().required(), - redirect_uri: Joi.string().required() -}).options({ abortEarly: false }) - export const loginUserDTOSchema = Joi.object({ req: Joi.object().required(), res: Joi.object().required(), body: loginUserDTOBodySchema.required(), - params: loginUserDTOParamsSchema.optional() + params: Joi.object().optional(), }).options({ abortEarly: false }) diff --git a/server/src/modules/users/application/use-cases/login-user/login-user-errors.ts b/server/src/modules/users/application/use-cases/login-user/login-user-errors.ts index e43f495..a3569c4 100644 --- a/server/src/modules/users/application/use-cases/login-user/login-user-errors.ts +++ b/server/src/modules/users/application/use-cases/login-user/login-user-errors.ts @@ -1,14 +1,8 @@ export namespace LoginUserErrors { - export class IncorrectPasswordError { - public message: string + export class IncorrectPasswordError extends Error { public constructor() { + super() this.message = `Incorrect email/password combination provided.` } } - - export class InvalidOpenIDParamsError extends Error { - public constructor() { - super(`Invalid OpenID parameters were provided.`) - } - } } diff --git a/server/src/modules/users/application/use-cases/login-user/login-user-use-case.ts b/server/src/modules/users/application/use-cases/login-user/login-user-use-case.ts index f9d85e1..5b95969 100644 --- a/server/src/modules/users/application/use-cases/login-user/login-user-use-case.ts +++ b/server/src/modules/users/application/use-cases/login-user/login-user-use-case.ts @@ -1,37 +1,32 @@ import { UseCaseWithDTO } from '../../../../../shared/app/use-case-with-dto' -import { UserAuthHandler, UserAuthHandlerLoginError, UserAuthHandlerLoginResponse } from '../../../../../shared/auth/user-auth-handler' -import '../../../../../shared/auth/user-auth-handler' +import { + UserAuthHandler, + UserAuthHandlerLoginError, + UserAuthHandlerLoginResponse, +} from '../../../../../shared/auth/user-auth-handler' +import '../../../../../shared/auth/user-auth-handler' import { LoginUserDTO } from './login-user-dto' import { ParamList, ParamPair } from '../../../../../shared/app/param-list' import { AppError } from '../../../../../shared/core/app-error' -import { LoginUserErrors } from './login-user-errors' import { Result } from '../../../../../shared/core/result' import { UserDTO } from '../../../mappers/user-dto' -import { CreateUserErrors } from '../create-user/create-user-errors' -export type LoginUserUseCaseError = - | LoginUserErrors.InvalidOpenIDParamsError - | UserAuthHandlerLoginError - | AppError.UnexpectedError +export type LoginUserUseCaseError = UserAuthHandlerLoginError | AppError.UnexpectedError -export interface LoginUserSuccessRedirect { +export interface LoginUserSuccessRedirect { redirectParams: ParamList redirectUrl: string } -export interface LoginUserSuccessUser { +export interface LoginUserSuccessUser { user: UserDTO } -export type LoginUserSuccess = LoginUserSuccessRedirect | LoginUserSuccessUser +export type LoginUserSuccess = LoginUserSuccessRedirect | LoginUserSuccessUser export type LoginUserUseCaseResponse = Result -export const OPEN_ID_SCOPE = 'open_id' -export const OPEN_ID_RESPONSE_TYPE = 'code' - -export class LoginUserUseCase - implements UseCaseWithDTO { +export class LoginUserUseCase implements UseCaseWithDTO { private userAuthHandler: UserAuthHandler constructor(userAuthHandler: UserAuthHandler) { @@ -42,30 +37,25 @@ export class LoginUserUseCase const userAuthHandlerLoginOptions = { req: dto.req, res: dto.res, - params: dto.params } - const userAuthHandlerLoginResponse: UserAuthHandlerLoginResponse = await this.userAuthHandler.login(userAuthHandlerLoginOptions) - if(userAuthHandlerLoginResponse.isErr()){ + const userAuthHandlerLoginResponse: UserAuthHandlerLoginResponse = + await this.userAuthHandler.login(userAuthHandlerLoginOptions) + if (userAuthHandlerLoginResponse.isErr()) { return Result.err(userAuthHandlerLoginResponse.error) } else { - const params = dto.params; - if(params && params.scope){ - if(params.scope !== OPEN_ID_SCOPE || params.response_type !== OPEN_ID_RESPONSE_TYPE){ - return Result.err(new CreateUserErrors.InvalidOpenIDParamsError()) - } - } - if(params && params.scope && userAuthHandlerLoginResponse.value.cert){ - const redirectParams = new ParamList([ - new ParamPair('code', userAuthHandlerLoginResponse.value.cert.getValue()) - ]) + const params = dto.params + if (params) { + const redirectParams = new ParamList( + Object.entries(params).map((paramPair) => new ParamPair(paramPair[0], paramPair[1])) + ) const loginUserSuccessResponse: LoginUserSuccess = { redirectParams: redirectParams, - redirectUrl: params.redirect_uri + redirectUrl: `${process.env.PUBLIC_HOST}/authorize`, } return Result.ok(loginUserSuccessResponse) } else { const loginUserSuccessResponse: LoginUserSuccess = { - user: userAuthHandlerLoginResponse.value.user + user: userAuthHandlerLoginResponse.value.user, } return Result.ok(loginUserSuccessResponse) } diff --git a/server/src/modules/users/application/use-cases/protected-user/__tests__/protected-user-controller.test.unit.ts b/server/src/modules/users/application/use-cases/protected-user/__tests__/protected-user-controller.test.unit.ts deleted file mode 100644 index 799e6cd..0000000 --- a/server/src/modules/users/application/use-cases/protected-user/__tests__/protected-user-controller.test.unit.ts +++ /dev/null @@ -1,56 +0,0 @@ -import httpMocks from 'node-mocks-http' -import { Result } from '../../../../../../shared/core/result' -import { ProtectedUserDTO } from '../protected-user-dto' -import { ProtectedUserSuccess, ProtectedUserUseCase } from '../protected-user-use-case' -import { AppError } from '../../../../../../shared/core/app-error' -import { ProtectedUserController } from '../protected-user-controller' -import { mocks } from '../../../../../../test-utils' -import { CreateUserDTOBody } from '../../create-user/create-user-dto' - -// TODO: how to show developer these mocks are necessary when building a controller? aka must be synced with buildController() -jest.mock('../../../../infra/repos/user-repo/implementations/mikro-user-repo') -jest.mock('../protected-user-use-case') - -describe('ProtectedUserController', () => { - - let protectedUserDTO: ProtectedUserDTO - let userDTO: CreateUserDTOBody - let protectedUserController: ProtectedUserController - beforeAll(async () => { - const protectedUser = await mocks.mockProtectedUser() - protectedUserController = protectedUser.protectedUserController - }) - - beforeEach(() => { - userDTO = { - email: 'loolabs@uwaterloo.ca', - password: 'password', - }, - protectedUserDTO = { - user: mocks.mockUser(userDTO) - } - }) - - test('When the ProtectedUserUserCase returns Ok, the ProtectedUserController returns 200 OK', async () => { - const mockResponse = httpMocks.createResponse() - const useCaseResolvedValue: ProtectedUserSuccess = { - email: userDTO.email - } - jest.spyOn(ProtectedUserUseCase.prototype, 'execute').mockResolvedValue(Result.ok(useCaseResolvedValue)) - - const result = await protectedUserController.executeImpl(protectedUserDTO, mockResponse) - - expect(result.statusCode).toBe(200) - }), - - test('When the ProtectedUserUseCase returns AppError.UnexpectedError, ProtectedUserController returns 500 Internal Server Error', async () => { - const mockResponse = httpMocks.createResponse() - jest - .spyOn(ProtectedUserUseCase.prototype, 'execute') - .mockResolvedValue(Result.err(new AppError.UnexpectedError('Unexpected error'))) - - const result = await protectedUserController.executeImpl(protectedUserDTO, mockResponse) - - expect(result.statusCode).toBe(500) - }) -}) diff --git a/server/src/modules/users/application/use-cases/protected-user/protected-user-controller.ts b/server/src/modules/users/application/use-cases/protected-user/protected-user-controller.ts deleted file mode 100644 index b6ba55f..0000000 --- a/server/src/modules/users/application/use-cases/protected-user/protected-user-controller.ts +++ /dev/null @@ -1,44 +0,0 @@ -import express from 'express' -import { ControllerWithDTO } from '../../../../../shared/app/controller-with-dto' -import { ProtectedUserUseCase } from './protected-user-use-case' -import { ProtectedUserDTO, protectedUserDTOSchema } from './protected-user-dto' -import { Result } from '../../../../../shared/core/result' -import { ValidationError } from 'joi' - -export class ProtectedUserController extends ControllerWithDTO { - constructor(useCase: ProtectedUserUseCase) { super(useCase) } - - buildDTO(req: express.Request): Result> { - const errs: Array = [] - const compiledBody = { - user: req.user - } - const bodyResult = this.validate(compiledBody, protectedUserDTOSchema) - if (bodyResult.isOk()) { - const body = bodyResult.value - return Result.ok(body) - } else { - errs.push(bodyResult.error) - return Result.err(errs) - } - } - - async executeImpl(dto: ProtectedUserDTO, res: express.Response): Promise { - try { - const result = await this.useCase.execute(dto) - - if (result.isOk()) { - return this.ok(res, result.value) - } else { - const error = result.error - - switch (error.constructor) { - default: - return this.fail(res, error.message) - } - } - } catch (err) { - return this.fail(res, err) - } - } -} diff --git a/server/src/modules/users/application/use-cases/protected-user/protected-user-dto.ts b/server/src/modules/users/application/use-cases/protected-user/protected-user-dto.ts deleted file mode 100644 index d443cd5..0000000 --- a/server/src/modules/users/application/use-cases/protected-user/protected-user-dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Joi from 'joi' -import { User } from '../../../domain/entities/user' - -export interface ProtectedUserDTO { - user: User -} - -export const protectedUserDTOSchema = Joi.object({ - user: Joi.object().required() -}).options({ abortEarly: false }) diff --git a/server/src/modules/users/application/use-cases/protected-user/protected-user-use-case.ts b/server/src/modules/users/application/use-cases/protected-user/protected-user-use-case.ts deleted file mode 100644 index cafdffa..0000000 --- a/server/src/modules/users/application/use-cases/protected-user/protected-user-use-case.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { UseCaseWithDTO } from '../../../../../shared/app/use-case-with-dto' -import { AppError } from '../../../../../shared/core/app-error' -import { Result } from '../../../../../shared/core/result' -import { ProtectedUserDTO } from './protected-user-dto' - -export type ProtectedUserUseCaseError = - | AppError.UnexpectedError - -export type ProtectedUserSuccess = { - email: string -} - -export type ProtectedUserUseCaseResponse = Result - -export class ProtectedUserUseCase - implements UseCaseWithDTO { - - async execute(dto: ProtectedUserDTO): Promise { - const res: ProtectedUserSuccess = { - email: dto.user.email.value - } - try { - return Result.ok(res) - } catch (err) { - return Result.err(new AppError.UnexpectedError(err)) - } - } -} diff --git a/server/src/modules/users/domain/entities/auth-code/__tests__/auth-code.test.unit.ts b/server/src/modules/users/domain/entities/auth-code/__tests__/auth-code.test.unit.ts index d6c2ac0..f21c6f5 100644 --- a/server/src/modules/users/domain/entities/auth-code/__tests__/auth-code.test.unit.ts +++ b/server/src/modules/users/domain/entities/auth-code/__tests__/auth-code.test.unit.ts @@ -1,6 +1,6 @@ import { DomainEvents } from '../../../../../../shared/domain/events/domain-events' import { AuthCodeCreated } from '../../../events/auth-code-created' -import { AuthCodeString } from '../../../value-objects/auth-code' +import { AuthCodeString } from '../../../value-objects/auth-code-string' import { AuthCode } from '../auth-code' jest.mock('../../../events/auth-code-created') @@ -8,11 +8,12 @@ jest.mock('../../../../../../shared/domain/events/domain-events') describe('Authcode AggregateRoot', () => { test('it adds a AuthCodeCreated domain event on new AuthCode creation', () => { - AuthCode.create({ clientId: 'test_client_id', userId: 'test_user_id', - authCodeString: new AuthCodeString('test_auth_code') + userEmail: 'testemail@uwaterloo.ca', + userEmailVerified: false, + authCodeString: new AuthCodeString('test_auth_code'), }) expect(AuthCodeCreated).toBeCalled() diff --git a/server/src/modules/users/domain/entities/auth-code/auth-code.ts b/server/src/modules/users/domain/entities/auth-code/auth-code.ts index d2726a2..df48d1a 100644 --- a/server/src/modules/users/domain/entities/auth-code/auth-code.ts +++ b/server/src/modules/users/domain/entities/auth-code/auth-code.ts @@ -3,11 +3,13 @@ import { Result } from '../../../../../shared/core/result' import { AggregateRoot } from '../../../../../shared/domain/aggregate-root' import { UniqueEntityID } from '../../../../../shared/domain/unique-entity-id' import { AuthCodeCreated } from '../../events/auth-code-created' -import { AuthCodeString } from '../../value-objects/auth-code' +import { AuthCodeString } from '../../value-objects/auth-code-string' interface AuthCodeProps { clientId: string userId: string + userEmail: string + userEmailVerified: boolean authCodeString: AuthCodeString } @@ -36,6 +38,14 @@ export class AuthCode extends AggregateRoot { return this.props.clientId } + get userEmail(): string { + return this.props.userEmail + } + + get userEmailVerified(): boolean { + return this.props.userEmailVerified + } + get userId(): string { return this.props.userId } diff --git a/server/src/modules/users/domain/entities/auth-secret/auth-secret.ts b/server/src/modules/users/domain/entities/auth-secret/auth-secret.ts index 5fe5111..78836be 100644 --- a/server/src/modules/users/domain/entities/auth-secret/auth-secret.ts +++ b/server/src/modules/users/domain/entities/auth-secret/auth-secret.ts @@ -4,8 +4,10 @@ import { UniqueEntityID } from '../../../../../shared/domain/unique-entity-id' import { EncryptedClientSecret } from '../../value-objects/encrypted-client-secret' interface AuthSecretProps { - clientId: string, - encryptedClientSecret: EncryptedClientSecret, + clientId: string + decodedRedirectUri: string + clientName: string + encryptedClientSecret: EncryptedClientSecret isVerified: boolean } @@ -25,15 +27,23 @@ export class AuthSecret extends AggregateRoot { super(props, id) } + get clientName(): string { + return this.props.clientName + } + get clientId(): string { return this.props.clientId } + get decodedRedirectUri(): string { + return this.props.decodedRedirectUri + } + get encryptedClientSecret(): EncryptedClientSecret { return this.props.encryptedClientSecret } - get isVerified() :boolean { + get isVerified(): boolean { return this.props.isVerified } } diff --git a/server/src/modules/users/domain/entities/user/__tests__/user.test.unit.ts b/server/src/modules/users/domain/entities/user/__tests__/user.test.unit.ts index f45e8c8..3e96628 100644 --- a/server/src/modules/users/domain/entities/user/__tests__/user.test.unit.ts +++ b/server/src/modules/users/domain/entities/user/__tests__/user.test.unit.ts @@ -30,29 +30,6 @@ describe('User AggregateRoot', () => { expect(DomainEvents.markAggregateForDispatch).toBeCalled() }) - test('it adds a UserLoggedIn domain event on user login', () => { - const emailResult = UserEmail.create('john.doe@uwaterloo.ca') - const passwordResult = UserPassword.create({ value: 'secretpassword', hashed: false }) - - if (emailResult.isErr() || passwordResult.isErr()) - throw new Error('Result should be isOk, not isErr') - - const userResult = User.create({ - email: emailResult.value, - password: passwordResult.value, - }) - - if (userResult.isErr()) throw new Error('User result should be isOk, not isErr') - - const user = userResult.value - user.setAccessToken('token', 'refresh') - - // note: not sure if this is the best way to test AggregateRoot.addDomainEvent() was called with UserLoggedIn event - // TODO: if changes made here, see clubs/domain/entities/__tests__/club.test.unit.ts as well. - expect(UserLoggedIn).toBeCalled() - expect(DomainEvents.markAggregateForDispatch).toBeCalled() - }) - test('it adds a UserDeleted domain event on user deletion', () => { const emailResult = UserEmail.create('john.doe@uwaterloo.ca') const passwordResult = UserPassword.create({ value: 'secretpassword', hashed: false }) diff --git a/server/src/modules/users/domain/entities/user/user.ts b/server/src/modules/users/domain/entities/user/user.ts index ea53f1e..8f6b81d 100644 --- a/server/src/modules/users/domain/entities/user/user.ts +++ b/server/src/modules/users/domain/entities/user/user.ts @@ -3,8 +3,6 @@ import { AggregateRoot } from '../../../../../shared/domain/aggregate-root' import { UniqueEntityID } from '../../../../../shared/domain/unique-entity-id' import { UserCreated } from '../../events/user-created' import { UserDeleted } from '../../events/user-deleted' -import { UserLoggedIn } from '../../events/user-logged-in' -import { JWTToken, RefreshToken } from '../../value-objects/jwt' import { UserEmail } from '../../value-objects/user-email' import { UserPassword } from '../../value-objects/user-password' import { UserId } from '../../value-objects/userId' @@ -13,8 +11,6 @@ interface UserProps { email: UserEmail password: UserPassword emailVerified?: boolean - accessToken?: JWTToken - refreshToken?: RefreshToken isDeleted?: boolean lastLogin?: Date } @@ -22,6 +18,8 @@ interface UserProps { export class User extends AggregateRoot { public static create(props: UserProps, id?: UniqueEntityID): Result { const isNewUser = !!id === false + props.emailVerified = false + props.isDeleted = false const user = new User( { ...props, @@ -38,17 +36,6 @@ export class User extends AggregateRoot { super(props, id) } - public setAccessToken(token: JWTToken, refreshToken: RefreshToken): void { - this.addDomainEvent(new UserLoggedIn(this)) - this.props.accessToken = token - this.props.refreshToken = refreshToken - this.props.lastLogin = new Date() - } - - public isLoggedIn(): boolean { - return !!this.props.accessToken && !!this.props.refreshToken - } - public delete(): void { if (!this.props.isDeleted) { this.addDomainEvent(new UserDeleted(this)) @@ -70,10 +57,6 @@ export class User extends AggregateRoot { return this.props.password } - get accessToken(): string | undefined { - return this.props.accessToken - } - get isDeleted(): boolean | undefined { return this.props.isDeleted } @@ -85,8 +68,4 @@ export class User extends AggregateRoot { get lastLogin(): Date | undefined { return this.props.lastLogin } - - get refreshToken(): RefreshToken | undefined { - return this.props.refreshToken - } } diff --git a/server/src/modules/users/domain/value-objects/__tests__/authCode.test.unit.ts b/server/src/modules/users/domain/value-objects/__tests__/authCode.test.unit.ts index 7c80259..529e2be 100644 --- a/server/src/modules/users/domain/value-objects/__tests__/authCode.test.unit.ts +++ b/server/src/modules/users/domain/value-objects/__tests__/authCode.test.unit.ts @@ -1,12 +1,11 @@ -import { AuthCodeString } from "../auth-code" +import { AuthCodeString } from '../auth-code-string' describe('AuthCodeString ValueObject', () => { test("When an AuthCodeString is created, it's value is hex", async () => { + const authCodeResult = new AuthCodeString() - const authCodeResult = new AuthCodeString(); + const hexRegex = /[0-9A-Fa-f]{6}/g - const hexRegex = /[0-9A-Fa-f]{6}/g; - expect(hexRegex.test(authCodeResult.getValue())).toBe(true) }) }) diff --git a/server/src/modules/users/domain/value-objects/auth-code.ts b/server/src/modules/users/domain/value-objects/auth-code-string.ts similarity index 74% rename from server/src/modules/users/domain/value-objects/auth-code.ts rename to server/src/modules/users/domain/value-objects/auth-code-string.ts index ff05f11..2359aad 100644 --- a/server/src/modules/users/domain/value-objects/auth-code.ts +++ b/server/src/modules/users/domain/value-objects/auth-code-string.ts @@ -1,10 +1,10 @@ import crypto from 'crypto' export class AuthCodeString { - private value: string; - + private value: string + public constructor(hashedValue?: string) { - if(hashedValue){ + if (hashedValue) { this.value = hashedValue } else { this.value = this.getRandomCode() @@ -12,14 +12,14 @@ export class AuthCodeString { } private getRandomCode() { - /*motivation for a 256 bit (= 32 byte) crypographic key can be found here + /*motivation for a 256 bit (= 32 byte) cryptographic key can be found here https://www.geeksforgeeks.org/node-js-crypto-randombytes-method/ */ const authCodeBuffer = crypto.randomBytes(32) return authCodeBuffer.toString('hex') } - public getValue(){ + public getValue() { return this.value } } diff --git a/server/src/modules/users/infra/repos/auth-code-repo/auth-code-repo.ts b/server/src/modules/users/infra/repos/auth-code-repo/auth-code-repo.ts index 6d08027..60a4bd2 100644 --- a/server/src/modules/users/infra/repos/auth-code-repo/auth-code-repo.ts +++ b/server/src/modules/users/infra/repos/auth-code-repo/auth-code-repo.ts @@ -1,7 +1,7 @@ import { Result } from '../../../../../shared/core/result' import { DBErrors } from '../../../../../shared/infra/db/errors/errors' import { AuthCode } from '../../../domain/entities/auth-code' -import { AuthCodeString } from '../../../domain/value-objects/auth-code' +import { AuthCodeString } from '../../../domain/value-objects/auth-code-string' export abstract class AuthCodeRepo { abstract getAuthCodeFromAuthCodeString( diff --git a/server/src/modules/users/infra/repos/auth-code-repo/implementations/mock-auth-code-repo.ts b/server/src/modules/users/infra/repos/auth-code-repo/implementations/mock-auth-code-repo.ts index 80e1812..33d3797 100644 --- a/server/src/modules/users/infra/repos/auth-code-repo/implementations/mock-auth-code-repo.ts +++ b/server/src/modules/users/infra/repos/auth-code-repo/implementations/mock-auth-code-repo.ts @@ -2,7 +2,7 @@ import { Result } from '../../../../../../shared/core/result' import { AuthCodeEntity } from '../../../../../../shared/infra/cache/entities/auth-code-entity' import { DBError, DBErrors } from '../../../../../../shared/infra/db/errors/errors' import { AuthCode } from '../../../../domain/entities/auth-code' -import { AuthCodeString } from '../../../../domain/value-objects/auth-code' +import { AuthCodeString } from '../../../../domain/value-objects/auth-code-string' import { AuthCodeMap } from '../../../../mappers/auth-code-map' import { AuthCodeRepo } from '../auth-code-repo' diff --git a/server/src/modules/users/infra/repos/auth-code-repo/implementations/redis-auth-code-repo.ts b/server/src/modules/users/infra/repos/auth-code-repo/implementations/redis-auth-code-repo.ts index 6b37102..378c3c5 100644 --- a/server/src/modules/users/infra/repos/auth-code-repo/implementations/redis-auth-code-repo.ts +++ b/server/src/modules/users/infra/repos/auth-code-repo/implementations/redis-auth-code-repo.ts @@ -3,7 +3,7 @@ import { AuthCodeEntity } from '../../../../../../shared/infra/cache/entities/au import { RedisRepository } from '../../../../../../shared/infra/cache/redis-repository' import { DBError, DBErrors } from '../../../../../../shared/infra/db/errors/errors' import { AuthCode } from '../../../../domain/entities/auth-code' -import { AuthCodeString } from '../../../../domain/value-objects/auth-code' +import { AuthCodeString } from '../../../../domain/value-objects/auth-code-string' import { AuthCodeMap } from '../../../../mappers/auth-code-map' import { AuthCodeRepo } from '../auth-code-repo' @@ -16,9 +16,13 @@ export class RedisAuthCodeRepo implements AuthCodeRepo { authCodeString: AuthCodeString ): Promise> { const authCode = await this.authCodeEntityRepo.getEntity(authCodeString.getValue()) - if (authCode.isErr()) - return Result.err(new DBError.AuthSecretNotFoundError(authCodeString.getValue())) - return Result.ok(AuthCodeMap.toDomain(authCode.value)) + if (authCode.isErr()) { + return Result.err(authCode.error) + } else if (authCode.value === null) { + return Result.err(new DBError.AuthCodeNotFoundError(authCodeString.getValue())) + } else { + return Result.ok(AuthCodeMap.toDomain(authCode.value)) + } } async save(authCode: AuthCode): Promise { diff --git a/server/src/modules/users/infra/repos/auth-secret-repo/auth-secret-repo.ts b/server/src/modules/users/infra/repos/auth-secret-repo/auth-secret-repo.ts index bc59720..406484e 100644 --- a/server/src/modules/users/infra/repos/auth-secret-repo/auth-secret-repo.ts +++ b/server/src/modules/users/infra/repos/auth-secret-repo/auth-secret-repo.ts @@ -1,9 +1,14 @@ import { Result } from '../../../../../shared/core/result' import { DBErrors } from '../../../../../shared/infra/db/errors/errors' import { AuthSecret } from '../../../domain/entities/auth-secret' +import { EncryptedClientSecret } from '../../../domain/value-objects/encrypted-client-secret' export abstract class AuthSecretRepo { - abstract exists(clientId: string): Promise> - abstract getAuthSecretByClientId(clientId: string): Promise> + abstract exists(clientId: string, decodedRedirectUri?: string): Promise> + abstract clientNameExists(clientName: string): Promise> + abstract getAuthSecretByClientIdandSecret( + clientId: string, + clientSecret: EncryptedClientSecret + ): Promise> abstract save(authSecret: AuthSecret): Promise } diff --git a/server/src/modules/users/infra/repos/auth-secret-repo/implementations/mikro-auth-secret-repo.ts b/server/src/modules/users/infra/repos/auth-secret-repo/implementations/mikro-auth-secret-repo.ts index 3c99a02..989fdd5 100644 --- a/server/src/modules/users/infra/repos/auth-secret-repo/implementations/mikro-auth-secret-repo.ts +++ b/server/src/modules/users/infra/repos/auth-secret-repo/implementations/mikro-auth-secret-repo.ts @@ -3,22 +3,39 @@ import { Result } from '../../../../../../shared/core/result' import { AuthSecretEntity } from '../../../../../../shared/infra/db/entities/auth-secret.entity' import { DBError, DBErrors } from '../../../../../../shared/infra/db/errors/errors' import { AuthSecret } from '../../../../domain/entities/auth-secret' +import { EncryptedClientSecret } from '../../../../domain/value-objects/encrypted-client-secret' import { AuthSecretMap } from '../../../../mappers/auth-secret-map' import { AuthSecretRepo } from '../../auth-secret-repo/auth-secret-repo' export class MikroAuthSecretRepo implements AuthSecretRepo { constructor(protected authSecretEntityRepo: EntityRepository) {} - async exists(clientId: string): Promise> { + async exists(clientId: string, decodedRedirectUri?: string): Promise> { const authSecretEntity = await this.authSecretEntityRepo.findOne({ clientId: clientId, + ...{ decodedRedirectUri }, }) return Result.ok(authSecretEntity !== null) } - async getAuthSecretByClientId(clientId: string): Promise> { + async clientNameExists(clientName: string): Promise> { + const authSecretEntity = await this.authSecretEntityRepo.findOne({ + clientName, + }) + return Result.ok(authSecretEntity !== null) + } + + async getAuthSecretByClientIdandSecret( + clientId: string, + clientSecret: EncryptedClientSecret + ): Promise> { const authSecret = await this.authSecretEntityRepo.findOne({ clientId: clientId }) if (authSecret === null) return Result.err(new DBError.AuthSecretNotFoundError(clientId)) + + const authSecretsEqual = await clientSecret.compareSecret(authSecret.encryptedClientSecret) + if (authSecretsEqual.isOk() && !authSecretsEqual.value) { + return Result.err(new DBError.AuthSecretsNotEqualError(clientSecret.value)) + } return Result.ok(AuthSecretMap.toDomain(authSecret)) } diff --git a/server/src/modules/users/infra/repos/auth-secret-repo/implementations/mock-auth-secret-repo.ts b/server/src/modules/users/infra/repos/auth-secret-repo/implementations/mock-auth-secret-repo.ts index edc83bc..f2b320f 100644 --- a/server/src/modules/users/infra/repos/auth-secret-repo/implementations/mock-auth-secret-repo.ts +++ b/server/src/modules/users/infra/repos/auth-secret-repo/implementations/mock-auth-secret-repo.ts @@ -2,6 +2,7 @@ import { Result } from '../../../../../../shared/core/result' import { AuthSecretEntity } from '../../../../../../shared/infra/db/entities/auth-secret.entity' import { DBError, DBErrors } from '../../../../../../shared/infra/db/errors/errors' import { AuthSecret } from '../../../../domain/entities/auth-secret' +import { EncryptedClientSecret } from '../../../../domain/value-objects/encrypted-client-secret' import { AuthSecretMap } from '../../../../mappers/auth-secret-map' import { AuthSecretRepo } from '../auth-secret-repo' @@ -18,10 +19,19 @@ export class MockAuthSecretRepo implements AuthSecretRepo { return Result.ok(this.authSecretEntities.has(clientId)) } - async getAuthSecretByClientId(clientId: string): Promise> { - const authSecretEntity = this.authSecretEntities.get(clientId) + async clientNameExists(clientName: string): Promise> { + return Result.ok(this.authSecretEntities.has(clientName)) + } - if (authSecretEntity === undefined) { + async getAuthSecretByClientIdandSecret( + clientId: string, + clientSecret: EncryptedClientSecret + ): Promise> { + const authSecretEntity = this.authSecretEntities.get(clientId) + if ( + authSecretEntity === undefined || + authSecretEntity.encryptedClientSecret != clientSecret.value + ) { return Result.err(new DBError.AuthSecretNotFoundError(clientId)) } return Result.ok(AuthSecretMap.toDomain(authSecretEntity)) @@ -33,5 +43,6 @@ export class MockAuthSecretRepo implements AuthSecretRepo { const authSecretEntity = await AuthSecretMap.toPersistence(authSecret) this.authSecretEntities.set(authSecretEntity.clientId, authSecretEntity) + this.authSecretEntities.set(authSecretEntity.clientName, authSecretEntity) } } diff --git a/server/src/modules/users/mappers/auth-code-map.ts b/server/src/modules/users/mappers/auth-code-map.ts index c005203..4a30e2f 100644 --- a/server/src/modules/users/mappers/auth-code-map.ts +++ b/server/src/modules/users/mappers/auth-code-map.ts @@ -1,12 +1,14 @@ import { AuthCodeEntity } from '../../../shared/infra/cache/entities/auth-code-entity' import { AuthCode } from '../domain/entities/auth-code' -import { AuthCodeString } from '../domain/value-objects/auth-code' +import { AuthCodeString } from '../domain/value-objects/auth-code-string' export class AuthCodeMap { public static toDomain(authCodeEntity: AuthCodeEntity): AuthCode { const authCode = AuthCode.create({ clientId: authCodeEntity.clientId, userId: authCodeEntity.clientId, + userEmail: authCodeEntity.userEmail, + userEmailVerified: authCodeEntity.userEmailVerified, authCodeString: new AuthCodeString(authCodeEntity.authCodeString), }) if (authCode.isErr()) throw new Error() // TODO: check if we should handle error differently @@ -19,7 +21,8 @@ export class AuthCodeMap { authCodeEntity.clientId = authCode.clientId authCodeEntity.userId = authCode.userId authCodeEntity.authCodeString = authCode.authCodeString.getValue() - + authCodeEntity.userEmail = authCode.userEmail + authCodeEntity.userEmailVerified = authCode.userEmailVerified return authCodeEntity } } diff --git a/server/src/modules/users/mappers/auth-secret-map.ts b/server/src/modules/users/mappers/auth-secret-map.ts index 7fd9484..2d6f9b2 100644 --- a/server/src/modules/users/mappers/auth-secret-map.ts +++ b/server/src/modules/users/mappers/auth-secret-map.ts @@ -15,6 +15,8 @@ export class AuthSecretMap { const authCodeResult = AuthSecret.create( { clientId: authSecretEntity.clientId, + decodedRedirectUri: authSecretEntity.decodedRedirectUri, + clientName: authSecretEntity.clientName, encryptedClientSecret: encryptedClientSecret.value, isVerified: authSecretEntity.isVerified, }, @@ -28,8 +30,11 @@ export class AuthSecretMap { public static async toPersistence(authSecret: AuthSecret): Promise { const authSecretEntity = new AuthSecretEntity() + authSecretEntity.decodedRedirectUri = authSecret.decodedRedirectUri + authSecretEntity.clientName = authSecret.clientName authSecretEntity.clientId = authSecret.clientId - authSecretEntity.encryptedClientSecret = authSecret.encryptedClientSecret.value + const hashedEncryptedClientSecret = await authSecret.encryptedClientSecret.getHashedValue() + authSecretEntity.encryptedClientSecret = hashedEncryptedClientSecret authSecretEntity.isVerified = authSecret.isVerified return authSecretEntity diff --git a/server/src/setup/application/controllers.ts b/server/src/setup/application/controllers.ts index 61b7410..c10f95a 100644 --- a/server/src/setup/application/controllers.ts +++ b/server/src/setup/application/controllers.ts @@ -1,12 +1,16 @@ import { UseCases, Controllers } from './types' import { CreateUserController } from '../../modules/users/application/use-cases/create-user/create-user-controller' import { LoginUserController } from '../../modules/users/application/use-cases/login-user/login-user-controller' -import { ProtectedUserController } from '../../modules/users/application/use-cases/protected-user/protected-user-controller' +import { AuthorizeUserController } from '../../modules/users/application/use-cases/authorize-user/authorize-user-controller' +import { DiscoverSPController } from '../../modules/users/application/use-cases/discover-sp/discover-sp-controller' +import { GetTokenController } from '../../modules/users/application/use-cases/get-token/get-token-controller' export const setupControllers = (useCases: UseCases): Controllers => { return { createUser: new CreateUserController(useCases.createUser), loginUser: new LoginUserController(useCases.loginUser), - protectedUser: new ProtectedUserController(useCases.protectedUser) + authorizeUser: new AuthorizeUserController(useCases.authorizeUser), + discoverSP: new DiscoverSPController(useCases.discoverSP), + getToken: new GetTokenController(useCases.getToken), } } diff --git a/server/src/setup/application/types.ts b/server/src/setup/application/types.ts index d87df88..41dc0c7 100644 --- a/server/src/setup/application/types.ts +++ b/server/src/setup/application/types.ts @@ -4,21 +4,29 @@ import { LoginUserUseCase } from '../../modules/users/application/use-cases/logi import { GetUserUseCase } from '../../modules/users/application/use-cases/get-user/get-user-use-case' import { AuthenticateUserUseCase } from '../../modules/users/application/use-cases/authenticate-user/authenticate-user-use-case' import { LoginUserController } from '../../modules/users/application/use-cases/login-user/login-user-controller' -import { ProtectedUserUseCase } from '../../modules/users/application/use-cases/protected-user/protected-user-use-case' -import { ProtectedUserController } from '../../modules/users/application/use-cases/protected-user/protected-user-controller' +import { AuthorizeUserUseCase } from '../../modules/users/application/use-cases/authorize-user/authorize-user-use-case' +import { DiscoverSPUseCase } from '../../modules/users/application/use-cases/discover-sp/discover-sp-use-case' +import { GetTokenUseCase } from '../../modules/users/application/use-cases/get-token/get-token-use-case' +import { AuthorizeUserController } from '../../modules/users/application/use-cases/authorize-user/authorize-user-controller' +import { DiscoverSPController } from '../../modules/users/application/use-cases/discover-sp/discover-sp-controller' +import { GetTokenController } from '../../modules/users/application/use-cases/get-token/get-token-controller' export interface UseCases { createUser: CreateUserUseCase loginUser: LoginUserUseCase getUser: GetUserUseCase - authUser: AuthenticateUserUseCase - protectedUser: ProtectedUserUseCase + authenticateUser: AuthenticateUserUseCase + authorizeUser: AuthorizeUserUseCase + discoverSP: DiscoverSPUseCase + getToken: GetTokenUseCase } export interface Controllers { createUser: CreateUserController loginUser: LoginUserController - protectedUser: ProtectedUserController + authorizeUser: AuthorizeUserController + discoverSP: DiscoverSPController + getToken: GetTokenController } export interface Application { diff --git a/server/src/setup/application/use-cases.ts b/server/src/setup/application/use-cases.ts index 9235025..e24b0c7 100644 --- a/server/src/setup/application/use-cases.ts +++ b/server/src/setup/application/use-cases.ts @@ -4,15 +4,19 @@ import { LoginUserUseCase } from '../../modules/users/application/use-cases/logi import { PassportUserAuthHandler } from '../../shared/auth/implementations/passport-user-auth-handler' import { GetUserUseCase } from '../../modules/users/application/use-cases/get-user/get-user-use-case' import { AuthenticateUserUseCase } from '../../modules/users/application/use-cases/authenticate-user/authenticate-user-use-case' -import { ProtectedUserUseCase } from '../../modules/users/application/use-cases/protected-user/protected-user-use-case' import { Persistence } from '../persistence/persistence' +import { AuthorizeUserUseCase } from '../../modules/users/application/use-cases/authorize-user/authorize-user-use-case' +import { GetTokenUseCase } from '../../modules/users/application/use-cases/get-token/get-token-use-case' +import { DiscoverSPUseCase } from '../../modules/users/application/use-cases/discover-sp/discover-sp-use-case' -export const setupUseCases = ({ db }: Persistence): UseCases => { +export const setupUseCases = ({ db, cache }: Persistence): UseCases => { return { createUser: new CreateUserUseCase(new PassportUserAuthHandler(), db.repos.user), loginUser: new LoginUserUseCase(new PassportUserAuthHandler()), getUser: new GetUserUseCase(db.repos.user), - authUser: new AuthenticateUserUseCase(db.repos.user), - protectedUser: new ProtectedUserUseCase(), + authenticateUser: new AuthenticateUserUseCase(db.repos.user), + authorizeUser: new AuthorizeUserUseCase(cache.repos.authCode, db.repos.authSecret), + discoverSP: new DiscoverSPUseCase(db.repos.authSecret), + getToken: new GetTokenUseCase(cache.repos.authCode, db.repos.authSecret), } } diff --git a/server/src/shared/app/base-controller.ts b/server/src/shared/app/base-controller.ts index 738000a..ea85514 100644 --- a/server/src/shared/app/base-controller.ts +++ b/server/src/shared/app/base-controller.ts @@ -16,8 +16,8 @@ export abstract class BaseController { } public redirect(res: Res, url: string, params: ParamList) { - res.status(200).redirect(params.getFormattedUrlWithParams(url)) - return this.ok(res) + res.redirect(params.getFormattedUrlWithParams(url)) + return res } public fail(res: Res, error: Error | string): Res { diff --git a/server/src/shared/auth/implementations/mock-user-auth-handler.ts b/server/src/shared/auth/implementations/mock-user-auth-handler.ts index 4e80d89..de56ed6 100644 --- a/server/src/shared/auth/implementations/mock-user-auth-handler.ts +++ b/server/src/shared/auth/implementations/mock-user-auth-handler.ts @@ -1,20 +1,17 @@ import { Result } from '../../core/result' import { UserAuthHandler, - UserAuthHandlerCreateOptions, - UserAuthHandlerCreateResponse, UserAuthHandlerLoginOptions, UserAuthHandlerLoginResponse, UserAuthHandlerLoginSuccess, } from '../user-auth-handler' import { AppError } from '../../core/app-error' -import { AuthCodeString } from '../../../modules/users/domain/value-objects/auth-code' +import { AuthCodeString } from '../../../modules/users/domain/value-objects/auth-code-string' import { mocks } from '../../../test-utils' import { CreateUserDTOBody } from '../../../modules/users/application/use-cases/create-user/create-user-dto' import { UserMap } from '../../../modules/users/mappers/user-map' export const LOGIN_ERROR = 'login_error' -export const CREATE_ERROR = 'create_error' //add implementation-specific auth functions here export class MockUserAuthHandler implements UserAuthHandler { @@ -37,11 +34,4 @@ export class MockUserAuthHandler implements UserAuthHandler { } }) } - - async create(options: UserAuthHandlerCreateOptions): Promise { - if (options.userId == CREATE_ERROR) - return Result.err(new AppError.UnexpectedError('Account creation failed')) - const authCodeString = new AuthCodeString('test_authcode_string') - return Result.ok(authCodeString) - } } diff --git a/server/src/shared/auth/implementations/passport-user-auth-handler.ts b/server/src/shared/auth/implementations/passport-user-auth-handler.ts index 54402e8..0d2c76c 100644 --- a/server/src/shared/auth/implementations/passport-user-auth-handler.ts +++ b/server/src/shared/auth/implementations/passport-user-auth-handler.ts @@ -3,69 +3,32 @@ import { Result } from '../../core/result' import { AuthenticateUserUseCaseResponse } from '../../../modules/users/application/use-cases/authenticate-user/authenticate-user-use-case' import { UserAuthHandler, - UserAuthHandlerCreateOptions, - UserAuthHandlerCreateResponse, UserAuthHandlerLoginOptions, UserAuthHandlerLoginResponse, UserAuthHandlerLoginSuccess, } from '../user-auth-handler' -import { AuthCode } from '../../../modules/users/domain/entities/auth-code' -import { AuthCodeString } from '../../../modules/users/domain/value-objects/auth-code' import { UserMap } from '../../../modules/users/mappers/user-map' //add implementation-specific auth functions here export class PassportUserAuthHandler implements UserAuthHandler { async login(options: UserAuthHandlerLoginOptions): Promise { return new Promise((resolve) => { - passport.authenticate('local', function (err, user: AuthenticateUserUseCaseResponse) { - if (err) resolve(Result.err(err)) - if (user.isErr()) { + passport.authenticate('local', async function (err, user: AuthenticateUserUseCaseResponse) { + if (err) { + resolve(Result.err(err)) + } else if (user.isErr()) { resolve(Result.err(user.error)) } else { - if (!options.params) { - const successResponse: UserAuthHandlerLoginSuccess = { user: UserMap.toDTO(user.value) } - options.req.login(user.value, function (err) { - if (err) { - resolve(Result.err(err)) - } else { - resolve(Result.ok(successResponse)) - } - }) - } - const authCode = AuthCode.create({ - clientId: options.clientId, - userId: user.value.userId.id.toString(), - authCodeString: new AuthCodeString(), - }) - if (authCode.isErr()) { - resolve(Result.err(authCode.error)) - } else { - const successResponse: UserAuthHandlerLoginSuccess = { - cert: authCode.value.authCodeString, - user: UserMap.toDTO(user.value), + const successResponse: UserAuthHandlerLoginSuccess = { user: UserMap.toDTO(user.value) } + options.req.login(user.value, function (err) { + if (err) { + resolve(Result.err(err)) + } else { + resolve(Result.ok(successResponse)) } - options.req.login(user.value, function (err) { - if (err) { - resolve(Result.err(err)) - } else { - resolve(Result.ok(successResponse)) - } - }) - } + }) } })(options.req, options.res) }) } - - async create(options: UserAuthHandlerCreateOptions): Promise { - const authCode = AuthCode.create({ - clientId: options.clientId, - userId: options.userId, - authCodeString: new AuthCodeString(), - }) - if (authCode.isErr()) { - return Result.err(authCode.error) - } - return Result.ok(authCode.value.authCodeString) - } } diff --git a/server/src/shared/auth/user-auth-handler.ts b/server/src/shared/auth/user-auth-handler.ts index e1d64e7..750b9e6 100644 --- a/server/src/shared/auth/user-auth-handler.ts +++ b/server/src/shared/auth/user-auth-handler.ts @@ -2,7 +2,7 @@ import express from 'express' import { Result } from '../core/result' import { LoginUserErrors } from '../../modules/users/application/use-cases/login-user/login-user-errors' import { AppError } from '../core/app-error' -import { AuthCodeString } from '../../modules/users/domain/value-objects/auth-code' +import { AuthCodeString } from '../../modules/users/domain/value-objects/auth-code-string' import { UserDTO } from '../../modules/users/mappers/user-dto' export type AuthToken = string @@ -30,20 +30,6 @@ export interface UserAuthHandlerLoginOptions { [key: string]: any //additional, implementation-specific options for auth handlers } -//creation -export type UserAuthHandlerCreateSuccess = AuthCertificate -export type UserAuthHandlerCreateError = AppError.UnexpectedError -export type UserAuthHandlerCreateResponse = Result< - UserAuthHandlerCreateSuccess, - UserAuthHandlerCreateError -> - -export interface UserAuthHandlerCreateOptions { - userId: string // we make the assumption that all auth handlers will require this at the least - [key: string]: any //additional, implementation-specific options for auth handlers -} - export abstract class UserAuthHandler { abstract login(options: UserAuthHandlerLoginOptions): Promise - abstract create(options: UserAuthHandlerCreateOptions): Promise } diff --git a/server/src/shared/infra/cache/entities/auth-code-entity.ts b/server/src/shared/infra/cache/entities/auth-code-entity.ts index 176121e..1320854 100644 --- a/server/src/shared/infra/cache/entities/auth-code-entity.ts +++ b/server/src/shared/infra/cache/entities/auth-code-entity.ts @@ -5,6 +5,10 @@ export class AuthCodeEntity extends RedisEntity { userId!: string + userEmail!: string + + userEmailVerified!: boolean + authCodeString!: string getEntityKey() { @@ -12,10 +16,12 @@ export class AuthCodeEntity extends RedisEntity { } toJSON(): object { - const { clientId, userId, authCodeString, id } = this + const { clientId, userId, userEmail, userEmailVerified, authCodeString, id } = this return { clientId, userId, + userEmail, + userEmailVerified, authCodeString, id, } diff --git a/server/src/shared/infra/cache/redis-repository.ts b/server/src/shared/infra/cache/redis-repository.ts index 6016884..1f33d93 100644 --- a/server/src/shared/infra/cache/redis-repository.ts +++ b/server/src/shared/infra/cache/redis-repository.ts @@ -7,7 +7,10 @@ export type RedisSaveEntityResponse = Result = Result +export type RedisGetEntityResponse = Result< + RedisEntityType | null, + RedisGetEntityError +> export type RedisGetEntityError = AppError.UnexpectedError export type RedisDeleteEntityResponse = Result @@ -22,8 +25,10 @@ export class RedisRepository { async getEntity(entityKey: string): Promise> { return new Promise((resolve) => { RedisClient().get(entityKey, (err, value) => { - if (err || value === null) { + if (err) { resolve(Result.err(new AppError.UnexpectedError(`Redis get operation failed. ${err}`))) + } else if (value === null) { + resolve(Result.ok(null)) } else { resolve(Result.ok(JSON.parse(value) as RedisEntityType)) } diff --git a/server/src/shared/infra/db/entities/auth-secret.entity.ts b/server/src/shared/infra/db/entities/auth-secret.entity.ts index 04be312..2a95100 100644 --- a/server/src/shared/infra/db/entities/auth-secret.entity.ts +++ b/server/src/shared/infra/db/entities/auth-secret.entity.ts @@ -7,6 +7,12 @@ export class AuthSecretEntity extends BaseEntity { @Index() clientId!: string + @Property() + decodedRedirectUri!: string + + @Property() + clientName!: string + @Property() encryptedClientSecret!: string diff --git a/server/src/shared/infra/db/entities/user.entity.ts b/server/src/shared/infra/db/entities/user.entity.ts index bb66839..2583e65 100644 --- a/server/src/shared/infra/db/entities/user.entity.ts +++ b/server/src/shared/infra/db/entities/user.entity.ts @@ -17,12 +17,6 @@ export class UserEntity extends BaseEntity { @Property({ default: false }) isDeleted!: boolean - @Property() - accessToken?: string - - @Property() - refreshToken?: string - @Property() lastLogin?: Date diff --git a/server/src/shared/infra/db/errors/errors.ts b/server/src/shared/infra/db/errors/errors.ts index 46b4d88..9a1d469 100644 --- a/server/src/shared/infra/db/errors/errors.ts +++ b/server/src/shared/infra/db/errors/errors.ts @@ -1,32 +1,41 @@ +import { AppError } from '../../../core/app-error' + export namespace DBError { - export class UserNotFoundError { - public message: string + export class UserNotFoundError extends Error { public constructor(identifier: string) { + super() this.message = `The user with attribute (id/email) ${identifier} could not be found.` } } - export class AuthSecretNotFoundError { - public message: string + export class AuthSecretNotFoundError extends Error { public constructor(identifier: string) { + super() this.message = `The auth secret with clientId ${identifier} could not be found.` } } - export class AuthCodeNotFoundError { - public message: string + export class AuthCodeNotFoundError extends Error { public constructor(identifier: string) { + super() this.message = `The auth code ${identifier} could not be found.` } } - export class PasswordsNotEqualError { - public message: string + export class PasswordsNotEqualError extends Error { public constructor(identifier: string) { + super() this.message = `An invalid password for the user with attribute (id/email) ${identifier} was provided.` } } + export class AuthSecretsNotEqualError extends Error { + public constructor(identifier: string) { + super() + this.message = `An invalid client secret ${identifier} was provided.` + } + } } -export type DBErrors = -DBError.UserNotFoundError -| DBError.AuthSecretNotFoundError -| DBError.AuthCodeNotFoundError -| DBError.PasswordsNotEqualError +export type DBErrors = + | DBError.UserNotFoundError + | DBError.AuthSecretNotFoundError + | DBError.AuthCodeNotFoundError + | DBError.PasswordsNotEqualError + | AppError.UnexpectedError diff --git a/server/src/test-utils/mocks/application/index.ts b/server/src/test-utils/mocks/application/index.ts index cfd0fb8..9406005 100644 --- a/server/src/test-utils/mocks/application/index.ts +++ b/server/src/test-utils/mocks/application/index.ts @@ -1,4 +1,6 @@ export { mockCreateUser } from './mock-create-user' export { mockGetUser } from './mock-get-user' export { mockLoginUser } from './mock-login-user' -export { mockProtectedUser } from './mock-protected-user' +export { mockGetToken } from './mock-get-token' +export { mockAuthorizeUser } from './mock-authorize-user' +export { mockDiscoverSP } from './mock-discover-sp' diff --git a/server/src/test-utils/mocks/application/mock-authorize-user.ts b/server/src/test-utils/mocks/application/mock-authorize-user.ts new file mode 100644 index 0000000..c08ecf9 --- /dev/null +++ b/server/src/test-utils/mocks/application/mock-authorize-user.ts @@ -0,0 +1,20 @@ +import { AuthorizeUserUseCase } from '../../../modules/users/application/use-cases/authorize-user/authorize-user-use-case' +import { AuthorizeUserController } from '../../../modules/users/application/use-cases/authorize-user/authorize-user-controller' +import { AuthSecretEntity } from '../../../shared/infra/db/entities/auth-secret.entity' +import { AuthCodeEntity } from '../../../shared/infra/cache/entities/auth-code-entity' +import { MockAuthCodeRepo } from '../../../modules/users/infra/repos/auth-code-repo/implementations/mock-auth-code-repo' +import { MockAuthSecretRepo } from '../../../modules/users/infra/repos/auth-secret-repo/implementations/mock-auth-secret-repo' + +const mockAuthorizeUser = async ( + authCodeEntities: Array = [], + authSecretEntities: Array = [] +) => { + const authCodeRepo = new MockAuthCodeRepo(authCodeEntities) + const authSecretRepo = new MockAuthSecretRepo(authSecretEntities) + const authorizeUserUseCase = new AuthorizeUserUseCase(authCodeRepo, authSecretRepo) + const authorizeUserController = new AuthorizeUserController(authorizeUserUseCase) + + return { authorizeUserUseCase, authorizeUserController } +} + +export { mockAuthorizeUser } diff --git a/server/src/test-utils/mocks/application/mock-discover-sp.ts b/server/src/test-utils/mocks/application/mock-discover-sp.ts new file mode 100644 index 0000000..4369a82 --- /dev/null +++ b/server/src/test-utils/mocks/application/mock-discover-sp.ts @@ -0,0 +1,14 @@ +import { DiscoverSPUseCase } from '../../../modules/users/application/use-cases/discover-sp/discover-sp-use-case' +import { DiscoverSPController } from '../../../modules/users/application/use-cases/discover-sp/discover-sp-controller' +import { MockAuthSecretRepo } from '../../../modules/users/infra/repos/auth-secret-repo/implementations/mock-auth-secret-repo' +import { AuthSecretEntity } from '../../../shared/infra/db/entities/auth-secret.entity' + +const mockDiscoverSP = async (authSecretEntities: Array = []) => { + const authSecretRepo = new MockAuthSecretRepo(authSecretEntities) + const discoverSPUseCase = new DiscoverSPUseCase(authSecretRepo) + const discoverSPController = new DiscoverSPController(discoverSPUseCase) + + return { discoverSPUseCase, discoverSPController } +} + +export { mockDiscoverSP } diff --git a/server/src/test-utils/mocks/application/mock-get-token.ts b/server/src/test-utils/mocks/application/mock-get-token.ts new file mode 100644 index 0000000..8b5acd8 --- /dev/null +++ b/server/src/test-utils/mocks/application/mock-get-token.ts @@ -0,0 +1,20 @@ +import { GetTokenUseCase } from '../../../modules/users/application/use-cases/get-token/get-token-use-case' +import { GetTokenController } from '../../../modules/users/application/use-cases/get-token/get-token-controller' +import { MockAuthCodeRepo } from '../../../modules/users/infra/repos/auth-code-repo/implementations/mock-auth-code-repo' +import { AuthCodeEntity } from '../../../shared/infra/cache/entities/auth-code-entity' +import { MockAuthSecretRepo } from '../../../modules/users/infra/repos/auth-secret-repo/implementations/mock-auth-secret-repo' +import { AuthSecretEntity } from '../../../shared/infra/db/entities/auth-secret.entity' + +const mockGetToken = async ( + authCodeEntities: Array = [], + authSecretEntities: Array = [] +) => { + const authCodeRepo = new MockAuthCodeRepo(authCodeEntities) + const authSecretRepo = new MockAuthSecretRepo(authSecretEntities) + const getTokenUseCase = new GetTokenUseCase(authCodeRepo, authSecretRepo) + const getTokenController = new GetTokenController(getTokenUseCase) + + return { getTokenUseCase, getTokenController } +} + +export { mockGetToken } diff --git a/server/src/test-utils/mocks/application/mock-protected-user.ts b/server/src/test-utils/mocks/application/mock-protected-user.ts deleted file mode 100644 index 2cf165e..0000000 --- a/server/src/test-utils/mocks/application/mock-protected-user.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ProtectedUserUseCase } from '../../../modules/users/application/use-cases/protected-user/protected-user-use-case' -import { ProtectedUserController } from '../../../modules/users/application/use-cases/protected-user/protected-user-controller' - -const mockProtectedUser = async () => { - const protectedUserUseCase = new ProtectedUserUseCase() - const protectedUserController = new ProtectedUserController(protectedUserUseCase) - - return { protectedUserUseCase, protectedUserController } -} - -export { mockProtectedUser } From 50efdca2da28f35753d5313707d2a33a9b435688 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 29 Aug 2021 01:56:33 -0400 Subject: [PATCH 2/3] Address compiler errors and add is_verified switch script --- server/scripts/approve_client.py | 51 ++++++++++++ server/scripts/requirements.txt | 1 + .../src/migrations/Migration20210829055229.ts | 12 +++ .../authenticate-user-use-case.ts | 25 +++--- .../create-user-use-case.test.unit.ts | 24 ++++-- .../create-user/create-user-controller.ts | 15 ++-- .../use-cases/create-user/create-user-dto.ts | 5 ++ .../create-user/create-user-use-case.ts | 78 ++++++++++--------- .../discover-sp/discover-sp-controller.ts | 6 +- .../discover-sp/discover-sp-use-case.ts | 10 +-- .../use-cases/get-token/get-token-use-case.ts | 8 +- .../use-cases/get-user/get-user-use-case.ts | 18 ++--- .../entities/user/__tests__/user.test.unit.ts | 1 - .../users/infra/http/routes/user-router.ts | 10 +-- server/src/modules/users/mappers/user-map.ts | 4 - .../setup/http/express/basic-web-server.ts | 54 +++++++------ server/src/shared/app/base-controller.ts | 2 +- 17 files changed, 199 insertions(+), 125 deletions(-) create mode 100644 server/scripts/approve_client.py create mode 100644 server/scripts/requirements.txt create mode 100644 server/src/migrations/Migration20210829055229.ts diff --git a/server/scripts/approve_client.py b/server/scripts/approve_client.py new file mode 100644 index 0000000..bea1f98 --- /dev/null +++ b/server/scripts/approve_client.py @@ -0,0 +1,51 @@ +from configparser import ConfigParser +from datetime import datetime +import sys +import base64 +import psycopg2 + +def approve_client(): + if(len(sys.argv)!=2): + print("Invalid number of cmd line arguments provided.") + client_id = base64.b64decode(sys.argv[1]) + with conn.cursor() as cursor: + cursor.execute("UPDATE auth_secret SET is_verified = 1 WHERE auth_secret.client_id = {client_id}".format( + client_id=client_id + )) + conn.commit() + print("Auth Secret updated") + + +def connect(): + conn = None + print("Connecting to PostgreSQL server...") + + parser = ConfigParser() + with open("db.ini") as f: + parser.read_file(f) + + keys = parser["postgresql"] + try: + conn = psycopg2.connect( + host=keys.get("host"), + database=keys.get("database"), + user=keys.get("user"), + password=keys.get("password")) + print("Connection Successful") + cursor = conn.cursor() + cursor.execute("SELECT version()") + print(cursor.fetchone()) + return conn + except (Exception, psycopg2.DatabaseError) as error: + print(error) + if conn is not None: + conn.close() + print("Connection Closed") + + +conn = connect() + +if conn is None: + exit() + +approve_client() diff --git a/server/scripts/requirements.txt b/server/scripts/requirements.txt new file mode 100644 index 0000000..010a125 --- /dev/null +++ b/server/scripts/requirements.txt @@ -0,0 +1 @@ +psycopg2==2.8.6 diff --git a/server/src/migrations/Migration20210829055229.ts b/server/src/migrations/Migration20210829055229.ts new file mode 100644 index 0000000..35ab193 --- /dev/null +++ b/server/src/migrations/Migration20210829055229.ts @@ -0,0 +1,12 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20210829055229 extends Migration { + + async up(): Promise { + this.addSql('alter table "user" drop column "access_token";'); + this.addSql('alter table "user" drop column "refresh_token";'); + + this.addSql('alter table "auth_secret" add column "decoded_redirect_uri" varchar(255) not null, add column "client_name" varchar(255) not null;'); + } + +} diff --git a/server/src/modules/users/application/use-cases/authenticate-user/authenticate-user-use-case.ts b/server/src/modules/users/application/use-cases/authenticate-user/authenticate-user-use-case.ts index 5ce1352..dc1cbe2 100644 --- a/server/src/modules/users/application/use-cases/authenticate-user/authenticate-user-use-case.ts +++ b/server/src/modules/users/application/use-cases/authenticate-user/authenticate-user-use-case.ts @@ -9,13 +9,14 @@ import { AuthenticateUserDTO } from './authenticate-user-dto' import { AuthenticateUserErrors } from './authenticate-user-errors' type AuthenticateUserUseCaseError = - AuthenticateUserErrors.AuthenticationFailedError + | AuthenticateUserErrors.AuthenticationFailedError | AppError.UnexpectedError export type AuthenticateUserUseCaseResponse = Result export class AuthenticateUserUseCase - implements UseCaseWithDTO { + implements UseCaseWithDTO +{ private userRepo: UserRepo constructor(userRepo: UserRepo) { @@ -37,14 +38,18 @@ export class AuthenticateUserUseCase const email = results[0].value const password = results[1].value - try { - const userByEmailAndPassword = await this.userRepo.getUserByUserEmailandUserPassword(email, password) - if (userByEmailAndPassword.isErr()) { - return Result.err(new AuthenticateUserErrors.AuthenticationFailedError(email.value, userByEmailAndPassword.error.message)) - } - return Result.ok(userByEmailAndPassword.value) - } catch (err) { - return Result.err(new AppError.UnexpectedError(err)) + const userByEmailAndPassword = await this.userRepo.getUserByUserEmailandUserPassword( + email, + password + ) + if (userByEmailAndPassword.isErr()) { + return Result.err( + new AuthenticateUserErrors.AuthenticationFailedError( + email.value, + userByEmailAndPassword.error.message + ) + ) } + return Result.ok(userByEmailAndPassword.value) } } diff --git a/server/src/modules/users/application/use-cases/create-user/__tests__/create-user-use-case.test.unit.ts b/server/src/modules/users/application/use-cases/create-user/__tests__/create-user-use-case.test.unit.ts index e21fe91..130149b 100644 --- a/server/src/modules/users/application/use-cases/create-user/__tests__/create-user-use-case.test.unit.ts +++ b/server/src/modules/users/application/use-cases/create-user/__tests__/create-user-use-case.test.unit.ts @@ -1,4 +1,5 @@ import { mocks } from '../../../../../../test-utils' +import httpMocks from 'node-mocks-http' import { Err, Result } from '../../../../../../shared/core/result' import { UserRepo } from '../../../../infra/repos/user-repo/user-repo' import { UserValueObjectErrors } from '../../../../domain/value-objects/errors' @@ -22,6 +23,8 @@ describe('CreateUserUseCase', () => { beforeEach(() => { createUserDTO = { + req: httpMocks.createRequest(), + res: httpMocks.createResponse(), body: { email: 'john.doe@uwaterloo.ca', password: 'secret23', @@ -31,9 +34,11 @@ describe('CreateUserUseCase', () => { test('When executed with valid DTO, should save the user and return an Ok', async () => { const mockUser = mocks.mockUser(createUserDTO.body) - jest.spyOn(userRepo, 'exists').mockResolvedValue(Result.err(new DBError.UserNotFoundError(createUserDTO.body.email))) + jest + .spyOn(userRepo, 'exists') + .mockResolvedValue(Result.err(new DBError.UserNotFoundError(createUserDTO.body.email))) jest.spyOn(userRepo, 'getUserByUserEmail').mockResolvedValue(Result.ok(mockUser)) - + const createUserResult = await createUserUseCase.execute(createUserDTO) expect(userRepo.save).toBeCalled() @@ -45,7 +50,10 @@ describe('CreateUserUseCase', () => { const createUserResult = await createUserUseCase.execute(createUserDTO) expect(createUserResult.isErr()).toBe(true) - const createUserErr = createUserResult as Err + const createUserErr = createUserResult as Err< + CreateUserSuccess, + UserValueObjectErrors.InvalidEmail + > expect(createUserErr.error instanceof UserValueObjectErrors.InvalidEmail).toBe(true) }) @@ -54,7 +62,10 @@ describe('CreateUserUseCase', () => { const createUserResult = await createUserUseCase.execute(createUserDTO) expect(createUserResult.isErr()).toBe(true) - const createUserErr = createUserResult as Err + const createUserErr = createUserResult as Err< + CreateUserSuccess, + UserValueObjectErrors.InvalidSecretValue + > expect(createUserErr.error instanceof UserValueObjectErrors.InvalidSecretValue).toBe(true) }) @@ -63,7 +74,10 @@ describe('CreateUserUseCase', () => { const createUserResult = await createUserUseCase.execute(createUserDTO) expect(createUserResult.isErr()).toBe(true) - const createUserErr = createUserResult as Err + const createUserErr = createUserResult as Err< + CreateUserSuccess, + CreateUserErrors.EmailAlreadyExistsError + > expect(createUserErr.error instanceof CreateUserErrors.EmailAlreadyExistsError).toBe(true) }) }) diff --git a/server/src/modules/users/application/use-cases/create-user/create-user-controller.ts b/server/src/modules/users/application/use-cases/create-user/create-user-controller.ts index a69b664..2735d50 100644 --- a/server/src/modules/users/application/use-cases/create-user/create-user-controller.ts +++ b/server/src/modules/users/application/use-cases/create-user/create-user-controller.ts @@ -12,15 +12,20 @@ export class CreateUserController extends ControllerWithDTO { super(useCase) } - buildDTO(req: express.Request): Result> { + buildDTO( + req: express.Request, + res: express.Response + ): Result> { let params: any = req.params - if(Object.keys(req.params).length === 0){ + if (Object.keys(req.params).length === 0) { params = undefined } const errs: Array = [] const compiledRequest = { + req, + res, body: req.body, - params + params, } const bodyResult = this.validate(compiledRequest, createUserDTOSchema) if (bodyResult.isOk()) { @@ -35,9 +40,9 @@ export class CreateUserController extends ControllerWithDTO { async executeImpl(dto: CreateUserDTO, res: Res): Promise { try { const result = await this.useCase.execute(dto) - + if (result.isOk()) { - if('user' in result.value){ + if ('user' in result.value) { return this.ok(res, result.value) } else { return this.redirect(res, result.value.redirectUrl, result.value.redirectParams) diff --git a/server/src/modules/users/application/use-cases/create-user/create-user-dto.ts b/server/src/modules/users/application/use-cases/create-user/create-user-dto.ts index fc771dc..ced33fd 100644 --- a/server/src/modules/users/application/use-cases/create-user/create-user-dto.ts +++ b/server/src/modules/users/application/use-cases/create-user/create-user-dto.ts @@ -1,3 +1,4 @@ +import express from 'express' import Joi from 'joi' export const SUPPORTED_OPEN_ID_RESPONSE_TYPES = ['code'] @@ -16,6 +17,8 @@ export interface CreateUserDTOParams { } export interface CreateUserDTO { + req: express.Request + res: express.Response body: CreateUserDTOBody params?: CreateUserDTOParams } @@ -26,6 +29,8 @@ export const createUserDTOBodySchema = Joi.object({ }).options({ abortEarly: false }) export const createUserDTOSchema = Joi.object({ + req: Joi.object().required(), + res: Joi.object().required(), body: createUserDTOBodySchema.required(), params: Joi.object().optional(), }).options({ abortEarly: false }) diff --git a/server/src/modules/users/application/use-cases/create-user/create-user-use-case.ts b/server/src/modules/users/application/use-cases/create-user/create-user-use-case.ts index b2d620a..d9fd9e3 100644 --- a/server/src/modules/users/application/use-cases/create-user/create-user-use-case.ts +++ b/server/src/modules/users/application/use-cases/create-user/create-user-use-case.ts @@ -8,7 +8,10 @@ import { UserPassword } from '../../../domain/value-objects/user-password' import { UserRepo } from '../../../infra/repos/user-repo/user-repo' import { CreateUserDTO } from './create-user-dto' import { CreateUserErrors } from './create-user-errors' -import { UserAuthHandler } from '../../../../../shared/auth/user-auth-handler' +import { + UserAuthHandler, + UserAuthHandlerLoginResponse, +} from '../../../../../shared/auth/user-auth-handler' import { ParamList, ParamPair } from '../../../../../shared/app/param-list' import { UserDTO } from '../../../mappers/user-dto' import { UserMap } from '../../../mappers/user-map' @@ -34,7 +37,7 @@ export type CreateUserSuccess = CreateUserClientRequestSuccess | CreateUserNonCl export type CreateUserUseCaseResponse = Result export class CreateUserUseCase implements UseCaseWithDTO { - constructor(private authHandler: UserAuthHandler, private userRepo: UserRepo) {} + constructor(private userAuthHandler: UserAuthHandler, private userRepo: UserRepo) {} async execute(dto: CreateUserDTO): Promise { const emailResult = UserEmail.create(dto.body.email) @@ -51,46 +54,47 @@ export class CreateUserUseCase implements UseCaseWithDTO new ParamPair(paramPair[0], paramPair[1])) + ) + const loginUserSuccessResponse: CreateUserSuccess = { + redirectParams: redirectParams, + redirectUrl: `${process.env.PUBLIC_HOST}/authorize`, } - const authHandlerResponse = await this.authHandler.create(userAuthHandlerCreateOptions) - - if (authHandlerResponse.isErr()) return authHandlerResponse - - return Result.ok(createUserSuccessResponse) + return Result.ok(loginUserSuccessResponse) } - } catch (err) { - return Result.err(new AppError.UnexpectedError(err)) + } else { + return Result.ok({ + user: UserMap.toDTO(updatedUser.value), + }) } } } diff --git a/server/src/modules/users/application/use-cases/discover-sp/discover-sp-controller.ts b/server/src/modules/users/application/use-cases/discover-sp/discover-sp-controller.ts index ad5ba85..e48ad60 100644 --- a/server/src/modules/users/application/use-cases/discover-sp/discover-sp-controller.ts +++ b/server/src/modules/users/application/use-cases/discover-sp/discover-sp-controller.ts @@ -13,11 +13,7 @@ export class DiscoverSPController extends ControllerWithDTO { buildDTO(req: express.Request): Result> { const errs: Array = [] - const compiledValidationBody = { - authHeader: req.headers.authorization, - params: req.params, - } - const bodyResult = this.validate(compiledValidationBody, discoverSPDTOSchema) + const bodyResult = this.validate(req.body, discoverSPDTOSchema) if (bodyResult.isOk()) { const body: DiscoverSPDTO = bodyResult.value return Result.ok(body) diff --git a/server/src/modules/users/application/use-cases/discover-sp/discover-sp-use-case.ts b/server/src/modules/users/application/use-cases/discover-sp/discover-sp-use-case.ts index 98859c7..353ce3c 100644 --- a/server/src/modules/users/application/use-cases/discover-sp/discover-sp-use-case.ts +++ b/server/src/modules/users/application/use-cases/discover-sp/discover-sp-use-case.ts @@ -21,11 +21,7 @@ export interface DiscoverSPSuccess { export type DiscoverSPUseCaseResponse = Result export class DiscoverSPUseCase implements UseCaseWithDTO { - private authSecretRepo - - constructor(authSecretRepo: AuthSecretRepo) { - this.authSecretRepo = authSecretRepo - } + constructor(private authSecretRepo: AuthSecretRepo) {} async execute(dto: DiscoverSPDTO): Promise { const authSecretExists = await this.authSecretRepo.clientNameExists(dto.client_name) @@ -58,8 +54,8 @@ export class DiscoverSPUseCase implements UseCaseWithDTO { - private authCodeRepo - private authSecretRepo - - constructor(authCodeRepo: AuthCodeRepo, authSecretRepo: AuthSecretRepo) { - this.authCodeRepo = authCodeRepo - this.authSecretRepo = authSecretRepo - } + constructor(private authCodeRepo: AuthCodeRepo, private authSecretRepo: AuthSecretRepo) {} async execute(dto: GetTokenDTO): Promise { const authHeader = dto.authHeader diff --git a/server/src/modules/users/application/use-cases/get-user/get-user-use-case.ts b/server/src/modules/users/application/use-cases/get-user/get-user-use-case.ts index deef69f..754905a 100644 --- a/server/src/modules/users/application/use-cases/get-user/get-user-use-case.ts +++ b/server/src/modules/users/application/use-cases/get-user/get-user-use-case.ts @@ -6,14 +6,11 @@ import { UserRepo } from '../../../infra/repos/user-repo/user-repo' import { GetUserDTO } from './get-user-dto' import { GetUserErrors } from './get-user-errors' -export type GetUserUseCaseError = - GetUserErrors.GetUserByIdFailedError - | AppError.UnexpectedError +export type GetUserUseCaseError = GetUserErrors.GetUserByIdFailedError | AppError.UnexpectedError export type GetUserUseCaseResponse = Result -export class GetUserUseCase - implements UseCaseWithDTO { +export class GetUserUseCase implements UseCaseWithDTO { private userRepo: UserRepo constructor(userRepo: UserRepo) { @@ -22,14 +19,9 @@ export class GetUserUseCase async execute(dto: GetUserDTO): Promise { const userId = dto.userId - try { - const userById = await this.userRepo.getUserByUserId(userId) - if (userById.isErr()) - return Result.err(new GetUserErrors.GetUserByIdFailedError(userId)) + const userById = await this.userRepo.getUserByUserId(userId) + if (userById.isErr()) return Result.err(new GetUserErrors.GetUserByIdFailedError(userId)) - return Result.ok(userById.value) - } catch (err) { - return Result.err(new AppError.UnexpectedError(err)) - } + return Result.ok(userById.value) } } diff --git a/server/src/modules/users/domain/entities/user/__tests__/user.test.unit.ts b/server/src/modules/users/domain/entities/user/__tests__/user.test.unit.ts index 3e96628..a161d4c 100644 --- a/server/src/modules/users/domain/entities/user/__tests__/user.test.unit.ts +++ b/server/src/modules/users/domain/entities/user/__tests__/user.test.unit.ts @@ -1,7 +1,6 @@ import { DomainEvents } from '../../../../../../shared/domain/events/domain-events' import { UserCreated } from '../../../events/user-created' import { UserDeleted } from '../../../events/user-deleted' -import { UserLoggedIn } from '../../../events/user-logged-in' import { UserEmail } from '../../../value-objects/user-email' import { UserPassword } from '../../../value-objects/user-password' import { User } from '../user' diff --git a/server/src/modules/users/infra/http/routes/user-router.ts b/server/src/modules/users/infra/http/routes/user-router.ts index f862b88..98b6439 100644 --- a/server/src/modules/users/infra/http/routes/user-router.ts +++ b/server/src/modules/users/infra/http/routes/user-router.ts @@ -11,17 +11,15 @@ class UserRouter { static using(controllers: Controllers): Router { const userRouter = Router() - userRouter.post('/', (req, res): void => { - controllers.createUser.execute(req, res) - }) - userRouter.post('/login', (req, res): void => { controllers.loginUser.execute(req, res) }) + userRouter.post('/create', (req, res) => controllers.createUser.execute(req, res)) + userRouter.get('/authorize', (req, res) => controllers.authorizeUser.execute(req, res)) + userRouter.get('/token', (req, res) => controllers.getToken.execute(req, res)) + userRouter.post('/discovery', (req, res) => controllers.discoverSP.execute(req, res)) - userRouter.get('/protected', (req, res) => controllers.protectedUser.execute(req, res)) userRouter.use(limiter) - return userRouter } } diff --git a/server/src/modules/users/mappers/user-map.ts b/server/src/modules/users/mappers/user-map.ts index 4ae1228..2940739 100644 --- a/server/src/modules/users/mappers/user-map.ts +++ b/server/src/modules/users/mappers/user-map.ts @@ -31,8 +31,6 @@ export class UserMap { email, password, emailVerified: userEntity.emailVerified, - accessToken: userEntity.accessToken, - refreshToken: userEntity.refreshToken, isDeleted: userEntity.isDeleted, lastLogin: userEntity.lastLogin, }, @@ -55,8 +53,6 @@ export class UserMap { if (user.isEmailVerified !== undefined) userEntity.emailVerified = user.isEmailVerified if (user.isDeleted !== undefined) userEntity.isDeleted = user.isDeleted - userEntity.accessToken = user.accessToken - userEntity.refreshToken = user.refreshToken userEntity.lastLogin = user.lastLogin return userEntity diff --git a/server/src/setup/http/express/basic-web-server.ts b/server/src/setup/http/express/basic-web-server.ts index 935b19f..df7ea92 100644 --- a/server/src/setup/http/express/basic-web-server.ts +++ b/server/src/setup/http/express/basic-web-server.ts @@ -9,50 +9,56 @@ import { MikroORM } from '../../database' import { APIRouter, WebServer } from './types' import { Controllers, UseCases } from '../../application/types' - interface BasicWebServerOptions { mikroORM?: MikroORM } -const setupBasicWebServer = (apiRouter: APIRouter, _controllers: Controllers, useCases: UseCases, options: BasicWebServerOptions): WebServer => { +const setupBasicWebServer = ( + apiRouter: APIRouter, + _controllers: Controllers, + useCases: UseCases, + options: BasicWebServerOptions +): WebServer => { const server = express() server.use(cors()) server.use(CookieParser()) server.use(express.json()) - server.use(session({ secret: `${process.env.EXPRESS_SESSION_SECRET}` })); + server.use(session({ secret: `${process.env.EXPRESS_SESSION_SECRET}` })) const entityManager = options?.mikroORM?.em if (entityManager !== undefined) { server.use((_req, _res, next) => RequestContext.create(entityManager, next)) } - server.use(passport.initialize()); - server.use(passport.session()); - passport.use(new LocalStrategy.Strategy({ - usernameField: 'email', - passwordField: 'password', - }, function (email, password, cb) { - useCases.authUser.execute({email, password}) - .then(result => { - return cb(null, result); - }) + server.use(passport.initialize()) + server.use(passport.session()) + passport.use( + new LocalStrategy.Strategy( + { + usernameField: 'email', + passwordField: 'password', + }, + function (email, password, cb) { + useCases.authenticateUser.execute({ email, password }).then((result) => { + return cb(null, result) + }) } - )); + ) + ) + + passport.serializeUser(function (user: any, done) { + done(null, user._id.value) + }) - passport.serializeUser(function(user: any, done) { - done(null, user._id.value); - }); - - passport.deserializeUser(function(userId: string, cb) { - useCases.getUser.execute({userId}) - .then(result => { - if(result.isOk()){ - cb(null, result.value); + passport.deserializeUser(function (userId: string, cb) { + useCases.getUser.execute({ userId }).then((result) => { + if (result.isOk()) { + cb(null, result.value) } else { cb(result.error, null) } }) - }); + }) server.use('/api', apiRouter) diff --git a/server/src/shared/app/base-controller.ts b/server/src/shared/app/base-controller.ts index ea85514..157fc20 100644 --- a/server/src/shared/app/base-controller.ts +++ b/server/src/shared/app/base-controller.ts @@ -20,7 +20,7 @@ export abstract class BaseController { return res } - public fail(res: Res, error: Error | string): Res { + public fail(res: Res, error: any): Res { console.log(error) return res.status(500).json({ message: error.toString(), From 07e68d3f3e5cae114580db280084a2b3842e4b32 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 29 Aug 2021 02:27:24 -0400 Subject: [PATCH 3/3] Address first batch of review comments --- .../authorize-user-controller.test.unit.ts | 25 ++++++------- .../create-user-controller.test.unit.ts | 17 +++++---- .../create-user/create-user-use-case.ts | 2 +- .../__tests__/discover-sp.test.unit.ts | 18 +++++----- .../get-token-controller.test.unit.ts | 12 +++---- .../login-user-controller.test.unit.ts | 35 +++++++++---------- .../domain/value-objects/auth-code-string.ts | 20 +++++------ 7 files changed, 58 insertions(+), 71 deletions(-) diff --git a/server/src/modules/users/application/use-cases/authorize-user/__tests__/authorize-user-controller.test.unit.ts b/server/src/modules/users/application/use-cases/authorize-user/__tests__/authorize-user-controller.test.unit.ts index 5c29676..f0b11ac 100644 --- a/server/src/modules/users/application/use-cases/authorize-user/__tests__/authorize-user-controller.test.unit.ts +++ b/server/src/modules/users/application/use-cases/authorize-user/__tests__/authorize-user-controller.test.unit.ts @@ -9,24 +9,23 @@ import { AuthorizeUserController } from '../authorize-user-controller' import { mocks } from '../../../../../../test-utils' import { ParamList, ParamPair } from '../../../../../../shared/app/param-list' -jest.mock('../authorize-user-use-case') -jest.mock('../authorize-user-use-case') - describe('AuthorizeUserController', () => { let authorizeUserDTO: AuthorizeUserDTO + let authorizeUserUseCase: AuthorizeUserUseCase let authorizeUserController: AuthorizeUserController let mockResponse: express.Response beforeAll(async () => { const authorizeUser = await mocks.mockAuthorizeUser() authorizeUserController = authorizeUser.authorizeUserController + authorizeUserUseCase = authorizeUser.authorizeUserUseCase mockResponse = httpMocks.createResponse() authorizeUserDTO = { req: httpMocks.createRequest(), params: { - client_id: 'i291u92jksdn', + client_id: '6a88757bceaddaf03540dbd891dfb828', response_type: 'code', - redirect_uri: 'www.loolabs.com', + redirect_uri: 'www.loolabs.org', scope: 'openid', }, } @@ -35,11 +34,9 @@ describe('AuthorizeUserController', () => { test('When the AuthorizeUserUseCase returns Ok, the AuthorizeUserController returns 302 Redirect', async () => { const useCaseResolvedValue = { redirectParams: new ParamList([new ParamPair('type', 'test')]), - redirectUrl: 'test@loolabs.com', + redirectUrl: 'www.loolabs.org', } - jest - .spyOn(AuthorizeUserUseCase.prototype, 'execute') - .mockResolvedValue(Result.ok(useCaseResolvedValue)) + jest.spyOn(authorizeUserUseCase, 'execute').mockResolvedValue(Result.ok(useCaseResolvedValue)) const result = await authorizeUserController.executeImpl(authorizeUserDTO, mockResponse) expect(result.statusCode).toBe(302) @@ -47,7 +44,7 @@ describe('AuthorizeUserController', () => { test('When the AuthorizeUserUseCase returns AuthorizeUserErrors.InvalidRequestParameters, AuthorizeUserController returns 400 Bad Request', async () => { jest - .spyOn(AuthorizeUserUseCase.prototype, 'execute') + .spyOn(authorizeUserUseCase, 'execute') .mockResolvedValue(Result.err(new AuthorizeUserErrors.InvalidRequestParameters())) const result = await authorizeUserController.executeImpl(authorizeUserDTO, mockResponse) @@ -58,11 +55,9 @@ describe('AuthorizeUserController', () => { test('When the AuthorizeUserUseCase returns AuthorizeUserErrors.UserNotAuthenticated, AuthorizeUserController returns 302 Redirect', async () => { const useCaseErrorValue = { redirectParams: new ParamList([new ParamPair('type', 'test')]), - redirectUrl: 'test@loolabs.com', + redirectUrl: 'www.loolabs.org', } - jest - .spyOn(AuthorizeUserUseCase.prototype, 'execute') - .mockResolvedValue(Result.err(useCaseErrorValue)) + jest.spyOn(authorizeUserUseCase, 'execute').mockResolvedValue(Result.err(useCaseErrorValue)) const result = await authorizeUserController.executeImpl(authorizeUserDTO, mockResponse) expect(result.statusCode).toBe(302) @@ -70,7 +65,7 @@ describe('AuthorizeUserController', () => { test('When the AuthorizeUserUseCase returns AppError.UnexpectedError, AuthorizeUserController returns 500 Internal Server Error', async () => { jest - .spyOn(AuthorizeUserUseCase.prototype, 'execute') + .spyOn(authorizeUserUseCase, 'execute') .mockResolvedValue(Result.err(new AppError.UnexpectedError('Unexpected error'))) const result = await authorizeUserController.executeImpl(authorizeUserDTO, mockResponse) diff --git a/server/src/modules/users/application/use-cases/create-user/__tests__/create-user-controller.test.unit.ts b/server/src/modules/users/application/use-cases/create-user/__tests__/create-user-controller.test.unit.ts index fb0b537..31fbf4c 100644 --- a/server/src/modules/users/application/use-cases/create-user/__tests__/create-user-controller.test.unit.ts +++ b/server/src/modules/users/application/use-cases/create-user/__tests__/create-user-controller.test.unit.ts @@ -8,18 +8,17 @@ import { CreateUserErrors } from '../create-user-errors' import { CreateUserSuccess, CreateUserUseCase } from '../create-user-use-case' import { UserMap } from '../../../../mappers/user-map' -// TODO: how to show developer these mocks are necessary when building a controller? aka must be synced with buildController() -jest.mock('../create-user-use-case') - describe('CreateUserController', () => { const createUserDTO: CreateUserDTOBody = { email: 'john.doe@uwaterloo.ca', password: 'secret23', } let createUserController: CreateUserController + let createUserUseCase: CreateUserUseCase beforeAll(async () => { const createUser = await mocks.mockCreateUser() createUserController = createUser.createUserController + createUserUseCase = createUser.createUserUseCase }) test('When the CreateUserUseCase returns Ok, the CreateUserController returns 200 OK', async () => { @@ -28,8 +27,8 @@ describe('CreateUserController', () => { const useCaseResolvedValue: CreateUserSuccess = { user: UserMap.toDTO(user), } - - jest.spyOn(CreateUserUseCase.prototype, 'execute').mockResolvedValue(Result.ok(useCaseResolvedValue)) + + jest.spyOn(createUserUseCase, 'execute').mockResolvedValue(Result.ok(useCaseResolvedValue)) const { req, res } = mocks.mockHandlerParams(createUserDTO) await createUserController.execute(req, res) @@ -38,7 +37,7 @@ describe('CreateUserController', () => { test('When the CreateUserUseCase returns UserValueObjectErrors.InvalidEmail, CreateUserController returns 400 Bad Request', async () => { jest - .spyOn(CreateUserUseCase.prototype, 'execute') + .spyOn(createUserUseCase, 'execute') .mockResolvedValue(Result.err(new UserValueObjectErrors.InvalidEmail(createUserDTO.email))) const { req, res } = mocks.mockHandlerParams(createUserDTO) @@ -57,7 +56,7 @@ describe('CreateUserController', () => { test('When the CreateUserUseCase returns UserValueObjectErrors.InvalidSecretValue, CreateUserController returns 400 Bad Request', async () => { jest - .spyOn(CreateUserUseCase.prototype, 'execute') + .spyOn(createUserUseCase, 'execute') .mockResolvedValue( Result.err(new UserValueObjectErrors.InvalidSecretValue(createUserDTO.password)) ) @@ -69,7 +68,7 @@ describe('CreateUserController', () => { test('When the CreateUserUseCase returns CreateUserErrors.EmailAlreadyExistsError, CreateUserController returns 409 Conflict', async () => { jest - .spyOn(CreateUserUseCase.prototype, 'execute') + .spyOn(createUserUseCase, 'execute') .mockResolvedValue( Result.err(new CreateUserErrors.EmailAlreadyExistsError(createUserDTO.email)) ) @@ -81,7 +80,7 @@ describe('CreateUserController', () => { test('When the CreateUserUseCase returns AppError.UnexpectedError, CreateUserController returns 500 Internal Server Error', async () => { jest - .spyOn(CreateUserUseCase.prototype, 'execute') + .spyOn(createUserUseCase, 'execute') .mockResolvedValue(Result.err(new AppError.UnexpectedError('Unexpected error'))) const { req, res } = mocks.mockHandlerParams(createUserDTO) diff --git a/server/src/modules/users/application/use-cases/create-user/create-user-use-case.ts b/server/src/modules/users/application/use-cases/create-user/create-user-use-case.ts index d9fd9e3..039a106 100644 --- a/server/src/modules/users/application/use-cases/create-user/create-user-use-case.ts +++ b/server/src/modules/users/application/use-cases/create-user/create-user-use-case.ts @@ -56,7 +56,7 @@ export class CreateUserUseCase implements UseCaseWithDTO { let discoverSPDTO: DiscoverSPDTO let discoverSPController: DiscoverSPController + let discoverSPUseCase: DiscoverSPUseCase let mockResponse: express.Response beforeAll(async () => { const discoverSP = await mocks.mockDiscoverSP() discoverSPController = discoverSP.discoverSPController + discoverSPUseCase = discoverSP.discoverSPUseCase mockResponse = httpMocks.createResponse() discoverSPDTO = { client_name: 'testclient', - redirect_uri: 'loolabs.com/cb', + redirect_uri: 'www.loolabs.org/cb', } }) test('When the DiscoverSPUseCase returns Ok, the DiscoverSPController returns 200 OK', async () => { const useCaseResolvedValue = { - clientId: '232039sdkljkasldj', - clientSecret: '65039sdasd123kljkasldj', + clientId: 'fcc89db61d93607afbb7008df9197570', + clientSecret: '81281dd17eafeda8f34b2192aff22f2a', } - jest - .spyOn(DiscoverSPUseCase.prototype, 'execute') - .mockResolvedValue(Result.ok(useCaseResolvedValue)) + jest.spyOn(discoverSPUseCase, 'execute').mockResolvedValue(Result.ok(useCaseResolvedValue)) const result = await discoverSPController.executeImpl(discoverSPDTO, mockResponse) @@ -41,7 +39,7 @@ describe('DiscoverSPController', () => { test('When the DiscoverSPUseCase returns DiscoverSPErrors, DiscoverSPController returns 400 Bad Request', async () => { jest - .spyOn(DiscoverSPUseCase.prototype, 'execute') + .spyOn(discoverSPUseCase, 'execute') .mockResolvedValue( Result.err(new DiscoverSPErrors.ClientNameAlreadyInUse(discoverSPDTO.client_name)) ) @@ -53,7 +51,7 @@ describe('DiscoverSPController', () => { test('When the DiscoverSPUseCase returns AppError.UnexpectedError, DiscoverSPController returns 500 Internal Server Error', async () => { jest - .spyOn(DiscoverSPUseCase.prototype, 'execute') + .spyOn(discoverSPUseCase, 'execute') .mockResolvedValue(Result.err(new AppError.UnexpectedError('Unexpected error'))) const result = await discoverSPController.executeImpl(discoverSPDTO, mockResponse) diff --git a/server/src/modules/users/application/use-cases/get-token/__tests__/get-token-controller.test.unit.ts b/server/src/modules/users/application/use-cases/get-token/__tests__/get-token-controller.test.unit.ts index 2b47773..6edc7ff 100644 --- a/server/src/modules/users/application/use-cases/get-token/__tests__/get-token-controller.test.unit.ts +++ b/server/src/modules/users/application/use-cases/get-token/__tests__/get-token-controller.test.unit.ts @@ -8,16 +8,16 @@ import { GetTokenUseCase } from '../get-token-use-case' import { GetTokenController } from '../get-token-controller' import { mocks } from '../../../../../../test-utils' -jest.mock('../get-token-use-case') - describe('GetTokenController', () => { let getTokenDTO: GetTokenDTO let getTokenController: GetTokenController + let getTokenUseCase: GetTokenUseCase let mockResponse: express.Response beforeAll(async () => { const getToken = await mocks.mockGetToken() getTokenController = getToken.getTokenController + getTokenUseCase = getToken.getTokenUseCase mockResponse = httpMocks.createResponse() getTokenDTO = { authHeader: 'asdklasdoladoassald', @@ -31,9 +31,7 @@ describe('GetTokenController', () => { test('When the GetTokenUseCase returns Ok, the GetTokenController returns 200 OK', async () => { const useCaseResolvedValue = 'asdklasdhnjkjkewhf' - jest - .spyOn(GetTokenUseCase.prototype, 'execute') - .mockResolvedValue(Result.ok(useCaseResolvedValue)) + jest.spyOn(getTokenUseCase, 'execute').mockResolvedValue(Result.ok(useCaseResolvedValue)) const result = await getTokenController.executeImpl(getTokenDTO, mockResponse) @@ -42,7 +40,7 @@ describe('GetTokenController', () => { test('When the GetTokenUseCase returns GetTokenErrors.InvalidCredentials, GetTokenController returns 400 Bad Request', async () => { jest - .spyOn(GetTokenUseCase.prototype, 'execute') + .spyOn(getTokenUseCase, 'execute') .mockResolvedValue(Result.err(new GetTokenErrors.InvalidCredentials())) const result = await getTokenController.executeImpl(getTokenDTO, mockResponse) @@ -52,7 +50,7 @@ describe('GetTokenController', () => { test('When the GetTokenUseCase returns AppError.UnexpectedError, GetTokenController returns 500 Internal Server Error', async () => { jest - .spyOn(GetTokenUseCase.prototype, 'execute') + .spyOn(getTokenUseCase, 'execute') .mockResolvedValue(Result.err(new AppError.UnexpectedError('Unexpected error'))) const result = await getTokenController.executeImpl(getTokenDTO, mockResponse) diff --git a/server/src/modules/users/application/use-cases/login-user/__tests__/login-user-controller.test.unit.ts b/server/src/modules/users/application/use-cases/login-user/__tests__/login-user-controller.test.unit.ts index 49d82ff..ed8f303 100644 --- a/server/src/modules/users/application/use-cases/login-user/__tests__/login-user-controller.test.unit.ts +++ b/server/src/modules/users/application/use-cases/login-user/__tests__/login-user-controller.test.unit.ts @@ -12,28 +12,29 @@ import { CreateUserDTOBody } from '../../create-user/create-user-dto' // TODO: how to show developer these mocks are necessary when building a controller? aka must be synced with buildController() jest.mock('../../../../infra/repos/user-repo/implementations/mikro-user-repo') -jest.mock('../login-user-use-case') describe('LoginUserController', () => { let loginUserDTO: LoginUserDTO let userDTO: CreateUserDTOBody let loginUserController: LoginUserController + let loginUserUseCase: LoginUserUseCase beforeAll(async () => { const loginUser = await mocks.mockLoginUser() loginUserController = loginUser.loginUserController + loginUserUseCase = loginUser.loginUserUseCase }) beforeEach(() => { - userDTO = { + ;(userDTO = { email: 'loolabs@uwaterloo.ca', password: 'password', - }, - loginUserDTO = { - req: httpMocks.createRequest(), - res: httpMocks.createResponse(), - body: userDTO, - } + }), + (loginUserDTO = { + req: httpMocks.createRequest(), + res: httpMocks.createResponse(), + body: userDTO, + }) }) test('When the LoginUserUseCase returns Ok, the LoginUserController returns 200 OK', async () => { @@ -41,7 +42,7 @@ describe('LoginUserController', () => { const useCaseResolvedValue = { user: UserMap.toDTO(user), } - jest.spyOn(LoginUserUseCase.prototype, 'execute').mockResolvedValue(Result.ok(useCaseResolvedValue)) + jest.spyOn(loginUserUseCase, 'execute').mockResolvedValue(Result.ok(useCaseResolvedValue)) const result = await loginUserController.executeImpl(loginUserDTO, loginUserDTO.res) @@ -50,7 +51,7 @@ describe('LoginUserController', () => { test('When the LoginUserUseCase returns UserValueObjectErrors.InvalidEmail, LoginUserController returns 400 Bad Request', async () => { jest - .spyOn(LoginUserUseCase.prototype, 'execute') + .spyOn(loginUserUseCase, 'execute') .mockResolvedValue(Result.err(new UserValueObjectErrors.InvalidEmail(userDTO.email))) const result = await loginUserController.executeImpl(loginUserDTO, loginUserDTO.res) @@ -61,10 +62,8 @@ describe('LoginUserController', () => { test('When the LoginUserUseCase returns UserValueObjectErrors.InvalidSecretValue, LoginUserController returns 400 Bad Request', async () => { const mockResponse = httpMocks.createResponse() jest - .spyOn(LoginUserUseCase.prototype, 'execute') - .mockResolvedValue( - Result.err(new UserValueObjectErrors.InvalidSecretValue(userDTO.password)) - ) + .spyOn(loginUserUseCase, 'execute') + .mockResolvedValue(Result.err(new UserValueObjectErrors.InvalidSecretValue(userDTO.password))) const result = await loginUserController.executeImpl(loginUserDTO, mockResponse) @@ -73,10 +72,8 @@ describe('LoginUserController', () => { test('When the LoginUserUseCase returns LoginUserErrors.IncorrectPasswordError, LoginUserController returns 400 Unauthorized', async () => { jest - .spyOn(LoginUserUseCase.prototype, 'execute') - .mockResolvedValue( - Result.err(new LoginUserErrors.IncorrectPasswordError()) - ) + .spyOn(loginUserUseCase, 'execute') + .mockResolvedValue(Result.err(new LoginUserErrors.IncorrectPasswordError())) const result = await loginUserController.executeImpl(loginUserDTO, loginUserDTO.res) @@ -85,7 +82,7 @@ describe('LoginUserController', () => { test('When the LoginUserUseCase returns AppError.UnexpectedError, LoginUserController returns 500 Internal Server Error', async () => { jest - .spyOn(LoginUserUseCase.prototype, 'execute') + .spyOn(loginUserUseCase, 'execute') .mockResolvedValue(Result.err(new AppError.UnexpectedError('Unexpected error'))) const result = await loginUserController.executeImpl(loginUserDTO, loginUserDTO.res) diff --git a/server/src/modules/users/domain/value-objects/auth-code-string.ts b/server/src/modules/users/domain/value-objects/auth-code-string.ts index 2359aad..56d79e0 100644 --- a/server/src/modules/users/domain/value-objects/auth-code-string.ts +++ b/server/src/modules/users/domain/value-objects/auth-code-string.ts @@ -1,24 +1,24 @@ import crypto from 'crypto' +export const getRandom256BitHexCode = () => { + /*motivation for a 256 bit (= 32 byte) cryptographic key can be found here + https://www.geeksforgeeks.org/node-js-crypto-randombytes-method/ + */ + const authCodeBuffer = crypto.randomBytes(32) + return authCodeBuffer.toString('hex') +} + export class AuthCodeString { private value: string public constructor(hashedValue?: string) { - if (hashedValue) { + if (hashedValue !== undefined) { this.value = hashedValue } else { - this.value = this.getRandomCode() + this.value = getRandom256BitHexCode() } } - private getRandomCode() { - /*motivation for a 256 bit (= 32 byte) cryptographic key can be found here - https://www.geeksforgeeks.org/node-js-crypto-randombytes-method/ - */ - const authCodeBuffer = crypto.randomBytes(32) - return authCodeBuffer.toString('hex') - } - public getValue() { return this.value }