From 5c1e65c409c29500e6e08ec527656e77d2188fa5 Mon Sep 17 00:00:00 2001 From: Aydan Pirani <aydanpirani@gmail.com> Date: Mon, 8 Apr 2024 01:10:46 -0500 Subject: [PATCH 1/9] Added initial auth workflow --- package.json | 3 ++ src/config.ts | 5 ++ src/services/auth/auth-router.ts | 68 +++++++++++++++++------- src/services/auth/auth-schema.ts | 12 ++++- src/utilities.ts | 9 ++-- yarn.lock | 88 +++++++++++++++++++++++++++++++- 6 files changed, 158 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index dbdbe4f..5706f6d 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@types/express": "^4.17.21", "@types/morgan": "^1.9.9", "@types/node": "^20.9.3", + "@types/passport-google-oauth20": "^2.0.14", "@typescript-eslint/eslint-plugin": "^6.4.0", "eslint": "8.2.0", "eslint-config-airbnb": "19.0.4", @@ -37,6 +38,8 @@ "http-status-codes": "^2.3.0", "mongoose": "^8.2.3", "morgan": "^1.10.0", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", "tsx": "^4.5.0", "typescript": "*", "zod": "^3.22.4" diff --git a/src/config.ts b/src/config.ts index e3a6f9f..9a065d0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,4 +13,9 @@ export const Config = { DATABASE_USERNAME: getEnv("DATABASE_USERNAME"), DATABASE_PASSWORD: getEnv("DATABASE_PASSWORD"), DATABASE_HOST: getEnv("DATABASE_HOST"), + + CLIENT_ID: getEnv("OAUTH_GOOGLE_CLIENT_ID"), + CLIENT_SECRET: getEnv("OAUTH_GOOGLE_CLIENT_SECRET"), + + REDIRECT_URI: "http://localhost:3000/auth/callback", }; diff --git a/src/services/auth/auth-router.ts b/src/services/auth/auth-router.ts index fe85ca2..83cf899 100644 --- a/src/services/auth/auth-router.ts +++ b/src/services/auth/auth-router.ts @@ -1,29 +1,59 @@ import { Router } from "express"; -import { Database } from "../../database"; +import passport, { AuthenticateOptions } from "passport"; +import { Strategy as GoogleStrategy } from "passport-google-oauth20"; +import { Config } from "../../config"; import { StatusCodes } from "http-status-codes"; -import { Role } from "./auth-schema"; -import { createId } from "@paralleldrive/cuid2"; +import { Devices } from "./auth-schema"; + +passport.use( + new GoogleStrategy( + { + clientID: Config.CLIENT_ID, + clientSecret: Config.CLIENT_SECRET, + callbackURL: Config.REDIRECT_URI, + }, + function (_1, _2, profile, cb) { + cb(null, profile); + } + ) +); const authRouter = Router(); -authRouter.get("/", async (req, res) => { - const result = await Database.ROLES.find(); - const mappedResult = result.map((item) => item.toObject()); - return res.status(StatusCodes.OK).send(mappedResult); -}); +authRouter.get("/login/:DEVICE/", (req, res) => { + const device = req.params["DEVICE"]; -authRouter.post("/", async (_, res, next) => { - const user = { - userId: createId(), - roles: [Role.Enum.USER], - }; - - try { - const result = (await Database.ROLES.create(user)).toObject(); - return res.status(StatusCodes.CREATED).send(result); - } catch (err) { - next(err); + console.log(device, Devices.Values); + + if (!Devices.safeParse(device).success) { + return res.status(StatusCodes.BAD_REQUEST).send({ error: "BadDevice" }); } + + const callbackURL = `${Config.REDIRECT_URI}/${device}`; + + console.log(`|${callbackURL}|`); + return passport.authenticate("google", { + callbackURL: callbackURL, + scope: ["profile", "email"], + } as AuthenticateOptions)(req, res); }); +authRouter.get( + "/callback/", + (req, _, next) => { + console.log("HI!!! params be", req.params, req.query); + return next(); + }, + passport.authenticate("google", { + session: false, + }), + function (req, res) { + console.log("IN HERE"); + console.log(req.params); + console.log(req.query); + console.log("redirecting!"); + return res.redirect("/"); + } +); + export default authRouter; diff --git a/src/services/auth/auth-schema.ts b/src/services/auth/auth-schema.ts index 1977d83..144680c 100644 --- a/src/services/auth/auth-schema.ts +++ b/src/services/auth/auth-schema.ts @@ -3,8 +3,11 @@ import { z } from "zod"; export const Role = z.enum(["USER", "ADMIN", "CORPORATE"]); -export const RoleInfo = z.object({ - userId: z.coerce.string().cuid2(), +export const Devices = z.enum(["web"]); + +export const RoleValidator = z.object({ + userId: z.coerce.string().regex(/user[0-9]*/), + email: z.coerce.string().email(), roles: z.array(Role), }); @@ -14,6 +17,11 @@ export const RoleSchema = new Schema({ required: true, unique: true, }, + email: { + type: String, + required: true, + unique: true, + }, roles: { type: [String], enum: Role.Values, diff --git a/src/utilities.ts b/src/utilities.ts index 685ef19..39fc28b 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -29,9 +29,10 @@ export function isDev() { return Config.ENV == Environment.enum.DEVELOPMENT; } -export function getEnv(target: string) { - if (process.env[target] === undefined) { - throw new Error(`env value ${target} not found, exiting...`); +export function getEnv(key: string): string { + const val = process.env[key]; + if (val === undefined) { + throw new Error(`env value ${key} not found, exiting...`); } - return process.env[target]; + return val; } diff --git a/yarn.lock b/yarn.lock index 981781b..7fad951 100644 --- a/yarn.lock +++ b/yarn.lock @@ -255,7 +255,7 @@ "@types/range-parser" "*" "@types/send" "*" -"@types/express@^4.17.21": +"@types/express@*", "@types/express@^4.17.21": version "4.17.21" resolved "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz" integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== @@ -304,6 +304,38 @@ dependencies: undici-types "~5.26.4" +"@types/oauth@*": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@types/oauth/-/oauth-0.9.4.tgz#dcbab5efa2f34f312b915f80685760ccc8111e0a" + integrity sha512-qk9orhti499fq5XxKCCEbd0OzdPZuancneyse3KtR+vgMiHRbh+mn8M4G6t64ob/Fg+GZGpa565MF/2dKWY32A== + dependencies: + "@types/node" "*" + +"@types/passport-google-oauth20@^2.0.14": + version "2.0.14" + resolved "https://registry.yarnpkg.com/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.14.tgz#6facba6f73f0aff3888564ebc907afe295c30fba" + integrity sha512-ZaZpRUAeMl3vy298ulKO1wGLn9SQtj/CyIfZL/Px5xU9pybMiQU3mhXDCBiWSbg0EK9uXT4ZoWC3ktuWY+5fwQ== + dependencies: + "@types/express" "*" + "@types/passport" "*" + "@types/passport-oauth2" "*" + +"@types/passport-oauth2@*": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@types/passport-oauth2/-/passport-oauth2-1.4.15.tgz#34f2684f53aad36e664cd01ca9879224229f47e7" + integrity sha512-9cUTP/HStNSZmhxXGuRrBJfEWzIEJRub2eyJu3CvkA+8HAMc9W3aKdFhVq+Qz1hi42qn+GvSAnz3zwacDSYWpw== + dependencies: + "@types/express" "*" + "@types/oauth" "*" + "@types/passport" "*" + +"@types/passport@*": + version "1.0.16" + resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.16.tgz#5a2918b180a16924c4d75c31254c31cdca5ce6cf" + integrity sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A== + dependencies: + "@types/express" "*" + "@types/qs@*": version "6.9.14" resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz" @@ -596,6 +628,11 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64url@3.x.x: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + basic-auth@~2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz" @@ -2108,6 +2145,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +oauth@0.10.x: + version "0.10.0" + resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.10.0.tgz#3551c4c9b95c53ea437e1e21e46b649482339c58" + integrity sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q== + object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -2220,6 +2262,38 @@ parseurl@~1.3.3: resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +passport-google-oauth20@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz#0d241b2d21ebd3dc7f2b60669ec4d587e3a674ef" + integrity sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ== + dependencies: + passport-oauth2 "1.x.x" + +passport-oauth2@1.x.x: + version "1.8.0" + resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.8.0.tgz#55725771d160f09bbb191828d5e3d559eee079c8" + integrity sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA== + dependencies: + base64url "3.x.x" + oauth "0.10.x" + passport-strategy "1.x.x" + uid2 "0.0.x" + utils-merge "1.x.x" + +passport-strategy@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.7.0.tgz#3688415a59a48cf8068417a8a8092d4492ca3a05" + integrity sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + utils-merge "^1.0.1" + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" @@ -2245,6 +2319,11 @@ path-type@^4.0.0: resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" @@ -2783,6 +2862,11 @@ typescript@*: resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz" integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== +uid2@0.0.x: + version "0.0.4" + resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.4.tgz#033f3b1d5d32505f5ce5f888b9f3b667123c0a44" + integrity sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" @@ -2815,7 +2899,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -utils-merge@1.0.1: +utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== From 149d707ab9c5a73aeada1a4b360d2a607d9fa905 Mon Sep 17 00:00:00 2001 From: Aydan Pirani <aydanpirani@gmail.com> Date: Thu, 11 Apr 2024 02:07:00 -0500 Subject: [PATCH 2/9] Auth template working --- src/config.ts | 8 +++- src/services/auth/auth-router.ts | 66 +++++++++++++------------------- src/services/auth/auth-schema.ts | 2 - src/services/auth/auth-utils.ts | 18 +++++++++ 4 files changed, 52 insertions(+), 42 deletions(-) create mode 100644 src/services/auth/auth-utils.ts diff --git a/src/config.ts b/src/config.ts index 9a065d0..228dcc8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,5 +17,11 @@ export const Config = { CLIENT_ID: getEnv("OAUTH_GOOGLE_CLIENT_ID"), CLIENT_SECRET: getEnv("OAUTH_GOOGLE_CLIENT_SECRET"), - REDIRECT_URI: "http://localhost:3000/auth/callback", + // REDIRECT_URI: "http://localhost:3000/auth/callback", + AUTH_CALLBACK_URI_BASE: "http://localhost:3000/auth/callback/", + // AUTH_CALLBACK_URI_BASE: "https://api.reflectionsprojections.org/auth/callback", +}; + +export const DeviceRedirects: Record<string, string> = { + web: `https://www.reflectionsprojections.org/`, }; diff --git a/src/services/auth/auth-router.ts b/src/services/auth/auth-router.ts index 83cf899..8642907 100644 --- a/src/services/auth/auth-router.ts +++ b/src/services/auth/auth-router.ts @@ -1,58 +1,46 @@ import { Router } from "express"; -import passport, { AuthenticateOptions } from "passport"; -import { Strategy as GoogleStrategy } from "passport-google-oauth20"; -import { Config } from "../../config"; +import passport from "passport"; +import { DeviceRedirects } from "../../config"; import { StatusCodes } from "http-status-codes"; -import { Devices } from "./auth-schema"; - -passport.use( - new GoogleStrategy( - { - clientID: Config.CLIENT_ID, - clientSecret: Config.CLIENT_SECRET, - callbackURL: Config.REDIRECT_URI, - }, - function (_1, _2, profile, cb) { - cb(null, profile); - } - ) -); +import { Strategy as GoogleStrategy } from "passport-google-oauth20"; +import { createGoogleStrategy } from "./auth-utils"; + +const authStrategies: Record<string, GoogleStrategy> = {}; const authRouter = Router(); authRouter.get("/login/:DEVICE/", (req, res) => { - const device = req.params["DEVICE"]; + const device = req.params.DEVICE; - console.log(device, Devices.Values); - - if (!Devices.safeParse(device).success) { + // Check if this is a valid device (i.e. does a redirectURI exist for it) + if (!(device in DeviceRedirects)) { return res.status(StatusCodes.BAD_REQUEST).send({ error: "BadDevice" }); } - const callbackURL = `${Config.REDIRECT_URI}/${device}`; + // Check if we've already created an auth strategy for the device + // If not, create a new one + if (!(device in authStrategies)) { + authStrategies[device] = createGoogleStrategy(device); + } + + // Use the pre-created strategy + passport.use(device, authStrategies[device]); - console.log(`|${callbackURL}|`); - return passport.authenticate("google", { - callbackURL: callbackURL, + return passport.authenticate(device, { scope: ["profile", "email"], - } as AuthenticateOptions)(req, res); + })(req, res); }); authRouter.get( - "/callback/", - (req, _, next) => { - console.log("HI!!! params be", req.params, req.query); - return next(); - }, - passport.authenticate("google", { - session: false, - }), + "/callback/:DEVICE", + (req, res, next) => + // Check based on the pre-existing strategy name + passport.authenticate(req.params.DEVICE, { + session: false, + })(req, res, next), function (req, res) { - console.log("IN HERE"); - console.log(req.params); - console.log(req.query); - console.log("redirecting!"); - return res.redirect("/"); + const redirectUri = `${DeviceRedirects[req.params.DEVICE]}` + return res.redirect(redirectUri); } ); diff --git a/src/services/auth/auth-schema.ts b/src/services/auth/auth-schema.ts index 144680c..15a2e8d 100644 --- a/src/services/auth/auth-schema.ts +++ b/src/services/auth/auth-schema.ts @@ -3,8 +3,6 @@ import { z } from "zod"; export const Role = z.enum(["USER", "ADMIN", "CORPORATE"]); -export const Devices = z.enum(["web"]); - export const RoleValidator = z.object({ userId: z.coerce.string().regex(/user[0-9]*/), email: z.coerce.string().email(), diff --git a/src/services/auth/auth-utils.ts b/src/services/auth/auth-utils.ts new file mode 100644 index 0000000..5b512bf --- /dev/null +++ b/src/services/auth/auth-utils.ts @@ -0,0 +1,18 @@ +// Create a function to generate GoogleStrategy instances +import { Strategy as GoogleStrategy } from "passport-google-oauth20"; +import { Config } from "../../config"; + +export function createGoogleStrategy(device: string) { + return new GoogleStrategy( + { + clientID: Config.CLIENT_ID, + clientSecret: Config.CLIENT_SECRET, + callbackURL: `${Config.AUTH_CALLBACK_URI_BASE}${device}`, + }, + async function (_1, _2, profile, cb) { + // Add profile to database here + console.log(profile); + cb(null, profile); + } + ); +} From 87e2d3a40d045de53cf775bd787713948a96b2e1 Mon Sep 17 00:00:00 2001 From: Aydan Pirani <aydanpirani@gmail.com> Date: Thu, 11 Apr 2024 02:22:25 -0500 Subject: [PATCH 3/9] Added changes for database insertion --- src/database.ts | 4 +-- src/services/auth/auth-router.ts | 2 +- src/services/auth/auth-schema.ts | 42 +++++++++++++++++++------------- src/services/auth/auth-utils.ts | 19 ++++++++++++--- 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/database.ts b/src/database.ts index 2e8c4c5..63c5d12 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,5 +1,5 @@ import mongoose, { Schema } from "mongoose"; -import { RoleInfo, RoleSchema } from "./services/auth/auth-schema"; +import { RoleValidator, RoleSchema } from "./services/auth/auth-schema"; mongoose.set("toObject", { versionKey: false }); @@ -31,5 +31,5 @@ function initializeModel( // Example usage export const Database = { - ROLES: initializeModel("roles", RoleSchema, RoleInfo), + ROLES: initializeModel("roles", RoleSchema, RoleValidator), }; diff --git a/src/services/auth/auth-router.ts b/src/services/auth/auth-router.ts index 8642907..148ebfa 100644 --- a/src/services/auth/auth-router.ts +++ b/src/services/auth/auth-router.ts @@ -39,7 +39,7 @@ authRouter.get( session: false, })(req, res, next), function (req, res) { - const redirectUri = `${DeviceRedirects[req.params.DEVICE]}` + const redirectUri = `${DeviceRedirects[req.params.DEVICE]}`; return res.redirect(redirectUri); } ); diff --git a/src/services/auth/auth-schema.ts b/src/services/auth/auth-schema.ts index 15a2e8d..619cf7a 100644 --- a/src/services/auth/auth-schema.ts +++ b/src/services/auth/auth-schema.ts @@ -5,25 +5,33 @@ export const Role = z.enum(["USER", "ADMIN", "CORPORATE"]); export const RoleValidator = z.object({ userId: z.coerce.string().regex(/user[0-9]*/), + name: z.coerce.string(), email: z.coerce.string().email(), roles: z.array(Role), }); -export const RoleSchema = new Schema({ - userId: { - type: String, - required: true, - unique: true, +export const RoleSchema = new Schema( + { + userId: { + type: String, + required: true, + unique: true, + }, + name: { + type: String, + required: true, + }, + email: { + type: String, + required: true, + unique: true, + }, + roles: { + type: [String], + enum: Role.Values, + default: [], + required: true, + }, }, - email: { - type: String, - required: true, - unique: true, - }, - roles: { - type: [String], - enum: Role.Values, - default: [], - required: true, - }, -}); + { timestamps: { createdAt: "createdAt" } } +); diff --git a/src/services/auth/auth-utils.ts b/src/services/auth/auth-utils.ts index 5b512bf..129940c 100644 --- a/src/services/auth/auth-utils.ts +++ b/src/services/auth/auth-utils.ts @@ -1,6 +1,8 @@ // Create a function to generate GoogleStrategy instances import { Strategy as GoogleStrategy } from "passport-google-oauth20"; import { Config } from "../../config"; +import { Role } from "./auth-schema"; +import { Database } from "../../database"; export function createGoogleStrategy(device: string) { return new GoogleStrategy( @@ -9,10 +11,21 @@ export function createGoogleStrategy(device: string) { clientSecret: Config.CLIENT_SECRET, callbackURL: `${Config.AUTH_CALLBACK_URI_BASE}${device}`, }, + + // Strategy -> insert user into database if they don't exist async function (_1, _2, profile, cb) { - // Add profile to database here - console.log(profile); - cb(null, profile); + const userId = `user${profile.id}`; + const name = profile.displayName; + const email = profile._json.email; + const roles = [Role.Enum.USER]; + + Database.ROLES.findOneAndUpdate( + { userId: userId }, + { userId, name, email, roles }, + { upsert: true } + ) + .then(() => cb(null, profile)) + .catch((err) => cb(err, profile)); } ); } From a8b2cd6064711381c77b225990026c0de7078df8 Mon Sep 17 00:00:00 2001 From: Aydan Pirani <aydanpirani@gmail.com> Date: Thu, 11 Apr 2024 02:41:54 -0500 Subject: [PATCH 4/9] Added JWT integration --- package.json | 2 + src/config.ts | 5 +- src/services/auth/auth-router.ts | 27 +++++++--- src/services/auth/auth-utils.ts | 10 ++++ yarn.lock | 89 +++++++++++++++++++++++++++++++- 5 files changed, 125 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 5706f6d..e064d03 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "devDependencies": { "@types/dotenv": "^8.2.0", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", "@types/morgan": "^1.9.9", "@types/node": "^20.9.3", "@types/passport-google-oauth20": "^2.0.14", @@ -36,6 +37,7 @@ "dotenv": "^16.4.5", "express": "^4.19.1", "http-status-codes": "^2.3.0", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.2.3", "morgan": "^1.10.0", "passport": "^0.7.0", diff --git a/src/config.ts b/src/config.ts index 228dcc8..deb7dc2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,8 +20,11 @@ export const Config = { // REDIRECT_URI: "http://localhost:3000/auth/callback", AUTH_CALLBACK_URI_BASE: "http://localhost:3000/auth/callback/", // AUTH_CALLBACK_URI_BASE: "https://api.reflectionsprojections.org/auth/callback", + + JWT_SIGNING_SECRET: getEnv("JWT_SIGNING_SECRET"), + JWT_EXPIRATION_TIME: "1 day", }; export const DeviceRedirects: Record<string, string> = { - web: `https://www.reflectionsprojections.org/`, + web: "https://www.google.com/", }; diff --git a/src/services/auth/auth-router.ts b/src/services/auth/auth-router.ts index 148ebfa..6a33379 100644 --- a/src/services/auth/auth-router.ts +++ b/src/services/auth/auth-router.ts @@ -1,9 +1,10 @@ import { Router } from "express"; import passport from "passport"; -import { DeviceRedirects } from "../../config"; +import { Config, DeviceRedirects } from "../../config"; import { StatusCodes } from "http-status-codes"; -import { Strategy as GoogleStrategy } from "passport-google-oauth20"; -import { createGoogleStrategy } from "./auth-utils"; +import { Strategy as GoogleStrategy, Profile } from "passport-google-oauth20"; +import { createGoogleStrategy, getJwtPayloadFromDatabase } from "./auth-utils"; +import jsonwebtoken from "jsonwebtoken"; const authStrategies: Record<string, GoogleStrategy> = {}; @@ -38,9 +39,23 @@ authRouter.get( passport.authenticate(req.params.DEVICE, { session: false, })(req, res, next), - function (req, res) { - const redirectUri = `${DeviceRedirects[req.params.DEVICE]}`; - return res.redirect(redirectUri); + async function (req, res, next) { + // Authentication failed - redirect to login + if (req.user == undefined) { + return res.redirect(`/auth/login/${req.params.DEVICE}`) + } + const userData = req.user as Profile; + const userId = `user${userData.id}`; + + // Generate the JWT, and redirect to JWT initialization + try { + const jwtPayload = (await getJwtPayloadFromDatabase(userId)).toObject(); + const token = jsonwebtoken.sign(jwtPayload, Config.JWT_SIGNING_SECRET, { expiresIn: Config.JWT_EXPIRATION_TIME }); + const redirectUri = `${DeviceRedirects[req.params.DEVICE]}/token=${token}`; + return res.redirect(redirectUri); + } catch (error) { + next(error); + } } ); diff --git a/src/services/auth/auth-utils.ts b/src/services/auth/auth-utils.ts index 129940c..08e82ab 100644 --- a/src/services/auth/auth-utils.ts +++ b/src/services/auth/auth-utils.ts @@ -29,3 +29,13 @@ export function createGoogleStrategy(device: string) { } ); } + + +export async function getJwtPayloadFromDatabase(userId: string) { + const payload = await Database.ROLES.findOne({userId: userId}).select(["userId", "roles"]); + if (!payload) { + throw new Error("NoUserFound"); + } + + return payload; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 7fad951..33f43b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -280,6 +280,13 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/jsonwebtoken@^9.0.6": + version "9.0.6" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz#d1af3544d99ad992fb6681bbe60676e06b032bd3" + integrity sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw== + dependencies: + "@types/node" "*" + "@types/mime@*": version "3.0.4" resolved "https://registry.npmjs.org/@types/mime/-/mime-3.0.4.tgz" @@ -690,6 +697,11 @@ bson@^6.2.0: resolved "https://registry.npmjs.org/bson/-/bson-6.5.0.tgz" integrity sha512-DXf1BTAS8vKyR90BO4x5v3rKVarmkdkzwOrnYDFdjAY694ILNDkmA3uRh1xXJEl+C1DAh8XCvAQ+Gh3kzubtpg== +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== + builtin-modules@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz" @@ -916,6 +928,13 @@ dotenv@*, dotenv@^16.4.5: resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== +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.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" @@ -1909,6 +1928,22 @@ json5@^1.0.2: dependencies: minimist "^1.2.0" +jsonwebtoken@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1: version "3.3.5" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" @@ -1919,6 +1954,23 @@ json5@^1.0.2: object.assign "^4.1.4" object.values "^1.1.6" +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" + +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" + kareem@2.5.1: version "2.5.1" resolved "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz" @@ -1951,11 +2003,46 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -2504,7 +2591,7 @@ safe-buffer@5.1.2: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1: +safe-buffer@5.2.1, safe-buffer@^5.0.1: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== From f1951fef57c96906969cc473dc467296be004579 Mon Sep 17 00:00:00 2001 From: Aydan Pirani <aydanpirani@gmail.com> Date: Thu, 11 Apr 2024 02:54:03 -0500 Subject: [PATCH 5/9] Added support for dev auth endpoint --- src/config.ts | 1 + src/services/auth/auth-router.ts | 6 +++++- src/services/auth/auth-utils.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/config.ts b/src/config.ts index deb7dc2..5d6d7ae 100644 --- a/src/config.ts +++ b/src/config.ts @@ -27,4 +27,5 @@ export const Config = { export const DeviceRedirects: Record<string, string> = { web: "https://www.google.com/", + dev: "http://127.0.0.1:3000/auth/dev/" }; diff --git a/src/services/auth/auth-router.ts b/src/services/auth/auth-router.ts index 6a33379..92d376c 100644 --- a/src/services/auth/auth-router.ts +++ b/src/services/auth/auth-router.ts @@ -51,7 +51,7 @@ authRouter.get( try { const jwtPayload = (await getJwtPayloadFromDatabase(userId)).toObject(); const token = jsonwebtoken.sign(jwtPayload, Config.JWT_SIGNING_SECRET, { expiresIn: Config.JWT_EXPIRATION_TIME }); - const redirectUri = `${DeviceRedirects[req.params.DEVICE]}/token=${token}`; + const redirectUri = DeviceRedirects[req.params.DEVICE] + `token=${token}`; return res.redirect(redirectUri); } catch (error) { next(error); @@ -59,4 +59,8 @@ authRouter.get( } ); +authRouter.get("/dev/", (req, res) => { + return res.status(StatusCodes.OK).json({"Token": req.headers.authorization}); +}) + export default authRouter; diff --git a/src/services/auth/auth-utils.ts b/src/services/auth/auth-utils.ts index 08e82ab..d163571 100644 --- a/src/services/auth/auth-utils.ts +++ b/src/services/auth/auth-utils.ts @@ -9,7 +9,7 @@ export function createGoogleStrategy(device: string) { { clientID: Config.CLIENT_ID, clientSecret: Config.CLIENT_SECRET, - callbackURL: `${Config.AUTH_CALLBACK_URI_BASE}${device}`, + callbackURL: Config.AUTH_CALLBACK_URI_BASE + device, }, // Strategy -> insert user into database if they don't exist From 65ac4cc67b3a8bf5bb639664cf9ed65278e5ded6 Mon Sep 17 00:00:00 2001 From: Aydan Pirani <aydanpirani@gmail.com> Date: Thu, 11 Apr 2024 10:37:21 -0500 Subject: [PATCH 6/9] Initial role checking middleware --- src/config.ts | 3 +-- src/middleware/role-checker.ts | 13 +++++++++++++ src/services/auth/auth-router.ts | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 src/middleware/role-checker.ts diff --git a/src/config.ts b/src/config.ts index 5d6d7ae..2620159 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,9 +17,8 @@ export const Config = { CLIENT_ID: getEnv("OAUTH_GOOGLE_CLIENT_ID"), CLIENT_SECRET: getEnv("OAUTH_GOOGLE_CLIENT_SECRET"), - // REDIRECT_URI: "http://localhost:3000/auth/callback", AUTH_CALLBACK_URI_BASE: "http://localhost:3000/auth/callback/", - // AUTH_CALLBACK_URI_BASE: "https://api.reflectionsprojections.org/auth/callback", + // AUTH_CALLBACK_URI_BASE: "https://api.reflectionsprojections.org/auth/callback/", JWT_SIGNING_SECRET: getEnv("JWT_SIGNING_SECRET"), JWT_EXPIRATION_TIME: "1 day", diff --git a/src/middleware/role-checker.ts b/src/middleware/role-checker.ts new file mode 100644 index 0000000..6999070 --- /dev/null +++ b/src/middleware/role-checker.ts @@ -0,0 +1,13 @@ +import { NextFunction } from "express"; +import { Role } from "../services/auth/auth-schema"; +import {z} from "zod"; + +export default function RoleChecker(req: Request, res: Response, next: NextFunction) { + const jwt = req.headers.get("authorization"); + console.log(jwt); + + return function (requiredRoles: z.infer<typeof Role>[], weakVerification: boolean = false) { + console.log("in here") + next() + } +} \ No newline at end of file diff --git a/src/services/auth/auth-router.ts b/src/services/auth/auth-router.ts index 92d376c..7b8bc9a 100644 --- a/src/services/auth/auth-router.ts +++ b/src/services/auth/auth-router.ts @@ -51,7 +51,7 @@ authRouter.get( try { const jwtPayload = (await getJwtPayloadFromDatabase(userId)).toObject(); const token = jsonwebtoken.sign(jwtPayload, Config.JWT_SIGNING_SECRET, { expiresIn: Config.JWT_EXPIRATION_TIME }); - const redirectUri = DeviceRedirects[req.params.DEVICE] + `token=${token}`; + const redirectUri = DeviceRedirects[req.params.DEVICE] + `?token=${token}`; return res.redirect(redirectUri); } catch (error) { next(error); @@ -60,7 +60,7 @@ authRouter.get( ); authRouter.get("/dev/", (req, res) => { - return res.status(StatusCodes.OK).json({"Token": req.headers.authorization}); + return res.status(StatusCodes.OK).json(req.query); }) export default authRouter; From 5888d71943f1043436ac6c96b9def95994f76231 Mon Sep 17 00:00:00 2001 From: Aydan Pirani <aydanpirani@gmail.com> Date: Thu, 11 Apr 2024 11:16:59 -0500 Subject: [PATCH 7/9] Added rolechecker support --- src/database.ts | 1 - src/middleware/role-checker.ts | 66 ++++++++++++++++++++++++++++---- src/services/auth/auth-models.ts | 8 ++++ src/services/auth/auth-schema.ts | 2 +- src/services/auth/auth-utils.ts | 4 +- 5 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 src/services/auth/auth-models.ts diff --git a/src/database.ts b/src/database.ts index 8f0cc67..ebe9031 100644 --- a/src/database.ts +++ b/src/database.ts @@ -37,7 +37,6 @@ function initializeModel( // Example usage export const Database = { ROLES: initializeModel("roles", RoleSchema, RoleValidator), - ROLES: initializeModel("roles", RoleSchema, RoleInfo), EVENTS: initializeModel("events", EventSchema, EventValidator), SUBSCRIPTION: initializeModel( "subscription", diff --git a/src/middleware/role-checker.ts b/src/middleware/role-checker.ts index 6999070..8bcd50b 100644 --- a/src/middleware/role-checker.ts +++ b/src/middleware/role-checker.ts @@ -1,13 +1,63 @@ -import { NextFunction } from "express"; -import { Role } from "../services/auth/auth-schema"; +import { NextFunction, Request, Response } from "express"; +import { JwtPayload, Role } from "../services/auth/auth-models"; import {z} from "zod"; +import jsonwebtoken from "jsonwebtoken"; +import { Config } from "../config"; +import { StatusCodes } from "http-status-codes"; -export default function RoleChecker(req: Request, res: Response, next: NextFunction) { - const jwt = req.headers.get("authorization"); - console.log(jwt); - return function (requiredRoles: z.infer<typeof Role>[], weakVerification: boolean = false) { - console.log("in here") - next() +export default function RoleChecker (requiredRoles: z.infer<typeof Role>[], weakVerification: boolean = false){ + return function (req: Request, res: Response, next: NextFunction) { + const jwt = req.headers.authorization; + + if (jwt == undefined) { + if (weakVerification) { + next(); + } + + return res.status(StatusCodes.BAD_REQUEST).json({error: "NoJWT"}) + } + + try { + console.log("in"); + const payloadData = jsonwebtoken.verify(jwt, Config.JWT_SIGNING_SECRET); + const payload = JwtPayload.parse(payloadData); + res.locals.payload = payload; + + const error = new Error("InvalidRoles"); + const userRoles = payload.roles; + + if (weakVerification) { + next(); + } + + if (requiredRoles.length == 0) { + next(); + } + + // Admins (staff) can access any endpoint + if (userRoles.includes(Role.Enum.ADMIN)) { + next(); + } + + // Corporate role can access corporate only endpoints + if (requiredRoles.includes(Role.Enum.CORPORATE)) { + if (userRoles.includes(Role.Enum.CORPORATE)) { + next(); + } + } + + // Need to be a user to access user endpoints (app users) + if (requiredRoles.includes(Role.Enum.USER)) { + if (userRoles.includes(Role.Enum.USER)) { + next(); + } + } + + throw error; + + } catch (error) { + next(error) + } } } \ No newline at end of file diff --git a/src/services/auth/auth-models.ts b/src/services/auth/auth-models.ts new file mode 100644 index 0000000..a34b6db --- /dev/null +++ b/src/services/auth/auth-models.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const Role = z.enum(["USER", "ADMIN", "CORPORATE"]); + +export const JwtPayload = z.object({ + userId: z.string(), + roles: Role.array(), +}) \ No newline at end of file diff --git a/src/services/auth/auth-schema.ts b/src/services/auth/auth-schema.ts index 619cf7a..b688077 100644 --- a/src/services/auth/auth-schema.ts +++ b/src/services/auth/auth-schema.ts @@ -1,7 +1,7 @@ import { Schema } from "mongoose"; import { z } from "zod"; +import { Role } from "./auth-models"; -export const Role = z.enum(["USER", "ADMIN", "CORPORATE"]); export const RoleValidator = z.object({ userId: z.coerce.string().regex(/user[0-9]*/), diff --git a/src/services/auth/auth-utils.ts b/src/services/auth/auth-utils.ts index d163571..fbb4e11 100644 --- a/src/services/auth/auth-utils.ts +++ b/src/services/auth/auth-utils.ts @@ -1,7 +1,6 @@ // Create a function to generate GoogleStrategy instances import { Strategy as GoogleStrategy } from "passport-google-oauth20"; import { Config } from "../../config"; -import { Role } from "./auth-schema"; import { Database } from "../../database"; export function createGoogleStrategy(device: string) { @@ -17,11 +16,10 @@ export function createGoogleStrategy(device: string) { const userId = `user${profile.id}`; const name = profile.displayName; const email = profile._json.email; - const roles = [Role.Enum.USER]; Database.ROLES.findOneAndUpdate( { userId: userId }, - { userId, name, email, roles }, + { userId, name, email }, { upsert: true } ) .then(() => cb(null, profile)) From dc4b9c8665c1cc42dc5a089125266b1a3fe9ce7f Mon Sep 17 00:00:00 2001 From: Aydan Pirani <aydanpirani@gmail.com> Date: Thu, 11 Apr 2024 11:17:54 -0500 Subject: [PATCH 8/9] Added linting --- src/config.ts | 2 +- src/middleware/role-checker.ts | 26 +++++++++++++++----------- src/services/auth/auth-models.ts | 4 ++-- src/services/auth/auth-router.ts | 19 +++++++++++++------ src/services/auth/auth-schema.ts | 1 - src/services/auth/auth-utils.ts | 10 ++++++---- 6 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/config.ts b/src/config.ts index 1239660..38aab55 100644 --- a/src/config.ts +++ b/src/config.ts @@ -28,5 +28,5 @@ export const Config = { export const DeviceRedirects: Record<string, string> = { web: "https://www.google.com/", - dev: "http://127.0.0.1:3000/auth/dev/" + dev: "http://127.0.0.1:3000/auth/dev/", }; diff --git a/src/middleware/role-checker.ts b/src/middleware/role-checker.ts index 8bcd50b..6fc5cf8 100644 --- a/src/middleware/role-checker.ts +++ b/src/middleware/role-checker.ts @@ -1,12 +1,14 @@ import { NextFunction, Request, Response } from "express"; import { JwtPayload, Role } from "../services/auth/auth-models"; -import {z} from "zod"; +import { z } from "zod"; import jsonwebtoken from "jsonwebtoken"; import { Config } from "../config"; import { StatusCodes } from "http-status-codes"; - -export default function RoleChecker (requiredRoles: z.infer<typeof Role>[], weakVerification: boolean = false){ +export default function RoleChecker( + requiredRoles: z.infer<typeof Role>[], + weakVerification: boolean = false +) { return function (req: Request, res: Response, next: NextFunction) { const jwt = req.headers.authorization; @@ -15,12 +17,15 @@ export default function RoleChecker (requiredRoles: z.infer<typeof Role>[], weak next(); } - return res.status(StatusCodes.BAD_REQUEST).json({error: "NoJWT"}) + return res.status(StatusCodes.BAD_REQUEST).json({ error: "NoJWT" }); } - + try { console.log("in"); - const payloadData = jsonwebtoken.verify(jwt, Config.JWT_SIGNING_SECRET); + const payloadData = jsonwebtoken.verify( + jwt, + Config.JWT_SIGNING_SECRET + ); const payload = JwtPayload.parse(payloadData); res.locals.payload = payload; @@ -30,7 +35,7 @@ export default function RoleChecker (requiredRoles: z.infer<typeof Role>[], weak if (weakVerification) { next(); } - + if (requiredRoles.length == 0) { next(); } @@ -55,9 +60,8 @@ export default function RoleChecker (requiredRoles: z.infer<typeof Role>[], weak } throw error; - } catch (error) { - next(error) + next(error); } - } -} \ No newline at end of file + }; +} diff --git a/src/services/auth/auth-models.ts b/src/services/auth/auth-models.ts index a34b6db..bda56ab 100644 --- a/src/services/auth/auth-models.ts +++ b/src/services/auth/auth-models.ts @@ -2,7 +2,7 @@ import { z } from "zod"; export const Role = z.enum(["USER", "ADMIN", "CORPORATE"]); -export const JwtPayload = z.object({ +export const JwtPayload = z.object({ userId: z.string(), roles: Role.array(), -}) \ No newline at end of file +}); diff --git a/src/services/auth/auth-router.ts b/src/services/auth/auth-router.ts index 7b8bc9a..8ab4301 100644 --- a/src/services/auth/auth-router.ts +++ b/src/services/auth/auth-router.ts @@ -42,16 +42,23 @@ authRouter.get( async function (req, res, next) { // Authentication failed - redirect to login if (req.user == undefined) { - return res.redirect(`/auth/login/${req.params.DEVICE}`) + return res.redirect(`/auth/login/${req.params.DEVICE}`); } const userData = req.user as Profile; const userId = `user${userData.id}`; - + // Generate the JWT, and redirect to JWT initialization try { - const jwtPayload = (await getJwtPayloadFromDatabase(userId)).toObject(); - const token = jsonwebtoken.sign(jwtPayload, Config.JWT_SIGNING_SECRET, { expiresIn: Config.JWT_EXPIRATION_TIME }); - const redirectUri = DeviceRedirects[req.params.DEVICE] + `?token=${token}`; + const jwtPayload = ( + await getJwtPayloadFromDatabase(userId) + ).toObject(); + const token = jsonwebtoken.sign( + jwtPayload, + Config.JWT_SIGNING_SECRET, + { expiresIn: Config.JWT_EXPIRATION_TIME } + ); + const redirectUri = + DeviceRedirects[req.params.DEVICE] + `?token=${token}`; return res.redirect(redirectUri); } catch (error) { next(error); @@ -61,6 +68,6 @@ authRouter.get( authRouter.get("/dev/", (req, res) => { return res.status(StatusCodes.OK).json(req.query); -}) +}); export default authRouter; diff --git a/src/services/auth/auth-schema.ts b/src/services/auth/auth-schema.ts index b688077..9434d11 100644 --- a/src/services/auth/auth-schema.ts +++ b/src/services/auth/auth-schema.ts @@ -2,7 +2,6 @@ import { Schema } from "mongoose"; import { z } from "zod"; import { Role } from "./auth-models"; - export const RoleValidator = z.object({ userId: z.coerce.string().regex(/user[0-9]*/), name: z.coerce.string(), diff --git a/src/services/auth/auth-utils.ts b/src/services/auth/auth-utils.ts index fbb4e11..b4a3c2e 100644 --- a/src/services/auth/auth-utils.ts +++ b/src/services/auth/auth-utils.ts @@ -28,12 +28,14 @@ export function createGoogleStrategy(device: string) { ); } - export async function getJwtPayloadFromDatabase(userId: string) { - const payload = await Database.ROLES.findOne({userId: userId}).select(["userId", "roles"]); + const payload = await Database.ROLES.findOne({ userId: userId }).select([ + "userId", + "roles", + ]); if (!payload) { throw new Error("NoUserFound"); } - + return payload; -} \ No newline at end of file +} From 7aab40db50a455a08606b5360ff8dea7447c09a6 Mon Sep 17 00:00:00 2001 From: Aydan Pirani <aydanpirani@gmail.com> Date: Thu, 11 Apr 2024 13:27:13 -0500 Subject: [PATCH 9/9] Fixed naming conventions --- src/app.ts | 1 - src/middleware/role-checker.ts | 6 +++--- src/services/auth/auth-models.ts | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/app.ts b/src/app.ts index bfd276b..397879a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -28,7 +28,6 @@ app.use("/event", eventRouter); app.use("/subscription", subscriptionRouter); app.get("/status", (_, res) => { - console.log(StatusCodes.OK); return res.status(StatusCodes.OK).send("API is alive!"); }); diff --git a/src/middleware/role-checker.ts b/src/middleware/role-checker.ts index 6fc5cf8..4c672a4 100644 --- a/src/middleware/role-checker.ts +++ b/src/middleware/role-checker.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from "express"; -import { JwtPayload, Role } from "../services/auth/auth-models"; +import { JwtPayloadValidator, Role } from "../services/auth/auth-models"; import { z } from "zod"; import jsonwebtoken from "jsonwebtoken"; import { Config } from "../config"; @@ -21,12 +21,12 @@ export default function RoleChecker( } try { - console.log("in"); const payloadData = jsonwebtoken.verify( jwt, Config.JWT_SIGNING_SECRET ); - const payload = JwtPayload.parse(payloadData); + + const payload = JwtPayloadValidator.parse(payloadData); res.locals.payload = payload; const error = new Error("InvalidRoles"); diff --git a/src/services/auth/auth-models.ts b/src/services/auth/auth-models.ts index bda56ab..2dbed28 100644 --- a/src/services/auth/auth-models.ts +++ b/src/services/auth/auth-models.ts @@ -2,7 +2,7 @@ import { z } from "zod"; export const Role = z.enum(["USER", "ADMIN", "CORPORATE"]); -export const JwtPayload = z.object({ +export const JwtPayloadValidator = z.object({ userId: z.string(), roles: Role.array(), });