From 4769a448e1f7e400dd6ea9304c7b5e34e5fd4223 Mon Sep 17 00:00:00 2001 From: Niraj Nandish Date: Thu, 9 Mar 2023 20:36:33 +0530 Subject: [PATCH] feat(api): sessions management (#49499) Co-authored-by: Oliver Eyton-Williams --- api/index.ts | 34 ++++++++++++++++++++---- api/middleware/index.ts | 9 ------- api/package.json | 3 +++ api/plugins/fastify-jwt-authz.ts | 2 +- api/plugins/session-auth.ts | 25 ++++++++++++++++++ api/routes/auth0.ts | 44 ++++++++++++++++++++++++++++++++ api/routes/test.ts | 26 +++++++++---------- api/utils/env.ts | 2 ++ pnpm-lock.yaml | 35 +++++++++++++++++++++++++ 9 files changed, 151 insertions(+), 29 deletions(-) create mode 100644 api/plugins/session-auth.ts create mode 100644 api/routes/auth0.ts diff --git a/api/index.ts b/api/index.ts index b80cc698490..367e1b9ac7a 100644 --- a/api/index.ts +++ b/api/index.ts @@ -1,12 +1,24 @@ import fastifyAuth0 from 'fastify-auth0-verify'; import Fastify from 'fastify'; import middie from '@fastify/middie'; +import fastifySession from '@fastify/session'; +import fastifyCookie from '@fastify/cookie'; +import MongoStore from 'connect-mongo'; import jwtAuthz from './plugins/fastify-jwt-authz'; +import sessionAuth from './plugins/session-auth'; import { testRoutes } from './routes/test'; +import { auth0Routes } from './routes/auth0'; import { dbConnector } from './db'; -import { auth0Verify, testMiddleware } from './middleware'; -import { AUTH0_AUDIENCE, AUTH0_DOMAIN, NODE_ENV, PORT } from './utils/env'; +import { testMiddleware } from './middleware'; +import { + AUTH0_AUDIENCE, + AUTH0_DOMAIN, + NODE_ENV, + PORT, + MONGOHQ_URL, + SESSION_SECRET +} from './utils/env'; const fastify = Fastify({ logger: { level: NODE_ENV === 'development' ? 'debug' : 'fatal' } @@ -19,6 +31,19 @@ fastify.get('/', async (_request, _reply) => { const start = async () => { // NOTE: Awaited to ensure `.use` is registered on `fastify` await fastify.register(middie); + await fastify.register(fastifyCookie); + await fastify.register(fastifySession, { + secret: SESSION_SECRET, + rolling: false, + saveUninitialized: false, + cookie: { + maxAge: 1000 * 60 * 60, // 1 hour + secure: NODE_ENV !== 'development' + }, + store: MongoStore.create({ + mongoUrl: MONGOHQ_URL + }) + }); // Auth0 plugin void fastify.register(fastifyAuth0, { @@ -26,14 +51,13 @@ const start = async () => { audience: AUTH0_AUDIENCE }); void fastify.register(jwtAuthz); + void fastify.register(sessionAuth); void fastify.use('/test', testMiddleware); - // Hooks - void fastify.addHook('preValidation', auth0Verify); - void fastify.register(dbConnector); void fastify.register(testRoutes); + void fastify.register(auth0Routes, { prefix: '/auth0' }); try { const port = Number(PORT); diff --git a/api/middleware/index.ts b/api/middleware/index.ts index 59fb5108041..96f85d751a5 100644 --- a/api/middleware/index.ts +++ b/api/middleware/index.ts @@ -1,14 +1,5 @@ -import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; import type { NextFunction, NextHandleFunction } from '@fastify/middie'; -export async function auth0Verify( - this: FastifyInstance, - request: FastifyRequest, - reply: FastifyReply -): Promise { - await this.authenticate(request, reply); -} - type MiddieRequest = Parameters[0]; type MiddieResponse = Parameters[1]; diff --git a/api/package.json b/api/package.json index 9505aea973c..f581e3be839 100644 --- a/api/package.json +++ b/api/package.json @@ -4,8 +4,11 @@ "url": "https://github.com/freeCodeCamp/freeCodeCamp/issues" }, "dependencies": { + "@fastify/cookie": "^8.3.0", "@fastify/middie": "8.1", "@fastify/mongodb": "6.2.0", + "@fastify/session": "^10.1.1", + "connect-mongo": "^4.6.0", "fastify": "4.14.0", "fastify-auth0-verify": "^1.0.0", "fastify-plugin": "^4.3.0", diff --git a/api/plugins/fastify-jwt-authz.ts b/api/plugins/fastify-jwt-authz.ts index 388d42bfb25..23eccac04f1 100644 --- a/api/plugins/fastify-jwt-authz.ts +++ b/api/plugins/fastify-jwt-authz.ts @@ -25,7 +25,7 @@ SOFTWARE. import { FastifyPluginCallback, FastifyRequest } from 'fastify'; import fp from 'fastify-plugin'; -interface UserObject { +export interface UserObject { scope?: string; } diff --git a/api/plugins/session-auth.ts b/api/plugins/session-auth.ts new file mode 100644 index 00000000000..aba4883f878 --- /dev/null +++ b/api/plugins/session-auth.ts @@ -0,0 +1,25 @@ +import { FastifyPluginCallback, onRequestHookHandler } from 'fastify'; +import fp from 'fastify-plugin'; + +const sessionAuth: FastifyPluginCallback = (fastify, _opts, done) => { + const authenticateSession: onRequestHookHandler = (req, res, done) => { + if (!req.session.user) { + res.statusCode = 401; + void res.send({ msg: 'Unauthorized' }); + } else { + done(); + } + }; + + fastify.decorate('authenticateSession', authenticateSession); + + done(); +}; + +declare module 'fastify' { + interface FastifyInstance { + authenticateSession: onRequestHookHandler; + } +} + +export default fp(sessionAuth); diff --git a/api/routes/auth0.ts b/api/routes/auth0.ts new file mode 100644 index 00000000000..1072b52a1df --- /dev/null +++ b/api/routes/auth0.ts @@ -0,0 +1,44 @@ +import { FastifyPluginCallback } from 'fastify'; + +import { AUTH0_DOMAIN } from '../utils/env'; + +declare module 'fastify' { + interface Session { + user: { + id: string; + }; + } +} + +export const auth0Routes: FastifyPluginCallback = (fastify, _options, done) => { + fastify.addHook('onRequest', fastify.authenticate); + const collection = fastify.mongo.db?.collection('user'); + + fastify.get('/callback', async (req, _res) => { + const auth0Res = await fetch( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + `https://${AUTH0_DOMAIN}/userinfo`, + { + headers: { + Authorization: req.headers.authorization ?? '' + } + } + ); + + if (!auth0Res.ok) { + fastify.log.error(auth0Res); + throw new Error('Invalid Auth0 Access Token'); + } + + const { email } = (await auth0Res.json()) as { email: string }; + const user = await collection?.findOne({ email }); + if (user) { + req.session.user = { id: user._id.toString() }; + } else { + const DBRes = await collection?.insertOne({ email }); + req.session.user = { id: DBRes?.insertedId.toString() ?? '' }; + } + await req.session.save(); + }); + done(); +}; diff --git a/api/routes/test.ts b/api/routes/test.ts index 73b313d764c..3a6a9f2817f 100644 --- a/api/routes/test.ts +++ b/api/routes/test.ts @@ -1,30 +1,26 @@ -import { FastifyPluginCallback, FastifyRequest } from 'fastify'; +import { ObjectId } from '@fastify/mongodb'; +import { FastifyPluginCallback } from 'fastify'; export const testRoutes: FastifyPluginCallback = (fastify, _options, done) => { const collection = fastify.mongo.db?.collection('user'); - fastify.get('/test', async (_request, _reply) => { + fastify.addHook('onRequest', fastify.authenticateSession); + + fastify.get('/test', async (request, _reply) => { if (!collection) { return { error: 'No collection' }; } - const user = await collection?.findOne({ email: 'bar@bar.com' }); + const userId = new ObjectId(request.session.user.id); + const user = await collection?.findOne({ _id: userId }); return { user }; }); - fastify.put( + fastify.put<{ Body: { quincyEmails: boolean } }>( '/update-privacy-terms', { - preHandler: [ - function ( - req: FastifyRequest<{ Body: { quincyEmails: boolean } }>, - _res, - done - ) { - void req.jwtAuthz(['write:user'], done); - } - ], schema: { body: { + type: 'object', required: ['quincyEmails'], properties: { quincyEmails: { type: 'boolean' } @@ -42,8 +38,10 @@ export const testRoutes: FastifyPluginCallback = (fastify, _options, done) => { sendQuincyEmail: !!quincyEmails }; + const userId = new ObjectId(req.session.user.id); + return collection - ?.updateOne({ email: 'bar@bar.com' }, { $set: update }) + ?.updateOne({ _id: userId }, { $set: update }) .then(() => { void res.code(200).send({ msg: 'Successfully updated' }); }) diff --git a/api/utils/env.ts b/api/utils/env.ts index a54e35c9c81..2e4befebb0e 100644 --- a/api/utils/env.ts +++ b/api/utils/env.ts @@ -21,6 +21,7 @@ if (error) { assert.ok(process.env.NODE_ENV); assert.ok(process.env.AUTH0_DOMAIN); assert.ok(process.env.AUTH0_AUDIENCE); +assert.ok(process.env.SESSION_SECRET); if (process.env.NODE_ENV !== 'development') { assert.ok(process.env.PORT); @@ -33,3 +34,4 @@ export const NODE_ENV = process.env.NODE_ENV; export const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN; export const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE; export const PORT = process.env.PORT || '3000'; +export const SESSION_SECRET = process.env.SESSION_SECRET; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9b6b4c05de..b4d98b2760d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,15 +113,21 @@ importers: api: specifiers: + '@fastify/cookie': ^8.3.0 '@fastify/middie': '8.1' '@fastify/mongodb': 6.2.0 + '@fastify/session': ^10.1.1 + connect-mongo: ^4.6.0 fastify: 4.14.0 fastify-auth0-verify: ^1.0.0 fastify-plugin: ^4.3.0 nodemon: 2.0.21 dependencies: + '@fastify/cookie': 8.3.0 '@fastify/middie': 8.1.0 '@fastify/mongodb': 6.2.0 + '@fastify/session': 10.1.1 + connect-mongo: 4.6.0_lw7oj3533zfvrhxigrep3g2fam fastify: 4.14.0 fastify-auth0-verify: 1.0.0 fastify-plugin: 4.5.0 @@ -4697,6 +4703,13 @@ packages: - aws-crt dev: false + /@fastify/session/10.1.1: + resolution: {integrity: sha512-8pKDTL9MuqU1FCTca6XNd1E4quZ/ipik69AHXqkANia9Z4xPFS5OSKIwmCClIdaMYD32/tPu4G/6wGgK5Buj5g==} + dependencies: + fastify-plugin: 4.5.0 + safe-stable-stringify: 2.4.2 + dev: false + /@fortawesome/fontawesome-common-types/6.3.0: resolution: {integrity: sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg==} engines: {node: '>=6'} @@ -12224,6 +12237,21 @@ packages: - snappy dev: false + /connect-mongo/4.6.0_lw7oj3533zfvrhxigrep3g2fam: + resolution: {integrity: sha512-8new4Z7NLP3CGP65Aw6ls3xDBeKVvHRSh39CXuDZTQsvpeeU9oNMzfFgvqmHqZ6gWpxIl663RyoVEmCAGf1yOg==} + engines: {node: '>=10'} + peerDependencies: + express-session: ^1.17.1 + mongodb: ^4.1.0 + dependencies: + debug: 4.3.4 + express-session: 1.17.3 + kruptein: 3.0.6 + mongodb: 4.14.0 + transitivePeerDependencies: + - supports-color + dev: false + /connect/3.7.0: resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} engines: {node: '>= 0.10.0'} @@ -19921,6 +19949,13 @@ packages: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} + /kruptein/3.0.6: + resolution: {integrity: sha512-EQJjTwAJfQkC4NfdQdo3HXM2a9pmBm8oidzH270cYu1MbgXPNPMJuldN7OPX+qdhPO5rw4X3/iKz0BFBfkXGKA==} + engines: {node: '>8'} + dependencies: + asn1.js: 5.4.1 + dev: false + /labeled-stream-splicer/2.0.2: resolution: {integrity: sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==} dependencies: