refactor: separate getAuthedUser from authorize (#66842)

Co-authored-by: Ahmad Abdolsaheb <ahmad.abdolsaheb@gmail.com>
This commit is contained in:
Oliver Eyton-Williams
2026-04-08 21:15:17 +02:00
committed by GitHub
parent d69f24b31b
commit 06c40cc23f
2 changed files with 173 additions and 9 deletions

View File

@@ -238,6 +238,147 @@ describe('auth', () => {
});
});
describe('req.getAuthedUser', () => {
test('returns message when access token is missing', async () => {
fastify.get('/test', async req => {
return req.getAuthedUser();
});
const res = await fastify.inject({
method: 'GET',
url: '/test'
});
expect(res.json()).toEqual({
message: 'Access token is required for this request'
});
});
test('returns message when access token is not signed', async () => {
fastify.get('/test', async req => {
return req.getAuthedUser();
});
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({
message: 'Access token is required for this request'
});
});
test('returns message when access token is invalid', async () => {
fastify.get('/test', async req => {
return req.getAuthedUser();
});
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({
message: 'Your access token is invalid'
});
});
test('returns message when access token has expired', async () => {
fastify.get('/test', async req => {
return req.getAuthedUser();
});
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({
message: 'Access token is no longer valid'
});
});
test('returns message when 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 } };
fastify.get('/test', async req => {
return req.getAuthedUser();
});
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({
message: 'Your access token is invalid'
});
});
test('returns user when 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', async req => {
return req.getAuthedUser();
});
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({
user: fakeUser
});
});
});
describe('onRequest Hook', () => {
test('should update the jwt_access_token to httpOnly and secure', async () => {
const rawValue = 'should-not-change';

View File

@@ -7,6 +7,16 @@ import { JWT_SECRET } from '../utils/env.js';
import { type Token, isExpired } from '../utils/tokens.js';
import { ERRORS } from '../exam-environment/utils/errors.js';
type AuthResult =
| {
message: string;
user?: never;
}
| {
message?: never;
user: user;
};
declare module 'fastify' {
interface FastifyReply {
setAccessTokenCookie: (this: FastifyReply, accessToken: Token) => void;
@@ -16,6 +26,7 @@ declare module 'fastify' {
// TODO: is the full user the correct type here?
user: user | null;
accessDeniedMessage: { type: 'info'; content: string } | null;
getAuthedUser: () => Promise<AuthResult>;
}
interface FastifyInstance {
@@ -60,26 +71,26 @@ const auth: FastifyPluginCallback = (fastify, _options, done) => {
const setAccessDenied = (req: FastifyRequest, content: string) =>
(req.accessDeniedMessage = { type: 'info', content });
const handleAuth = async (req: FastifyRequest): Promise<void> => {
const tokenCookie = req.cookies.jwt_access_token;
if (!tokenCookie) return void setAccessDenied(req, TOKEN_REQUIRED);
async function getAuthedUser(this: FastifyRequest): Promise<AuthResult> {
const tokenCookie = this.cookies.jwt_access_token;
if (!tokenCookie) return { message: TOKEN_REQUIRED };
const unsignedToken = req.unsignCookie(tokenCookie);
if (!unsignedToken.valid) return void setAccessDenied(req, TOKEN_REQUIRED);
const unsignedToken = this.unsignCookie(tokenCookie);
if (!unsignedToken.valid) return { message: TOKEN_REQUIRED };
const jwtAccessToken = unsignedToken.value;
try {
jwt.verify(jwtAccessToken, JWT_SECRET);
} catch {
return void setAccessDenied(req, TOKEN_INVALID);
return { message: TOKEN_INVALID };
}
const { accessToken } = jwt.decode(jwtAccessToken) as {
accessToken: Token;
};
if (isExpired(accessToken)) return void setAccessDenied(req, TOKEN_EXPIRED);
if (isExpired(accessToken)) return { message: TOKEN_EXPIRED };
// We're using token.userId since it's possible for the user record to be
// malformed and for prisma to throw while trying to find the user.
fastify.Sentry?.setUser({
@@ -89,8 +100,20 @@ const auth: FastifyPluginCallback = (fastify, _options, done) => {
const user = await fastify.prisma.user.findUnique({
where: { id: accessToken.userId }
});
if (!user) return void setAccessDenied(req, TOKEN_INVALID);
req.user = user;
return user ? { user } : { message: TOKEN_INVALID };
}
fastify.decorateRequest('getAuthedUser', getAuthedUser);
const handleAuth = async (req: FastifyRequest): Promise<void> => {
const { message, user } = await req.getAuthedUser();
if (user) {
req.user = user;
} else {
setAccessDenied(req, message);
}
};
async function handleExamEnvironmentTokenAuth(