From 6cf86f209cbb6a9743a99ec45fcf2a9df145fd9c Mon Sep 17 00:00:00 2001 From: kadraman Date: Thu, 2 May 2024 15:53:23 +0100 Subject: [PATCH] Migrating to Auth0 API management --- .vscode/settings.json | 3 + config/default.json | 9 +- config/production.json | 2 +- package-lock.json | 191 ++++++++++++++++++----- package.json | 9 +- src/configs/app.config.ts | 20 ++- src/controllers/product.controller.ts | 2 +- src/controllers/site.controller.ts | 2 +- src/controllers/user.controller.ts | 2 +- src/middleware/authentication.handler.ts | 5 +- src/middleware/authorization.handler.ts | 82 +++++++--- src/middleware/error.handler.ts | 28 ++++ src/modules/products/permissions.ts | 6 + src/modules/users/permissions.ts | 6 + src/routes/product.routes.ts | 15 +- src/routes/site.routes.ts | 3 +- src/routes/user.routes.ts | 13 +- 17 files changed, 306 insertions(+), 92 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/middleware/error.handler.ts create mode 100644 src/modules/products/permissions.ts create mode 100644 src/modules/users/permissions.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2253120 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "fortifyRemediation.softwareSecurityCenterURL": "https://ssc.uat.fortifyhosted.net" +} \ No newline at end of file diff --git a/config/default.json b/config/default.json index 11bfbe1..14c8aed 100644 --- a/config/default.json +++ b/config/default.json @@ -2,7 +2,7 @@ "App": { "name": "IWA-API", "version": "1.0", - "description": "IWA-API - An insecure Node/Express REST API for use in Fortify demonstrations. For this sample, you can use the api key 'demo-access-token' to test the authorization filters.", + "description": "IWA-API - An insecure Node/Express REST API for use in Fortify demonstrations.", "port": 3000, "dbConfig": { "host": "127.0.0.1", @@ -11,10 +11,9 @@ "password": "iwa-dev", "database": "iwa-dev" }, - "oktaConfig": { - "domain": "dev-67973733.okta.com", - "authServer": "default", - "audience": "api://default" + "auth0": { + "domain": "dev-ahui5f878pgtbrpr.us.auth0.com", + "audience": "https://iwa-api.onfortify.com" }, "apiConfig": { "url": "http://localhost:3000", diff --git a/config/production.json b/config/production.json index 6de6dfa..84c3e42 100644 --- a/config/production.json +++ b/config/production.json @@ -8,7 +8,7 @@ "database": "iwa" }, "apiConfig": { - "url": "https://iwaapi.onfortify.com", + "url": "https://iwa-api.onfortify.com", "version": "v1", "description": "Production server" }, diff --git a/package-lock.json b/package-lock.json index db8439a..0dbd241 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,20 +9,25 @@ "version": "1.0.0", "license": "GPLv3", "dependencies": { - "@okta/jwt-verifier": "^3.1.0", "@types/express-session": "^1.17.9", "@types/morgan": "^1.9.7", "@types/swagger-ui-express": "^4.1.5", "body-parser": "^1.20.2", "config": "^3.3.6", + "cors": "^2.8.5", "crypto-browserify": "^3.12.0", "express": "^4.18.2", + "express-async-errors": "^3.1.1", + "express-jwt": "^8.4.1", + "express-jwt-authz": "^2.4.1", + "express-oauth2-jwt-bearer": "^1.6.0", "express-session": "^1.17.3", "helmet": "^6.2.0", "jest": "^29.7.0", "jquery": "^3.6.0", "jsdom": "^17.0.0", "jsonwebtoken": "^8.5.1", + "jwks-rsa": "^3.1.0", "moment": "^2.29.2", "mongoose": "^5.9.7", "morgan": "^1.10.0", @@ -34,7 +39,9 @@ "devDependencies": { "@tsconfig/node-lts": "^18.12.5", "@types/config": "^3.3.2", + "@types/cors": "^2.8.17", "@types/express": "^4.17.20", + "@types/express-jwt": "^7.4.2", "@types/helmet": "^4.0.0", "@types/jsonwebtoken": "^9.0.4", "@types/node": "^20.8.7", @@ -1037,18 +1044,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@okta/jwt-verifier": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@okta/jwt-verifier/-/jwt-verifier-3.1.0.tgz", - "integrity": "sha512-Gy7cstZRV71MafaezFS6VW6f9H44hdpE+q77q2O4Rog1rZpLnw3l7pmN/LD/6jhYapOu+IhUZ1QN5v2E6Bi9PA==", - "dependencies": { - "jwks-rsa": "^3.1.0", - "njwt": "^2.0.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1176,6 +1171,15 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -1187,6 +1191,16 @@ "@types/serve-static": "*" } }, + "node_modules/@types/express-jwt": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-7.4.2.tgz", + "integrity": "sha512-wb3wbD/XaWdBu9366M07Ugb3fuIiW7B2oUdnXfvUchI892eLGZ5eQqgfnVw2oNfN0dF9L2Px859DpZKPDtxzlA==", + "deprecated": "This is a stub types definition. express-jwt provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "express-jwt": "*" + } + }, "node_modules/@types/express-serve-static-core": { "version": "4.17.41", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", @@ -2185,6 +2199,18 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-ecdh": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", @@ -2760,6 +2786,106 @@ "node": ">= 0.10.0" } }, + "node_modules/express-async-errors": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz", + "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==", + "peerDependencies": { + "express": "^4.16.2" + } + }, + "node_modules/express-jwt": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-8.4.1.tgz", + "integrity": "sha512-IZoZiDv2yZJAb3QrbaSATVtTCYT11OcqgFGoTN4iKVyN6NBkBkhtVIixww5fmakF0Upt5HfOxJuS6ZmJVeOtTQ==", + "dependencies": { + "@types/jsonwebtoken": "^9", + "express-unless": "^2.1.3", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express-jwt-authz": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/express-jwt-authz/-/express-jwt-authz-2.4.1.tgz", + "integrity": "sha512-ruH86e2NvWicG9maStztyAyBJV0E8RsInXUm6Kuc/9pDtVJmJw3qigv1MEVs5bH+aksZuxocYZdz+N1V/9F+Dg==", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "@types/express": "^4.0.0", + "express": "^4.0.0" + } + }, + "node_modules/express-jwt/node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/express-jwt/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/express-jwt/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/express-jwt/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/express-jwt/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/express-oauth2-jwt-bearer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/express-oauth2-jwt-bearer/-/express-oauth2-jwt-bearer-1.6.0.tgz", + "integrity": "sha512-HXnez7vocYlOqlfF3ozPcf/WE3zxT7zfUNfeg5FHJnvNwhBYlNXiPOvuCtBalis8xcigvwtInzEKhBuH87+9ug==", + "dependencies": { + "jose": "^4.13.1" + }, + "engines": { + "node": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0 || ^20.2.0" + } + }, "node_modules/express-session": { "version": "1.17.3", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", @@ -2786,6 +2912,11 @@ "node": ">= 0.6" } }, + "node_modules/express-unless": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", + "integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==" + }, "node_modules/express/node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -4789,24 +4920,6 @@ "node": ">= 0.6" } }, - "node_modules/njwt": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/njwt/-/njwt-2.0.0.tgz", - "integrity": "sha512-1RcqirhCqThBEe4KO83pFg0wPBa1c9NiXNCrocD2EbZqb6ksWWDVnp/w/p0gsyUcVa05PhhaaPjs9rc/GLmdxQ==", - "dependencies": { - "@types/node": "^15.0.1", - "ecdsa-sig-formatter": "^1.0.5", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">=12.0" - } - }, - "node_modules/njwt/node_modules/@types/node": { - "version": "15.14.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz", - "integrity": "sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A==" - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4944,6 +5057,14 @@ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -6274,14 +6395,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index 441d7e9..7a0186e 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "devDependencies": { "@tsconfig/node-lts": "^18.12.5", "@types/config": "^3.3.2", + "@types/cors": "^2.8.17", "@types/express": "^4.17.20", + "@types/express-jwt": "^7.4.2", "@types/helmet": "^4.0.0", "@types/jsonwebtoken": "^9.0.4", "@types/node": "^20.8.7", @@ -28,20 +30,25 @@ "typescript": "^4.9.5" }, "dependencies": { - "@okta/jwt-verifier": "^3.1.0", "@types/express-session": "^1.17.9", "@types/morgan": "^1.9.7", "@types/swagger-ui-express": "^4.1.5", "body-parser": "^1.20.2", "config": "^3.3.6", + "cors": "^2.8.5", "crypto-browserify": "^3.12.0", "express": "^4.18.2", + "express-async-errors": "^3.1.1", + "express-jwt": "^8.4.1", + "express-jwt-authz": "^2.4.1", + "express-oauth2-jwt-bearer": "^1.6.0", "express-session": "^1.17.3", "helmet": "^6.2.0", "jest": "^29.7.0", "jquery": "^3.6.0", "jsdom": "^17.0.0", "jsonwebtoken": "^8.5.1", + "jwks-rsa": "^3.1.0", "moment": "^2.29.2", "mongoose": "^5.9.7", "morgan": "^1.10.0", diff --git a/src/configs/app.config.ts b/src/configs/app.config.ts index 6774e68..25c2d6d 100644 --- a/src/configs/app.config.ts +++ b/src/configs/app.config.ts @@ -23,11 +23,14 @@ import config from 'config'; import bodyParser from "body-parser"; import mongoose from 'mongoose'; import swaggerUi from "swagger-ui-express"; +import cors from "cors"; import helmet from "helmet"; -import OktaJwtVerifier from "@okta/jwt-verifier"; +import "express-async-errors"; import Logger from "../middleware/logger"; import morganConfig from './morgan.config' +import errorHandler from "../middleware/error.handler"; + // @ts-ignore import swaggerOutput from './swagger_output.json'; @@ -36,6 +39,8 @@ import {userRoutes} from "../routes/user.routes"; import {productRoutes} from "../routes/product.routes"; import {commonRoutes} from "../routes/common.routes"; +import {AuthorizationHandler} from "../middleware/authorization.handler"; + class AppConfig { public app: express.Application; @@ -44,10 +49,7 @@ class AppConfig { private dbHost: string = config.get('App.dbConfig.host') || 'localhost'; private dbPort: number = config.get('App.dbConfig.port') || 27017; private dbName: string = config.get('App.dbConfig.database') || 'iwa'; - private oktaDomain: string = "dev-67973733.okta.com"; - private oktaAuthServer: string = "default"; public mongoUrl: string = `mongodb://${this.dbHost}:${this.dbPort}/${this.dbName}`; - public oktaJwtVerifier; constructor() { this.app = express(); @@ -88,6 +90,8 @@ class AppConfig { saveUninitialized: true, cookie: {secure: false} })) + // enabled CORS for all domains! + this.app.use(cors()); // configure helmet this.app.use(helmet({ ieNoOpen: false @@ -99,10 +103,10 @@ class AppConfig { ); // configure swagger API this.app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerOutput)); - - this.app.oktaJwtVerifier = new OktaJwtVerifier({ - issuer: `https://${this.oktaDomain}/oauth2/${this.oktaAuthServer}` - }); + // configure default error handler + this.app.use(errorHandler); + // configure global authorization handler + //this.app.use(AuthorizationHandler.checkJwt); } } diff --git a/src/controllers/product.controller.ts b/src/controllers/product.controller.ts index 9712c02..370b756 100644 --- a/src/controllers/product.controller.ts +++ b/src/controllers/product.controller.ts @@ -108,7 +108,7 @@ export class ProductController { public create_product(req: Request, res: Response) { Logger.debug(`Creating product with request body: ${JSON.stringify(req.body)}`); - // this check whether all the fields were sent through the request or not + // this checks whether all the fields were sent through the request or not if (req.body.code && req.body.name && req.body.summary) { const product_params: IProduct = { diff --git a/src/controllers/site.controller.ts b/src/controllers/site.controller.ts index 8e37bbe..cb30894 100644 --- a/src/controllers/site.controller.ts +++ b/src/controllers/site.controller.ts @@ -46,7 +46,7 @@ export class SiteController { public login_user(req: Request, res: Response) { Logger.debug(`Logging in user with with request body: ${JSON.stringify(req.body)}`); - // this check whether all the fields were sent through the request or not + // this checks whether all the fields were sent through the request or not if (req.body.email && req.body.password) { const hashPassword = EncryptUtils.cryptPassword(req.body.password); const user_filter = {email: req.body.email, password: hashPassword}; diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index b3c1dd2..1495289 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -62,7 +62,7 @@ export class UserController { public create_user(req: Request, res: Response) { Logger.debug(`Creating user with request body: ${JSON.stringify(req.body)}`); - // this check whether all the fields were sent through with the request or not + // this checks whether all the fields were sent through with the request or not if (req.body.name && req.body.name.first_name && req.body.name.last_name && req.body.email && req.body.phone_number && diff --git a/src/middleware/authentication.handler.ts b/src/middleware/authentication.handler.ts index a624557..9d109ab 100644 --- a/src/middleware/authentication.handler.ts +++ b/src/middleware/authentication.handler.ts @@ -21,13 +21,16 @@ import Logger from "./logger"; import {IUser} from "../modules/users/model"; import jwt from "jsonwebtoken"; import config from "config"; -import {JwtJson} from "../common/types"; import {NextFunction, Request, Response} from "express"; + import {forbidden, unauthorised} from "../modules/common/service"; +import {JwtJson} from "../common/types"; const jwtSecret: string = config.get('App.jwtSecret') || "changeme"; const jwtExpiration: number = config.get('App.jwtExpiration') || 36000; +// NOTE: no longer used as delegated to Auth0 + export class AuthenticationHandler { public static createJWT(user_data: IUser): JwtJson { diff --git a/src/middleware/authorization.handler.ts b/src/middleware/authorization.handler.ts index a8611d1..13b6b2c 100644 --- a/src/middleware/authorization.handler.ts +++ b/src/middleware/authorization.handler.ts @@ -21,19 +21,57 @@ import Logger from "./logger"; import config from "config"; import {NextFunction, Request, Response} from "express"; import {forbidden, unauthorised} from "../modules/common/service"; -import OktaJwtVerifier from "@okta/jwt-verifier"; + +const { expressjwt: jwt } = require("express-jwt"); +const jwtAuthz = require("express-jwt-authz"); + +import jwksRsa from "jwks-rsa"; const staticAccessToken: string = config.get('App.staticAccessToken') || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; -const oktaDomain: string = config.get('App.oktaConfig.domain'); -const oktaAuthServer: string = config.get('App.oktaConfig.authServer') || "default"; -const oktaAudience: string = config.get('App.oktaConfig.audience') || "api://default"; -const oktaJwtVerifier: OktaJwtVerifier = new OktaJwtVerifier({ - issuer: `https://${oktaDomain}/oauth2/${oktaAuthServer}` -}); +const auth0Domain: string = config.get('App.auth0.domain'); +const auth0Audience: string = config.get('App.auth0.audience'); export class AuthorizationHandler { - public static requireAccessToken = async (req: Request, res: Response, next: NextFunction) => { + // routes open to all + private static unprotected = [ + /\//, + /\/api-docs/, + /\/api-docs\/*/, + /favicon.ico/, + /\/site\/*/, + ]; + + /*public static checkJwt = jwt({ + secret: jwksRsa.expressJwtSecret({ + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: `https://${auth0Domain}/.well-known/jwks.json` + }), + + // Validate the audience and the issuer. + audience: `${auth0Audience}`, + issuer: `https://${auth0Domain}/`, + algorithms: ["RS256"] + }).unless({path: this.unprotected}) + */ + + public static requirePermission(permissions: string | string[]) { + try { + const jwtAuth = jwtAuthz([permissions], { + customScopeKey: "permissions", + customUserKey: "auth", + checkAllScopes: true, + failWithError: false // should be true and catch with custom error handler + }); + return jwtAuth; + } catch (error: any) { + unauthorised(error.message, res); + } + }; + + public static requireAccessToken(req: Request, res: Response, next: NextFunction) { Logger.debug(`AuthorizationHandler::requireAccessToken`); let accessToken: string | undefined; @@ -44,18 +82,20 @@ export class AuthorizationHandler { } else { console.log(`accessToken = ${accessToken}`); - if (accessToken == staticAccessToken) { - Logger.debug("Using staticAccessToken; skipping verification") - } else { - oktaJwtVerifier.verifyAccessToken(accessToken, oktaAudience) - .then(jwt => { - // the token is valid (per definition of 'valid' above) - console.log(jwt.claims); - }) - .catch(err => { - // a validation failed, inspect the error - }); - } + const jwtAccess = jwt({ + secret: jwksRsa.expressJwtSecret({ + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: `https://${auth0Domain}/.well-known/jwks.json` + }), + + // Validate the audience and the issuer. + audience: 'https://iwa-api.onfortify.com', + issuer: `https://${auth0Domain}/`, + algorithms: ["RS256"] + }).unless({path: this.unprotected}); + } next(); @@ -69,6 +109,7 @@ export class AuthorizationHandler { next(); } + // TODO: update for Auth0 public static permitSelf(req: Request, res: Response, next: NextFunction) { Logger.debug(`AuthorizationHandler::permitSelf`); Logger.debug(`Verifying if user has authorization to self endpoint: '${req.url}`); @@ -84,6 +125,7 @@ export class AuthorizationHandler { } } + // NOTE: no longer required - delegated to Auth0 permissions public static permitAdmin(req: Request, res: Response, next: NextFunction) { Logger.debug(`AuthorizationHandler::permitAdmin`); Logger.debug(`Verifying if user has authorization to admin endpoint: '${req.url}`); diff --git a/src/middleware/error.handler.ts b/src/middleware/error.handler.ts new file mode 100644 index 0000000..7629eb0 --- /dev/null +++ b/src/middleware/error.handler.ts @@ -0,0 +1,28 @@ +/* + IWA-API - An insecure Node/Express REST API for use in Fortify demonstrations. + + Copyright 2024 Open Text or one of its affiliates. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { NextFunction, Request, Response } from "express"; +import Logger from "./logger"; + +const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => { + Logger.error(err); + res.status(500).send({ errors: [{ message: "Something went wrong" }] }); +}; + +export default errorHandler; \ No newline at end of file diff --git a/src/modules/products/permissions.ts b/src/modules/products/permissions.ts new file mode 100644 index 0000000..8dbcff1 --- /dev/null +++ b/src/modules/products/permissions.ts @@ -0,0 +1,6 @@ +export enum ProductPermission { + Create = "create:products", + Read = "read:products", + Update = "update:products", + Delete = "delete:products", +} \ No newline at end of file diff --git a/src/modules/users/permissions.ts b/src/modules/users/permissions.ts new file mode 100644 index 0000000..de63fcb --- /dev/null +++ b/src/modules/users/permissions.ts @@ -0,0 +1,6 @@ +export enum UserPermission { + Create = "create:users", + Read = "read:users", + Update = "update:users", + Delete = "delete:users", +} \ No newline at end of file diff --git a/src/routes/product.routes.ts b/src/routes/product.routes.ts index 7ae83ab..de45975 100644 --- a/src/routes/product.routes.ts +++ b/src/routes/product.routes.ts @@ -23,12 +23,13 @@ import {Request, Response, Router} from 'express'; import {ProductController} from '../controllers/product.controller'; import {AuthenticationHandler} from "../middleware/authentication.handler"; import {AuthorizationHandler} from "../middleware/authorization.handler"; +import {ProductPermission} from "../modules/products/permissions" const product_controller: ProductController = new ProductController(); export const productRoutes = Router(); -productRoutes.get('/api/v1/products', [AuthorizationHandler.requireAccessToken], (req: Request, res: Response) => { +productRoutes.get('/api/v1/products', [AuthorizationHandler.permitAll], (req: Request, res: Response) => { /* #swagger.tags = ['Products'] #swagger.summary = "Find products by keyword(s)" @@ -64,7 +65,7 @@ productRoutes.get('/api/v1/products', [AuthorizationHandler.requireAccessToken], product_controller.get_products(req, res); }); -productRoutes.get('/api/v1/products/:id', [AuthorizationHandler.requireAccessToken], (req: Request, res: Response) => { +productRoutes.get('/api/v1/products/:id', [AuthorizationHandler.permitAll], (req: Request, res: Response) => { /* #swagger.tags = ['Products'] #swagger.summary = "Get a product" @@ -92,7 +93,7 @@ productRoutes.get('/api/v1/products/:id', [AuthorizationHandler.requireAccessTok product_controller.get_product(req, res); }); -productRoutes.get('/api/v1/products/:id/image', [AuthorizationHandler.requireAccessToken], (req: Request, res: Response) => { +productRoutes.get('/api/v1/products/:id/image', [AuthorizationHandler.permitAll], (req: Request, res: Response) => { /* #swagger.tags = ['Products'] #swagger.summary = "Get product image by Id" @@ -120,7 +121,7 @@ productRoutes.get('/api/v1/products/:id/image', [AuthorizationHandler.requireAcc product_controller.get_product_image_by_id(req, res); }); -productRoutes.get('/api/v1/products/:name/image', [AuthorizationHandler.requireAccessToken], (req: Request, res: Response) => { +productRoutes.get('/api/v1/products/:name/image', [AuthorizationHandler.permitAll], (req: Request, res: Response) => { /* #swagger.tags = ['Products'] #swagger.summary = "Get product image by name" @@ -149,7 +150,7 @@ productRoutes.get('/api/v1/products/:name/image', [AuthorizationHandler.requireA }); -productRoutes.post('/api/v1/products', [AuthorizationHandler.requireAccessToken], (req: Request, res: Response) => { +productRoutes.post('/api/v1/products', [AuthorizationHandler.requireAccessToken, AuthorizationHandler.requirePermission(ProductPermission.Create)], (req: Request, res: Response) => { /* #swagger.tags = ['Products'] #swagger.summary = "Create new product" @@ -181,7 +182,7 @@ productRoutes.post('/api/v1/products', [AuthorizationHandler.requireAccessToken] product_controller.create_product(req, res); }); -productRoutes.put('/api/v1/products/:id', [AuthorizationHandler.requireAccessToken], (req: Request, res: Response) => { +productRoutes.put('/api/v1/products/:id', [AuthorizationHandler.requireAccessToken, AuthorizationHandler.requirePermission(ProductPermission.Update)], (req: Request, res: Response) => { /* #swagger.tags = ['Products'] #swagger.summary = "Update a product" @@ -209,7 +210,7 @@ productRoutes.put('/api/v1/products/:id', [AuthorizationHandler.requireAccessTok product_controller.update_product(req, res); }); -productRoutes.delete('/api/v1/products/:id', [AuthorizationHandler.requireAccessToken], (req: Request, res: Response) => { +productRoutes.delete('/api/v1/products/:id', [AuthorizationHandler.requireAccessToken, AuthorizationHandler.requirePermission(ProductPermission.Delete)], (req: Request, res: Response) => { /* #swagger.tags = ['Products'] #swagger.summary = "Delete a product" diff --git a/src/routes/site.routes.ts b/src/routes/site.routes.ts index b5ac3cd..554b623 100644 --- a/src/routes/site.routes.ts +++ b/src/routes/site.routes.ts @@ -18,6 +18,7 @@ */ import {Request, Response, Router} from 'express'; +import { requiredScopes } from 'express-oauth2-jwt-bearer'; import {SiteController} from "../controllers/site.controller"; import {AuthorizationHandler} from "../middleware/authorization.handler"; @@ -257,7 +258,7 @@ siteRoutes.post('/api/v1/site/refresh-token', [AuthorizationHandler.permitAll], res.status(200).json({message: "Post request successfull"}); }); -siteRoutes.post('/api/v1/site/backup-newsletter-db', [AuthorizationHandler.requireAccessToken], (req: Request, res: Response) => { +siteRoutes.post('/api/v1/site/backup-newsletter-db', [AuthorizationHandler.permitAll], (req: Request, res: Response) => { /* #swagger.tags = ['Site'] diff --git a/src/routes/user.routes.ts b/src/routes/user.routes.ts index 0ad2762..8048e54 100644 --- a/src/routes/user.routes.ts +++ b/src/routes/user.routes.ts @@ -26,6 +26,7 @@ import Logger from "../middleware/logger"; import {IUser} from "../modules/users/model"; import {mongoError, successResponse} from "../modules/common/service"; import {EncryptUtils} from "../utils/encrypt.utils"; +import {UserPermission} from "../modules/users/permissions" import users from '../modules/users/schema'; @@ -38,7 +39,7 @@ userRoutes.param('id', function (req, res, next, id, name) { next(); }); -userRoutes.get('/api/v1/users', [AuthorizationHandler.requireAccessToken], (req: Request, res: Response) => { +userRoutes.get('/api/v1/users', [AuthorizationHandler.requireAccessToken, AuthorizationHandler.requirePermission(UserPermission.Read)], (req: Request, res: Response) => { /* #swagger.tags = ['Users'] #swagger.summary = "Find users by keyword(s)" @@ -75,7 +76,7 @@ userRoutes.get('/api/v1/users', [AuthorizationHandler.requireAccessToken], (req: user_controller.get_users(req, res); }); -userRoutes.get('/api/v1/users/:id', [AuthorizationHandler.requireAccessToken], (req: Request, res: Response) => { +userRoutes.get('/api/v1/users/:id', [AuthorizationHandler.requireAccessToken, AuthorizationHandler.requirePermission(UserPermission.Read)], (req: Request, res: Response) => { /* #swagger.tags = ['Users'] #swagger.summary = "Get a user" @@ -103,7 +104,7 @@ userRoutes.get('/api/v1/users/:id', [AuthorizationHandler.requireAccessToken], ( user_controller.get_user(req, res); }); -userRoutes.get('/api/v1/user', [AuthorizationHandler.requireAccessToken], (req: Request, res: Response) => { +userRoutes.get('/api/v1/user', [AuthorizationHandler.requireAccessToken, AuthorizationHandler.requirePermission(UserPermission.Read)], (req: Request, res: Response) => { /* #swagger.tags = ['Users'] #swagger.summary = "Get a user using query" @@ -140,7 +141,7 @@ userRoutes.get('/api/v1/user', [AuthorizationHandler.requireAccessToken], (req: }); }); -userRoutes.post('/api/v1/users', [AuthorizationHandler.requireAccessToken], (req: Request, res: Response) => { +userRoutes.post('/api/v1/users', [AuthorizationHandler.requireAccessToken, AuthorizationHandler.requirePermission(UserPermission.Create)], (req: Request, res: Response) => { /* #swagger.tags = ['Users'] #swagger.summary = "Create new user" @@ -171,7 +172,7 @@ userRoutes.post('/api/v1/users', [AuthorizationHandler.requireAccessToken], (req user_controller.create_user(req, res); }); -userRoutes.put('/api/v1/users/:id', [AuthorizationHandler.requireAccessToken], (req: Request, res: Response) => { +userRoutes.put('/api/v1/users/:id', [AuthorizationHandler.requireAccessToken, AuthorizationHandler.requirePermission(UserPermission.Update)], (req: Request, res: Response) => { /* #swagger.tags = ['Users'] #swagger.summary = "Update a user" @@ -199,7 +200,7 @@ userRoutes.put('/api/v1/users/:id', [AuthorizationHandler.requireAccessToken], ( user_controller.update_user(req, res); }); -userRoutes.delete('/api/v1/users/:id', [AuthorizationHandler.requireAccessToken], (req: Request, res: Response) => { +userRoutes.delete('/api/v1/users/:id', [AuthorizationHandler.requireAccessToken, AuthorizationHandler.requirePermission(UserPermission.Read)], (req: Request, res: Response) => { /* #swagger.tags = ['Users'] #swagger.summary = "Delete a user"