feat(api): sessions management (#49499)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Niraj Nandish
2023-03-09 20:36:33 +05:30
committed by GitHub
parent 1da3e95ee3
commit 4769a448e1
9 changed files with 151 additions and 29 deletions
+29 -5
View File
@@ -1,12 +1,24 @@
import fastifyAuth0 from 'fastify-auth0-verify'; import fastifyAuth0 from 'fastify-auth0-verify';
import Fastify from 'fastify'; import Fastify from 'fastify';
import middie from '@fastify/middie'; 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 jwtAuthz from './plugins/fastify-jwt-authz';
import sessionAuth from './plugins/session-auth';
import { testRoutes } from './routes/test'; import { testRoutes } from './routes/test';
import { auth0Routes } from './routes/auth0';
import { dbConnector } from './db'; import { dbConnector } from './db';
import { auth0Verify, testMiddleware } from './middleware'; import { testMiddleware } from './middleware';
import { AUTH0_AUDIENCE, AUTH0_DOMAIN, NODE_ENV, PORT } from './utils/env'; import {
AUTH0_AUDIENCE,
AUTH0_DOMAIN,
NODE_ENV,
PORT,
MONGOHQ_URL,
SESSION_SECRET
} from './utils/env';
const fastify = Fastify({ const fastify = Fastify({
logger: { level: NODE_ENV === 'development' ? 'debug' : 'fatal' } logger: { level: NODE_ENV === 'development' ? 'debug' : 'fatal' }
@@ -19,6 +31,19 @@ fastify.get('/', async (_request, _reply) => {
const start = async () => { const start = async () => {
// NOTE: Awaited to ensure `.use` is registered on `fastify` // NOTE: Awaited to ensure `.use` is registered on `fastify`
await fastify.register(middie); 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 // Auth0 plugin
void fastify.register(fastifyAuth0, { void fastify.register(fastifyAuth0, {
@@ -26,14 +51,13 @@ const start = async () => {
audience: AUTH0_AUDIENCE audience: AUTH0_AUDIENCE
}); });
void fastify.register(jwtAuthz); void fastify.register(jwtAuthz);
void fastify.register(sessionAuth);
void fastify.use('/test', testMiddleware); void fastify.use('/test', testMiddleware);
// Hooks
void fastify.addHook('preValidation', auth0Verify);
void fastify.register(dbConnector); void fastify.register(dbConnector);
void fastify.register(testRoutes); void fastify.register(testRoutes);
void fastify.register(auth0Routes, { prefix: '/auth0' });
try { try {
const port = Number(PORT); const port = Number(PORT);
-9
View File
@@ -1,14 +1,5 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import type { NextFunction, NextHandleFunction } from '@fastify/middie'; import type { NextFunction, NextHandleFunction } from '@fastify/middie';
export async function auth0Verify(
this: FastifyInstance,
request: FastifyRequest,
reply: FastifyReply
): Promise<void> {
await this.authenticate(request, reply);
}
type MiddieRequest = Parameters<NextHandleFunction>[0]; type MiddieRequest = Parameters<NextHandleFunction>[0];
type MiddieResponse = Parameters<NextHandleFunction>[1]; type MiddieResponse = Parameters<NextHandleFunction>[1];
+3
View File
@@ -4,8 +4,11 @@
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues" "url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
}, },
"dependencies": { "dependencies": {
"@fastify/cookie": "^8.3.0",
"@fastify/middie": "8.1", "@fastify/middie": "8.1",
"@fastify/mongodb": "6.2.0", "@fastify/mongodb": "6.2.0",
"@fastify/session": "^10.1.1",
"connect-mongo": "^4.6.0",
"fastify": "4.14.0", "fastify": "4.14.0",
"fastify-auth0-verify": "^1.0.0", "fastify-auth0-verify": "^1.0.0",
"fastify-plugin": "^4.3.0", "fastify-plugin": "^4.3.0",
+1 -1
View File
@@ -25,7 +25,7 @@ SOFTWARE.
import { FastifyPluginCallback, FastifyRequest } from 'fastify'; import { FastifyPluginCallback, FastifyRequest } from 'fastify';
import fp from 'fastify-plugin'; import fp from 'fastify-plugin';
interface UserObject { export interface UserObject {
scope?: string; scope?: string;
} }
+25
View File
@@ -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);
+44
View File
@@ -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();
};
+12 -14
View File
@@ -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) => { export const testRoutes: FastifyPluginCallback = (fastify, _options, done) => {
const collection = fastify.mongo.db?.collection('user'); 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) { if (!collection) {
return { error: 'No 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 }; return { user };
}); });
fastify.put( fastify.put<{ Body: { quincyEmails: boolean } }>(
'/update-privacy-terms', '/update-privacy-terms',
{ {
preHandler: [
function (
req: FastifyRequest<{ Body: { quincyEmails: boolean } }>,
_res,
done
) {
void req.jwtAuthz(['write:user'], done);
}
],
schema: { schema: {
body: { body: {
type: 'object',
required: ['quincyEmails'], required: ['quincyEmails'],
properties: { properties: {
quincyEmails: { type: 'boolean' } quincyEmails: { type: 'boolean' }
@@ -42,8 +38,10 @@ export const testRoutes: FastifyPluginCallback = (fastify, _options, done) => {
sendQuincyEmail: !!quincyEmails sendQuincyEmail: !!quincyEmails
}; };
const userId = new ObjectId(req.session.user.id);
return collection return collection
?.updateOne({ email: 'bar@bar.com' }, { $set: update }) ?.updateOne({ _id: userId }, { $set: update })
.then(() => { .then(() => {
void res.code(200).send({ msg: 'Successfully updated' }); void res.code(200).send({ msg: 'Successfully updated' });
}) })
+2
View File
@@ -21,6 +21,7 @@ if (error) {
assert.ok(process.env.NODE_ENV); assert.ok(process.env.NODE_ENV);
assert.ok(process.env.AUTH0_DOMAIN); assert.ok(process.env.AUTH0_DOMAIN);
assert.ok(process.env.AUTH0_AUDIENCE); assert.ok(process.env.AUTH0_AUDIENCE);
assert.ok(process.env.SESSION_SECRET);
if (process.env.NODE_ENV !== 'development') { if (process.env.NODE_ENV !== 'development') {
assert.ok(process.env.PORT); 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_DOMAIN = process.env.AUTH0_DOMAIN;
export const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE; export const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;
export const PORT = process.env.PORT || '3000'; export const PORT = process.env.PORT || '3000';
export const SESSION_SECRET = process.env.SESSION_SECRET;
+35
View File
@@ -113,15 +113,21 @@ importers:
api: api:
specifiers: specifiers:
'@fastify/cookie': ^8.3.0
'@fastify/middie': '8.1' '@fastify/middie': '8.1'
'@fastify/mongodb': 6.2.0 '@fastify/mongodb': 6.2.0
'@fastify/session': ^10.1.1
connect-mongo: ^4.6.0
fastify: 4.14.0 fastify: 4.14.0
fastify-auth0-verify: ^1.0.0 fastify-auth0-verify: ^1.0.0
fastify-plugin: ^4.3.0 fastify-plugin: ^4.3.0
nodemon: 2.0.21 nodemon: 2.0.21
dependencies: dependencies:
'@fastify/cookie': 8.3.0
'@fastify/middie': 8.1.0 '@fastify/middie': 8.1.0
'@fastify/mongodb': 6.2.0 '@fastify/mongodb': 6.2.0
'@fastify/session': 10.1.1
connect-mongo: 4.6.0_lw7oj3533zfvrhxigrep3g2fam
fastify: 4.14.0 fastify: 4.14.0
fastify-auth0-verify: 1.0.0 fastify-auth0-verify: 1.0.0
fastify-plugin: 4.5.0 fastify-plugin: 4.5.0
@@ -4697,6 +4703,13 @@ packages:
- aws-crt - aws-crt
dev: false 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: /@fortawesome/fontawesome-common-types/6.3.0:
resolution: {integrity: sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg==} resolution: {integrity: sha512-4BC1NMoacEBzSXRwKjZ/X/gmnbp/HU5Qqat7E8xqorUtBFZS+bwfGH5/wqOC2K6GV0rgEobp3OjGRMa5fK9pFg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -12224,6 +12237,21 @@ packages:
- snappy - snappy
dev: false 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: /connect/3.7.0:
resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
engines: {node: '>= 0.10.0'} engines: {node: '>= 0.10.0'}
@@ -19921,6 +19949,13 @@ packages:
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
engines: {node: '>= 8'} 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: /labeled-stream-splicer/2.0.2:
resolution: {integrity: sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==} resolution: {integrity: sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==}
dependencies: dependencies: