mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
fix(api): modularize auth handlers (#55671)
This commit is contained in:
committed by
GitHub
parent
7d84da184a
commit
e9ac6c5e72
+36
-33
@@ -24,7 +24,8 @@ import { SESProvider } from './plugins/mail-providers/ses';
|
|||||||
import mailer from './plugins/mailer';
|
import mailer from './plugins/mailer';
|
||||||
import redirectWithMessage from './plugins/redirect-with-message';
|
import redirectWithMessage from './plugins/redirect-with-message';
|
||||||
import security from './plugins/security';
|
import security from './plugins/security';
|
||||||
import codeFlowAuth from './plugins/code-flow-auth';
|
import auth from './plugins/auth';
|
||||||
|
import bouncer from './plugins/bouncer';
|
||||||
import notFound from './plugins/not-found';
|
import notFound from './plugins/not-found';
|
||||||
import { authRoutes, mobileAuth0Routes } from './routes/auth';
|
import { authRoutes, mobileAuth0Routes } from './routes/auth';
|
||||||
import { devAuthRoutes } from './routes/auth-dev';
|
import { devAuthRoutes } from './routes/auth-dev';
|
||||||
@@ -182,44 +183,46 @@ export const build = async (
|
|||||||
|
|
||||||
// redirectWithMessage must be registered before codeFlowAuth
|
// redirectWithMessage must be registered before codeFlowAuth
|
||||||
void fastify.register(redirectWithMessage);
|
void fastify.register(redirectWithMessage);
|
||||||
void fastify.register(codeFlowAuth);
|
void fastify.register(auth);
|
||||||
void fastify.register(notFound);
|
void fastify.register(notFound);
|
||||||
void fastify.register(prismaPlugin);
|
void fastify.register(prismaPlugin);
|
||||||
|
|
||||||
// Routes requiring authentication and CSRF protection
|
// Routes requiring authentication:
|
||||||
void fastify.register(function (fastify, _opts, done) {
|
void fastify.register(async function (fastify, _opts) {
|
||||||
// The order matters here, since we want to reject invalid cross site requests
|
await fastify.register(bouncer);
|
||||||
// before checking if the user is authenticated.
|
|
||||||
// @ts-expect-error - @fastify/csrf-protection needs to update their types
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
||||||
fastify.addHook('onRequest', fastify.csrfProtection);
|
|
||||||
fastify.addHook('onRequest', fastify.authorize);
|
fastify.addHook('onRequest', fastify.authorize);
|
||||||
|
// CSRF protection enabled:
|
||||||
|
await fastify.register(async function (fastify, _opts) {
|
||||||
|
// TODO: bounce unauthed requests before checking CSRF token. This will
|
||||||
|
// mean moving csrfProtection into custom plugin and testing separately,
|
||||||
|
// because it's a pain to mess around with other cookies/hook order.
|
||||||
|
// @ts-expect-error - @fastify/csrf-protection needs to update their types
|
||||||
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
|
fastify.addHook('onRequest', fastify.csrfProtection);
|
||||||
|
fastify.addHook('onRequest', fastify.send401IfNoUser);
|
||||||
|
|
||||||
void fastify.register(challengeRoutes);
|
await fastify.register(challengeRoutes);
|
||||||
void fastify.register(donateRoutes);
|
await fastify.register(donateRoutes);
|
||||||
void fastify.register(protectedCertificateRoutes);
|
await fastify.register(protectedCertificateRoutes);
|
||||||
void fastify.register(settingRoutes);
|
await fastify.register(settingRoutes);
|
||||||
void fastify.register(userRoutes);
|
await fastify.register(userRoutes);
|
||||||
done();
|
});
|
||||||
|
|
||||||
|
// CSRF protection disabled:
|
||||||
|
await fastify.register(async function (fastify, _opts) {
|
||||||
|
fastify.addHook('onRequest', fastify.send401IfNoUser);
|
||||||
|
|
||||||
|
await fastify.register(userGetRoutes);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes that redirect if access is denied:
|
||||||
|
await fastify.register(async function (fastify, _opts) {
|
||||||
|
fastify.addHook('onRequest', fastify.redirectIfNoUser);
|
||||||
|
|
||||||
|
await fastify.register(settingRedirectRoutes);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
// Routes not requiring authentication:
|
||||||
// Routes requiring authentication and NOT CSRF protection
|
|
||||||
void fastify.register(function (fastify, _opts, done) {
|
|
||||||
fastify.addHook('onRequest', fastify.authorize);
|
|
||||||
|
|
||||||
void fastify.register(userGetRoutes);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Routes requiring authentication that redirect on failure
|
|
||||||
void fastify.register(function (fastify, _opts, done) {
|
|
||||||
fastify.addHook('onRequest', fastify.authorizeOrRedirect);
|
|
||||||
|
|
||||||
void fastify.register(settingRedirectRoutes);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Routes not requiring authentication
|
|
||||||
void fastify.register(mobileAuth0Routes);
|
void fastify.register(mobileAuth0Routes);
|
||||||
// TODO: consolidate with LOCAL_MOCK_AUTH
|
// TODO: consolidate with LOCAL_MOCK_AUTH
|
||||||
if (FCC_ENABLE_DEV_LOGIN_MODE) {
|
if (FCC_ENABLE_DEV_LOGIN_MODE) {
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import Fastify, { FastifyInstance } from 'fastify';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
import { COOKIE_DOMAIN, JWT_SECRET } from '../utils/env';
|
||||||
|
import { type Token, createAccessToken } from '../utils/tokens';
|
||||||
|
import cookies, { sign as signCookie, unsign as unsignCookie } from './cookies';
|
||||||
|
import auth from './auth';
|
||||||
|
|
||||||
|
async function setupServer() {
|
||||||
|
const fastify = Fastify();
|
||||||
|
await fastify.register(cookies);
|
||||||
|
await fastify.register(auth);
|
||||||
|
return fastify;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('auth', () => {
|
||||||
|
let fastify: FastifyInstance;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
fastify = await setupServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await fastify.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setAccessTokenCookie', () => {
|
||||||
|
// We won't need to keep doubly signing the cookie when we migrate the
|
||||||
|
// authentication, but for the MVP we have to be able to read the cookies
|
||||||
|
// set by the api-server. So, double signing:
|
||||||
|
it('should doubly sign the cookie', async () => {
|
||||||
|
const token = createAccessToken('test-id');
|
||||||
|
fastify.get('/test', async (req, reply) => {
|
||||||
|
reply.setAccessTokenCookie(token);
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fastify.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/test'
|
||||||
|
});
|
||||||
|
|
||||||
|
const { value, ...rest } = res.cookies[0]!;
|
||||||
|
const unsignedOnce = unsignCookie(value);
|
||||||
|
const unsignedTwice = jwt.verify(unsignedOnce.value!, JWT_SECRET) as {
|
||||||
|
accessToken: Token;
|
||||||
|
};
|
||||||
|
expect(unsignedTwice.accessToken).toEqual(token);
|
||||||
|
expect(rest).toEqual({
|
||||||
|
name: 'jwt_access_token',
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'Lax',
|
||||||
|
domain: COOKIE_DOMAIN,
|
||||||
|
maxAge: token.ttl
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Post-MVP sync the cookie max-age with the token ttl (i.e. the
|
||||||
|
// max-age should be the ttl/1000, not ttl)
|
||||||
|
it('should set the max-age of the cookie to match the ttl of the token', async () => {
|
||||||
|
const token = createAccessToken('test-id', 123000);
|
||||||
|
fastify.get('/test', async (req, reply) => {
|
||||||
|
reply.setAccessTokenCookie(token);
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fastify.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/test'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.cookies[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
maxAge: 123000
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authorize', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fastify.get('/test', (_req, reply) => {
|
||||||
|
void reply.send({ ok: true });
|
||||||
|
});
|
||||||
|
fastify.addHook('onRequest', fastify.authorize);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deny if the access token is missing', async () => {
|
||||||
|
expect.assertions(4);
|
||||||
|
|
||||||
|
fastify.addHook('onRequest', (req, _reply, done) => {
|
||||||
|
expect(req.accessDeniedMessage).toEqual({
|
||||||
|
type: 'info',
|
||||||
|
content: 'Access token is required for this request'
|
||||||
|
});
|
||||||
|
expect(req.user).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fastify.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/test'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.json()).toEqual({ ok: true });
|
||||||
|
expect(res.statusCode).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deny if the access token is not signed', async () => {
|
||||||
|
expect.assertions(4);
|
||||||
|
|
||||||
|
fastify.addHook('onRequest', (req, _reply, done) => {
|
||||||
|
expect(req.accessDeniedMessage).toEqual({
|
||||||
|
type: 'info',
|
||||||
|
content: 'Access token is required for this request'
|
||||||
|
});
|
||||||
|
expect(req.user).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ accessToken: createAccessToken('123') },
|
||||||
|
JWT_SECRET
|
||||||
|
);
|
||||||
|
const res = await fastify.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/test',
|
||||||
|
cookies: {
|
||||||
|
jwt_access_token: token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.json()).toEqual({ ok: true });
|
||||||
|
expect(res.statusCode).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deny if the access token is invalid', async () => {
|
||||||
|
expect.assertions(4);
|
||||||
|
|
||||||
|
fastify.addHook('onRequest', (req, _reply, done) => {
|
||||||
|
expect(req.accessDeniedMessage).toEqual({
|
||||||
|
type: 'info',
|
||||||
|
content: 'Your access token is invalid'
|
||||||
|
});
|
||||||
|
expect(req.user).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ accessToken: createAccessToken('123') },
|
||||||
|
'invalid-secret'
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await fastify.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/test',
|
||||||
|
cookies: {
|
||||||
|
jwt_access_token: signCookie(token)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.json()).toEqual({ ok: true });
|
||||||
|
expect(res.statusCode).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deny if the access token has expired', async () => {
|
||||||
|
expect.assertions(4);
|
||||||
|
|
||||||
|
fastify.addHook('onRequest', (req, _reply, done) => {
|
||||||
|
expect(req.accessDeniedMessage).toEqual({
|
||||||
|
type: 'info',
|
||||||
|
content: 'Access token is no longer valid'
|
||||||
|
});
|
||||||
|
expect(req.user).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ accessToken: createAccessToken('123', -1) },
|
||||||
|
JWT_SECRET
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await fastify.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/test',
|
||||||
|
cookies: {
|
||||||
|
jwt_access_token: signCookie(token)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.json()).toEqual({ ok: true });
|
||||||
|
expect(res.statusCode).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deny if the user is not found', async () => {
|
||||||
|
expect.assertions(4);
|
||||||
|
|
||||||
|
fastify.addHook('onRequest', (req, _reply, done) => {
|
||||||
|
expect(req.accessDeniedMessage).toEqual({
|
||||||
|
type: 'info',
|
||||||
|
content: 'Your access token is invalid'
|
||||||
|
});
|
||||||
|
expect(req.user).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error prisma isn't defined, since we're not building the
|
||||||
|
// full application here.
|
||||||
|
fastify.prisma = { user: { findUnique: () => null } };
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ accessToken: createAccessToken('123') },
|
||||||
|
JWT_SECRET
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await fastify.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/test',
|
||||||
|
cookies: {
|
||||||
|
jwt_access_token: signCookie(token)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.json()).toEqual({ ok: true });
|
||||||
|
expect(res.statusCode).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should populate the request with the user if the token is valid', async () => {
|
||||||
|
const fakeUser = { id: '123', username: 'test-user' };
|
||||||
|
// @ts-expect-error prisma isn't defined, since we're not building the
|
||||||
|
// full application here.
|
||||||
|
fastify.prisma = { user: { findUnique: () => fakeUser } };
|
||||||
|
fastify.get('/test-user', req => {
|
||||||
|
expect(req.user).toEqual(fakeUser);
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ accessToken: createAccessToken('123') },
|
||||||
|
JWT_SECRET
|
||||||
|
);
|
||||||
|
const res = await fastify.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/test-user',
|
||||||
|
cookies: {
|
||||||
|
jwt_access_token: signCookie(token)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.json()).toEqual({ ok: true });
|
||||||
|
expect(res.statusCode).toEqual(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||||
|
import fp from 'fastify-plugin';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { type user } from '@prisma/client';
|
||||||
|
|
||||||
|
import { JWT_SECRET } from '../utils/env';
|
||||||
|
import { type Token, isExpired } from '../utils/tokens';
|
||||||
|
|
||||||
|
declare module 'fastify' {
|
||||||
|
interface FastifyReply {
|
||||||
|
setAccessTokenCookie: (this: FastifyReply, accessToken: Token) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FastifyRequest {
|
||||||
|
// TODO: is the full user the correct type here?
|
||||||
|
user: user | null;
|
||||||
|
accessDeniedMessage: { type: 'info'; content: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FastifyInstance {
|
||||||
|
authorize: (req: FastifyRequest, reply: FastifyReply) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth: FastifyPluginCallback = (fastify, _options, done) => {
|
||||||
|
fastify.decorateReply('setAccessTokenCookie', function (accessToken: Token) {
|
||||||
|
const signedToken = jwt.sign({ accessToken }, JWT_SECRET);
|
||||||
|
void this.setCookie('jwt_access_token', signedToken, {
|
||||||
|
httpOnly: false,
|
||||||
|
secure: false,
|
||||||
|
maxAge: accessToken.ttl
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.decorateRequest('accessDeniedMessage', null);
|
||||||
|
fastify.decorateRequest('user', null);
|
||||||
|
|
||||||
|
const TOKEN_REQUIRED = 'Access token is required for this request';
|
||||||
|
const TOKEN_INVALID = 'Your access token is invalid';
|
||||||
|
const TOKEN_EXPIRED = 'Access token is no longer valid';
|
||||||
|
|
||||||
|
const setAccessDenied = (req: FastifyRequest, content: string) =>
|
||||||
|
(req.accessDeniedMessage = { type: 'info', content });
|
||||||
|
|
||||||
|
const handleAuth = async (req: FastifyRequest) => {
|
||||||
|
const tokenCookie = req.cookies.jwt_access_token;
|
||||||
|
if (!tokenCookie) return setAccessDenied(req, TOKEN_REQUIRED);
|
||||||
|
|
||||||
|
const unsignedToken = req.unsignCookie(tokenCookie);
|
||||||
|
if (!unsignedToken.valid) return setAccessDenied(req, TOKEN_REQUIRED);
|
||||||
|
|
||||||
|
const jwtAccessToken = unsignedToken.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
jwt.verify(jwtAccessToken, JWT_SECRET);
|
||||||
|
} catch {
|
||||||
|
return setAccessDenied(req, TOKEN_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accessToken } = jwt.decode(jwtAccessToken) as {
|
||||||
|
accessToken: Token;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isExpired(accessToken)) return setAccessDenied(req, TOKEN_EXPIRED);
|
||||||
|
|
||||||
|
const user = await fastify.prisma.user.findUnique({
|
||||||
|
where: { id: accessToken.userId }
|
||||||
|
});
|
||||||
|
if (!user) return setAccessDenied(req, TOKEN_INVALID);
|
||||||
|
req.user = user;
|
||||||
|
};
|
||||||
|
|
||||||
|
fastify.decorate('authorize', handleAuth);
|
||||||
|
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fp(auth, { name: 'auth', dependencies: ['cookies'] });
|
||||||
@@ -7,7 +7,7 @@ import prismaPlugin from '../db/prisma';
|
|||||||
import cookies, { sign, unsign } from './cookies';
|
import cookies, { sign, unsign } from './cookies';
|
||||||
import { auth0Client } from './auth0';
|
import { auth0Client } from './auth0';
|
||||||
import redirectWithMessage, { formatMessage } from './redirect-with-message';
|
import redirectWithMessage, { formatMessage } from './redirect-with-message';
|
||||||
import codeFlowAuth from './code-flow-auth';
|
import auth from './auth';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
jest.mock('../utils/env', () => ({
|
jest.mock('../utils/env', () => ({
|
||||||
@@ -23,7 +23,7 @@ describe('auth0 plugin', () => {
|
|||||||
|
|
||||||
await fastify.register(cookies);
|
await fastify.register(cookies);
|
||||||
await fastify.register(redirectWithMessage);
|
await fastify.register(redirectWithMessage);
|
||||||
await fastify.register(codeFlowAuth);
|
await fastify.register(auth);
|
||||||
await fastify.register(auth0Client);
|
await fastify.register(auth0Client);
|
||||||
await fastify.register(prismaPlugin);
|
await fastify.register(prismaPlugin);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
import Fastify, { type FastifyInstance } from 'fastify';
|
||||||
|
|
||||||
|
import { HOME_LOCATION } from '../utils/env';
|
||||||
|
import bouncer from './bouncer';
|
||||||
|
import auth from './auth';
|
||||||
|
import cookies from './cookies';
|
||||||
|
import redirectWithMessage, { formatMessage } from './redirect-with-message';
|
||||||
|
|
||||||
|
let authorizeSpy: jest.SpyInstance;
|
||||||
|
|
||||||
|
async function setupServer() {
|
||||||
|
const fastify = Fastify();
|
||||||
|
await fastify.register(cookies);
|
||||||
|
await fastify.register(auth);
|
||||||
|
authorizeSpy = jest.spyOn(fastify, 'authorize');
|
||||||
|
|
||||||
|
await fastify.register(redirectWithMessage);
|
||||||
|
await fastify.register(bouncer);
|
||||||
|
fastify.addHook('onRequest', fastify.authorize);
|
||||||
|
fastify.get('/', (_req, reply) => {
|
||||||
|
void reply.send({ foo: 'bar' });
|
||||||
|
});
|
||||||
|
return fastify;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('bouncer', () => {
|
||||||
|
let fastify: FastifyInstance;
|
||||||
|
beforeEach(async () => {
|
||||||
|
fastify = await setupServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await fastify.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('send401IfNoUser', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fastify.addHook('onRequest', fastify.send401IfNoUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 if no user is present', async () => {
|
||||||
|
const message = {
|
||||||
|
type: 'danger',
|
||||||
|
content: 'Something undesirable occurred'
|
||||||
|
};
|
||||||
|
authorizeSpy.mockImplementationOnce((req, _reply, done) => {
|
||||||
|
req.accessDeniedMessage = message;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
const res = await fastify.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.json()).toStrictEqual({
|
||||||
|
type: message.type,
|
||||||
|
message: message.content
|
||||||
|
});
|
||||||
|
expect(res.statusCode).toEqual(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not alter the response if a user is present', async () => {
|
||||||
|
authorizeSpy.mockImplementationOnce((req, _reply, done) => {
|
||||||
|
req.user = { id: '123' };
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fastify.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.json()).toEqual({ foo: 'bar' });
|
||||||
|
expect(res.statusCode).toEqual(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('redirectIfNoUser', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fastify.addHook('onRequest', fastify.redirectIfNoUser);
|
||||||
|
});
|
||||||
|
const redirectLocation = `${HOME_LOCATION}?${formatMessage({ type: 'info', content: 'Only authenticated users can access this route. Please sign in and try again.' })}`;
|
||||||
|
|
||||||
|
it('should redirect to HOME_LOCATION if no user is present', async () => {
|
||||||
|
const message = {
|
||||||
|
type: 'danger',
|
||||||
|
content: 'At the moment, content is ignored'
|
||||||
|
};
|
||||||
|
authorizeSpy.mockImplementationOnce((req, _reply, done) => {
|
||||||
|
req.accessDeniedMessage = message;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
const res = await fastify.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.headers.location).toBe(redirectLocation);
|
||||||
|
expect(res.statusCode).toEqual(302);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not alter the response if a user is present', async () => {
|
||||||
|
authorizeSpy.mockImplementationOnce((req, _reply, done) => {
|
||||||
|
req.user = { id: '123' };
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fastify.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.json()).toEqual({ foo: 'bar' });
|
||||||
|
expect(res.statusCode).toEqual(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fallback hook', () => {
|
||||||
|
it('should reject unauthed requests when no other reject hooks are added', async () => {
|
||||||
|
const message = {
|
||||||
|
type: 'danger',
|
||||||
|
content: 'Something undesirable occurred'
|
||||||
|
};
|
||||||
|
authorizeSpy.mockImplementationOnce((req, _reply, done) => {
|
||||||
|
req.accessDeniedMessage = message;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
const res = await fastify.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.json()).toStrictEqual({
|
||||||
|
type: message.type,
|
||||||
|
message: message.content
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be called if another reject hook is added', async () => {
|
||||||
|
const redirectLocation = `${HOME_LOCATION}?${formatMessage({ type: 'info', content: 'Only authenticated users can access this route. Please sign in and try again.' })}`;
|
||||||
|
const message = {
|
||||||
|
type: 'danger',
|
||||||
|
content: 'Something undesirable occurred'
|
||||||
|
};
|
||||||
|
// using redirectIfNoUser as the reject hook since then it's obvious that
|
||||||
|
// the fallback hook is not called.
|
||||||
|
fastify.addHook('onRequest', fastify.redirectIfNoUser);
|
||||||
|
authorizeSpy.mockImplementationOnce((req, _reply, done) => {
|
||||||
|
req.accessDeniedMessage = message;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
const res = await fastify.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.headers.location).toBe(redirectLocation);
|
||||||
|
expect(res.statusCode).toEqual(302);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type {
|
||||||
|
FastifyPluginCallback,
|
||||||
|
FastifyRequest,
|
||||||
|
FastifyReply
|
||||||
|
} from 'fastify';
|
||||||
|
import fp from 'fastify-plugin';
|
||||||
|
import { getRedirectParams } from '../utils/redirection';
|
||||||
|
|
||||||
|
declare module 'fastify' {
|
||||||
|
interface FastifyInstance {
|
||||||
|
send401IfNoUser: (req: FastifyRequest, reply: FastifyReply) => void;
|
||||||
|
redirectIfNoUser: (req: FastifyRequest, reply: FastifyReply) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin: FastifyPluginCallback = (fastify, _options, done) => {
|
||||||
|
fastify.decorate(
|
||||||
|
'send401IfNoUser',
|
||||||
|
async function (req: FastifyRequest, reply: FastifyReply) {
|
||||||
|
if (!req.user) {
|
||||||
|
await reply.status(401).send({
|
||||||
|
type: req.accessDeniedMessage?.type,
|
||||||
|
message: req.accessDeniedMessage?.content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
fastify.decorate(
|
||||||
|
'redirectIfNoUser',
|
||||||
|
async function (req: FastifyRequest, reply: FastifyReply) {
|
||||||
|
if (!req.user) {
|
||||||
|
const { origin } = getRedirectParams(req);
|
||||||
|
await reply.redirectWithMessage(origin, {
|
||||||
|
type: 'info',
|
||||||
|
content:
|
||||||
|
'Only authenticated users can access this route. Please sign in and try again.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
fastify.addHook('preParsing', fastify.send401IfNoUser);
|
||||||
|
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fp(plugin, { dependencies: ['auth', 'redirect-with-message'] });
|
||||||
@@ -1,328 +0,0 @@
|
|||||||
import Fastify, { FastifyInstance } from 'fastify';
|
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
|
|
||||||
import { COOKIE_DOMAIN, HOME_LOCATION, JWT_SECRET } from '../utils/env';
|
|
||||||
import { type Token, createAccessToken } from '../utils/tokens';
|
|
||||||
import cookies, { sign as signCookie, unsign as unsignCookie } from './cookies';
|
|
||||||
import codeFlowAuth from './code-flow-auth';
|
|
||||||
import redirectWithMessage, { formatMessage } from './redirect-with-message';
|
|
||||||
|
|
||||||
describe('auth', () => {
|
|
||||||
let fastify: FastifyInstance;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
fastify = Fastify();
|
|
||||||
await fastify.register(cookies);
|
|
||||||
await fastify.register(redirectWithMessage);
|
|
||||||
await fastify.register(codeFlowAuth);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await fastify.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('setAccessTokenCookie', () => {
|
|
||||||
// We won't need to keep doubly signing the cookie when we migrate the
|
|
||||||
// authentication, but for the MVP we have to be able to read the cookies
|
|
||||||
// set by the api-server. So, double signing:
|
|
||||||
it('should doubly sign the cookie', async () => {
|
|
||||||
const token = createAccessToken('test-id');
|
|
||||||
fastify.get('/test', async (req, reply) => {
|
|
||||||
reply.setAccessTokenCookie(token);
|
|
||||||
return { ok: true };
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/test'
|
|
||||||
});
|
|
||||||
|
|
||||||
const { value, ...rest } = res.cookies[0]!;
|
|
||||||
const unsignedOnce = unsignCookie(value);
|
|
||||||
const unsignedTwice = jwt.verify(unsignedOnce.value!, JWT_SECRET) as {
|
|
||||||
accessToken: Token;
|
|
||||||
};
|
|
||||||
expect(unsignedTwice.accessToken).toEqual(token);
|
|
||||||
expect(rest).toEqual({
|
|
||||||
name: 'jwt_access_token',
|
|
||||||
path: '/',
|
|
||||||
sameSite: 'Lax',
|
|
||||||
domain: COOKIE_DOMAIN,
|
|
||||||
maxAge: token.ttl
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Post-MVP sync the cookie max-age with the token ttl (i.e. the
|
|
||||||
// max-age should be the ttl/1000, not ttl)
|
|
||||||
it('should set the max-age of the cookie to match the ttl of the token', async () => {
|
|
||||||
const token = createAccessToken('test-id', 123000);
|
|
||||||
fastify.get('/test', async (req, reply) => {
|
|
||||||
reply.setAccessTokenCookie(token);
|
|
||||||
return { ok: true };
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/test'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.cookies[0]).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
maxAge: 123000
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('authorize', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
fastify.addHook('onRequest', fastify.authorize);
|
|
||||||
fastify.get('/test', () => {
|
|
||||||
return { message: 'ok' };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject if the access token is missing', async () => {
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/test'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.json()).toEqual({
|
|
||||||
type: 'info',
|
|
||||||
message: 'Access token is required for this request'
|
|
||||||
});
|
|
||||||
expect(res.statusCode).toBe(401);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject if the access token is not signed', async () => {
|
|
||||||
const token = jwt.sign(
|
|
||||||
{ accessToken: createAccessToken('123') },
|
|
||||||
JWT_SECRET
|
|
||||||
);
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/test',
|
|
||||||
cookies: {
|
|
||||||
jwt_access_token: token
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.json()).toEqual({
|
|
||||||
type: 'info',
|
|
||||||
message: 'Access token is required for this request'
|
|
||||||
});
|
|
||||||
expect(res.statusCode).toBe(401);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject if the access token is invalid', async () => {
|
|
||||||
const token = jwt.sign(
|
|
||||||
{ accessToken: createAccessToken('123') },
|
|
||||||
'invalid-secret'
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/test',
|
|
||||||
cookies: {
|
|
||||||
jwt_access_token: signCookie(token)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.json()).toEqual({
|
|
||||||
type: 'info',
|
|
||||||
message: 'Your access token is invalid'
|
|
||||||
});
|
|
||||||
expect(res.statusCode).toBe(401);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject if the access token has expired', async () => {
|
|
||||||
const token = jwt.sign(
|
|
||||||
{ accessToken: createAccessToken('123', -1) },
|
|
||||||
JWT_SECRET
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/test',
|
|
||||||
cookies: {
|
|
||||||
jwt_access_token: signCookie(token)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.json()).toEqual({
|
|
||||||
type: 'info',
|
|
||||||
message: 'Access token is no longer valid'
|
|
||||||
});
|
|
||||||
expect(res.statusCode).toBe(401);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject if the user is not found', async () => {
|
|
||||||
// @ts-expect-error prisma isn't defined, since we're not building the
|
|
||||||
// full application here.
|
|
||||||
fastify.prisma = { user: { findUnique: () => null } };
|
|
||||||
const token = jwt.sign(
|
|
||||||
{ accessToken: createAccessToken('123') },
|
|
||||||
JWT_SECRET
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/test',
|
|
||||||
cookies: {
|
|
||||||
jwt_access_token: signCookie(token)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.json()).toEqual({
|
|
||||||
type: 'info',
|
|
||||||
message: 'Your access token is invalid'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should populate the request with the user if the token is valid', async () => {
|
|
||||||
const fakeUser = { id: '123', username: 'test-user' };
|
|
||||||
// @ts-expect-error prisma isn't defined, since we're not building the
|
|
||||||
// full application here.
|
|
||||||
fastify.prisma = { user: { findUnique: () => fakeUser } };
|
|
||||||
fastify.get('/test-user', req => {
|
|
||||||
expect(req.user).toEqual(fakeUser);
|
|
||||||
return { ok: true };
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = jwt.sign(
|
|
||||||
{ accessToken: createAccessToken('123') },
|
|
||||||
JWT_SECRET
|
|
||||||
);
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/test-user',
|
|
||||||
cookies: {
|
|
||||||
jwt_access_token: signCookie(token)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.json()).toEqual({ ok: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('authorizeOrRedirect', () => {
|
|
||||||
const redirectLocation = `${HOME_LOCATION}?${formatMessage({ type: 'info', content: 'Only authenticated users can access this route. Please sign in and try again.' })}`;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fastify.addHook('onRequest', fastify.authorizeOrRedirect);
|
|
||||||
fastify.get('/test', () => {
|
|
||||||
return { message: 'ok' };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to the origin if the access token is missing', async () => {
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/test'
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.headers.location).toBe(redirectLocation);
|
|
||||||
expect(res.statusCode).toBe(302);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to the origin if the access token is not signed', async () => {
|
|
||||||
const token = jwt.sign(
|
|
||||||
{ accessToken: createAccessToken('123') },
|
|
||||||
JWT_SECRET
|
|
||||||
);
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/test',
|
|
||||||
cookies: {
|
|
||||||
jwt_access_token: token
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.headers.location).toBe(redirectLocation);
|
|
||||||
expect(res.statusCode).toBe(302);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to the origin if the access token is invalid', async () => {
|
|
||||||
const token = jwt.sign(
|
|
||||||
{ accessToken: createAccessToken('123') },
|
|
||||||
'invalid-secret'
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/test',
|
|
||||||
cookies: {
|
|
||||||
jwt_access_token: signCookie(token)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.headers.location).toBe(redirectLocation);
|
|
||||||
expect(res.statusCode).toBe(302);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to the origin if the access token has expired', async () => {
|
|
||||||
const token = jwt.sign(
|
|
||||||
{ accessToken: createAccessToken('123', -1) },
|
|
||||||
JWT_SECRET
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/test',
|
|
||||||
cookies: {
|
|
||||||
jwt_access_token: signCookie(token)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.headers.location).toBe(redirectLocation);
|
|
||||||
expect(res.statusCode).toBe(302);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to the origin if the user is not found', async () => {
|
|
||||||
// @ts-expect-error prisma isn't defined, since we're not building the
|
|
||||||
// full application here.
|
|
||||||
fastify.prisma = { user: { findUnique: () => null } };
|
|
||||||
const token = jwt.sign(
|
|
||||||
{ accessToken: createAccessToken('123') },
|
|
||||||
JWT_SECRET
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/test',
|
|
||||||
cookies: {
|
|
||||||
jwt_access_token: signCookie(token)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.headers.location).toBe(redirectLocation);
|
|
||||||
expect(res.statusCode).toBe(302);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should populate the request with the user if the token is valid', async () => {
|
|
||||||
const fakeUser = { id: '123', username: 'test-user' };
|
|
||||||
// @ts-expect-error prisma isn't defined, since we're not building the
|
|
||||||
// full application here.
|
|
||||||
fastify.prisma = { user: { findUnique: () => fakeUser } };
|
|
||||||
fastify.get('/test-user', req => {
|
|
||||||
expect(req.user).toEqual(fakeUser);
|
|
||||||
return { ok: true };
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = jwt.sign(
|
|
||||||
{ accessToken: createAccessToken('123') },
|
|
||||||
JWT_SECRET
|
|
||||||
);
|
|
||||||
const res = await fastify.inject({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/test-user',
|
|
||||||
cookies: {
|
|
||||||
jwt_access_token: signCookie(token)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.json()).toEqual({ ok: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { FastifyPluginCallback, FastifyReply, FastifyRequest } from 'fastify';
|
|
||||||
import fp from 'fastify-plugin';
|
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
import { type user } from '@prisma/client';
|
|
||||||
|
|
||||||
import { JWT_SECRET } from '../utils/env';
|
|
||||||
import { type Token, isExpired } from '../utils/tokens';
|
|
||||||
import { getRedirectParams } from '../utils/redirection';
|
|
||||||
|
|
||||||
declare module 'fastify' {
|
|
||||||
interface FastifyReply {
|
|
||||||
setAccessTokenCookie: (this: FastifyReply, accessToken: Token) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FastifyRequest {
|
|
||||||
// TODO: is the full user the correct type here?
|
|
||||||
user?: user;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FastifyInstance {
|
|
||||||
authorize: (req: FastifyRequest, reply: FastifyReply) => void;
|
|
||||||
authorizeOrRedirect: (req: FastifyRequest, reply: FastifyReply) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const codeFlowAuth: FastifyPluginCallback = (fastify, _options, done) => {
|
|
||||||
fastify.decorateReply('setAccessTokenCookie', function (accessToken: Token) {
|
|
||||||
const signedToken = jwt.sign({ accessToken }, JWT_SECRET);
|
|
||||||
void this.setCookie('jwt_access_token', signedToken, {
|
|
||||||
httpOnly: false,
|
|
||||||
secure: false,
|
|
||||||
maxAge: accessToken.ttl
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const TOKEN_REQUIRED = 'Access token is required for this request';
|
|
||||||
const TOKEN_INVALID = 'Your access token is invalid';
|
|
||||||
const TOKEN_EXPIRED = 'Access token is no longer valid';
|
|
||||||
|
|
||||||
const send401 = (
|
|
||||||
_req: FastifyRequest,
|
|
||||||
reply: FastifyReply,
|
|
||||||
message: string
|
|
||||||
): void => {
|
|
||||||
void reply.status(401).send({ type: 'info', message });
|
|
||||||
};
|
|
||||||
|
|
||||||
const redirectHome = (
|
|
||||||
req: FastifyRequest,
|
|
||||||
reply: FastifyReply,
|
|
||||||
_ignored: string
|
|
||||||
) => {
|
|
||||||
const { origin } = getRedirectParams(req);
|
|
||||||
|
|
||||||
void reply.redirectWithMessage(origin, {
|
|
||||||
type: 'info',
|
|
||||||
content:
|
|
||||||
'Only authenticated users can access this route. Please sign in and try again.'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAuth =
|
|
||||||
(
|
|
||||||
rejectStrategy: (
|
|
||||||
req: FastifyRequest,
|
|
||||||
reply: FastifyReply,
|
|
||||||
message: string
|
|
||||||
) => void
|
|
||||||
) =>
|
|
||||||
async (req: FastifyRequest, reply: FastifyReply) => {
|
|
||||||
const tokenCookie = req.cookies.jwt_access_token;
|
|
||||||
if (!tokenCookie) return rejectStrategy(req, reply, TOKEN_REQUIRED);
|
|
||||||
|
|
||||||
const unsignedToken = req.unsignCookie(tokenCookie);
|
|
||||||
if (!unsignedToken.valid)
|
|
||||||
return rejectStrategy(req, reply, TOKEN_REQUIRED);
|
|
||||||
|
|
||||||
const jwtAccessToken = unsignedToken.value;
|
|
||||||
|
|
||||||
try {
|
|
||||||
jwt.verify(jwtAccessToken, JWT_SECRET);
|
|
||||||
} catch {
|
|
||||||
return rejectStrategy(req, reply, TOKEN_INVALID);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { accessToken } = jwt.decode(jwtAccessToken) as {
|
|
||||||
accessToken: Token;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isExpired(accessToken))
|
|
||||||
return rejectStrategy(req, reply, TOKEN_EXPIRED);
|
|
||||||
|
|
||||||
const user = await fastify.prisma.user.findUnique({
|
|
||||||
where: { id: accessToken.userId }
|
|
||||||
});
|
|
||||||
if (!user) return rejectStrategy(req, reply, TOKEN_INVALID);
|
|
||||||
req.user = user;
|
|
||||||
};
|
|
||||||
|
|
||||||
fastify.decorate('authorize', handleAuth(send401));
|
|
||||||
fastify.decorate('authorizeOrRedirect', handleAuth(redirectHome));
|
|
||||||
|
|
||||||
done();
|
|
||||||
};
|
|
||||||
|
|
||||||
export default fp(codeFlowAuth, { dependencies: ['redirect-with-message'] });
|
|
||||||
@@ -75,4 +75,4 @@ const cookies: FastifyPluginCallback = (fastify, _options, done) => {
|
|||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default fp(cookies);
|
export default fp(cookies, { name: 'cookies' });
|
||||||
|
|||||||
Reference in New Issue
Block a user