feat(api): update mobile-login to match api-server (#55863)

This commit is contained in:
Oliver Eyton-Williams
2024-09-04 03:38:50 +02:00
committed by GitHub
parent 36ddf89f9d
commit 838f30e2be
2 changed files with 154 additions and 10 deletions

View File

@@ -1,7 +1,30 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { setupServer, superRequest } from '../../jest.utils';
import {
setupServer,
superRequest,
createSuperRequest
} from '../../jest.utils';
import { AUTH0_DOMAIN } from '../utils/env';
const mockedFetch = jest.fn();
jest.spyOn(globalThis, 'fetch').mockImplementation(mockedFetch);
const newUserEmail = 'a.n.random@user.com';
const mockAuth0NotOk = () => ({
ok: false
});
const mockAuth0InvalidEmail = () => ({
ok: true,
json: () => ({ email: 'invalid-email' })
});
const mockAuth0ValidEmail = () => ({
ok: true,
json: () => ({ email: newUserEmail })
});
jest.mock('../utils/env', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
@@ -23,4 +46,107 @@ describe('auth0 routes', () => {
expect(redirectUrl.pathname).toBe('/authorize');
});
});
describe('GET /mobile-login', () => {
let superGet: ReturnType<typeof createSuperRequest>;
beforeAll(() => {
superGet = createSuperRequest({ method: 'GET' });
});
beforeEach(async () => {
await fastifyTestInstance.prisma.userRateLimit.deleteMany({});
await fastifyTestInstance.prisma.user.deleteMany({
where: { email: newUserEmail }
});
});
it('should be rate-limited', async () => {
await Promise.all(
[...Array(10).keys()].map(() => superGet('/mobile-login'))
);
const res = await superGet('/mobile-login');
expect(res.status).toBe(429);
});
it('should return 401 if the authorization header is invalid', async () => {
mockedFetch.mockResolvedValueOnce(mockAuth0NotOk());
const res = await superGet('/mobile-login').set(
'Authorization',
'Bearer invalid-token'
);
expect(res.body).toStrictEqual({
type: 'danger',
message: 'We could not log you in, please try again in a moment.'
});
expect(res.status).toBe(401);
});
it('should return 400 if the email is not valid', async () => {
mockedFetch.mockResolvedValueOnce(mockAuth0InvalidEmail());
const res = await superGet('/mobile-login').set(
'Authorization',
'Bearer valid-token'
);
expect(res.body).toStrictEqual({
type: 'danger',
message: 'The email is incorrectly formatted'
});
expect(res.status).toBe(400);
});
it('should set the jwt_access_token cookie if the authorization header is valid', async () => {
mockedFetch.mockResolvedValueOnce(mockAuth0ValidEmail());
const res = await superGet('/mobile-login').set(
'Authorization',
'Bearer valid-token'
);
expect(res.status).toBe(200);
expect(res.get('Set-Cookie')).toEqual(
expect.arrayContaining([expect.stringMatching(/jwt_access_token=/)])
);
});
it('should create a user if they do not exist', async () => {
mockedFetch.mockResolvedValueOnce(mockAuth0ValidEmail());
const existingUserCount = await fastifyTestInstance.prisma.user.count();
const res = await superGet('/mobile-login').set(
'Authorization',
'Bearer valid-token'
);
const newUserCount = await fastifyTestInstance.prisma.user.count();
expect(existingUserCount).toBe(0);
expect(newUserCount).toBe(1);
expect(res.status).toBe(200);
});
it('should redirect to returnTo if already logged in', async () => {
mockedFetch.mockResolvedValueOnce(mockAuth0ValidEmail());
const firstRes = await superGet('/mobile-login').set(
'Authorization',
'Bearer valid-token'
);
expect(firstRes.status).toBe(200);
const res = await superRequest('/mobile-login', {
method: 'GET',
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
setCookies: firstRes.get('Set-Cookie')
})
.set('Authorization', 'Bearer does-not-matter')
.set('Referer', 'https://www.freecodecamp.org/back-home');
expect(res.status).toBe(302);
expect(res.headers.location).toBe(
'https://www.freecodecamp.org/back-home'
);
});
});
});

View File

@@ -3,25 +3,28 @@ import { FastifyPluginCallback, FastifyRequest } from 'fastify';
import rateLimit from 'express-rate-limit';
// @ts-expect-error - no types
import MongoStoreRL from 'rate-limit-mongo';
import isEmail from 'validator/lib/isEmail';
import { AUTH0_DOMAIN, MONGOHQ_URL } from '../utils/env';
import { auth0Client } from '../plugins/auth0';
import { createAccessToken } from '../utils/tokens';
import { findOrCreateUser } from './helpers/auth-helpers';
const getEmailFromAuth0 = async (req: FastifyRequest) => {
const getEmailFromAuth0 = async (
req: FastifyRequest
): Promise<string | null> => {
const auth0Res = await fetch(`https://${AUTH0_DOMAIN}/userinfo`, {
headers: {
Authorization: req.headers.authorization ?? ''
}
});
if (!auth0Res.ok) {
req.log.error(auth0Res);
throw new Error('Invalid Auth0 Access Token');
}
if (!auth0Res.ok) return null;
const { email } = (await auth0Res.json()) as { email: string };
return email;
// For now, we assume the response is a JSON object. If not, we can't proceed
// and the only safe thing to do is to throw.
const { email } = (await auth0Res.json()) as { email?: string };
return typeof email === 'string' ? email : null;
};
/**
@@ -60,10 +63,25 @@ export const mobileAuth0Routes: FastifyPluginCallback = (
// all auth routes.
fastify.addHook('onRequest', fastify.redirectIfSignedIn);
fastify.get('/mobile-login', async req => {
fastify.get('/mobile-login', async (req, reply) => {
const email = await getEmailFromAuth0(req);
await findOrCreateUser(fastify, email);
if (!email) {
return reply.status(401).send({
message: 'We could not log you in, please try again in a moment.',
type: 'danger'
});
}
if (!isEmail(email)) {
return reply.status(400).send({
message: 'The email is incorrectly formatted',
type: 'danger'
});
}
const { id } = await findOrCreateUser(fastify, email);
reply.setAccessTokenCookie(createAccessToken(id));
});
done();