Skip to content

Commit

Permalink
Merge pull request #109 from skgndi12/feature/issue-88/implement-issu…
Browse files Browse the repository at this point in the history
…e-passport

[#88] Implement issue passport
  • Loading branch information
skgndi12 authored Jan 11, 2024
2 parents 77f8196 + b4eb854 commit 45487ef
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 31 deletions.
46 changes: 40 additions & 6 deletions api/generate/openapi.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,25 @@
"Test"
],
"summary": "Authorization header required",
"security": [
{
"jwt": []
}
],
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {}
"schema": {
"$ref": "#/components/schemas/AuthRequiredResponse"
}
}
}
},
"default": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HttpErrorResponse"
}
}
}
}
Expand Down Expand Up @@ -143,6 +150,33 @@
"additionalProperties": {
"type": "string"
}
},
"AuthRequiredResponse": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"message"
]
},
"HttpErrorResponse": {
"type": "object",
"properties": {
"messages": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
"required": [
"messages"
]
}
},
"securitySchemes": {
Expand Down
31 changes: 31 additions & 0 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/config": "^3.3.0",
"@types/convict-format-with-validator": "^6.0.2",
"@types/cookie-parser": "^1.4.6",
"@types/express": "^4.17.17",
"@types/express-serve-static-core": "^4.17.35",
"@types/jest": "^29.5.1",
Expand All @@ -55,6 +56,7 @@
"@prisma/client": "^5.5.2",
"axios": "^1.6.3",
"config": "^3.3.9",
"cookie-parser": "^1.4.6",
"crockford-base32": "^2.0.0",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
Expand Down
3 changes: 2 additions & 1 deletion api/src/config/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export function buildHttpConfig(config: Config): HttpConfig {
return {
env: config.env,
host: config.http.host,
port: config.http.port
port: config.http.port,
cookieExpirationHours: config.jwt.expirationHour
};
}

Expand Down
53 changes: 50 additions & 3 deletions api/src/controller/http/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import 'express-async-errors';
import { HttpError as ValidationError } from 'express-openapi-validator/dist/framework/types';
import { Logger } from 'winston';

import { CustomError, HttpErrorCode } from '@src/error/errors';
import { JwtHandler } from '@src/core/ports/jwt.handler';
import { AppErrorCode, CustomError, HttpErrorCode } from '@src/error/errors';

import { HttpErrorResponse } from '@controller/http/response';
import { idTokenCookieName } from '@controller/http/types';

export class Middleware {
constructor(public logger: Logger) {}
constructor(
private readonly logger: Logger,
private readonly jwtHandler: JwtHandler
) {}

public accessLog = (req: Request, res: Response, next: NextFunction) => {
const start = new Date().getTime();
Expand All @@ -33,7 +38,7 @@ export class Middleware {
};

public handleError = (
err: Error,
err: unknown,
req: Request,
res: Response<HttpErrorResponse>,
next: NextFunction
Expand Down Expand Up @@ -70,6 +75,17 @@ export class Middleware {
);
};

public issuePassport = (req: Request, res: Response, next: NextFunction) => {
let passport = this.issuePassportFromHeader(req);

if (passport === null) {
passport = this.issuePassportFromCookie(req);
}

res.locals.passport = passport;
next();
};

private convertValidationErrorToCustomError = (
err: ValidationError
): CustomError => {
Expand Down Expand Up @@ -171,4 +187,35 @@ export class Middleware {
messages: err.messages
});
};

private issuePassportFromHeader = (req: Request) => {
const authHeader = req.get('Authorization');
if (!authHeader) {
return null;
}

const splittedAuthHeader = authHeader.split(' ', 2);
if (splittedAuthHeader.length !== 2 || splittedAuthHeader[0] !== 'Bearer') {
throw new CustomError({
code: AppErrorCode.UNAUTHENTICATED,
message: 'Authorization header must be in Bearer format',
context: { splittedAuthHeader }
});
}

return this.jwtHandler.verifyAppIdToken(splittedAuthHeader[1]);
};

private issuePassportFromCookie = (req: Request) => {
const cookie = req.cookies[idTokenCookieName];
if (!cookie) {
throw new CustomError({
code: AppErrorCode.UNAUTHENTICATED,
message: 'cookie does not exist',
context: { cookie }
});
}

return this.jwtHandler.verifyAppIdToken(cookie);
};
}
14 changes: 11 additions & 3 deletions api/src/controller/http/server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import cookieParser from 'cookie-parser';
import express from 'express';
import { middleware as OpenApiValidatorMiddleware } from 'express-openapi-validator';
import { Express } from 'express-serve-static-core';
Expand All @@ -10,6 +11,7 @@ import { Logger } from 'winston';
import apiSpecification from '@root/generate/openapi.json';
import { name, version } from '@root/package.json';

import { JwtHandler } from '@src/core/ports/jwt.handler';
import { AuthService } from '@src/core/services/auth/auth.service';

import { AuthV1Controller } from '@controller/http/auth/auth.v1.controller';
Expand All @@ -26,9 +28,10 @@ export class HttpServer {
constructor(
private readonly logger: Logger,
private readonly config: HttpConfig,
private readonly authService: AuthService
private readonly authService: AuthService,
private readonly jwtHandler: JwtHandler
) {
this.middleware = new Middleware(this.logger);
this.middleware = new Middleware(this.logger, this.jwtHandler);
}

public start = async (): Promise<void> => {
Expand All @@ -37,7 +40,9 @@ export class HttpServer {
this.app.set('trust proxy', 0);
this.app.use(express.json());
await this.buildApiDocument();
this.app.use('/api', cookieParser());
this.app.use('/api', this.middleware.accessLog);
this.app.use('/api/v1/dev', this.middleware.issuePassport);
this.app.use(
OpenApiValidatorMiddleware({
apiSpec: path.join(__dirname, '../../../generate/openapi.json'),
Expand Down Expand Up @@ -71,7 +76,10 @@ export class HttpServer {
private getApiRouters = (): express.Router[] => {
const routers = [
new DevV1Controller().routes(),
new AuthV1Controller(this.authService).routes()
new AuthV1Controller(
this.authService,
this.config.cookieExpirationHours
).routes()
];
return routers;
};
Expand Down
3 changes: 3 additions & 0 deletions api/src/controller/http/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export const idTokenCookieName = 'mrcToken';

export interface HttpConfig {
env: string;
host: string;
port: number;
cookieExpirationHours: number;
}
3 changes: 2 additions & 1 deletion api/test/config/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ describe('Test build http config', () => {
expect(buildHttpConfig(loadConfig())).toStrictEqual({
env: 'test',
host: '127.0.0.1',
port: 0
port: 0,
cookieExpirationHours: 1
});
});
});
Expand Down
15 changes: 12 additions & 3 deletions api/test/controller/http/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ import { Logger } from 'winston';

import { methodNotAllowed } from '@src/controller/http/handler';
import { Middleware } from '@src/controller/http/middleware';
import { JwtHandler } from '@src/core/ports/jwt.handler';

// TODO: https://github.com/MovieReviewComment/Mr.C/issues/49
class TestHttpServer {
middleware: Middleware;
server!: http.Server;
app!: Express;

constructor(private readonly logger: Logger) {
this.middleware = new Middleware(this.logger);
constructor(
private readonly logger: Logger,
private readonly jwtHandler: JwtHandler
) {
this.middleware = new Middleware(this.logger, this.jwtHandler);
}

public start = (): Promise<void> => {
Expand Down Expand Up @@ -70,12 +74,17 @@ class TestController {

describe('Test handler', () => {
let mockLogger: Partial<Logger>;
let mockJwtHandler: Partial<JwtHandler>;
let testHttpServer: TestHttpServer;
let baseUrl: string;

beforeAll(async () => {
mockLogger = {};
testHttpServer = new TestHttpServer(mockLogger as Logger);
mockJwtHandler = {};
testHttpServer = new TestHttpServer(
mockLogger as Logger,
mockJwtHandler as JwtHandler
);
baseUrl = '/api/v1/dev';
await testHttpServer.start();
});
Expand Down
Loading

0 comments on commit 45487ef

Please sign in to comment.