From d5c6c9238fb4339153fd2702dda106d36c1893e0 Mon Sep 17 00:00:00 2001 From: Philipp Dormann Date: Fri, 27 Nov 2020 19:45:44 +0100 Subject: [PATCH 1/7] sample implementation of authorizationChecker --- src/app.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/src/app.ts b/src/app.ts index 488b549..53f7723 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,22 +1,66 @@ import "reflect-metadata"; import * as dotenvSafe from "dotenv-safe"; -import { createExpressServer } from "routing-controllers"; +import { Action, createExpressServer, HttpError } from "routing-controllers"; import consola from "consola"; import loaders from "./loaders/index"; - +// +import * as jwt from "jsonwebtoken"; +// dotenvSafe.config(); const PORT = process.env.APP_PORT || 4010; +const sampletoken = jwt.sign({ + "permissions": { + "TRACKS": ["read", "update", "delete", "add"] + } +}, process.env.JWT_SECRET || "secretjwtsecret") +console.log(`sampletoken: ${sampletoken}`); + const app = createExpressServer({ - controllers: [__dirname + "/controllers/*.ts"], + authorizationChecker: async (action: Action, permissions: string | string[]) => { + let required_permissions = permissions + if (typeof permissions === "string") { + required_permissions = [permissions] + } + // const token = action.request.headers["authorization"]; + const provided_token = action.request.query["auth"]; + try { + const jwtPayload = jwt.verify(provided_token, process.env.JWT_SECRET || "secretjwtsecret"); + if (jwtPayload.permissions) { + action.response.local = {} + action.response.local.jwtPayload = jwtPayload.permissions + required_permissions.forEach(r => { + const permission_key = r.split(":")[0] + const permission_access_level = r.split(":")[1] + // console.log(permission_key); + // console.log(permission_access_level); + if (jwtPayload.permissions[permission_key].indexOf(r) === 1) { + return true; + } else { + // TODO: throw/return proper HttpError + return false; + } + }); + } else { + // TODO: throw/return proper HttpError + return false; + } + } catch (error) { + console.log(error); + // throw new HttpError(401, "jwt_illegal") + return false + } + return true; + }, + development: false, + controllers: [`${__dirname}/controllers/*.ts`], }); -async function main() { +(async () => { await loaders(app); app.listen(PORT, () => { consola.success( `⚡️[server]: Server is running at http://localhost:${PORT}` ); }); -} -main(); +})(); \ No newline at end of file From e29d59ac02f6d73aba5452cba6801898fe1900b9 Mon Sep 17 00:00:00 2001 From: Philipp Dormann Date: Fri, 27 Nov 2020 19:55:35 +0100 Subject: [PATCH 2/7] move to module ref #6 #4 --- src/app.ts | 46 ++------------------------------------- src/authchecker.ts | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 44 deletions(-) create mode 100644 src/authchecker.ts diff --git a/src/app.ts b/src/app.ts index 53f7723..06a8a52 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,55 +3,13 @@ import * as dotenvSafe from "dotenv-safe"; import { Action, createExpressServer, HttpError } from "routing-controllers"; import consola from "consola"; import loaders from "./loaders/index"; -// -import * as jwt from "jsonwebtoken"; +import authchecker from "./authchecker"; // dotenvSafe.config(); const PORT = process.env.APP_PORT || 4010; -const sampletoken = jwt.sign({ - "permissions": { - "TRACKS": ["read", "update", "delete", "add"] - } -}, process.env.JWT_SECRET || "secretjwtsecret") -console.log(`sampletoken: ${sampletoken}`); - const app = createExpressServer({ - authorizationChecker: async (action: Action, permissions: string | string[]) => { - let required_permissions = permissions - if (typeof permissions === "string") { - required_permissions = [permissions] - } - // const token = action.request.headers["authorization"]; - const provided_token = action.request.query["auth"]; - try { - const jwtPayload = jwt.verify(provided_token, process.env.JWT_SECRET || "secretjwtsecret"); - if (jwtPayload.permissions) { - action.response.local = {} - action.response.local.jwtPayload = jwtPayload.permissions - required_permissions.forEach(r => { - const permission_key = r.split(":")[0] - const permission_access_level = r.split(":")[1] - // console.log(permission_key); - // console.log(permission_access_level); - if (jwtPayload.permissions[permission_key].indexOf(r) === 1) { - return true; - } else { - // TODO: throw/return proper HttpError - return false; - } - }); - } else { - // TODO: throw/return proper HttpError - return false; - } - } catch (error) { - console.log(error); - // throw new HttpError(401, "jwt_illegal") - return false - } - return true; - }, + authorizationChecker: authchecker, development: false, controllers: [`${__dirname}/controllers/*.ts`], }); diff --git a/src/authchecker.ts b/src/authchecker.ts new file mode 100644 index 0000000..e5823e0 --- /dev/null +++ b/src/authchecker.ts @@ -0,0 +1,54 @@ +import * as jwt from "jsonwebtoken"; +import { Action, createExpressServer, HttpError } from "routing-controllers"; +// ----------- +const sampletoken = jwt.sign({ + "permissions": { + // "TRACKS": ["read", "update", "delete", "add"] + "TRACKS": [] + } +}, process.env.JWT_SECRET || "secretjwtsecret") +console.log(`sampletoken: ${sampletoken}`); +// ----------- +const authchecker = async (action: Action, permissions: string | string[]) => { + let required_permissions = undefined + if (typeof permissions === "string") { + required_permissions = [permissions] + } else { + required_permissions = permissions + } + // const token = action.request.headers["authorization"]; + const provided_token = action.request.query["auth"]; + try { + const jwtPayload = jwt.verify(provided_token, process.env.JWT_SECRET || "secretjwtsecret"); + if (jwtPayload.permissions) { + action.response.local = {} + action.response.local.jwtPayload = jwtPayload.permissions + required_permissions.forEach(r => { + const permission_key = r.split(":")[0] + const actual_accesslevel_for_permission = jwtPayload.permissions[permission_key] + console.log(actual_accesslevel_for_permission); + const permission_access_level = r.split(":")[1] + console.log(permission_key); + console.log(permission_access_level); + // console.log(permission_key); + // console.log(permission_access_level); + if (actual_accesslevel_for_permission.includes(permission_access_level)) { + return true; + } else { + // TODO: throw/return proper HttpError + throw new HttpError(403, "no") + return false; + } + }); + } else { + // TODO: throw/return proper HttpError + return false; + } + } catch (error) { + console.log(error); + // throw new HttpError(401, "jwt_illegal") + return false + } + return true; +} +export default authchecker \ No newline at end of file From c15b6501819ec22870c67f16e3d306bd01364ac6 Mon Sep 17 00:00:00 2001 From: Philipp Dormann Date: Fri, 27 Nov 2020 19:55:49 +0100 Subject: [PATCH 3/7] sample in TrackController ref #6 --- src/controllers/TrackController.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/controllers/TrackController.ts b/src/controllers/TrackController.ts index 0f68282..a52891b 100644 --- a/src/controllers/TrackController.ts +++ b/src/controllers/TrackController.ts @@ -6,6 +6,7 @@ import { Post, Put, Delete, + Authorized, } from "routing-controllers"; import { getConnectionManager, Repository } from "typeorm"; import { EntityFromBody } from "typeorm-routing-controllers-extensions"; @@ -24,6 +25,7 @@ class CreateTrack { } @JsonController("/track") +@Authorized("TRACKS:read") export class TrackController { private trackRepository: Repository; @@ -43,11 +45,13 @@ export class TrackController { return this.trackRepository.findOne({ id: id }); } + @Authorized("TRACKS:write") @Post() post(@Body({ validate: true }) track: CreateTrack) { return this.trackRepository.save(track); } + @Authorized("TRACKS:write") @Put("/:id") put(@Param("id") id: number, @EntityFromBody() track: Track) { return this.trackRepository.update({ id: id }, track); From cb5f5b9ecb9425566a4621a87499ab13b3eaad02 Mon Sep 17 00:00:00 2001 From: Philipp Dormann Date: Fri, 27 Nov 2020 20:15:48 +0100 Subject: [PATCH 4/7] debugging ref #6 --- src/app.ts | 2 +- src/authchecker.ts | 2 +- src/controllers/TrackController.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app.ts b/src/app.ts index 06a8a52..16b712e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,6 @@ import "reflect-metadata"; import * as dotenvSafe from "dotenv-safe"; -import { Action, createExpressServer, HttpError } from "routing-controllers"; +import { createExpressServer } from "routing-controllers"; import consola from "consola"; import loaders from "./loaders/index"; import authchecker from "./authchecker"; diff --git a/src/authchecker.ts b/src/authchecker.ts index e5823e0..c93af9a 100644 --- a/src/authchecker.ts +++ b/src/authchecker.ts @@ -1,5 +1,5 @@ import * as jwt from "jsonwebtoken"; -import { Action, createExpressServer, HttpError } from "routing-controllers"; +import { Action, HttpError } from "routing-controllers"; // ----------- const sampletoken = jwt.sign({ "permissions": { diff --git a/src/controllers/TrackController.ts b/src/controllers/TrackController.ts index a52891b..243a088 100644 --- a/src/controllers/TrackController.ts +++ b/src/controllers/TrackController.ts @@ -25,7 +25,7 @@ class CreateTrack { } @JsonController("/track") -@Authorized("TRACKS:read") +// @Authorized("TRACKS:read") export class TrackController { private trackRepository: Repository; @@ -45,7 +45,7 @@ export class TrackController { return this.trackRepository.findOne({ id: id }); } - @Authorized("TRACKS:write") + @Authorized() @Post() post(@Body({ validate: true }) track: CreateTrack) { return this.trackRepository.save(track); From 5f4aed2f02b22799d3e043394641bc3ac549caf2 Mon Sep 17 00:00:00 2001 From: Philipp Dormann Date: Fri, 27 Nov 2020 21:16:15 +0100 Subject: [PATCH 5/7] fixed auth parsing ref #6 --- src/app.ts | 2 ++ src/authchecker.ts | 51 ++++++++++++++------------------- src/middlewares/ErrorHandler.ts | 20 +++++++++++++ 3 files changed, 43 insertions(+), 30 deletions(-) create mode 100644 src/middlewares/ErrorHandler.ts diff --git a/src/app.ts b/src/app.ts index 16b712e..873ff9f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,12 +4,14 @@ import { createExpressServer } from "routing-controllers"; import consola from "consola"; import loaders from "./loaders/index"; import authchecker from "./authchecker"; +import { ErrorHandler } from './middlewares/ErrorHandler'; // dotenvSafe.config(); const PORT = process.env.APP_PORT || 4010; const app = createExpressServer({ authorizationChecker: authchecker, + middlewares: [ErrorHandler], development: false, controllers: [`${__dirname}/controllers/*.ts`], }); diff --git a/src/authchecker.ts b/src/authchecker.ts index c93af9a..0052b68 100644 --- a/src/authchecker.ts +++ b/src/authchecker.ts @@ -3,8 +3,8 @@ import { Action, HttpError } from "routing-controllers"; // ----------- const sampletoken = jwt.sign({ "permissions": { - // "TRACKS": ["read", "update", "delete", "add"] - "TRACKS": [] + "TRACKS": ["read", "update", "delete", "add"] + // "TRACKS": [] } }, process.env.JWT_SECRET || "secretjwtsecret") console.log(`sampletoken: ${sampletoken}`); @@ -18,36 +18,27 @@ const authchecker = async (action: Action, permissions: string | string[]) => { } // const token = action.request.headers["authorization"]; const provided_token = action.request.query["auth"]; + let jwtPayload = undefined try { - const jwtPayload = jwt.verify(provided_token, process.env.JWT_SECRET || "secretjwtsecret"); - if (jwtPayload.permissions) { - action.response.local = {} - action.response.local.jwtPayload = jwtPayload.permissions - required_permissions.forEach(r => { - const permission_key = r.split(":")[0] - const actual_accesslevel_for_permission = jwtPayload.permissions[permission_key] - console.log(actual_accesslevel_for_permission); - const permission_access_level = r.split(":")[1] - console.log(permission_key); - console.log(permission_access_level); - // console.log(permission_key); - // console.log(permission_access_level); - if (actual_accesslevel_for_permission.includes(permission_access_level)) { - return true; - } else { - // TODO: throw/return proper HttpError - throw new HttpError(403, "no") - return false; - } - }); - } else { - // TODO: throw/return proper HttpError - return false; - } + jwtPayload = jwt.verify(provided_token, process.env.JWT_SECRET || "secretjwtsecret"); } catch (error) { - console.log(error); - // throw new HttpError(401, "jwt_illegal") - return false + throw new HttpError(401, "jwt_illegal") + } + if (jwtPayload.permissions) { + action.response.local = {} + action.response.local.jwtPayload = jwtPayload.permissions + required_permissions.forEach(r => { + const permission_key = r.split(":")[0] + const actual_accesslevel_for_permission = jwtPayload.permissions[permission_key] + const permission_access_level = r.split(":")[1] + if (actual_accesslevel_for_permission.includes(permission_access_level)) { + return true; + } else { + throw new HttpError(403, "no") + } + }); + } else { + throw new HttpError(403, "no") } return true; } diff --git a/src/middlewares/ErrorHandler.ts b/src/middlewares/ErrorHandler.ts new file mode 100644 index 0000000..7f45073 --- /dev/null +++ b/src/middlewares/ErrorHandler.ts @@ -0,0 +1,20 @@ +import { + Middleware, + ExpressErrorMiddlewareInterface +} from "routing-controllers"; + +@Middleware({ type: "after" }) +export class ErrorHandler implements ExpressErrorMiddlewareInterface { + public error( + error: any, + request: any, + response: any, + next: (err: any) => any + ) { + if (response.headersSent) { + return; + } + + response.json(error); + } +} From 37baa4ea45e64fce0cc5e14bbb29b7aabed22145 Mon Sep 17 00:00:00 2001 From: Philipp Dormann Date: Fri, 27 Nov 2020 21:18:42 +0100 Subject: [PATCH 6/7] default to only jwt checking (empty @Authorized() ) ref #6 --- src/authchecker.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/authchecker.ts b/src/authchecker.ts index 0052b68..9b62dfa 100644 --- a/src/authchecker.ts +++ b/src/authchecker.ts @@ -40,6 +40,12 @@ const authchecker = async (action: Action, permissions: string | string[]) => { } else { throw new HttpError(403, "no") } - return true; + // + try { + jwt.verify(provided_token, process.env.JWT_SECRET || "secretjwtsecret"); + return true + } catch (error) { + return false + } } export default authchecker \ No newline at end of file From d85c126c276a91996178ccf8b8e7a566d33dac69 Mon Sep 17 00:00:00 2001 From: Philipp Dormann Date: Fri, 27 Nov 2020 21:24:55 +0100 Subject: [PATCH 7/7] implementation details ref #6 --- src/app.ts | 4 ++++ src/controllers/TrackController.ts | 17 +++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/app.ts b/src/app.ts index 6a3e801..9e15de1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,11 +3,15 @@ import * as dotenvSafe from "dotenv-safe"; import { createExpressServer } from "routing-controllers"; import consola from "consola"; import loaders from "./loaders/index"; +import authchecker from "./authchecker"; +import { ErrorHandler } from './middlewares/ErrorHandler'; dotenvSafe.config(); const PORT = process.env.APP_PORT || 4010; const app = createExpressServer({ + authorizationChecker: authchecker, + middlewares: [ErrorHandler], development: process.env.NODE_ENV === "production", cors: true, routePrefix: "/api", diff --git a/src/controllers/TrackController.ts b/src/controllers/TrackController.ts index 965fde9..b826582 100644 --- a/src/controllers/TrackController.ts +++ b/src/controllers/TrackController.ts @@ -1,4 +1,4 @@ -import { JsonController, Param, Body, Get, Post, Put, Delete, NotFoundError, OnUndefined, NotAcceptableError } from 'routing-controllers'; +import { JsonController, Param, Body, Get, Post, Put, Delete, NotFoundError, OnUndefined, NotAcceptableError, Authorized } from 'routing-controllers'; import { getConnectionManager, Repository } from 'typeorm'; import { EntityFromBody } from 'typeorm-routing-controllers-extensions'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; @@ -22,6 +22,7 @@ export class TrackNotFoundError extends NotFoundError { } @JsonController('/tracks') +@Authorized("TRACKS:read") export class TrackController { private trackRepository: Repository; @@ -34,7 +35,7 @@ export class TrackController { @Get() @ResponseSchema(Track, { isArray: true }) - @OpenAPI({description: "Lists all tracks."}) + @OpenAPI({ description: "Lists all tracks." }) getAll() { return this.trackRepository.find(); } @@ -42,14 +43,14 @@ export class TrackController { @Get('/:id') @ResponseSchema(Track) @OnUndefined(TrackNotFoundError) - @OpenAPI({description: "Returns a track of a specified id (if it exists)"}) + @OpenAPI({ description: "Returns a track of a specified id (if it exists)" }) getOne(@Param('id') id: number) { return this.trackRepository.findOne({ id: id }); } @Post() @ResponseSchema(Track) - @OpenAPI({description: "Create a new track object (id will be generated automagicly)."}) + @OpenAPI({ description: "Create a new track object (id will be generated automagicly)." }) post( @Body({ validate: true }) track: CreateTrack @@ -59,15 +60,15 @@ export class TrackController { @Put('/:id') @ResponseSchema(Track) - @OpenAPI({description: "Update a track object (id can't be changed)."}) + @OpenAPI({ description: "Update a track object (id can't be changed)." }) async put(@Param('id') id: number, @EntityFromBody() track: Track) { let oldTrack = await this.trackRepository.findOne({ id: id }); if (!oldTrack) { - throw new TrackNotFoundError(); + throw new TrackNotFoundError(); } - if(oldTrack.id != track.id){ + if (oldTrack.id != track.id) { throw new NotAcceptableError("The id's don't match!"); } @@ -77,7 +78,7 @@ export class TrackController { @Delete('/:id') @ResponseSchema(Track) - @OpenAPI({description: "Delete a specified track (if it exists)."}) + @OpenAPI({ description: "Delete a specified track (if it exists)." }) async remove(@Param('id') id: number) { let track = await this.trackRepository.findOne({ id: id });