From 154359c442a7912121aa4ec4baee8467134c5286 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Fri, 21 Jul 2023 18:30:27 +0300 Subject: [PATCH 01/10] feat: add intial auth library --- lib/lib-auth/.eslintignore | 1 + lib/lib-auth/.eslintrc | 6 + lib/lib-auth/.gitignore | 8 + lib/lib-auth/LICENSE.md | 21 + lib/lib-auth/README.md | 3 + lib/lib-auth/package.json | 40 ++ lib/lib-auth/src/UnauthorizedError.ts | 23 ++ lib/lib-auth/src/authenticator.ts | 150 +++++++ lib/lib-auth/tsconfig.es.json | 11 + lib/lib-auth/tsconfig.json | 11 + yarn.lock | 542 +++++++++++++++++++++++++- 11 files changed, 804 insertions(+), 12 deletions(-) create mode 100644 lib/lib-auth/.eslintignore create mode 100644 lib/lib-auth/.eslintrc create mode 100644 lib/lib-auth/.gitignore create mode 100644 lib/lib-auth/LICENSE.md create mode 100644 lib/lib-auth/README.md create mode 100644 lib/lib-auth/package.json create mode 100644 lib/lib-auth/src/UnauthorizedError.ts create mode 100644 lib/lib-auth/src/authenticator.ts create mode 100644 lib/lib-auth/tsconfig.es.json create mode 100644 lib/lib-auth/tsconfig.json diff --git a/lib/lib-auth/.eslintignore b/lib/lib-auth/.eslintignore new file mode 100644 index 0000000..5924ae3 --- /dev/null +++ b/lib/lib-auth/.eslintignore @@ -0,0 +1 @@ +src/typings/ \ No newline at end of file diff --git a/lib/lib-auth/.eslintrc b/lib/lib-auth/.eslintrc new file mode 100644 index 0000000..644bb1a --- /dev/null +++ b/lib/lib-auth/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": "../../.eslintrc", + "parserOptions": { + "project": "tsconfig.json" + } +} diff --git a/lib/lib-auth/.gitignore b/lib/lib-auth/.gitignore new file mode 100644 index 0000000..3d1714c --- /dev/null +++ b/lib/lib-auth/.gitignore @@ -0,0 +1,8 @@ +/node_modules/ +/build/ +/coverage/ +/docs/ +*.tsbuildinfo +*.tgz +*.log +package-lock.json diff --git a/lib/lib-auth/LICENSE.md b/lib/lib-auth/LICENSE.md new file mode 100644 index 0000000..283bac5 --- /dev/null +++ b/lib/lib-auth/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2023] [Topcoder] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/lib-auth/README.md b/lib/lib-auth/README.md new file mode 100644 index 0000000..5b45307 --- /dev/null +++ b/lib/lib-auth/README.md @@ -0,0 +1,3 @@ +# @topcoder-framework/lib-auth + +TODO diff --git a/lib/lib-auth/package.json b/lib/lib-auth/package.json new file mode 100644 index 0000000..e0eb9a7 --- /dev/null +++ b/lib/lib-auth/package.json @@ -0,0 +1,40 @@ +{ + "name": "@topcoder-framework/lib-auth", + "version": "0.20.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "scripty", + "build:es": "scripty", + "build:app": "scripty", + "build:include:deps": "scripty", + "clean": "scripty", + "lint": "scripty", + "format": "scripty" + }, + "scripty": { + "path": "../../scripts/packages", + "windowsPath": "../../scripts-win/packages" + }, + "author": "Rakib Ansary ", + "license": "ISC", + "volta": { + "node": "18.14.1", + "typescript": "4.9.3", + "npm": "9.1.2" + }, + "dependencies": { + "@types/jsonwebtoken": "^9.0.2", + "jsonwebtoken": "^9.0.1", + "jwks-rsa": "^3.0.1", + "lodash": "^4.17.21" + }, + "devDependencies": { + "@types/express": "^4.17.17", + "express": "^4.18.2" + }, + "files": [ + "dist-*" + ], + "gitHead": "8c19fdcd36bcccc5e952aa909e4a30caf2943340" +} diff --git a/lib/lib-auth/src/UnauthorizedError.ts b/lib/lib-auth/src/UnauthorizedError.ts new file mode 100644 index 0000000..da32339 --- /dev/null +++ b/lib/lib-auth/src/UnauthorizedError.ts @@ -0,0 +1,23 @@ +export type ErrorLike = Error | { message: string }; + +type ErrorCode = + | "credentials_bad_scheme" + | "credentials_bad_format" + | "credentials_required" + | "invalid_token" + | "revoked_token"; + +export class UnauthorizedError extends Error { + readonly status: number; + readonly inner: ErrorLike; + readonly code: string; + + constructor(code: ErrorCode, error: ErrorLike) { + super(error.message); + Object.setPrototypeOf(this, UnauthorizedError.prototype); + this.code = code; + this.status = 401; + this.name = "UnauthorizedError"; + this.inner = error; + } +} diff --git a/lib/lib-auth/src/authenticator.ts b/lib/lib-auth/src/authenticator.ts new file mode 100644 index 0000000..255b6ae --- /dev/null +++ b/lib/lib-auth/src/authenticator.ts @@ -0,0 +1,150 @@ +import { Request, Response, NextFunction } from "express"; +import { Jwt, JwtPayload, Secret, VerifyOptions, verify } from "jsonwebtoken"; +import JwksRSA, { JwksClient } from "jwks-rsa"; +import _ from "lodash"; +import { UnauthorizedError } from "./UnauthorizedError"; + +const jwksClients: { [iss: string]: JwksClient } = {}; + +/** + * A function that defines how to retrieve the verification key given the express request and the JWT. + */ +export type GetVerificationKey = ( + req: Request, + token: Jwt | undefined +) => Secret | undefined | Promise; + +/** + * A function to check if a token is revoked + */ +export type IsRevoked = ( + req: Request, + token: Jwt | undefined +) => boolean | Promise; + +/** + * A function to customize how a token is retrieved from the express request. + */ +export type TokenGetter = ( + req: Request +) => string | Promise | undefined; + +export type AuthOptions = { + test: string; + method?: AuthMethod; + secret?: Secret | GetVerificationKey; + algorithms?: Algorithm[]; + /** + * If sets to true, continue to the next middleware when the + * request doesn't include a token without failing. + * + * @default true + */ + credentialsRequired?: boolean; + jwtKeyCacheTimeInHours?: number; + cookieName?: string; + getToken?: TokenGetter; + isRevoked?: IsRevoked; +} & Pick; + +export enum AuthMethod { + "Bearer", +} + +export type Algorithm = RSA | HSA; + +type HSA = "HS256" | "HS384" | "HS512"; +type RSA = "RS256" | "RS384" | "RS512"; + +const isHSA = (algorithm: Algorithm): algorithm is HSA => + _.includes(["HS256", "HS384", "HS512"], algorithm); + +const isRSA = (algorithm: Algorithm): algorithm is RSA => + _.includes(["RS256", "RS384", "RS512"], algorithm); + +const bearerTokenGetter = (req: Request): string | undefined => { + if (req.headers && req.headers.authorization) { + const parts = req.headers.authorization.split(" "); + if (parts.length == 2) { + const scheme = parts[0]; + const credentials = parts[1]; + + if (/^Bearer$/i.test(scheme)) { + return credentials; + } else { + throw new UnauthorizedError("credentials_bad_scheme", { + message: "Format is Authorization: Bearer [token]", + }); + } + } else { + throw new UnauthorizedError("credentials_bad_format", { + message: "Format is Authorization: Bearer [token]", + }); + } + } else { + throw new UnauthorizedError("credentials_bad_format", { + message: "Format is Authorization: Bearer [token]", + }); + } +}; + +const defaultOptions = { + method: AuthMethod.Bearer, + algorithms: ["HS256", "RS256"], + credentialsRequired: true, + jwtKeyCacheTimeInHours: 24, + getToken: bearerTokenGetter, +}; + +export const authenticator = (options: AuthOptions) => { + const authOptions = { + ...defaultOptions, + ...options, + }; + if (_.some(options.algorithms, isHSA) && _.isUndefined(authOptions.secret)) { + throw new RangeError("lib-auth: secret is required when algorithm is HSA"); + } + + const middleware = (req: Request, res: Response, next: NextFunction) => { + const token = authOptions.getToken(req); + if (!token) { + if (authOptions.credentialsRequired) { + throw new UnauthorizedError("credentials_required", { + message: "No authorization token was found", + }); + } else { + return next(); + } + } + }; + return middleware; +}; + +const getRSAKey = async (token: Jwt, cacheTime: number) => { + const kid = token.header.kid; + const jwksClient = getJwksClient(token, cacheTime); + const key = await jwksClient.getSigningKey(kid); + return key.getPublicKey(); +}; + +const hasIss = ( + payload: string | JwtPayload +): payload is JwtPayload & { iss: string } => { + return typeof payload === "object" && payload.iss !== undefined; +}; + +const getJwksClient = (token: Jwt, cacheTime: number): JwksClient => { + if (!hasIss(token.payload)) { + throw new Error(""); + } + const iss = token.payload.iss; + if (!jwksClients[iss]) { + jwksClients[iss] = JwksRSA({ + cache: true, + cacheMaxEntries: 5, + cacheMaxAge: cacheTime * 36e5, + jwksUri: token.payload.iss + ".well-known/jwks.json", + }); + } + return jwksClients[iss]; +}; diff --git a/lib/lib-auth/tsconfig.es.json b/lib/lib-auth/tsconfig.es.json new file mode 100644 index 0000000..4568f0b --- /dev/null +++ b/lib/lib-auth/tsconfig.es.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.es.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist-es", + "rootDir": "src", + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/lib/lib-auth/tsconfig.json b/lib/lib-auth/tsconfig.json new file mode 100644 index 0000000..888903a --- /dev/null +++ b/lib/lib-auth/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.cjs.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist-cjs", + "rootDir": "src", + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/yarn.lock b/yarn.lock index 4fce5c8..f1a5714 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1050,11 +1050,58 @@ "@tufjs/canonical-json" "1.0.0" minimatch "^9.0.0" +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.17.35" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f" + integrity sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^4.17.14", "@types/express@^4.17.17": + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" + integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.1.tgz#20172f9578b225f6c7da63446f56d4ce108d5a65" + integrity sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ== + "@types/json-schema@^7.0.9": version "7.0.12" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== +"@types/jsonwebtoken@^9.0.0", "@types/jsonwebtoken@^9.0.2": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#9eeb56c76dd555039be2a3972218de5bd3b8d83e" + integrity sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q== + dependencies: + "@types/node" "*" + "@types/lodash@^4.14.189": version "4.14.195" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632" @@ -1065,6 +1112,16 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== +"@types/mime@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + "@types/minimatch@^3.0.3": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" @@ -1100,11 +1157,38 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + "@types/semver@^7.3.12": version "7.5.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== +"@types/send@*": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301" + integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.2" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.2.tgz#3e5419ecd1e40e7405d34093f10befb43f63381a" + integrity sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw== + dependencies: + "@types/http-errors" "*" + "@types/mime" "*" + "@types/node" "*" + "@types/triple-beam@^1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.2.tgz#38ecb64f01aa0d02b7c8f4222d7c38af6316fef8" @@ -1239,6 +1323,14 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1402,6 +1494,11 @@ array-differ@^3.0.0: resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + array-ify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" @@ -1492,6 +1589,24 @@ bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1514,6 +1629,11 @@ braces@^3.0.2: dependencies: fill-range "^7.0.1" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -1552,6 +1672,11 @@ byte-size@7.0.0: resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-7.0.0.tgz#36528cd1ca87d39bd9abd51f5715dc93b6ceb032" integrity sha512-NNiBxKgxybMBtWdmvx7ZITJi4ZG+CYUgwOSZTfqB1qogkRHrhbQE/R2r5Fh94X+InN5MCYz6SvB/ejHMj/HbsQ== +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + cacache@^15.2.0: version "15.3.0" resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" @@ -1618,6 +1743,14 @@ cacache@^17.0.0, cacache@^17.0.4: tar "^6.1.11" unique-filename "^3.0.0" +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1933,6 +2066,18 @@ console-control-strings@^1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + conventional-changelog-angular@5.0.12: version "5.0.12" resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.12.tgz#c979b8b921cbfe26402eb3da5bbfda02d865a2b9" @@ -2032,6 +2177,16 @@ conventional-recommended-bump@6.1.0: meow "^8.0.0" q "^1.5.1" +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -2120,6 +2275,13 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -2186,7 +2348,7 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== -depd@^2.0.0: +depd@2.0.0, depd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -2196,6 +2358,11 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" @@ -2271,6 +2438,18 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + ejs@^3.1.7: version "3.1.9" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" @@ -2293,6 +2472,11 @@ enabled@2.0.x: resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + encoding@^0.1.12, encoding@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" @@ -2341,6 +2525,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -2472,6 +2661,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" @@ -2537,6 +2731,43 @@ exponential-backoff@^3.1.1: resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== +express@^4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + external-editor@^3.0.3: version "3.1.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" @@ -2633,6 +2864,19 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + find-up@5.0.0, find-up@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" @@ -2701,6 +2945,16 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -2782,6 +3036,16 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-intrinsic@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-proto "^1.0.1" + has-symbols "^1.0.3" + get-pkg-repo@^4.0.0: version "4.2.1" resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz#75973e1c8050c73f48190c52047c4cee3acbf385" @@ -2999,6 +3263,16 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + has-unicode@2.0.1, has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -3054,6 +3328,17 @@ http-cache-semantics@^4.1.0, http-cache-semantics@^4.1.1: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + http-proxy-agent@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" @@ -3102,7 +3387,7 @@ husky@^8.0.0: resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== -iconv-lite@^0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -3184,7 +3469,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3254,6 +3539,11 @@ ip@^2.0.0: resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -3448,6 +3738,11 @@ joi@^17.7.0: "@sideway/formula" "^3.0.1" "@sideway/pinpoint" "^2.0.0" +jose@^4.10.4: + version "4.14.4" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.4.tgz#59e09204e2670c3164ee24cbfe7115c6f8bff9ca" + integrity sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -3532,6 +3827,16 @@ jsonparse@^1.2.0, jsonparse@^1.3.1: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== +jsonwebtoken@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#81d8c901c112c24e497a55daf6b2be1225b40145" + integrity sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg== + dependencies: + jws "^3.2.2" + lodash "^4.17.21" + ms "^2.1.1" + semver "^7.3.8" + just-diff-apply@^5.2.0: version "5.5.0" resolved "https://registry.yarnpkg.com/just-diff-apply/-/just-diff-apply-5.5.0.tgz#771c2ca9fa69f3d2b54e7c3f5c1dfcbcc47f9f0f" @@ -3542,6 +3847,35 @@ just-diff@^6.0.0: resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-6.0.2.tgz#03b65908543ac0521caf6d8eb85035f7d27ea285" integrity sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA== +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jwks-rsa@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-3.0.1.tgz#ba79ddca7ee7520f7bb26b942ef1aee91df8d7e4" + integrity sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw== + dependencies: + "@types/express" "^4.17.14" + "@types/jsonwebtoken" "^9.0.0" + debug "^4.3.4" + jose "^4.10.4" + limiter "^1.1.5" + lru-memoizer "^2.1.4" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -3685,6 +4019,11 @@ lilconfig@2.1.0: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== +limiter@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" + integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -3775,6 +4114,11 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + lodash.isfunction@^3.0.9: version "3.0.9" resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" @@ -3887,6 +4231,22 @@ lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.0.tgz#b9e2a6a72a129d81ab317202d93c7691df727e61" integrity sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw== +lru-cache@~4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e" + integrity sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw== + dependencies: + pseudomap "^1.0.1" + yallist "^2.0.0" + +lru-memoizer@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/lru-memoizer/-/lru-memoizer-2.2.0.tgz#b9d90c91637b4b1a423ef76f3156566691293df8" + integrity sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw== + dependencies: + lodash.clonedeep "^4.5.0" + lru-cache "~4.0.0" + lunr@^2.3.9: version "2.3.9" resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" @@ -3992,6 +4352,11 @@ marked@^4.2.12: resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + meow@^8.0.0: version "8.1.2" resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" @@ -4009,6 +4374,11 @@ meow@^8.0.0: type-fest "^0.18.0" yargs-parser "^20.2.3" +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -4019,6 +4389,11 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" @@ -4032,13 +4407,18 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12: +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -4235,12 +4615,17 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.0.0, ms@^2.1.1: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -4280,7 +4665,7 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -negotiator@^0.6.2, negotiator@^0.6.3: +negotiator@0.6.3, negotiator@^0.6.2, negotiator@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== @@ -4616,11 +5001,18 @@ object-hash@^3.0.0: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== -object-inspect@^1.12.3: +object-inspect@^1.12.3, object-inspect@^1.9.0: version "1.12.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + once@^1.3.0, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -4911,6 +5303,11 @@ parse5@^6.0.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -4949,6 +5346,11 @@ path-scurry@^1.10.0, path-scurry@^1.6.1: lru-cache "^9.1.1 || ^10.0.0" minipass "^5.0.0 || ^6.0.2" +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -5134,11 +5536,24 @@ protocols@^2.0.0, protocols@^2.0.1: resolved "https://registry.yarnpkg.com/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86" integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q== +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +pseudomap@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== + punycode@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" @@ -5149,6 +5564,13 @@ q@^1.5.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -5159,6 +5581,21 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + react-is@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" @@ -5420,16 +5857,16 @@ rxjs@^7.0.0, rxjs@^7.5.5, rxjs@^7.8.0: dependencies: tslib "^2.1.0" +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - safe-stable-stringify@^2.3.1: version "2.4.3" resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" @@ -5488,11 +5925,45 @@ semver@^7.0.0, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semve dependencies: lru-cache "^6.0.0" +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -5527,6 +5998,15 @@ shiki@^0.14.1: vscode-oniguruma "^1.7.0" vscode-textmate "^8.0.0" +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@3.0.7, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -5710,6 +6190,11 @@ stack-trace@0.0.x: resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + string-argv@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" @@ -5954,6 +6439,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + "topcoder-interface@github:topcoder-platform/plat-interface-definition#v0.0.56": version "1.0.0" resolved "https://codeload.github.com/topcoder-platform/plat-interface-definition/tar.gz/ba65ef794552c35ffa86346ca29110531833bcc0" @@ -6107,6 +6597,14 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -6203,6 +6701,11 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + upath@2.0.1, upath@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" @@ -6220,6 +6723,11 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + uuid@8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -6264,6 +6772,11 @@ validate-npm-package-name@^5.0.0: dependencies: builtins "^5.0.0" +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + vscode-oniguruma@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b" @@ -6461,6 +6974,11 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yallist@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" From 1f2a86aac15aa93f4d311be78a6c94734793f351 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Mon, 24 Jul 2023 01:03:23 +0300 Subject: [PATCH 02/10] feat: update authenticator --- lib/lib-auth/src/UnauthorizedError.ts | 21 +- lib/lib-auth/src/authenticator.ts | 388 ++++++++++++++++++++++---- 2 files changed, 329 insertions(+), 80 deletions(-) diff --git a/lib/lib-auth/src/UnauthorizedError.ts b/lib/lib-auth/src/UnauthorizedError.ts index da32339..b2d50ff 100644 --- a/lib/lib-auth/src/UnauthorizedError.ts +++ b/lib/lib-auth/src/UnauthorizedError.ts @@ -1,23 +1,6 @@ -export type ErrorLike = Error | { message: string }; - -type ErrorCode = - | "credentials_bad_scheme" - | "credentials_bad_format" - | "credentials_required" - | "invalid_token" - | "revoked_token"; - export class UnauthorizedError extends Error { - readonly status: number; - readonly inner: ErrorLike; - readonly code: string; - - constructor(code: ErrorCode, error: ErrorLike) { - super(error.message); - Object.setPrototypeOf(this, UnauthorizedError.prototype); - this.code = code; - this.status = 401; + constructor(message: string) { + super(message); this.name = "UnauthorizedError"; - this.inner = error; } } diff --git a/lib/lib-auth/src/authenticator.ts b/lib/lib-auth/src/authenticator.ts index 255b6ae..ec28478 100644 --- a/lib/lib-auth/src/authenticator.ts +++ b/lib/lib-auth/src/authenticator.ts @@ -1,7 +1,14 @@ import { Request, Response, NextFunction } from "express"; -import { Jwt, JwtPayload, Secret, VerifyOptions, verify } from "jsonwebtoken"; -import JwksRSA, { JwksClient } from "jwks-rsa"; -import _ from "lodash"; +import { + Jwt, + JwtPayload, + Secret, + VerifyOptions, + decode, + verify, +} from "jsonwebtoken"; +import JwksRSA, { JwksClient, SigningKey } from "jwks-rsa"; +import _, { set } from "lodash"; import { UnauthorizedError } from "./UnauthorizedError"; const jwksClients: { [iss: string]: JwksClient } = {}; @@ -30,25 +37,31 @@ export type TokenGetter = ( ) => string | Promise | undefined; export type AuthOptions = { - test: string; method?: AuthMethod; secret?: Secret | GetVerificationKey; algorithms?: Algorithm[]; /** - * If sets to true, continue to the next middleware when the - * request doesn't include a token without failing. + * If you set it to false, the system will proceed to the next middleware + * if the request does not include a token, without causing an error. + * @defaultValue `true` * - * @default true */ credentialsRequired?: boolean; jwtKeyCacheTimeInHours?: number; + /** + * This feature allows you to personalize the name of the property + * in the request object where the decoded payload is stored. + * @defaultValue `authUser` + */ + requestProperty?: string; cookieName?: string; getToken?: TokenGetter; isRevoked?: IsRevoked; -} & Pick; +} & Required> & + Pick; export enum AuthMethod { - "Bearer", + "Bearer" = 1, } export type Algorithm = RSA | HSA; @@ -56,74 +69,212 @@ export type Algorithm = RSA | HSA; type HSA = "HS256" | "HS384" | "HS512"; type RSA = "RS256" | "RS384" | "RS512"; -const isHSA = (algorithm: Algorithm): algorithm is HSA => - _.includes(["HS256", "HS384", "HS512"], algorithm); - -const isRSA = (algorithm: Algorithm): algorithm is RSA => - _.includes(["RS256", "RS384", "RS512"], algorithm); +export type ExpressAuthMiddleware = ( + req: Request, + res: Response, + next: NextFunction +) => Promise; -const bearerTokenGetter = (req: Request): string | undefined => { - if (req.headers && req.headers.authorization) { - const parts = req.headers.authorization.split(" "); - if (parts.length == 2) { - const scheme = parts[0]; - const credentials = parts[1]; +export const authenticator = (options: AuthOptions): ExpressAuthMiddleware => { + const method: AuthMethod = validateAndGetMethod(options.method); + const algorithms = validateAndGetAlgorithms(options.algorithms); + const issuers = validateAndGetIssuers(options.issuer); + const secretGetter = validateAndGetSecret(options.secret); + if (_.some(algorithms, isHSA) && _.isUndefined(secretGetter)) { + throw new RangeError("lib-auth: secret is required when algorithm is HSA"); + } + const tokenGetter: TokenGetter = validateAndGetTokenGetter( + method, + options.getToken + ); + const credentialsRequired = _.defaultTo(options.credentialsRequired, true); + const jwtKeyCacheTimeInHours = validateAndGetCacheTime( + options.jwtKeyCacheTimeInHours + ); + const requestProperty = validateAndGetRequestProperty( + options.requestProperty + ); - if (/^Bearer$/i.test(scheme)) { - return credentials; + const middleware = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + try { + const token = await tokenGetter(req); + if (!token) { + if (credentialsRequired) { + throw new UnauthorizedError("No authorization token was found"); + } else { + return next(); + } + } + const decodedToken = decode(token, { complete: true }); + if (decodedToken === null) { + throw new UnauthorizedError("Invalid Token"); + } + const algorithm = decodedToken.header.alg; + if (!_.includes(algorithms, algorithm)) { + throw new UnauthorizedError("Invalid Algorithm"); + } + if (isHSA(algorithm)) { + await verifyHSAToken( + req, + token, + decodedToken, + [algorithm], + options.audience, + secretGetter + ); + } else if (isRSA(algorithm)) { + await verifyRSAToken( + token, + decodedToken, + jwtKeyCacheTimeInHours, + algorithms, + issuers, + options.audience + ); } else { - throw new UnauthorizedError("credentials_bad_scheme", { - message: "Format is Authorization: Bearer [token]", - }); + throw new UnauthorizedError("Invalid Algorithm"); } - } else { - throw new UnauthorizedError("credentials_bad_format", { - message: "Format is Authorization: Bearer [token]", - }); + if (options.isRevoked && (await options.isRevoked(req, decodedToken))) { + throw new UnauthorizedError("The token has been revoked."); + } + set(req, requestProperty, buildRequestProperty(decodedToken.payload)); + next(); + } catch (err) { + next(err); } - } else { - throw new UnauthorizedError("credentials_bad_format", { - message: "Format is Authorization: Bearer [token]", + }; + return middleware; +}; + +const verifyHSAToken = async ( + req: Request, + token: string, + decodedToken: Jwt, + algorithms: Algorithm[], + audience?: string | RegExp | (string | RegExp)[], + secretGetter?: GetVerificationKey +) => { + let secret; + if (secretGetter !== undefined) { + secret = await secretGetter(req, decodedToken); + } + if (secret === undefined) { + throw new UnauthorizedError(""); + } + try { + verify(token, secret, { + audience, + algorithms, }); + } catch { + throw new UnauthorizedError(""); } }; -const defaultOptions = { - method: AuthMethod.Bearer, - algorithms: ["HS256", "RS256"], - credentialsRequired: true, - jwtKeyCacheTimeInHours: 24, - getToken: bearerTokenGetter, +const verifyRSAToken = async ( + token: string, + decodedToken: Jwt, + cacheTime: number, + algorithms: Algorithm[], + validIsuers: string[], + audience?: string | RegExp | (string | RegExp)[] +) => { + const key = await getRSAKey(decodedToken, cacheTime, validIsuers); + try { + verify(token, key, { + audience, + algorithms, + }); + } catch { + throw new UnauthorizedError(""); + } }; -export const authenticator = (options: AuthOptions) => { - const authOptions = { - ...defaultOptions, - ...options, - }; - if (_.some(options.algorithms, isHSA) && _.isUndefined(authOptions.secret)) { - throw new RangeError("lib-auth: secret is required when algorithm is HSA"); +type AuthProperty = { + userId: number; + handle: string; + roles: string[]; + email: string; + isMachine: boolean; + azpHash?: number; +}; + +const buildRequestProperty = (token: JwtPayload): AuthProperty => { + const payload: Partial = {}; + payload.userId = _.find(token, (_v, k) => _.includes(k, "userId")); + payload.handle = _.find(token, (_v, k) => _.includes(k, "handle")); + payload.roles = _.find(token, (_v, k) => _.includes(k, "roles")); + if (!token.email) { + payload.email = _.find(token, (_v, k) => _.includes(k, "email")); } + const scopes = _.find(token, (_v, k) => _.includes(k, "scope")); + if (scopes) { + payload.scopes = scopes.split(" "); - const middleware = (req: Request, res: Response, next: NextFunction) => { - const token = authOptions.getToken(req); - if (!token) { - if (authOptions.credentialsRequired) { - throw new UnauthorizedError("credentials_required", { - message: "No authorization token was found", - }); - } else { - return next(); + const grantType = _.find(token, (_v, k) => _.includes(k, "gty")); + if ( + grantType === "client-credentials" && + !payload.userId && + !payload.roles + ) { + payload.isMachine = true; + payload.userId = 0; + payload.handle = ""; + try { + payload.azpHash = getAzpHash(token.azp); + } catch { + throw new UnauthorizedError("AZP not provided."); } } - }; - return middleware; + } + return payload; +}; + +const isValidAuthMethod = (method: unknown): method is AuthMethod => + _.isNumber(method) && method in AuthMethod; + +const isValidAlgorithm = (algorithm: unknown): algorithm is Algorithm => + _.includes(["HS256", "HS384", "HS512", "RS256", "RS384", "RS512"], algorithm); + +const isValidAlgorithms = (algorithms: unknown[]): algorithms is Algorithm[] => + _.every(algorithms, isValidAlgorithm); + +const isHSA = (algorithm: unknown): algorithm is HSA => + _.includes(["HS256", "HS384", "HS512"], algorithm); + +const isRSA = (algorithm: unknown): algorithm is RSA => + _.includes(["RS256", "RS384", "RS512"], algorithm); + +const bearerTokenGetter = (req: Request): string | undefined => { + if (req.headers && req.headers.authorization) { + const parts = req.headers.authorization.split(" "); + if (parts.length == 2) { + const scheme = parts[0]; + const credentials = parts[1]; + if (/^Bearer$/i.test(scheme)) { + return credentials; + } + } + } }; -const getRSAKey = async (token: Jwt, cacheTime: number) => { +const getRSAKey = async ( + token: Jwt, + cacheTime: number, + validIssuers?: string[] +) => { const kid = token.header.kid; - const jwksClient = getJwksClient(token, cacheTime); - const key = await jwksClient.getSigningKey(kid); + const jwksClient = getJwksClient(token, cacheTime, validIssuers); + let key: SigningKey; + try { + key = await jwksClient.getSigningKey(kid); + } catch { + throw new UnauthorizedError(""); + } return key.getPublicKey(); }; @@ -133,9 +284,16 @@ const hasIss = ( return typeof payload === "object" && payload.iss !== undefined; }; -const getJwksClient = (token: Jwt, cacheTime: number): JwksClient => { +const getJwksClient = ( + token: Jwt, + cacheTime: number, + validIssuers?: string[] +): JwksClient => { if (!hasIss(token.payload)) { - throw new Error(""); + throw new UnauthorizedError(""); + } + if (validIssuers && !_.includes(validIssuers, token.payload.iss)) { + throw new UnauthorizedError("Invalid token issuer."); } const iss = token.payload.iss; if (!jwksClients[iss]) { @@ -148,3 +306,111 @@ const getJwksClient = (token: Jwt, cacheTime: number): JwksClient => { } return jwksClients[iss]; }; + +const getAzpHash = (azp: string) => { + if (!azp || azp.length === 0) { + throw new Error("AZP not provided."); + } + // default offset value + let azphash = 100000; + for (let i = 0; i < azp.length; i++) { + const v = azp.charCodeAt(i); + azphash += v * (i + 1); + } + return azphash * -1; +}; + +const validateAndGetAlgorithms = (algorithms?: unknown[]): Algorithm[] => { + if (algorithms === undefined) { + return ["HS256", "RS256"]; + } + if (_.isEmpty(algorithms)) { + throw new RangeError(""); + } + if (!isValidAlgorithms(algorithms)) { + throw new RangeError(""); + } + return algorithms; +}; + +const validateAndGetMethod = (method?: unknown): AuthMethod => { + if (method === undefined) { + return AuthMethod.Bearer; + } + if (isValidAuthMethod(method)) { + return method; + } else { + throw new RangeError(""); + } +}; + +const validateAndGetTokenGetter = ( + method: AuthMethod, + getToken?: unknown +): TokenGetter => { + if (getToken === undefined) { + return getDefaultTokenGetter(method); + } + if (typeof getToken === "function") { + return getToken as TokenGetter; + } else { + throw new RangeError(""); + } +}; + +const validateAndGetCacheTime = (cacheTime?: unknown): number => { + if (cacheTime === undefined) { + return 24; + } + if (_.isNumber(cacheTime)) { + return cacheTime; + } else { + throw new RangeError(""); + } +}; + +const validateAndGetRequestProperty = (requestProperty?: unknown): string => { + if (requestProperty === undefined) { + return "authUser"; + } + if (_.isString(requestProperty)) { + return requestProperty; + } else { + throw new RangeError(""); + } +}; + +const validateAndGetSecret = ( + secret?: unknown +): GetVerificationKey | undefined => { + if (secret === undefined) { + return undefined; + } else if (typeof secret === "function") { + return secret as GetVerificationKey; + } else { + return () => secret as Secret; + } +}; + +const validateAndGetIssuers = (issuer: unknown): string[] => { + if (issuer === undefined) { + throw new RangeError(""); + } else if (_.isString(issuer)) { + return [issuer]; + } else if ( + _.isArray(issuer) && + !_.isEmpty(issuer) && + _.every(issuer, (iss) => _.isString(iss)) + ) { + return issuer as string[]; + } else { + throw new RangeError(""); + } +}; + +const getDefaultTokenGetter = (method: AuthMethod): TokenGetter => { + switch (method) { + case AuthMethod.Bearer: + return bearerTokenGetter; + } +}; From b15bb67878e20bba8b4f67f6a7f26d6eaf476859 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 26 Jul 2023 11:39:37 +0300 Subject: [PATCH 03/10] feat: add permission verification --- lib/lib-auth/.eslintrc | 3 + lib/lib-auth/src/authenticator.ts | 132 +++++++++++++++++------------- 2 files changed, 79 insertions(+), 56 deletions(-) diff --git a/lib/lib-auth/.eslintrc b/lib/lib-auth/.eslintrc index 644bb1a..c32f262 100644 --- a/lib/lib-auth/.eslintrc +++ b/lib/lib-auth/.eslintrc @@ -2,5 +2,8 @@ "extends": "../../.eslintrc", "parserOptions": { "project": "tsconfig.json" + }, + "rules": { + "@typescript-eslint/no-unsafe-assignment": "off" } } diff --git a/lib/lib-auth/src/authenticator.ts b/lib/lib-auth/src/authenticator.ts index ec28478..0c0a233 100644 --- a/lib/lib-auth/src/authenticator.ts +++ b/lib/lib-auth/src/authenticator.ts @@ -8,7 +8,7 @@ import { verify, } from "jsonwebtoken"; import JwksRSA, { JwksClient, SigningKey } from "jwks-rsa"; -import _, { set } from "lodash"; +import _ from "lodash"; import { UnauthorizedError } from "./UnauthorizedError"; const jwksClients: { [iss: string]: JwksClient } = {}; @@ -54,9 +54,9 @@ export type AuthOptions = { * @defaultValue `authUser` */ requestProperty?: string; - cookieName?: string; getToken?: TokenGetter; isRevoked?: IsRevoked; + claimScope: string; } & Required> & Pick; @@ -72,9 +72,15 @@ type RSA = "RS256" | "RS384" | "RS512"; export type ExpressAuthMiddleware = ( req: Request, res: Response, - next: NextFunction + next: NextFunction, + permissions?: Permissions ) => Promise; +export type Permissions = { + allowedRoles?: string[]; + allowedScopes?: string[]; +}; + export const authenticator = (options: AuthOptions): ExpressAuthMiddleware => { const method: AuthMethod = validateAndGetMethod(options.method); const algorithms = validateAndGetAlgorithms(options.algorithms); @@ -94,23 +100,21 @@ export const authenticator = (options: AuthOptions): ExpressAuthMiddleware => { const requestProperty = validateAndGetRequestProperty( options.requestProperty ); + const claimScope = _.toString(options.claimScope); const middleware = async ( req: Request, res: Response, - next: NextFunction + next: NextFunction, + permissions?: Permissions ) => { try { const token = await tokenGetter(req); if (!token) { - if (credentialsRequired) { - throw new UnauthorizedError("No authorization token was found"); - } else { - return next(); - } + throw new UnauthorizedError("No authorization token was found"); } const decodedToken = decode(token, { complete: true }); - if (decodedToken === null) { + if (decodedToken === null || typeof decodedToken.payload == "string") { throw new UnauthorizedError("Invalid Token"); } const algorithm = decodedToken.header.alg; @@ -141,15 +145,53 @@ export const authenticator = (options: AuthOptions): ExpressAuthMiddleware => { if (options.isRevoked && (await options.isRevoked(req, decodedToken))) { throw new UnauthorizedError("The token has been revoked."); } - set(req, requestProperty, buildRequestProperty(decodedToken.payload)); + const authProperty: AuthProperty = buildRequestProperty( + decodedToken.payload, + claimScope + ); + _.set(req, requestProperty, authProperty); + if (credentialsRequired && permissions) { + validatePermissions(authProperty, permissions); + } next(); } catch (err) { - next(err); + if (credentialsRequired) { + next(err); + } else { + next(); + } } }; return middleware; }; +const validatePermissions = (auth: AuthProperty, permissions: Permissions) => { + if (auth.isMachine) { + if (permissions.allowedScopes) { + if ( + _.some(permissions.allowedScopes, (scope) => + _.includes(auth.scopes, scope) + ) + ) { + return true; + } + } else { + return true; + } + } else { + if (permissions.allowedRoles) { + if ( + _.some(permissions.allowedRoles, (role) => _.includes(auth.roles, role)) + ) { + return true; + } + } else { + return true; + } + } + throw new UnauthorizedError(""); +}; + const verifyHSAToken = async ( req: Request, token: string, @@ -195,40 +237,31 @@ const verifyRSAToken = async ( }; type AuthProperty = { - userId: number; - handle: string; - roles: string[]; - email: string; + userId?: string; + handle?: string; + roles?: string[]; + email?: string; isMachine: boolean; - azpHash?: number; + scopes?: string[]; }; -const buildRequestProperty = (token: JwtPayload): AuthProperty => { - const payload: Partial = {}; - payload.userId = _.find(token, (_v, k) => _.includes(k, "userId")); - payload.handle = _.find(token, (_v, k) => _.includes(k, "handle")); - payload.roles = _.find(token, (_v, k) => _.includes(k, "roles")); - if (!token.email) { - payload.email = _.find(token, (_v, k) => _.includes(k, "email")); - } - const scopes = _.find(token, (_v, k) => _.includes(k, "scope")); - if (scopes) { - payload.scopes = scopes.split(" "); - - const grantType = _.find(token, (_v, k) => _.includes(k, "gty")); - if ( - grantType === "client-credentials" && - !payload.userId && - !payload.roles - ) { - payload.isMachine = true; - payload.userId = 0; - payload.handle = ""; - try { - payload.azpHash = getAzpHash(token.azp); - } catch { - throw new UnauthorizedError("AZP not provided."); - } +const buildRequestProperty = ( + token: JwtPayload, + claimScope: string +): AuthProperty => { + const payload: AuthProperty = { + userId: token[claimScope + "/userId"], + handle: token[claimScope + "/handle"], + roles: token[claimScope + "/roles"], + email: token.email ?? token[claimScope + "/email"], + isMachine: token.gty === "client-credentials", + }; + if (payload.isMachine) { + const scopes = token[claimScope + "/scope"]; + if (typeof scopes === "string") { + payload.scopes = scopes.split(" "); + } else if (_.isArray(scopes)) { + payload.scopes = scopes; } } return payload; @@ -307,19 +340,6 @@ const getJwksClient = ( return jwksClients[iss]; }; -const getAzpHash = (azp: string) => { - if (!azp || azp.length === 0) { - throw new Error("AZP not provided."); - } - // default offset value - let azphash = 100000; - for (let i = 0; i < azp.length; i++) { - const v = azp.charCodeAt(i); - azphash += v * (i + 1); - } - return azphash * -1; -}; - const validateAndGetAlgorithms = (algorithms?: unknown[]): Algorithm[] => { if (algorithms === undefined) { return ["HS256", "RS256"]; From f3a9a7397405bb61b402912912bd966eaea3ba0d Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 26 Jul 2023 12:55:47 +0300 Subject: [PATCH 04/10] feat: add ErrorHelper to lib-common --- lib/lib-common/src/util/ErrorHelper.ts | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 lib/lib-common/src/util/ErrorHelper.ts diff --git a/lib/lib-common/src/util/ErrorHelper.ts b/lib/lib-common/src/util/ErrorHelper.ts new file mode 100644 index 0000000..c5097f2 --- /dev/null +++ b/lib/lib-common/src/util/ErrorHelper.ts @@ -0,0 +1,29 @@ +import { Metadata, status, StatusBuilder, StatusObject } from "@grpc/grpc-js"; + +class ErrorHelper { + public static wrapError(error: GrpcError): Partial { + if (error.code && error.details) { + return error; + } + + const metadata = new Metadata(); + + if (error.name) { + metadata.set("name", error.name); + } + + if (error.stack) { + metadata.set("stack", error.stack); + } + + return new StatusBuilder() + .withCode(error.code || status.INTERNAL) + .withDetails(error.details || error.message || "Internal Server Error") + .withMetadata(metadata) + .build(); + } +} + +export type GrpcError = Partial & Partial; + +export default ErrorHelper; From 9635bcb0513e49ca73a73f13b025345efe651f43 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 26 Jul 2023 13:08:12 +0300 Subject: [PATCH 05/10] feat: export error helper --- lib/lib-common/src/index.ts | 1 + lib/lib-common/src/util/ErrorHelper.ts | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/lib-common/src/index.ts b/lib/lib-common/src/index.ts index bc1f321..521eb3e 100644 --- a/lib/lib-common/src/index.ts +++ b/lib/lib-common/src/index.ts @@ -3,3 +3,4 @@ export * from "./models/google/protobuf/empty"; export * from "./models/google/protobuf/struct"; export * from "./models/google/protobuf/timestamp"; export * from "./util/DomainHelper"; +export * from "./util/ErrorHelper"; diff --git a/lib/lib-common/src/util/ErrorHelper.ts b/lib/lib-common/src/util/ErrorHelper.ts index c5097f2..6494a60 100644 --- a/lib/lib-common/src/util/ErrorHelper.ts +++ b/lib/lib-common/src/util/ErrorHelper.ts @@ -1,6 +1,6 @@ import { Metadata, status, StatusBuilder, StatusObject } from "@grpc/grpc-js"; -class ErrorHelper { +export class ErrorHelper { public static wrapError(error: GrpcError): Partial { if (error.code && error.details) { return error; @@ -25,5 +25,3 @@ class ErrorHelper { } export type GrpcError = Partial & Partial; - -export default ErrorHelper; From 714bc140fcc9472014df195fa13e936053f08dd8 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 26 Jul 2023 14:32:53 +0300 Subject: [PATCH 06/10] feat: add lib-interceptor --- lib/lib-interceptor/.eslintignore | 1 + lib/lib-interceptor/.eslintrc | 12 ++++ lib/lib-interceptor/.gitignore | 8 +++ lib/lib-interceptor/LICENSE.md | 21 +++++++ lib/lib-interceptor/README.md | 3 + lib/lib-interceptor/package.json | 35 +++++++++++ lib/lib-interceptor/src/InterceptorWrapper.ts | 62 +++++++++++++++++++ lib/lib-interceptor/src/index.ts | 2 + .../src/interfaces/Interceptor.ts | 20 ++++++ lib/lib-interceptor/src/interfaces/index.ts | 1 + lib/lib-interceptor/tsconfig.es.json | 11 ++++ lib/lib-interceptor/tsconfig.json | 11 ++++ 12 files changed, 187 insertions(+) create mode 100644 lib/lib-interceptor/.eslintignore create mode 100644 lib/lib-interceptor/.eslintrc create mode 100644 lib/lib-interceptor/.gitignore create mode 100644 lib/lib-interceptor/LICENSE.md create mode 100644 lib/lib-interceptor/README.md create mode 100644 lib/lib-interceptor/package.json create mode 100644 lib/lib-interceptor/src/InterceptorWrapper.ts create mode 100644 lib/lib-interceptor/src/index.ts create mode 100644 lib/lib-interceptor/src/interfaces/Interceptor.ts create mode 100644 lib/lib-interceptor/src/interfaces/index.ts create mode 100644 lib/lib-interceptor/tsconfig.es.json create mode 100644 lib/lib-interceptor/tsconfig.json diff --git a/lib/lib-interceptor/.eslintignore b/lib/lib-interceptor/.eslintignore new file mode 100644 index 0000000..5924ae3 --- /dev/null +++ b/lib/lib-interceptor/.eslintignore @@ -0,0 +1 @@ +src/typings/ \ No newline at end of file diff --git a/lib/lib-interceptor/.eslintrc b/lib/lib-interceptor/.eslintrc new file mode 100644 index 0000000..624854f --- /dev/null +++ b/lib/lib-interceptor/.eslintrc @@ -0,0 +1,12 @@ +{ + "extends": "../../.eslintrc", + "parserOptions": { + "project": "tsconfig.json" + }, + "rules": { + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unsafe-argument": "off" + } +} \ No newline at end of file diff --git a/lib/lib-interceptor/.gitignore b/lib/lib-interceptor/.gitignore new file mode 100644 index 0000000..3d1714c --- /dev/null +++ b/lib/lib-interceptor/.gitignore @@ -0,0 +1,8 @@ +/node_modules/ +/build/ +/coverage/ +/docs/ +*.tsbuildinfo +*.tgz +*.log +package-lock.json diff --git a/lib/lib-interceptor/LICENSE.md b/lib/lib-interceptor/LICENSE.md new file mode 100644 index 0000000..283bac5 --- /dev/null +++ b/lib/lib-interceptor/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2023] [Topcoder] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/lib-interceptor/README.md b/lib/lib-interceptor/README.md new file mode 100644 index 0000000..e8c36a4 --- /dev/null +++ b/lib/lib-interceptor/README.md @@ -0,0 +1,3 @@ +# @topcoder-framework/lib-interceptor + +TODO diff --git a/lib/lib-interceptor/package.json b/lib/lib-interceptor/package.json new file mode 100644 index 0000000..ec60979 --- /dev/null +++ b/lib/lib-interceptor/package.json @@ -0,0 +1,35 @@ +{ + "name": "@topcoder-framework/lib-interceptor", + "version": "0.20.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "scripty", + "build:es": "scripty", + "build:app": "scripty", + "build:include:deps": "scripty", + "clean": "scripty", + "lint": "scripty", + "format": "scripty" + }, + "scripty": { + "path": "../../scripts/packages", + "windowsPath": "../../scripts-win/packages" + }, + "author": "Rakib Ansary ", + "license": "ISC", + "volta": { + "node": "18.14.1", + "typescript": "4.9.3", + "npm": "9.1.2" + }, + "dependencies": { + "@grpc/grpc-js": "^1.8.0", + "@topcoder-framework/lib-common": "^0.20.0" + }, + "devDependencies": {}, + "files": [ + "dist-*" + ], + "gitHead": "8c19fdcd36bcccc5e952aa909e4a30caf2943340" +} \ No newline at end of file diff --git a/lib/lib-interceptor/src/InterceptorWrapper.ts b/lib/lib-interceptor/src/InterceptorWrapper.ts new file mode 100644 index 0000000..1d47744 --- /dev/null +++ b/lib/lib-interceptor/src/InterceptorWrapper.ts @@ -0,0 +1,62 @@ +import { + ServerUnaryCall, + ServiceDefinition, + UntypedHandleCall, + UntypedServiceImplementation, + handleUnaryCall, + sendUnaryData, +} from "@grpc/grpc-js"; +import { Interceptor } from "./interfaces"; +import { GrpcError } from "@topcoder-framework/lib-common"; + +type ServerImplementation = { [name: string]: UntypedHandleCall }; + +const wrapCallWithInterceptor = ( + interceptor: Interceptor, + callHandler: handleUnaryCall, + serviceName: string, + method: string +) => { + return function ( + call: ServerUnaryCall, + callback: sendUnaryData + ) { + const newCallback = (error: GrpcError | null, res: any) => { + if (error) { + return interceptor.onError(error, call, callback); + } + return interceptor.onSuccess(res, call, callback); + }; + try { + interceptor.onMessage(call, serviceName, method); + callHandler(call, newCallback); + } catch (err: any) { + interceptor.onError(err, call, callback); + } + }; +}; + +export const wrapServiceWithInterceptors = ( + serviceDefinition: ServiceDefinition, + implementation: ServerImplementation, + serviceName: string, + interceptors: Interceptor[] +): UntypedServiceImplementation => { + const wrappedImplementation: { [key: string]: handleUnaryCall } = + {}; + + for (const method in serviceDefinition) { + let callHandler = implementation[method] as handleUnaryCall; + interceptors.forEach((interceptor: Interceptor) => { + callHandler = wrapCallWithInterceptor( + interceptor, + callHandler, + serviceName, + method + ); + }); + wrappedImplementation[method] = callHandler; + } + + return wrappedImplementation; +}; diff --git a/lib/lib-interceptor/src/index.ts b/lib/lib-interceptor/src/index.ts new file mode 100644 index 0000000..2aec8c0 --- /dev/null +++ b/lib/lib-interceptor/src/index.ts @@ -0,0 +1,2 @@ +export * from "./interfaces"; +export * from "./InterceptorWrapper"; diff --git a/lib/lib-interceptor/src/interfaces/Interceptor.ts b/lib/lib-interceptor/src/interfaces/Interceptor.ts new file mode 100644 index 0000000..9cf71b1 --- /dev/null +++ b/lib/lib-interceptor/src/interfaces/Interceptor.ts @@ -0,0 +1,20 @@ +import { ServerUnaryCall, sendUnaryData } from "@grpc/grpc-js"; +import { GrpcError } from "@topcoder-framework/lib-common"; + +export type Interceptor = { + onMessage: ( + call: ServerUnaryCall, + serviceName: string, + method: string + ) => void; + onSuccess: ( + response: any, + call: ServerUnaryCall, + callback: sendUnaryData + ) => void; + onError: ( + error: GrpcError, + call: ServerUnaryCall, + callback: sendUnaryData + ) => void; +}; diff --git a/lib/lib-interceptor/src/interfaces/index.ts b/lib/lib-interceptor/src/interfaces/index.ts new file mode 100644 index 0000000..0fcbb61 --- /dev/null +++ b/lib/lib-interceptor/src/interfaces/index.ts @@ -0,0 +1 @@ +export * from "./Interceptor"; diff --git a/lib/lib-interceptor/tsconfig.es.json b/lib/lib-interceptor/tsconfig.es.json new file mode 100644 index 0000000..4568f0b --- /dev/null +++ b/lib/lib-interceptor/tsconfig.es.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.es.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist-es", + "rootDir": "src", + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/lib/lib-interceptor/tsconfig.json b/lib/lib-interceptor/tsconfig.json new file mode 100644 index 0000000..888903a --- /dev/null +++ b/lib/lib-interceptor/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.cjs.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist-cjs", + "rootDir": "src", + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} From f06ff62d04da9192c4aeabc2a9d25b3375aa0d55 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 26 Jul 2023 16:32:58 +0300 Subject: [PATCH 07/10] feat: lib-interceptor: add options --- lib/lib-interceptor/src/InterceptorWrapper.ts | 35 ++++++++----------- .../src/interfaces/Interceptor.ts | 9 +++-- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/lib/lib-interceptor/src/InterceptorWrapper.ts b/lib/lib-interceptor/src/InterceptorWrapper.ts index 1d47744..9c935d8 100644 --- a/lib/lib-interceptor/src/InterceptorWrapper.ts +++ b/lib/lib-interceptor/src/InterceptorWrapper.ts @@ -6,7 +6,7 @@ import { handleUnaryCall, sendUnaryData, } from "@grpc/grpc-js"; -import { Interceptor } from "./interfaces"; +import { Interceptor, OnMessageOptions } from "./interfaces"; import { GrpcError } from "@topcoder-framework/lib-common"; type ServerImplementation = { [name: string]: UntypedHandleCall }; @@ -14,8 +14,7 @@ type ServerImplementation = { [name: string]: UntypedHandleCall }; const wrapCallWithInterceptor = ( interceptor: Interceptor, callHandler: handleUnaryCall, - serviceName: string, - method: string + options?: OnMessageOptions ) => { return function ( call: ServerUnaryCall, @@ -27,35 +26,31 @@ const wrapCallWithInterceptor = ( } return interceptor.onSuccess(res, call, callback); }; - try { - interceptor.onMessage(call, serviceName, method); - callHandler(call, newCallback); - } catch (err: any) { - interceptor.onError(err, call, callback); - } + interceptor + .onMessage(call, options) + .then(() => callHandler(call, newCallback)) + .catch((err: any) => interceptor.onError(err, call, callback)); }; }; export const wrapServiceWithInterceptors = ( serviceDefinition: ServiceDefinition, implementation: ServerImplementation, - serviceName: string, - interceptors: Interceptor[] + interceptors: Interceptor[], + options?: OnMessageOptions ): UntypedServiceImplementation => { const wrappedImplementation: { [key: string]: handleUnaryCall } = {}; - for (const method in serviceDefinition) { - let callHandler = implementation[method] as handleUnaryCall; + for (const methodName in serviceDefinition) { + let callHandler = implementation[methodName] as handleUnaryCall; interceptors.forEach((interceptor: Interceptor) => { - callHandler = wrapCallWithInterceptor( - interceptor, - callHandler, - serviceName, - method - ); + callHandler = wrapCallWithInterceptor(interceptor, callHandler, { + ...options, + methodName, + }); }); - wrappedImplementation[method] = callHandler; + wrappedImplementation[methodName] = callHandler; } return wrappedImplementation; diff --git a/lib/lib-interceptor/src/interfaces/Interceptor.ts b/lib/lib-interceptor/src/interfaces/Interceptor.ts index 9cf71b1..5e57f8f 100644 --- a/lib/lib-interceptor/src/interfaces/Interceptor.ts +++ b/lib/lib-interceptor/src/interfaces/Interceptor.ts @@ -1,12 +1,15 @@ import { ServerUnaryCall, sendUnaryData } from "@grpc/grpc-js"; import { GrpcError } from "@topcoder-framework/lib-common"; +export type OnMessageOptions = { + [key: string]: string | string[] | boolean; +}; + export type Interceptor = { onMessage: ( call: ServerUnaryCall, - serviceName: string, - method: string - ) => void; + options?: OnMessageOptions + ) => Promise; onSuccess: ( response: any, call: ServerUnaryCall, From 1f384ef23f47c8d57f7cdb7664e36db574f544e0 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 26 Jul 2023 16:36:38 +0300 Subject: [PATCH 08/10] feat: add grpc authenticator & refactor --- lib/lib-auth/.eslintrc | 9 +- lib/lib-auth/package.json | 1 + lib/lib-auth/src/authenticator.ts | 436 ------------------ lib/lib-auth/src/common/JwksHelper.ts | 46 ++ lib/lib-auth/src/common/TokenGetter.ts | 24 + lib/lib-auth/src/common/Validators.ts | 124 +++++ lib/lib-auth/src/common/Verifiers.ts | 95 ++++ lib/lib-auth/src/common/index.ts | 4 + .../src/{ => errors}/UnauthorizedError.ts | 0 lib/lib-auth/src/expressAuthenticator.ts | 104 +++++ lib/lib-auth/src/grpcAuthenticator.ts | 99 ++++ lib/lib-auth/src/index.ts | 3 + lib/lib-auth/src/interfaces/Algorithm.ts | 3 + lib/lib-auth/src/interfaces/AuthMethod.ts | 2 + lib/lib-auth/src/interfaces/AuthOptions.ts | 30 ++ lib/lib-auth/src/interfaces/AuthProperty.ts | 8 + .../src/interfaces/ExpressAuthMiddleware.ts | 9 + lib/lib-auth/src/interfaces/IsRevoked.ts | 6 + lib/lib-auth/src/interfaces/Permissions.ts | 4 + lib/lib-auth/src/interfaces/TokenGetter.ts | 8 + lib/lib-auth/src/interfaces/index.ts | 8 + 21 files changed, 585 insertions(+), 438 deletions(-) delete mode 100644 lib/lib-auth/src/authenticator.ts create mode 100644 lib/lib-auth/src/common/JwksHelper.ts create mode 100644 lib/lib-auth/src/common/TokenGetter.ts create mode 100644 lib/lib-auth/src/common/Validators.ts create mode 100644 lib/lib-auth/src/common/Verifiers.ts create mode 100644 lib/lib-auth/src/common/index.ts rename lib/lib-auth/src/{ => errors}/UnauthorizedError.ts (100%) create mode 100644 lib/lib-auth/src/expressAuthenticator.ts create mode 100644 lib/lib-auth/src/grpcAuthenticator.ts create mode 100644 lib/lib-auth/src/index.ts create mode 100644 lib/lib-auth/src/interfaces/Algorithm.ts create mode 100644 lib/lib-auth/src/interfaces/AuthMethod.ts create mode 100644 lib/lib-auth/src/interfaces/AuthOptions.ts create mode 100644 lib/lib-auth/src/interfaces/AuthProperty.ts create mode 100644 lib/lib-auth/src/interfaces/ExpressAuthMiddleware.ts create mode 100644 lib/lib-auth/src/interfaces/IsRevoked.ts create mode 100644 lib/lib-auth/src/interfaces/Permissions.ts create mode 100644 lib/lib-auth/src/interfaces/TokenGetter.ts create mode 100644 lib/lib-auth/src/interfaces/index.ts diff --git a/lib/lib-auth/.eslintrc b/lib/lib-auth/.eslintrc index c32f262..ea6f6c9 100644 --- a/lib/lib-auth/.eslintrc +++ b/lib/lib-auth/.eslintrc @@ -4,6 +4,11 @@ "project": "tsconfig.json" }, "rules": { - "@typescript-eslint/no-unsafe-assignment": "off" + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-explicit-any": "off" } -} +} \ No newline at end of file diff --git a/lib/lib-auth/package.json b/lib/lib-auth/package.json index e0eb9a7..5874b6b 100644 --- a/lib/lib-auth/package.json +++ b/lib/lib-auth/package.json @@ -24,6 +24,7 @@ "npm": "9.1.2" }, "dependencies": { + "@grpc/grpc-js": "^1.8.0", "@types/jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.1", "jwks-rsa": "^3.0.1", diff --git a/lib/lib-auth/src/authenticator.ts b/lib/lib-auth/src/authenticator.ts deleted file mode 100644 index 0c0a233..0000000 --- a/lib/lib-auth/src/authenticator.ts +++ /dev/null @@ -1,436 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { - Jwt, - JwtPayload, - Secret, - VerifyOptions, - decode, - verify, -} from "jsonwebtoken"; -import JwksRSA, { JwksClient, SigningKey } from "jwks-rsa"; -import _ from "lodash"; -import { UnauthorizedError } from "./UnauthorizedError"; - -const jwksClients: { [iss: string]: JwksClient } = {}; - -/** - * A function that defines how to retrieve the verification key given the express request and the JWT. - */ -export type GetVerificationKey = ( - req: Request, - token: Jwt | undefined -) => Secret | undefined | Promise; - -/** - * A function to check if a token is revoked - */ -export type IsRevoked = ( - req: Request, - token: Jwt | undefined -) => boolean | Promise; - -/** - * A function to customize how a token is retrieved from the express request. - */ -export type TokenGetter = ( - req: Request -) => string | Promise | undefined; - -export type AuthOptions = { - method?: AuthMethod; - secret?: Secret | GetVerificationKey; - algorithms?: Algorithm[]; - /** - * If you set it to false, the system will proceed to the next middleware - * if the request does not include a token, without causing an error. - * @defaultValue `true` - * - */ - credentialsRequired?: boolean; - jwtKeyCacheTimeInHours?: number; - /** - * This feature allows you to personalize the name of the property - * in the request object where the decoded payload is stored. - * @defaultValue `authUser` - */ - requestProperty?: string; - getToken?: TokenGetter; - isRevoked?: IsRevoked; - claimScope: string; -} & Required> & - Pick; - -export enum AuthMethod { - "Bearer" = 1, -} - -export type Algorithm = RSA | HSA; - -type HSA = "HS256" | "HS384" | "HS512"; -type RSA = "RS256" | "RS384" | "RS512"; - -export type ExpressAuthMiddleware = ( - req: Request, - res: Response, - next: NextFunction, - permissions?: Permissions -) => Promise; - -export type Permissions = { - allowedRoles?: string[]; - allowedScopes?: string[]; -}; - -export const authenticator = (options: AuthOptions): ExpressAuthMiddleware => { - const method: AuthMethod = validateAndGetMethod(options.method); - const algorithms = validateAndGetAlgorithms(options.algorithms); - const issuers = validateAndGetIssuers(options.issuer); - const secretGetter = validateAndGetSecret(options.secret); - if (_.some(algorithms, isHSA) && _.isUndefined(secretGetter)) { - throw new RangeError("lib-auth: secret is required when algorithm is HSA"); - } - const tokenGetter: TokenGetter = validateAndGetTokenGetter( - method, - options.getToken - ); - const credentialsRequired = _.defaultTo(options.credentialsRequired, true); - const jwtKeyCacheTimeInHours = validateAndGetCacheTime( - options.jwtKeyCacheTimeInHours - ); - const requestProperty = validateAndGetRequestProperty( - options.requestProperty - ); - const claimScope = _.toString(options.claimScope); - - const middleware = async ( - req: Request, - res: Response, - next: NextFunction, - permissions?: Permissions - ) => { - try { - const token = await tokenGetter(req); - if (!token) { - throw new UnauthorizedError("No authorization token was found"); - } - const decodedToken = decode(token, { complete: true }); - if (decodedToken === null || typeof decodedToken.payload == "string") { - throw new UnauthorizedError("Invalid Token"); - } - const algorithm = decodedToken.header.alg; - if (!_.includes(algorithms, algorithm)) { - throw new UnauthorizedError("Invalid Algorithm"); - } - if (isHSA(algorithm)) { - await verifyHSAToken( - req, - token, - decodedToken, - [algorithm], - options.audience, - secretGetter - ); - } else if (isRSA(algorithm)) { - await verifyRSAToken( - token, - decodedToken, - jwtKeyCacheTimeInHours, - algorithms, - issuers, - options.audience - ); - } else { - throw new UnauthorizedError("Invalid Algorithm"); - } - if (options.isRevoked && (await options.isRevoked(req, decodedToken))) { - throw new UnauthorizedError("The token has been revoked."); - } - const authProperty: AuthProperty = buildRequestProperty( - decodedToken.payload, - claimScope - ); - _.set(req, requestProperty, authProperty); - if (credentialsRequired && permissions) { - validatePermissions(authProperty, permissions); - } - next(); - } catch (err) { - if (credentialsRequired) { - next(err); - } else { - next(); - } - } - }; - return middleware; -}; - -const validatePermissions = (auth: AuthProperty, permissions: Permissions) => { - if (auth.isMachine) { - if (permissions.allowedScopes) { - if ( - _.some(permissions.allowedScopes, (scope) => - _.includes(auth.scopes, scope) - ) - ) { - return true; - } - } else { - return true; - } - } else { - if (permissions.allowedRoles) { - if ( - _.some(permissions.allowedRoles, (role) => _.includes(auth.roles, role)) - ) { - return true; - } - } else { - return true; - } - } - throw new UnauthorizedError(""); -}; - -const verifyHSAToken = async ( - req: Request, - token: string, - decodedToken: Jwt, - algorithms: Algorithm[], - audience?: string | RegExp | (string | RegExp)[], - secretGetter?: GetVerificationKey -) => { - let secret; - if (secretGetter !== undefined) { - secret = await secretGetter(req, decodedToken); - } - if (secret === undefined) { - throw new UnauthorizedError(""); - } - try { - verify(token, secret, { - audience, - algorithms, - }); - } catch { - throw new UnauthorizedError(""); - } -}; - -const verifyRSAToken = async ( - token: string, - decodedToken: Jwt, - cacheTime: number, - algorithms: Algorithm[], - validIsuers: string[], - audience?: string | RegExp | (string | RegExp)[] -) => { - const key = await getRSAKey(decodedToken, cacheTime, validIsuers); - try { - verify(token, key, { - audience, - algorithms, - }); - } catch { - throw new UnauthorizedError(""); - } -}; - -type AuthProperty = { - userId?: string; - handle?: string; - roles?: string[]; - email?: string; - isMachine: boolean; - scopes?: string[]; -}; - -const buildRequestProperty = ( - token: JwtPayload, - claimScope: string -): AuthProperty => { - const payload: AuthProperty = { - userId: token[claimScope + "/userId"], - handle: token[claimScope + "/handle"], - roles: token[claimScope + "/roles"], - email: token.email ?? token[claimScope + "/email"], - isMachine: token.gty === "client-credentials", - }; - if (payload.isMachine) { - const scopes = token[claimScope + "/scope"]; - if (typeof scopes === "string") { - payload.scopes = scopes.split(" "); - } else if (_.isArray(scopes)) { - payload.scopes = scopes; - } - } - return payload; -}; - -const isValidAuthMethod = (method: unknown): method is AuthMethod => - _.isNumber(method) && method in AuthMethod; - -const isValidAlgorithm = (algorithm: unknown): algorithm is Algorithm => - _.includes(["HS256", "HS384", "HS512", "RS256", "RS384", "RS512"], algorithm); - -const isValidAlgorithms = (algorithms: unknown[]): algorithms is Algorithm[] => - _.every(algorithms, isValidAlgorithm); - -const isHSA = (algorithm: unknown): algorithm is HSA => - _.includes(["HS256", "HS384", "HS512"], algorithm); - -const isRSA = (algorithm: unknown): algorithm is RSA => - _.includes(["RS256", "RS384", "RS512"], algorithm); - -const bearerTokenGetter = (req: Request): string | undefined => { - if (req.headers && req.headers.authorization) { - const parts = req.headers.authorization.split(" "); - if (parts.length == 2) { - const scheme = parts[0]; - const credentials = parts[1]; - if (/^Bearer$/i.test(scheme)) { - return credentials; - } - } - } -}; - -const getRSAKey = async ( - token: Jwt, - cacheTime: number, - validIssuers?: string[] -) => { - const kid = token.header.kid; - const jwksClient = getJwksClient(token, cacheTime, validIssuers); - let key: SigningKey; - try { - key = await jwksClient.getSigningKey(kid); - } catch { - throw new UnauthorizedError(""); - } - return key.getPublicKey(); -}; - -const hasIss = ( - payload: string | JwtPayload -): payload is JwtPayload & { iss: string } => { - return typeof payload === "object" && payload.iss !== undefined; -}; - -const getJwksClient = ( - token: Jwt, - cacheTime: number, - validIssuers?: string[] -): JwksClient => { - if (!hasIss(token.payload)) { - throw new UnauthorizedError(""); - } - if (validIssuers && !_.includes(validIssuers, token.payload.iss)) { - throw new UnauthorizedError("Invalid token issuer."); - } - const iss = token.payload.iss; - if (!jwksClients[iss]) { - jwksClients[iss] = JwksRSA({ - cache: true, - cacheMaxEntries: 5, - cacheMaxAge: cacheTime * 36e5, - jwksUri: token.payload.iss + ".well-known/jwks.json", - }); - } - return jwksClients[iss]; -}; - -const validateAndGetAlgorithms = (algorithms?: unknown[]): Algorithm[] => { - if (algorithms === undefined) { - return ["HS256", "RS256"]; - } - if (_.isEmpty(algorithms)) { - throw new RangeError(""); - } - if (!isValidAlgorithms(algorithms)) { - throw new RangeError(""); - } - return algorithms; -}; - -const validateAndGetMethod = (method?: unknown): AuthMethod => { - if (method === undefined) { - return AuthMethod.Bearer; - } - if (isValidAuthMethod(method)) { - return method; - } else { - throw new RangeError(""); - } -}; - -const validateAndGetTokenGetter = ( - method: AuthMethod, - getToken?: unknown -): TokenGetter => { - if (getToken === undefined) { - return getDefaultTokenGetter(method); - } - if (typeof getToken === "function") { - return getToken as TokenGetter; - } else { - throw new RangeError(""); - } -}; - -const validateAndGetCacheTime = (cacheTime?: unknown): number => { - if (cacheTime === undefined) { - return 24; - } - if (_.isNumber(cacheTime)) { - return cacheTime; - } else { - throw new RangeError(""); - } -}; - -const validateAndGetRequestProperty = (requestProperty?: unknown): string => { - if (requestProperty === undefined) { - return "authUser"; - } - if (_.isString(requestProperty)) { - return requestProperty; - } else { - throw new RangeError(""); - } -}; - -const validateAndGetSecret = ( - secret?: unknown -): GetVerificationKey | undefined => { - if (secret === undefined) { - return undefined; - } else if (typeof secret === "function") { - return secret as GetVerificationKey; - } else { - return () => secret as Secret; - } -}; - -const validateAndGetIssuers = (issuer: unknown): string[] => { - if (issuer === undefined) { - throw new RangeError(""); - } else if (_.isString(issuer)) { - return [issuer]; - } else if ( - _.isArray(issuer) && - !_.isEmpty(issuer) && - _.every(issuer, (iss) => _.isString(iss)) - ) { - return issuer as string[]; - } else { - throw new RangeError(""); - } -}; - -const getDefaultTokenGetter = (method: AuthMethod): TokenGetter => { - switch (method) { - case AuthMethod.Bearer: - return bearerTokenGetter; - } -}; diff --git a/lib/lib-auth/src/common/JwksHelper.ts b/lib/lib-auth/src/common/JwksHelper.ts new file mode 100644 index 0000000..4d7b19e --- /dev/null +++ b/lib/lib-auth/src/common/JwksHelper.ts @@ -0,0 +1,46 @@ +import { Jwt } from "jsonwebtoken"; +import JwksRSA, { JwksClient, SigningKey } from "jwks-rsa"; +import _ from "lodash"; +import { UnauthorizedError } from "src/errors/UnauthorizedError"; +import { hasIss } from "./Validators"; + +const jwksClients: { [iss: string]: JwksClient } = {}; + +const getJwksClient = ( + token: Jwt, + cacheTime: number, + validIssuers?: string[] +): JwksClient => { + if (!hasIss(token.payload)) { + throw new UnauthorizedError(""); + } + if (validIssuers && !_.includes(validIssuers, token.payload.iss)) { + throw new UnauthorizedError("Invalid token issuer."); + } + const iss = token.payload.iss; + if (!jwksClients[iss]) { + jwksClients[iss] = JwksRSA({ + cache: true, + cacheMaxEntries: 5, + cacheMaxAge: cacheTime * 36e5, + jwksUri: token.payload.iss + ".well-known/jwks.json", + }); + } + return jwksClients[iss]; +}; + +export const getRSAKey = async ( + token: Jwt, + cacheTime: number, + validIssuers?: string[] +) => { + const kid = token.header.kid; + const jwksClient = getJwksClient(token, cacheTime, validIssuers); + let key: SigningKey; + try { + key = await jwksClient.getSigningKey(kid); + } catch { + throw new UnauthorizedError(""); + } + return key.getPublicKey(); +}; diff --git a/lib/lib-auth/src/common/TokenGetter.ts b/lib/lib-auth/src/common/TokenGetter.ts new file mode 100644 index 0000000..ccd213f --- /dev/null +++ b/lib/lib-auth/src/common/TokenGetter.ts @@ -0,0 +1,24 @@ +import { Request } from "express"; +import { AuthMethod, TokenGetter } from "src/interfaces"; + +export const getDefaultTokenGetter = (method: AuthMethod): TokenGetter => { + switch (method) { + case "Bearer": + return bearerTokenGetter; + default: + throw new Error(""); + } +}; + +const bearerTokenGetter = (req: Request): string | undefined => { + if (req.headers && req.headers.authorization) { + const parts = req.headers.authorization.split(" "); + if (parts.length == 2) { + const scheme = parts[0]; + const credentials = parts[1]; + if (/^Bearer$/i.test(scheme)) { + return credentials; + } + } + } +}; diff --git a/lib/lib-auth/src/common/Validators.ts b/lib/lib-auth/src/common/Validators.ts new file mode 100644 index 0000000..bc2928d --- /dev/null +++ b/lib/lib-auth/src/common/Validators.ts @@ -0,0 +1,124 @@ +import { JwtPayload } from "jsonwebtoken"; +import _ from "lodash"; +import { + Algorithm, + AuthMethod, + AuthMethods, + HSA, + RSA, + TokenGetter, +} from "src/interfaces"; +import { getDefaultTokenGetter } from "./TokenGetter"; + +export const isValidAuthMethod = (method: unknown): method is AuthMethod => + _.isString(method) && method in AuthMethods; + +export const isValidAlgorithm = (algorithm: unknown): algorithm is Algorithm => + _.includes(["HS256", "HS384", "HS512", "RS256", "RS384", "RS512"], algorithm); + +export const isValidAlgorithms = ( + algorithms: unknown[] +): algorithms is Algorithm[] => _.every(algorithms, isValidAlgorithm); + +export const isHSA = (algorithm: unknown): algorithm is HSA => + _.includes(["HS256", "HS384", "HS512"], algorithm); + +export const isRSA = (algorithm: unknown): algorithm is RSA => + _.includes(["RS256", "RS384", "RS512"], algorithm); + +export const hasIss = ( + payload: string | JwtPayload +): payload is JwtPayload & { iss: string } => { + return typeof payload === "object" && payload.iss !== undefined; +}; + +export const validateAndGetAlgorithms = ( + algorithms?: unknown[] +): Algorithm[] => { + if (algorithms === undefined) { + return ["HS256", "RS256"]; + } + if (_.isEmpty(algorithms)) { + throw new RangeError(""); + } + if (!isValidAlgorithms(algorithms)) { + throw new RangeError(""); + } + return algorithms; +}; + +export const validateAndGetMethod = (method?: unknown): AuthMethod => { + if (method === undefined) { + return "Bearer"; + } + if (isValidAuthMethod(method)) { + return method; + } else { + throw new RangeError(""); + } +}; + +export const validateAndGetTokenGetter = ( + method: AuthMethod, + getToken?: unknown +): TokenGetter => { + if (getToken === undefined) { + return getDefaultTokenGetter(method); + } + if (typeof getToken === "function") { + return getToken as TokenGetter; + } else { + throw new RangeError(""); + } +}; + +export const validateAndGetCacheTime = (cacheTime?: unknown): number => { + if (cacheTime === undefined) { + return 24; + } + if (_.isNumber(cacheTime)) { + return cacheTime; + } else { + throw new RangeError(""); + } +}; + +export const validateAndGetRequestProperty = ( + requestProperty?: unknown +): string => { + if (requestProperty === undefined) { + return "authUser"; + } + if (_.isString(requestProperty)) { + return requestProperty; + } else { + throw new RangeError(""); + } +}; + +export const validateAndGetIssuers = (issuer: unknown): string[] => { + if (issuer === undefined) { + throw new RangeError(""); + } else if (_.isString(issuer)) { + return [issuer]; + } else if ( + _.isArray(issuer) && + !_.isEmpty(issuer) && + _.every(issuer, (iss) => _.isString(iss)) + ) { + return issuer as string[]; + } else { + throw new RangeError(""); + } +}; + +export const validateAndGetTokenKey = (tokenKey?: unknown): string => { + if (tokenKey === undefined) { + return "token"; + } + if (_.isString(tokenKey)) { + return tokenKey; + } else { + throw new RangeError(""); + } +}; diff --git a/lib/lib-auth/src/common/Verifiers.ts b/lib/lib-auth/src/common/Verifiers.ts new file mode 100644 index 0000000..6faabdd --- /dev/null +++ b/lib/lib-auth/src/common/Verifiers.ts @@ -0,0 +1,95 @@ +import { Jwt, JwtPayload, Secret, verify } from "jsonwebtoken"; +import _ from "lodash"; +import { UnauthorizedError } from "src/errors/UnauthorizedError"; +import { Algorithm, AuthProperty, Permissions } from "src/interfaces"; +import { getRSAKey } from "./JwksHelper"; + +export const validatePermissions = ( + auth: AuthProperty, + permissions: Permissions +) => { + if (auth.isMachine) { + if (permissions.allowedScopes) { + if ( + _.some(permissions.allowedScopes, (scope) => + _.includes(auth.scopes, scope) + ) + ) { + return true; + } + } else { + return true; + } + } else { + if (permissions.allowedRoles) { + if ( + _.some(permissions.allowedRoles, (role) => _.includes(auth.roles, role)) + ) { + return true; + } + } else { + return true; + } + } + throw new UnauthorizedError(""); +}; + +export const verifyHSAToken = ( + token: string, + algorithms: Algorithm[], + audience?: string | RegExp | (string | RegExp)[], + secret?: Secret +) => { + if (secret === undefined) { + throw new UnauthorizedError(""); + } + try { + verify(token, secret, { + audience, + algorithms, + }); + } catch { + throw new UnauthorizedError(""); + } +}; + +export const verifyRSAToken = async ( + token: string, + decodedToken: Jwt, + cacheTime: number, + algorithms: Algorithm[], + validIsuers: string[], + audience?: string | RegExp | (string | RegExp)[] +) => { + const key = await getRSAKey(decodedToken, cacheTime, validIsuers); + try { + verify(token, key, { + audience, + algorithms, + }); + } catch { + throw new UnauthorizedError(""); + } +}; + +export const buildRequestProperty = ( + token: JwtPayload, + claimScope: string +): AuthProperty => { + const payload: AuthProperty = { + userId: token[claimScope + "/userId"], + handle: token[claimScope + "/handle"], + roles: token[claimScope + "/roles"], + email: token.email ?? token[claimScope + "/email"], + isMachine: token.gty === "client-credentials", + }; + if (payload.isMachine) { + const scopes = token[claimScope + "/scope"]; + if (typeof scopes === "string") { + payload.scopes = scopes.split(" "); + } else if (_.isArray(scopes)) { + payload.scopes = scopes; + } + } + return payload; +}; diff --git a/lib/lib-auth/src/common/index.ts b/lib/lib-auth/src/common/index.ts new file mode 100644 index 0000000..2e1db8f --- /dev/null +++ b/lib/lib-auth/src/common/index.ts @@ -0,0 +1,4 @@ +export * from "./JwksHelper"; +export * from "./TokenGetter"; +export * from "./Validators"; +export * from "./Verifiers"; diff --git a/lib/lib-auth/src/UnauthorizedError.ts b/lib/lib-auth/src/errors/UnauthorizedError.ts similarity index 100% rename from lib/lib-auth/src/UnauthorizedError.ts rename to lib/lib-auth/src/errors/UnauthorizedError.ts diff --git a/lib/lib-auth/src/expressAuthenticator.ts b/lib/lib-auth/src/expressAuthenticator.ts new file mode 100644 index 0000000..5ec7751 --- /dev/null +++ b/lib/lib-auth/src/expressAuthenticator.ts @@ -0,0 +1,104 @@ +import { Request, Response, NextFunction } from "express"; +import { decode } from "jsonwebtoken"; +import _ from "lodash"; +import { UnauthorizedError } from "./errors/UnauthorizedError"; +import { + AuthMethod, + AuthOptions, + AuthProperty, + ExpressAuthMiddleware, + Permissions, + TokenGetter, +} from "./interfaces"; +import { + buildRequestProperty, + isHSA, + isRSA, + validateAndGetAlgorithms, + validateAndGetCacheTime, + validateAndGetIssuers, + validateAndGetMethod, + validateAndGetRequestProperty, + validateAndGetTokenGetter, + validatePermissions, + verifyHSAToken, + verifyRSAToken, +} from "./common"; + +export const expressAuthenticator = ( + options: AuthOptions +): ExpressAuthMiddleware => { + const method: AuthMethod = validateAndGetMethod(options.method); + const algorithms = validateAndGetAlgorithms(options.algorithms); + const issuers = validateAndGetIssuers(options.issuer); + if (_.some(algorithms, isHSA) && _.isUndefined(options.secret)) { + throw new RangeError("lib-auth: secret is required when algorithm is HSA"); + } + const tokenGetter: TokenGetter = validateAndGetTokenGetter( + method, + options.getToken + ); + const credentialsRequired = _.defaultTo(options.credentialsRequired, true); + const jwtKeyCacheTimeInHours = validateAndGetCacheTime( + options.jwtKeyCacheTimeInHours + ); + const requestProperty = validateAndGetRequestProperty( + options.requestProperty + ); + const claimScope = _.toString(options.claimScope); + + const middleware = async ( + req: Request, + res: Response, + next: NextFunction, + permissions?: Permissions + ) => { + try { + const token = await tokenGetter(req); + if (!token) { + throw new UnauthorizedError("No authorization token was found"); + } + const decodedToken = decode(token, { complete: true }); + if (decodedToken === null || typeof decodedToken.payload === "string") { + throw new UnauthorizedError("Invalid Token"); + } + const algorithm = decodedToken.header.alg; + if (!_.includes(algorithms, algorithm)) { + throw new UnauthorizedError("Invalid Algorithm"); + } + if (isHSA(algorithm)) { + verifyHSAToken(token, [algorithm], options.audience, options.secret); + } else if (isRSA(algorithm)) { + await verifyRSAToken( + token, + decodedToken, + jwtKeyCacheTimeInHours, + [algorithm], + issuers, + options.audience + ); + } else { + throw new UnauthorizedError("Invalid Algorithm"); + } + if (options.isRevoked && (await options.isRevoked(decodedToken))) { + throw new UnauthorizedError("The token has been revoked."); + } + const authProperty: AuthProperty = buildRequestProperty( + decodedToken.payload, + claimScope + ); + _.set(req, requestProperty, authProperty); + if (credentialsRequired && permissions) { + validatePermissions(authProperty, permissions); + } + next(); + } catch (err) { + if (credentialsRequired) { + next(err); + } else { + next(); + } + } + }; + return middleware; +}; diff --git a/lib/lib-auth/src/grpcAuthenticator.ts b/lib/lib-auth/src/grpcAuthenticator.ts new file mode 100644 index 0000000..c439bd1 --- /dev/null +++ b/lib/lib-auth/src/grpcAuthenticator.ts @@ -0,0 +1,99 @@ +import { sendUnaryData, ServerUnaryCall } from "@grpc/grpc-js"; +import { ErrorHelper, GrpcError } from "@topcoder-framework/lib-common"; +import { + Interceptor, + OnMessageOptions, +} from "@topcoder-framework/lib-interceptor"; +import { AuthOptions, AuthProperty, Permissions } from "./interfaces"; +import { + buildRequestProperty, + isHSA, + isRSA, + validateAndGetAlgorithms, + validateAndGetCacheTime, + validateAndGetIssuers, + validateAndGetTokenKey, + validatePermissions, + verifyHSAToken, + verifyRSAToken, +} from "./common"; +import { UnauthorizedError } from "./errors/UnauthorizedError"; +import { decode } from "jsonwebtoken"; +import _ from "lodash"; + +export const authInterceptor = (options: AuthOptions): Interceptor => { + const tokenKey = validateAndGetTokenKey(options.tokenKey); + const algorithms = validateAndGetAlgorithms(options.algorithms); + const issuers = validateAndGetIssuers(options.issuer); + const credentialsRequired = _.defaultTo(options.credentialsRequired, true); + const jwtKeyCacheTimeInHours = validateAndGetCacheTime( + options.jwtKeyCacheTimeInHours + ); + const claimScope = _.toString(options.claimScope); + + const interceptor = { + onMessage: async ( + call: ServerUnaryCall, + messageOptions?: OnMessageOptions & { permissions?: Permissions } + ) => { + const tokenArr = call.metadata.get(tokenKey); + if (!tokenArr || tokenArr.length === 0) { + throw new UnauthorizedError("No authorization token was found"); + } + const token = tokenArr[0]; + if (typeof token != "string") { + throw new UnauthorizedError("No authorization token was found"); + } + const decodedToken = decode(token, { complete: true }); + if (decodedToken === null || typeof decodedToken.payload === "string") { + throw new UnauthorizedError("Invalid Token"); + } + const algorithm = decodedToken.header.alg; + if (!_.includes(algorithms, algorithm)) { + throw new UnauthorizedError("Invalid Algorithm"); + } + if (isHSA(algorithm)) { + verifyHSAToken(token, [algorithm], options.audience, options.secret); + } else if (isRSA(algorithm)) { + await verifyRSAToken( + token, + decodedToken, + jwtKeyCacheTimeInHours, + [algorithm], + issuers, + options.audience + ); + } else { + throw new UnauthorizedError("Invalid Algorithm"); + } + if (options.isRevoked && (await options.isRevoked(decodedToken))) { + throw new UnauthorizedError("The token has been revoked."); + } + const authProperty: AuthProperty = buildRequestProperty( + decodedToken.payload, + claimScope + ); + _.forOwn(authProperty, (v, k) => { + call.metadata.set(k, _.toString(v)); + }); + if (credentialsRequired && messageOptions?.permissions) { + validatePermissions(authProperty, messageOptions.permissions); + } + }, + onSuccess: ( + response: any, + call: ServerUnaryCall, + callback: sendUnaryData + ) => { + callback(null, response); + }, + onError: ( + error: GrpcError, + call: ServerUnaryCall, + callback: sendUnaryData + ) => { + callback(ErrorHelper.wrapError(error)); + }, + }; + return interceptor; +}; diff --git a/lib/lib-auth/src/index.ts b/lib/lib-auth/src/index.ts new file mode 100644 index 0000000..41cb330 --- /dev/null +++ b/lib/lib-auth/src/index.ts @@ -0,0 +1,3 @@ +export * from "./expressAuthenticator"; +export * from "./grpcAuthenticator"; +export * from "./interfaces"; diff --git a/lib/lib-auth/src/interfaces/Algorithm.ts b/lib/lib-auth/src/interfaces/Algorithm.ts new file mode 100644 index 0000000..1a8cce2 --- /dev/null +++ b/lib/lib-auth/src/interfaces/Algorithm.ts @@ -0,0 +1,3 @@ +export type Algorithm = RSA | HSA; +export type HSA = "HS256" | "HS384" | "HS512"; +export type RSA = "RS256" | "RS384" | "RS512"; diff --git a/lib/lib-auth/src/interfaces/AuthMethod.ts b/lib/lib-auth/src/interfaces/AuthMethod.ts new file mode 100644 index 0000000..e393187 --- /dev/null +++ b/lib/lib-auth/src/interfaces/AuthMethod.ts @@ -0,0 +1,2 @@ +export const AuthMethods = ["Bearer"] as const; +export type AuthMethod = (typeof AuthMethods)[number]; diff --git a/lib/lib-auth/src/interfaces/AuthOptions.ts b/lib/lib-auth/src/interfaces/AuthOptions.ts new file mode 100644 index 0000000..48a0dc1 --- /dev/null +++ b/lib/lib-auth/src/interfaces/AuthOptions.ts @@ -0,0 +1,30 @@ +import { Secret, VerifyOptions } from "jsonwebtoken"; +import { Algorithm } from "./Algorithm"; +import { AuthMethod } from "./AuthMethod"; +import { IsRevoked } from "./IsRevoked"; +import { TokenGetter } from "./TokenGetter"; + +export type AuthOptions = { + method?: AuthMethod; + secret?: Secret; + algorithms?: Algorithm[]; + /** + * If you set it to false, the system will proceed to the next middleware + * if the request does not include a token, without causing an error. + * @defaultValue `true` + * + */ + credentialsRequired?: boolean; + jwtKeyCacheTimeInHours?: number; + /** + * This feature allows you to personalize the name of the property + * in the request object where the decoded payload is stored. + * @defaultValue `authUser` + */ + requestProperty?: string; + tokenKey?: string; + getToken?: TokenGetter; + isRevoked?: IsRevoked; + claimScope: string; +} & Required> & + Pick; diff --git a/lib/lib-auth/src/interfaces/AuthProperty.ts b/lib/lib-auth/src/interfaces/AuthProperty.ts new file mode 100644 index 0000000..e1d602d --- /dev/null +++ b/lib/lib-auth/src/interfaces/AuthProperty.ts @@ -0,0 +1,8 @@ +export type AuthProperty = { + userId?: string; + handle?: string; + roles?: string[]; + email?: string; + isMachine: boolean; + scopes?: string[]; +}; diff --git a/lib/lib-auth/src/interfaces/ExpressAuthMiddleware.ts b/lib/lib-auth/src/interfaces/ExpressAuthMiddleware.ts new file mode 100644 index 0000000..e34cd8d --- /dev/null +++ b/lib/lib-auth/src/interfaces/ExpressAuthMiddleware.ts @@ -0,0 +1,9 @@ +import { NextFunction, Request, Response } from "express"; +import { Permissions } from "./Permissions"; + +export type ExpressAuthMiddleware = ( + req: Request, + res: Response, + next: NextFunction, + permissions?: Permissions +) => Promise; diff --git a/lib/lib-auth/src/interfaces/IsRevoked.ts b/lib/lib-auth/src/interfaces/IsRevoked.ts new file mode 100644 index 0000000..884b200 --- /dev/null +++ b/lib/lib-auth/src/interfaces/IsRevoked.ts @@ -0,0 +1,6 @@ +import { Jwt } from "jsonwebtoken"; + +/** + * A function to check if a token is revoked + */ +export type IsRevoked = (token: Jwt | undefined) => boolean | Promise; diff --git a/lib/lib-auth/src/interfaces/Permissions.ts b/lib/lib-auth/src/interfaces/Permissions.ts new file mode 100644 index 0000000..8612bf8 --- /dev/null +++ b/lib/lib-auth/src/interfaces/Permissions.ts @@ -0,0 +1,4 @@ +export type Permissions = { + allowedRoles?: string[]; + allowedScopes?: string[]; +}; diff --git a/lib/lib-auth/src/interfaces/TokenGetter.ts b/lib/lib-auth/src/interfaces/TokenGetter.ts new file mode 100644 index 0000000..143227a --- /dev/null +++ b/lib/lib-auth/src/interfaces/TokenGetter.ts @@ -0,0 +1,8 @@ +import { Request } from "express"; + +/** + * A function to customize how a token is retrieved from the express request. + */ +export type TokenGetter = ( + req: Request +) => string | Promise | undefined; diff --git a/lib/lib-auth/src/interfaces/index.ts b/lib/lib-auth/src/interfaces/index.ts new file mode 100644 index 0000000..d5c2275 --- /dev/null +++ b/lib/lib-auth/src/interfaces/index.ts @@ -0,0 +1,8 @@ +export * from "./Algorithm"; +export * from "./AuthMethod"; +export * from "./AuthOptions"; +export * from "./AuthProperty"; +export * from "./ExpressAuthMiddleware"; +export * from "./IsRevoked"; +export * from "./Permissions"; +export * from "./TokenGetter"; From 78b3a5c14a76ae73531a0a5ec8758e919a406be3 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 26 Jul 2023 16:46:49 +0300 Subject: [PATCH 09/10] refactor: rename exported method --- lib/lib-auth/src/grpcAuthenticator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/lib-auth/src/grpcAuthenticator.ts b/lib/lib-auth/src/grpcAuthenticator.ts index c439bd1..ca8c01b 100644 --- a/lib/lib-auth/src/grpcAuthenticator.ts +++ b/lib/lib-auth/src/grpcAuthenticator.ts @@ -21,7 +21,7 @@ import { UnauthorizedError } from "./errors/UnauthorizedError"; import { decode } from "jsonwebtoken"; import _ from "lodash"; -export const authInterceptor = (options: AuthOptions): Interceptor => { +export const grpcAuthenticator = (options: AuthOptions): Interceptor => { const tokenKey = validateAndGetTokenKey(options.tokenKey); const algorithms = validateAndGetAlgorithms(options.algorithms); const issuers = validateAndGetIssuers(options.issuer); From a2fa18d394fe833ed04094512bc8741f96c6d173 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 26 Jul 2023 18:36:59 +0300 Subject: [PATCH 10/10] feat: lib-auth: add m2m --- lib/lib-auth/package.json | 1 + lib/lib-auth/src/index.ts | 1 + lib/lib-auth/src/m2m.ts | 86 +++++++++++++++++++++++++++++++++++++++ yarn.lock | 2 +- 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 lib/lib-auth/src/m2m.ts diff --git a/lib/lib-auth/package.json b/lib/lib-auth/package.json index 5874b6b..8969f5d 100644 --- a/lib/lib-auth/package.json +++ b/lib/lib-auth/package.json @@ -26,6 +26,7 @@ "dependencies": { "@grpc/grpc-js": "^1.8.0", "@types/jsonwebtoken": "^9.0.2", + "axios": "^1.4.0", "jsonwebtoken": "^9.0.1", "jwks-rsa": "^3.0.1", "lodash": "^4.17.21" diff --git a/lib/lib-auth/src/index.ts b/lib/lib-auth/src/index.ts index 41cb330..c0c82c0 100644 --- a/lib/lib-auth/src/index.ts +++ b/lib/lib-auth/src/index.ts @@ -1,3 +1,4 @@ export * from "./expressAuthenticator"; export * from "./grpcAuthenticator"; export * from "./interfaces"; +export * from "./m2m"; diff --git a/lib/lib-auth/src/m2m.ts b/lib/lib-auth/src/m2m.ts new file mode 100644 index 0000000..7125aac --- /dev/null +++ b/lib/lib-auth/src/m2m.ts @@ -0,0 +1,86 @@ +import axios from "axios"; +import { decode } from "jsonwebtoken"; +import _ from "lodash"; + +const cachedToken: { [id: string]: string } = {}; + +const isTokenExpired = (token: string): boolean => { + let expiryTime = 0; + if (token) { + const decodedToken = decode(token, { json: true }); + if (decodedToken === null) { + return true; + } + const expiryTimeInMilliSeconds = + ((decodedToken.exp ?? 0) - 60) * 1000 - new Date().getTime(); + expiryTime = Math.floor(expiryTimeInMilliSeconds / 1000); + } + return expiryTime <= 0; +}; + +export type m2mOptions = { + authUrl: string; + audience?: string; + authProxyServerUrl: string; + authScope?: string; + provider?: string; + contentType?: "application/json" | "application/x-www-form-urlencoded"; +}; + +const defaultOptions: Partial = { + provider: "auth0", + contentType: "application/json", +}; + +export const m2m = (options: m2mOptions) => { + options = { ...defaultOptions, ...options }; + if (!options.authUrl) { + throw new RangeError("authUrl is required"); + } + if ( + !/^https:\/\/auth0proxy\.topcoder(-dev|-qa)?\.com\/token$/i.test( + options.authProxyServerUrl + ) + ) { + throw new RangeError("Proxy server url is not correct"); + } + + const body: { [name: string]: string | undefined } = { + grant_type: "client_credentials", + auth0_url: options.authUrl, + provider: options.provider, + content_type: options.contentType, + }; + if (options.audience) { + body.audience = options.audience; + } else if (options.authScope) { + body.scope = options.authScope; + } else { + throw new RangeError("Audience or Scope is required"); + } + + const getM2M = async (clientId: string, clientSecret: string) => { + if ( + _.isUndefined(cachedToken[clientId]) || + isTokenExpired(cachedToken[clientId]) + ) { + let response; + body.client_id = clientId; + body.client_secret = clientSecret; + try { + response = await axios.post(options.authProxyServerUrl, body); + } catch (err) { + throw new Error( + `Error when getting m2m token: ${_.toString(err.message)}` + ); + } + if (response.data && response.data.access_token) { + cachedToken[clientId] = response.data.access_token; + } else { + throw new Error("Error when getting m2m token"); + } + } + return cachedToken[clientId]; + }; + return getM2M; +}; diff --git a/yarn.lock b/yarn.lock index f1a5714..711a53e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1546,7 +1546,7 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -axios@^1.0.0: +axios@^1.0.0, axios@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==