mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-03-17 22:01:24 -04:00
feat(api): redirect auth requests if already signed in (#55829)
This commit is contained in:
committed by
GitHub
parent
bbf6356214
commit
609cdb0c4a
@@ -8,6 +8,7 @@ import cookies, { sign, unsign } from './cookies';
|
||||
import { auth0Client } from './auth0';
|
||||
import redirectWithMessage, { formatMessage } from './redirect-with-message';
|
||||
import auth from './auth';
|
||||
import bouncer from './bouncer';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
jest.mock('../utils/env', () => ({
|
||||
@@ -24,6 +25,7 @@ describe('auth0 plugin', () => {
|
||||
await fastify.register(cookies);
|
||||
await fastify.register(redirectWithMessage);
|
||||
await fastify.register(auth);
|
||||
await fastify.register(bouncer);
|
||||
await fastify.register(auth0Client);
|
||||
await fastify.register(prismaPlugin);
|
||||
});
|
||||
|
||||
@@ -53,21 +53,28 @@ export const auth0Client: FastifyPluginCallbackTypebox = fp(
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get('/signin', async function (request, reply) {
|
||||
const returnTo = request.headers.referer ?? `${HOME_LOCATION}/learn`;
|
||||
void reply.setCookie('login-returnto', returnTo, {
|
||||
domain: COOKIE_DOMAIN,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
signed: true,
|
||||
sameSite: 'lax'
|
||||
});
|
||||
void fastify.register(function (fastify, _options, done) {
|
||||
// TODO(Post-MVP): move this into the app, so that we add this hook once for
|
||||
// all auth routes.
|
||||
fastify.addHook('onRequest', fastify.redirectIfSignedIn);
|
||||
|
||||
const redirectUrl = await this.auth0OAuth.generateAuthorizationUri(
|
||||
request,
|
||||
reply
|
||||
);
|
||||
void reply.redirect(redirectUrl);
|
||||
fastify.get('/signin', async function (request, reply) {
|
||||
const returnTo = request.headers.referer ?? `${HOME_LOCATION}/learn`;
|
||||
void reply.setCookie('login-returnto', returnTo, {
|
||||
domain: COOKIE_DOMAIN,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
signed: true,
|
||||
sameSite: 'lax'
|
||||
});
|
||||
|
||||
const redirectUrl = await this.auth0OAuth.generateAuthorizationUri(
|
||||
request,
|
||||
reply
|
||||
);
|
||||
void reply.redirect(redirectUrl);
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
// TODO: use a schema to validate the query params.
|
||||
@@ -151,5 +158,7 @@ export const auth0Client: FastifyPluginCallbackTypebox = fp(
|
||||
|
||||
done();
|
||||
},
|
||||
{ dependencies: ['redirect-with-message'] }
|
||||
// TODO(Post-MVP): remove bouncer dependency when moving redirectIfSignedIn
|
||||
// out of this plugin.
|
||||
{ dependencies: ['redirect-with-message', 'bouncer'] }
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('bouncer', () => {
|
||||
fastify.addHook('onRequest', fastify.send401IfNoUser);
|
||||
});
|
||||
|
||||
it('should return 401 if no user is present', async () => {
|
||||
it('should return 401 if NO user is present', async () => {
|
||||
const message = {
|
||||
type: 'danger',
|
||||
content: 'Something undesirable occurred'
|
||||
@@ -83,7 +83,10 @@ describe('bouncer', () => {
|
||||
});
|
||||
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 () => {
|
||||
// TODO(Post-MVP): make the redirects consistent between redirectIfNoUser
|
||||
// and redirectIfSignedIn. Either both should redirect to the referer or
|
||||
// both should redirect to HOME_LOCATION.
|
||||
it('should redirect to HOME_LOCATION if NO user is present', async () => {
|
||||
const message = {
|
||||
type: 'danger',
|
||||
content: 'At the moment, content is ignored'
|
||||
@@ -117,36 +120,35 @@ describe('bouncer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
describe('redirectIfSignedIn', () => {
|
||||
beforeEach(() => {
|
||||
fastify.addHook('onRequest', fastify.redirectIfSignedIn);
|
||||
});
|
||||
|
||||
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.' })}`;
|
||||
it('should redirect to the referer if a user is present', async () => {
|
||||
authorizeSpy.mockImplementationOnce((req, _reply, done) => {
|
||||
req.user = { id: '123' };
|
||||
done();
|
||||
});
|
||||
const res = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
headers: {
|
||||
referer: 'https://www.freecodecamp.org/some/other/path'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.headers.location).toBe(
|
||||
'https://www.freecodecamp.org/some/other/path'
|
||||
);
|
||||
expect(res.statusCode).toEqual(302);
|
||||
});
|
||||
|
||||
it('should not alter the response if NO user is present', async () => {
|
||||
const message = {
|
||||
type: 'danger',
|
||||
content: 'Something undesirable occurred'
|
||||
content: 'At the moment, content is ignored'
|
||||
};
|
||||
// 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();
|
||||
@@ -156,8 +158,8 @@ describe('bouncer', () => {
|
||||
url: '/'
|
||||
});
|
||||
|
||||
expect(res.headers.location).toBe(redirectLocation);
|
||||
expect(res.statusCode).toEqual(302);
|
||||
expect(res.json()).toEqual({ foo: 'bar' });
|
||||
expect(res.statusCode).toEqual(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
send401IfNoUser: (req: FastifyRequest, reply: FastifyReply) => void;
|
||||
redirectIfNoUser: (req: FastifyRequest, reply: FastifyReply) => void;
|
||||
redirectIfSignedIn: (req: FastifyRequest, reply: FastifyReply) => void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +41,20 @@ const plugin: FastifyPluginCallback = (fastify, _options, done) => {
|
||||
}
|
||||
);
|
||||
|
||||
fastify.addHook('preParsing', fastify.send401IfNoUser);
|
||||
fastify.decorate(
|
||||
'redirectIfSignedIn',
|
||||
async function (req: FastifyRequest, reply: FastifyReply) {
|
||||
if (req.user) {
|
||||
const { returnTo } = getRedirectParams(req);
|
||||
await reply.redirect(returnTo);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
export default fp(plugin, { dependencies: ['auth', 'redirect-with-message'] });
|
||||
export default fp(plugin, {
|
||||
dependencies: ['auth', 'redirect-with-message'],
|
||||
name: 'bouncer'
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user