Change:
This endpoint needs to use jwtaccesstoken in the same way that web login does.

Discussion:
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ <del> import { setupServer, superRequest } from '../../jest.utils'; </del> <ins> import { setupServer, superRequest, createSuperRequest } from '../../jest.utils'; </ins> import { AUTH0_DOMAIN } from '../utils/env'; <ins> 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 }) }); </ins> jest.mock('../utils/env', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return {

expect(redirectUrl.pathname).toBe('/authorize'); }); }); <ins> 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' ); }); }); </ins> });

import rateLimit from 'express-rate-limit'; // @ts-expect-error - no types import MongoStoreRL from 'rate-limit-mongo'; <ins> import isEmail from 'validator/lib/isEmail'; </ins> import { AUTH0_DOMAIN, MONGOHQ_URL } from '../utils/env'; import { auth0Client } from '../plugins/auth0'; <ins> import { createAccessToken } from '../utils/tokens'; </ins> import { findOrCreateUser } from './helpers/auth-helpers'; <del> const getEmailFromAuth0 = async (req: FastifyRequest) => { </del> <ins> const getEmailFromAuth0 = async ( req: FastifyRequest ): Promise<string | null> => { </ins> const auth0Res = await fetch(`https://${AUTH0_DOMAIN}/userinfo`, { headers: { Authorization: req.headers.authorization ?? '' } }); <del> if (!auth0Res.ok) { req.log.error(auth0Res); throw new Error('Invalid Auth0 Access Token'); } </del> <ins> if (!auth0Res.ok) return null; </ins> <del> const { email } = (await auth0Res.json()) as { email: string }; return email; </del> <ins> // 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; </ins> }; /**

// all auth routes. fastify.addHook('onRequest', fastify.redirectIfSignedIn); <del> fastify.get('/mobile-login', async req => { </del> <ins> fastify.get('/mobile-login', async (req, reply) => { </ins> const email = await getEmailFromAuth0(req); <del> await findOrCreateUser(fastify, email); </del> <ins> 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)); </ins> }); done();