From f956170820efc443d22a2a1a01756da9bf95e191 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 27 Apr 2023 01:38:06 +0800 Subject: [PATCH] added auth v3 endpoints for login1 and login2 --- backend/src/controllers/v3/authController.ts | 274 +++++++++++++++++++ backend/src/controllers/v3/index.ts | 6 +- backend/src/index.ts | 5 +- backend/src/models/loginSRPDetail.ts | 6 +- backend/src/routes/v3/auth.ts | 29 ++ backend/src/routes/v3/index.ts | 4 +- 6 files changed, 318 insertions(+), 6 deletions(-) create mode 100644 backend/src/controllers/v3/authController.ts create mode 100644 backend/src/routes/v3/auth.ts diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts new file mode 100644 index 000000000..3e4b3e48a --- /dev/null +++ b/backend/src/controllers/v3/authController.ts @@ -0,0 +1,274 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +import { Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; +import * as Sentry from '@sentry/node'; +import * as bigintConversion from 'bigint-conversion'; +const jsrp = require('jsrp'); +import { User, LoginSRPDetail } from '../../models'; +import { issueAuthTokens, createToken } from '../../helpers/auth'; +import { checkUserDevice } from '../../helpers/user'; +import { sendMail } from '../../helpers/nodemailer'; +import { TokenService } from '../../services'; +import { EELogService } from '../../ee/services'; +import { BadRequestError, InternalServerError } from '../../utils/errors'; +import { + TOKEN_EMAIL_MFA, + ACTION_LOGIN +} from '../../variables'; +import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this +import { + getJwtMfaLifetime, + getJwtMfaSecret, + getHttpsEnabled, + getJwtProviderAuthSecret +} from '../../config'; + +declare module 'jsonwebtoken' { + export interface ProviderAuthJwtPayload extends jwt.JwtPayload { + userId: string; + } +} + +/** + * Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol + * @param req + * @param res + * @returns + */ +export const login1 = async (req: Request, res: Response) => { + try { + const { + email, + providerAuthToken, + clientPublicKey + }: { + email?: string; + clientPublicKey: string, + providerAuthToken?: string; + } = req.body; + + let userId = ''; + if (providerAuthToken) { + const decodedToken = ( + jwt.verify(providerAuthToken, getJwtProviderAuthSecret()) + ); + userId = decodedToken.userId; + } + + const filter = userId ? { + _id: userId, + } : { + email, + } + + const user = await User.findOne(filter).select('+salt +verifier'); + + if (!user) throw new Error('Failed to find user'); + + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier + }, + async () => { + // generate server-side public key + const serverPublicKey = server.getPublicKey(); + const identifier = userId ? { + userId, + } : { + email, + } + + await LoginSRPDetail.findOneAndReplace(filter, { + ...identifier, + clientPublicKey: clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt), + }, { upsert: true, returnNewDocument: false }); + + return res.status(200).send({ + serverPublicKey, + salt: user.salt + }); + } + ); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to start authentication process' + }); + } +}; + +/** + * Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted) + * private key + * @param req + * @param res + * @returns + */ +export const login2 = async (req: Request, res: Response) => { + try { + + if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' }); + + const { email, clientProof, providerAuthToken } = req.body; + + let userId = ''; + if (providerAuthToken) { + const decodedToken = ( + jwt.verify(providerAuthToken, getJwtProviderAuthSecret()) + ); + userId = decodedToken.userId; + } + + const filter = userId ? { + _id: userId, + } : { + email, + } + + const user = await User.findOne(filter).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag'); + + if (!user) throw new Error('Failed to find user'); + + const identifier = userId ? { + userId, + } : { + email, + } + + const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ ...identifier }); + + if (!loginSRPDetail) { + return BadRequestError(Error("Failed to find login details for SRP")) + } + + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier, + b: loginSRPDetail.serverBInt + }, + async () => { + server.setClientPublicKey(loginSRPDetail.clientPublicKey); + + // compare server and client shared keys + if (server.checkClientProof(clientProof)) { + + if (user.isMfaEnabled) { + // case: user has MFA enabled + + // generate temporary MFA token + const token = createToken({ + payload: { + userId: user._id.toString() + }, + expiresIn: getJwtMfaLifetime(), + secret: getJwtMfaSecret() + }); + + const code = await TokenService.createToken({ + type: TOKEN_EMAIL_MFA, + email + }); + + // send MFA code [code] to [email] + await sendMail({ + template: 'emailMfa.handlebars', + subjectLine: 'Infisical MFA code', + recipients: [user.email], + substitutions: { + code + } + }); + + return res.status(200).send({ + mfaEnabled: true, + token + }); + } + + await checkUserDevice({ + user, + ip: req.ip, + userAgent: req.headers['user-agent'] ?? '' + }); + + // issue tokens + const tokens = await issueAuthTokens({ userId: user._id.toString() }); + + // store (refresh) token in httpOnly cookie + res.cookie('jid', tokens.refreshToken, { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: getHttpsEnabled() + }); + + // case: user does not have MFA enablgged + // return (access) token in response + + interface ResponseData { + mfaEnabled: boolean; + encryptionVersion: any; + protectedKey?: string; + protectedKeyIV?: string; + protectedKeyTag?: string; + token: string; + publicKey?: string; + encryptedPrivateKey?: string; + iv?: string; + tag?: string; + } + + const response: ResponseData = { + mfaEnabled: false, + encryptionVersion: user.encryptionVersion, + token: tokens.token, + publicKey: user.publicKey, + encryptedPrivateKey: user.encryptedPrivateKey, + iv: user.iv, + tag: user.tag + } + + if ( + user?.protectedKey && + user?.protectedKeyIV && + user?.protectedKeyTag + ) { + response.protectedKey = user.protectedKey; + response.protectedKeyIV = user.protectedKeyIV + response.protectedKeyTag = user.protectedKeyTag; + } + + const loginAction = await EELogService.createAction({ + name: ACTION_LOGIN, + userId: user._id + }); + + loginAction && await EELogService.createLog({ + userId: user._id, + actions: [loginAction], + channel: getChannelFromUserAgent(req.headers['user-agent']), + ipAddress: req.ip + }); + + return res.status(200).send(response); + } + + return res.status(400).send({ + message: 'Failed to authenticate. Try again?' + }); + } + ); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to authenticate. Try again?' + }); + } +}; diff --git a/backend/src/controllers/v3/index.ts b/backend/src/controllers/v3/index.ts index 1cbb36595..ffe25a6c2 100644 --- a/backend/src/controllers/v3/index.ts +++ b/backend/src/controllers/v3/index.ts @@ -1,7 +1,9 @@ import * as secretsController from './secretsController'; import * as workspacesController from './workspacesController'; +import * as authController from './authController'; export { + authController, secretsController, - workspacesController -} \ No newline at end of file + workspacesController, +} diff --git a/backend/src/index.ts b/backend/src/index.ts index 3e2248463..3b3091f92 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,4 +1,3 @@ -import mongoose from 'mongoose'; import dotenv from 'dotenv'; dotenv.config(); import infisical from 'infisical-node'; @@ -63,6 +62,7 @@ import { tags as v2TagsRouter, } from './routes/v2'; import { + auth as v3AuthRouter, secrets as v3SecretsRouter, workspaces as v3WorkspacesRouter } from './routes/v3'; @@ -164,8 +164,9 @@ const main = async () => { app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new app.use('/api/v2/api-key', v2APIKeyDataRouter); - + // v3 routes (experimental) + app.use('/api/v3/auth', v3AuthRouter); app.use('/api/v3/secrets', v3SecretsRouter); app.use('/api/v3/workspaces', v3WorkspacesRouter); diff --git a/backend/src/models/loginSRPDetail.ts b/backend/src/models/loginSRPDetail.ts index 6f3b579bd..0cd3ed1d8 100644 --- a/backend/src/models/loginSRPDetail.ts +++ b/backend/src/models/loginSRPDetail.ts @@ -5,6 +5,7 @@ export interface ILoginSRPDetail { clientPublicKey: string; email: string; serverBInt: mongoose.Schema.Types.Buffer; + userId: string; expireAt: Date; } @@ -16,10 +17,13 @@ const loginSRPDetailSchema = new Schema( }, email: { type: String, - required: true, unique: true }, serverBInt: { type: mongoose.Schema.Types.Buffer }, + userId: { + type: String, + unique: true, + }, expireAt: { type: Date } } ); diff --git a/backend/src/routes/v3/auth.ts b/backend/src/routes/v3/auth.ts new file mode 100644 index 000000000..dbe7481a4 --- /dev/null +++ b/backend/src/routes/v3/auth.ts @@ -0,0 +1,29 @@ +import express from 'express'; +import { body } from 'express-validator'; +import { validateRequest } from '../../middleware'; +import { authController } from '../../controllers/v3'; +import { authLimiter } from '../../helpers/rateLimiter'; + +const router = express.Router(); + +router.post( + '/login1', + authLimiter, + body('email').isString().trim(), + body('providerAuthToken').isString().trim(), + body('clientPublicKey').isString().trim().notEmpty(), + validateRequest, + authController.login1 +); + +router.post( + '/login2', + authLimiter, + body('email').isString().trim(), + body('providerAuthToken').isString().trim(), + body('clientProof').isString().trim().notEmpty(), + validateRequest, + authController.login2 +); + +export default router; diff --git a/backend/src/routes/v3/index.ts b/backend/src/routes/v3/index.ts index e66723133..55f6ec120 100644 --- a/backend/src/routes/v3/index.ts +++ b/backend/src/routes/v3/index.ts @@ -1,7 +1,9 @@ +import auth from './auth'; import secrets from './secrets'; import workspaces from './workspaces'; export { + auth, secrets, workspaces -} \ No newline at end of file +}