diff --git a/development.env b/development.env index d001b4ebc..52784814c 100644 --- a/development.env +++ b/development.env @@ -10,8 +10,8 @@ JWT_SECRET=9&D*+(D*(jwt.verify); -const promisifiedJwtSign = promisify<{ sub: string }, Secret, SignOptions, string>(jwt.sign); +const promisifiedJwtVerify = promisify(jwt.verify); +const promisifiedJwtSign = promisify<{ sub: string, challenge?: string }, Secret, SignOptions, string>(jwt.sign); const plugin: FastifyPluginAsync = async (fastify, options) => { const { @@ -147,6 +148,19 @@ const plugin: FastifyPluginAsync = async (fastify, options) = } fastify.decorate('generateAuthTokensPair', generateAuthTokensPair); + async function generateLoginLinkAndEmailIt(member, reRegistrationAttempt?, challenge?) { + // generate token with member info and expiration + const token = await promisifiedJwtSign({ + sub: member.id, + challenge, + }, JWT_SECRET, { expiresIn: `${LOGIN_TOKEN_EXPIRATION_IN_MINUTES}m` }); + + const link = `${PROTOCOL}://${EMAIL_LINKS_HOST}${challenge ? '/m' : ''}/auth?t=${token}`; + // don't wait for mailer's response; log error and link if it fails. + fastify.mailer.sendLoginEmail(member, link, reRegistrationAttempt) + .catch(err => log.warn(err, `mailer failed. link: ${link}`)); + } + // cookie based auth and api endpoints fastify.register(async function (fastify) { @@ -191,17 +205,6 @@ const plugin: FastifyPluginAsync = async (fastify, options) = } ); - async function generateLoginLinkAndEmailIt(member, reRegistrationAttempt?) { - // generate token with member info and expiration - const token = await promisifiedJwtSign({ sub: member.id }, JWT_SECRET, - { expiresIn: `${LOGIN_TOKEN_EXPIRATION_IN_MINUTES}m` }); - - const link = `${PROTOCOL}://${EMAIL_LINKS_HOST}/auth?t=${token}`; - // don't wait for mailer's response; log error and link if it fails. - fastify.mailer.sendLoginEmail(member, link, reRegistrationAttempt) - .catch(err => log.warn(err, `mailer failed. link: ${link}`)); - } - // login fastify.post<{ Body: { email: string } }>( '/login', @@ -269,14 +272,39 @@ const plugin: FastifyPluginAsync = async (fastify, options) = fastify.decorateRequest('memberId', null); - fastify.get<{ Querystring: { t: string } }>( - '/auth', - { schema: auth }, - async (request, reply) => { - const { query: { t: token } } = request; + fastify.post<{ Body: { email: string, challenge: string } }>( + '/login', + { schema: mlogin }, + async ({ body, log }, reply) => { + const { email, challenge } = body; + const task = memberTaskManager.createGetByTask(GRAASP_ACTOR, { email }); + task.skipActorChecks = true; + const members = await runner.runSingle(task, log); + if (members.length) { + const member = members[0]; + await generateLoginLinkAndEmailIt(member, false, challenge); + } else { + log.warn(`Login attempt with non-existent email '${email}'`); + } + + reply.status(204); + } + ); + + fastify.post<{ Body: { t: string, verifier: string } }>( + '/auth', + { schema: mauth }, + async ({ body: { t: token, verifier } }, reply) => { try { - const { sub: memberId } = await promisifiedJwtVerify(token, JWT_SECRET, {}); + const { sub: memberId, challenge } = await promisifiedJwtVerify(token, JWT_SECRET, {}); + + const verifierHash = crypto.createHash('sha256').update(verifier).digest('hex'); + if (challenge !== verifierHash) { + reply.status(401); + throw new Error('challenge fail'); + } + // TODO: should we fetch/test the member from the DB? return generateAuthTokensPair(memberId); } catch (error) { @@ -289,7 +317,7 @@ const plugin: FastifyPluginAsync = async (fastify, options) = ); fastify.get( - '/auth/refresh', + '/auth/refresh', // there's a hardcoded reference to this path above: "verifyMemberInAuthToken()" { preHandler: fastify.verifyBearerAuth }, async ({ memberId }) => generateAuthTokensPair(memberId) ); diff --git a/src/plugins/auth/schemas.ts b/src/plugins/auth/schemas.ts index 79ad1155c..7a3796b97 100644 --- a/src/plugins/auth/schemas.ts +++ b/src/plugins/auth/schemas.ts @@ -21,6 +21,18 @@ const login = { }, }; +const mlogin = { + body: { + type: 'object', + required: ['email', 'challenge'], + properties: { + email: { type: 'string', format: 'email' }, + challenge: { type: 'string' } + }, + additionalProperties: false + }, +}; + const auth = { querystring: { type: 'object', @@ -32,8 +44,22 @@ const auth = { } }; +const mauth = { + body: { + type: 'object', + required: ['t', 'verifier'], + properties: { + t: { type: 'string' }, + verifier: { type: 'string' } + }, + additionalProperties: false + } +}; + export { register, login, - auth + mlogin, + auth, + mauth };