Merge pull request #628 from Infisical/pentest-remediation

Fix issues/bugs
This commit is contained in:
BlackMagiq
2023-06-07 22:52:08 +01:00
committed by GitHub
54 changed files with 6707 additions and 1342 deletions
+851 -806
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -40,7 +40,6 @@
"posthog-node": "^2.6.0", "posthog-node": "^2.6.0",
"query-string": "^7.1.3", "query-string": "^7.1.3",
"rate-limit-mongo": "^2.3.2", "rate-limit-mongo": "^2.3.2",
"request-ip": "^3.3.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"stripe": "^10.7.0", "stripe": "^10.7.0",
"swagger-autogen": "^2.22.0", "swagger-autogen": "^2.22.0",
+66 -15
View File
@@ -1,18 +1,28 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models'; import {
User,
LoginSRPDetail,
TokenVersion
} from '../../models';
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth'; import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
import { checkUserDevice } from '../../helpers/user'; import { checkUserDevice } from '../../helpers/user';
import { import {
ACTION_LOGIN, ACTION_LOGIN,
ACTION_LOGOUT ACTION_LOGOUT,
AUTH_MODE_JWT
} from '../../variables'; } from '../../variables';
import { BadRequestError } from '../../utils/errors'; import {
BadRequestError,
UnauthorizedRequestError
} from '../../utils/errors';
import { EELogService } from '../../ee/services'; import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this import { getChannelFromUserAgent } from '../../utils/posthog';
import { import {
getJwtRefreshSecret, getJwtRefreshSecret,
getJwtAuthLifetime, getJwtAuthLifetime,
@@ -23,6 +33,7 @@ import {
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string; userId: string;
refreshVersion?: number;
} }
} }
@@ -105,11 +116,15 @@ export const login2 = async (req: Request, res: Response) => {
await checkUserDevice({ await checkUserDevice({
user, user,
ip: req.ip, ip: req.realIP,
userAgent: req.headers['user-agent'] ?? '' userAgent: req.headers['user-agent'] ?? ''
}); });
const tokens = await issueAuthTokens({ userId: user._id.toString() }); const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie('jid', tokens.refreshToken, {
@@ -128,7 +143,7 @@ export const login2 = async (req: Request, res: Response) => {
userId: user._id, userId: user._id,
actions: [loginAction], actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']), channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip ipAddress: req.realIP
}); });
// return (access) token in response // return (access) token in response
@@ -155,9 +170,9 @@ export const login2 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const logout = async (req: Request, res: Response) => { export const logout = async (req: Request, res: Response) => {
await clearTokens({ if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
userId: req.user._id.toString() await clearTokens(req.authData.tokenVersionId)
}); }
// clear httpOnly cookie // clear httpOnly cookie
res.cookie('jid', '', { res.cookie('jid', '', {
@@ -176,7 +191,7 @@ export const logout = async (req: Request, res: Response) => {
userId: req.user._id, userId: req.user._id,
actions: [logoutAction], actions: [logoutAction],
channel: getChannelFromUserAgent(req.headers['user-agent']), channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip ipAddress: req.realIP
}); });
return res.status(200).send({ return res.status(200).send({
@@ -184,6 +199,30 @@ export const logout = async (req: Request, res: Response) => {
}); });
}; };
export const getCommonPasswords = async (req: Request, res: Response) => {
const commonPasswords = fs.readFileSync(
path.resolve(__dirname, '../../data/' + 'common_passwords.txt'),
'utf8'
).split('\n');
return res.status(200).send(commonPasswords);
}
export const revokeAllSessions = async (req: Request, res: Response) => {
await TokenVersion.updateMany({
user: req.user._id
}, {
$inc: {
refreshVersion: 1,
accessVersion: 1
}
});
return res.status(200).send({
message: 'Successfully revoked all sessions.'
});
}
/** /**
* Return user is authenticated * Return user is authenticated
* @param req * @param req
@@ -197,7 +236,7 @@ export const checkAuth = async (req: Request, res: Response) => {
} }
/** /**
* Return new token by redeeming refresh token * Return new JWT access token by first validating the refresh token
* @param req * @param req
* @param res * @param res
* @returns * @returns
@@ -206,7 +245,7 @@ export const getNewToken = async (req: Request, res: Response) => {
const refreshToken = req.cookies.jid; const refreshToken = req.cookies.jid;
if (!refreshToken) { if (!refreshToken) {
throw new Error('Failed to find token in request cookies'); throw new Error('Failed to find refresh token in request cookies');
} }
const decodedToken = <jwt.UserIDJwtPayload>( const decodedToken = <jwt.UserIDJwtPayload>(
@@ -215,15 +254,27 @@ export const getNewToken = async (req: Request, res: Response) => {
const user = await User.findOne({ const user = await User.findOne({
_id: decodedToken.userId _id: decodedToken.userId
}).select('+publicKey'); }).select('+publicKey +refreshVersion +accessVersion');
if (!user) throw new Error('Failed to authenticate unfound user'); if (!user) throw new Error('Failed to authenticate unfound user');
if (!user?.publicKey) if (!user?.publicKey)
throw new Error('Failed to authenticate not fully set up account'); throw new Error('Failed to authenticate not fully set up account');
const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId);
if (!tokenVersion) throw UnauthorizedRequestError({
message: 'Failed to validate refresh token'
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({
message: 'Failed to validate refresh token'
});
const token = createToken({ const token = createToken({
payload: { payload: {
userId: decodedToken.userId userId: decodedToken.userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.refreshVersion
}, },
expiresIn: await getJwtAuthLifetime(), expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret() secret: await getJwtAuthSecret()
@@ -6,8 +6,10 @@ import { createToken } from '../../helpers/auth';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { sendMail } from '../../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services'; import { TokenService } from '../../services';
import { EELicenseService } from '../../ee/services';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables'; import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config'; import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
import { validateUserEmail } from '../../validation';
/** /**
* Delete organization membership with id [membershipOrgId] from organization * Delete organization membership with id [membershipOrgId] from organization
@@ -96,6 +98,19 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
if (!membershipOrg) { if (!membershipOrg) {
throw new Error('Failed to validate organization membership'); throw new Error('Failed to validate organization membership');
} }
const plan = await EELicenseService.getOrganizationPlan(organizationId);
if (plan.memberLimit !== null) {
// case: limit imposed on number of members allowed
if (plan.membersUsed >= plan.memberLimit) {
// case: number of members used exceeds the number of members allowed
return res.status(400).send({
message: 'Failed to invite member due to member limit reached. Upgrade plan to invite more members.'
});
}
}
invitee = await User.findOne({ invitee = await User.findOne({
email: inviteeEmail email: inviteeEmail
@@ -134,6 +149,9 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
if (!inviteeMembershipOrg) { if (!inviteeMembershipOrg) {
// case: invitee has never been invited before // case: invitee has never been invited before
// validate that email is not disposable
validateUserEmail(inviteeEmail);
await new MembershipOrg({ await new MembershipOrg({
inviteEmail: inviteeEmail, inviteEmail: inviteeEmail,
@@ -3,12 +3,23 @@ import { Request, Response } from 'express';
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
import { User, BackupPrivateKey, LoginSRPDetail } from '../../models'; import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
import { createToken } from '../../helpers/auth'; import {
import { sendMail } from '../../helpers/nodemailer'; createToken,
sendMail,
clearTokens
} from '../../helpers';
import { TokenService } from '../../services'; import { TokenService } from '../../services';
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables'; import {
TOKEN_EMAIL_PASSWORD_RESET,
AUTH_MODE_JWT
} from '../../variables';
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from '../../utils/errors';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config'; import {
getSiteURL,
getJwtSignupLifetime,
getJwtSignupSecret,
getHttpsEnabled
} from '../../config';
/** /**
* Password reset step 1: Send email verification link to email [email] * Password reset step 1: Send email verification link to email [email]
@@ -18,14 +29,15 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../conf
* @returns * @returns
*/ */
export const emailPasswordReset = async (req: Request, res: Response) => { export const emailPasswordReset = async (req: Request, res: Response) => {
const email = req.body.email; let email: string;
email = req.body.email;
const user = await User.findOne({ email }).select('+publicKey'); const user = await User.findOne({ email }).select('+publicKey');
if (!user || !user?.publicKey) { if (!user || !user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: 'Failed to send email verification for password reset' message: "If an account exists with this email, a password reset link has been sent"
}); });
} }
@@ -46,7 +58,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
}); });
return res.status(200).send({ return res.status(200).send({
message: `Sent an email for account recovery to ${email}` message:"If an account exists with this email, a password reset link has been sent"
}); });
} }
@@ -98,6 +110,7 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
*/ */
export const srp1 = async (req: Request, res: Response) => { export const srp1 = async (req: Request, res: Response) => {
// return salt, serverPublicKey as part of first step of SRP protocol // return salt, serverPublicKey as part of first step of SRP protocol
const { clientPublicKey } = req.body; const { clientPublicKey } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email: req.user.email email: req.user.email
@@ -127,7 +140,8 @@ export const srp1 = async (req: Request, res: Response) => {
}); });
} }
); );
}; }
/** /**
* Change account SRP authentication information for user * Change account SRP authentication information for user
@@ -193,6 +207,19 @@ export const changePassword = async (req: Request, res: Response) => {
new: true new: true
} }
); );
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId)
}
// clear httpOnly cookie
res.cookie('jid', '', {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: (await getHttpsEnabled()) as boolean
});
return res.status(200).send({ return res.status(200).send({
message: 'Successfully changed password' message: 'Successfully changed password'
+83 -78
View File
@@ -100,50 +100,52 @@ export const pushSecrets = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); let secrets;
const environment: string = req.query.environment as string; let key;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment const postHogClient = await TelemetryService.getPostHogClient();
const workspaceEnvs = req.membership.workspace.environments; const environment: string = req.query.environment as string;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { const channel: string = req.query.channel as string;
throw new Error('Failed to validate environment'); const { workspaceId } = req.params;
}
let secrets = await pull({ // validate environment
userId: req.user._id.toString(), const workspaceEnvs = req.membership.workspace.environments;
workspaceId, if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
environment, throw new Error('Failed to validate environment');
channel: channel ? channel : 'cli', }
ipAddress: req.ip
});
const key = await Key.findOne({ secrets = await pull({
workspace: workspaceId, userId: req.user._id.toString(),
receiver: req.user._id workspaceId,
}) environment,
.sort({ createdAt: -1 }) channel: channel ? channel : 'cli',
.populate('sender', '+publicKey'); ipAddress: req.realIP
});
if (channel !== 'cli') {
// FIX: Fix this any
secrets = reformatPullSecrets({ secrets }) as any;
}
if (postHogClient) { key = await Key.findOne({
// capture secrets pushed event in production workspace: workspaceId,
postHogClient.capture({ receiver: req.user._id
distinctId: req.user.email, })
event: 'secrets pulled', .sort({ createdAt: -1 })
properties: { .populate('sender', '+publicKey');
numberOfSecrets: secrets.length,
environment, if (channel !== 'cli') {
workspaceId, secrets = reformatPullSecrets({ secrets });
channel: channel ? channel : 'cli' }
}
}); if (postHogClient) {
} // capture secrets pushed event in production
postHogClient.capture({
distinctId: req.user.email,
event: 'secrets pulled',
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
return res.status(200).send({ return res.status(200).send({
secrets, secrets,
@@ -160,48 +162,51 @@ export const pullSecrets = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const pullSecretsServiceToken = async (req: Request, res: Response) => { export const pullSecretsServiceToken = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); let secrets;
const environment: string = req.query.environment as string; let key;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment const postHogClient = await TelemetryService.getPostHogClient();
const workspaceEnvs = req.membership.workspace.environments; const environment: string = req.query.environment as string;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { const channel: string = req.query.channel as string;
throw new Error('Failed to validate environment'); const { workspaceId } = req.params;
}
const secrets = await pull({ // validate environment
userId: req.serviceToken.user._id.toString(), const workspaceEnvs = req.membership.workspace.environments;
workspaceId, if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
environment, throw new Error('Failed to validate environment');
channel: 'cli', }
ipAddress: req.ip
});
const key = { secrets = await pull({
encryptedKey: req.serviceToken.encryptedKey, userId: req.serviceToken.user._id.toString(),
nonce: req.serviceToken.nonce, workspaceId,
sender: { environment,
publicKey: req.serviceToken.publicKey channel: 'cli',
}, ipAddress: req.realIP
receiver: req.serviceToken.user, });
workspace: req.serviceToken.workspace
};
if (postHogClient) { key = {
// capture secrets pulled event in production encryptedKey: req.serviceToken.encryptedKey,
postHogClient.capture({ nonce: req.serviceToken.nonce,
distinctId: req.serviceToken.user.email, sender: {
event: 'secrets pulled', publicKey: req.serviceToken.publicKey
properties: { },
numberOfSecrets: secrets.length, receiver: req.serviceToken.user,
environment, workspace: req.serviceToken.workspace
workspaceId, };
channel: channel ? channel : 'cli'
} if (postHogClient) {
}); // capture secrets pulled event in production
} postHogClient.capture({
distinctId: req.serviceToken.user.email,
event: 'secrets pulled',
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
return res.status(200).send({ return res.status(200).send({
secrets: reformatPullSecrets({ secrets }), secrets: reformatPullSecrets({ secrets }),
@@ -7,6 +7,7 @@ import {
import { createToken } from '../../helpers/auth'; import { createToken } from '../../helpers/auth';
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from '../../utils/errors';
import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config'; import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
import { validateUserEmail } from '../../validation';
/** /**
* Signup step 1: Initialize account for user under email [email] and send a verification code * Signup step 1: Initialize account for user under email [email] and send a verification code
@@ -16,7 +17,11 @@ import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpC
* @returns * @returns
*/ */
export const beginEmailSignup = async (req: Request, res: Response) => { export const beginEmailSignup = async (req: Request, res: Response) => {
const email = req.body.email; let email: string;
email = req.body.email;
// validate that email is not disposable
validateUserEmail(email);
const user = await User.findOne({ email }).select('+publicKey'); const user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) { if (user && user?.publicKey) {
@@ -13,6 +13,7 @@ import {
createWorkspace as create, createWorkspace as create,
deleteWorkspace as deleteWork, deleteWorkspace as deleteWork,
} from "../../helpers/workspace"; } from "../../helpers/workspace";
import { EELicenseService } from '../../ee/services';
import { addMemberships } from "../../helpers/membership"; import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables"; import { ADMIN } from "../../variables";
@@ -115,6 +116,18 @@ export const createWorkspace = async (req: Request, res: Response) => {
throw new Error("Failed to validate organization membership"); throw new Error("Failed to validate organization membership");
} }
const plan = await EELicenseService.getOrganizationPlan(organizationId);
if (plan.workspaceLimit !== null) {
// case: limit imposed on number of workspaces allowed
if (plan.workspacesUsed >= plan.workspaceLimit) {
// case: number of workspaces used exceeds the number of workspaces allowed
return res.status(400).send({
message: 'Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces.'
});
}
}
if (workspaceName.length < 1) { if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long"); throw new Error("Workspace names must be at least 1-character long");
} }
+15 -8
View File
@@ -83,7 +83,7 @@ export const login2 = async (req: Request, res: Response) => {
const { email, clientProof } = req.body; const { email, clientProof } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag'); }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
@@ -105,7 +105,6 @@ export const login2 = async (req: Request, res: Response) => {
// compare server and client shared keys // compare server and client shared keys
if (server.checkClientProof(clientProof)) { if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) { if (user.isMfaEnabled) {
// case: user has MFA enabled // case: user has MFA enabled
@@ -141,12 +140,16 @@ export const login2 = async (req: Request, res: Response) => {
await checkUserDevice({ await checkUserDevice({
user, user,
ip: req.ip, ip: req.realIP,
userAgent: req.headers['user-agent'] ?? '' userAgent: req.headers['user-agent'] ?? ''
}); });
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() }); const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie('jid', tokens.refreshToken, {
@@ -156,7 +159,7 @@ export const login2 = async (req: Request, res: Response) => {
secure: await getHttpsEnabled() secure: await getHttpsEnabled()
}); });
// case: user does not have MFA enablgged // case: user does not have MFA enabled
// return (access) token in response // return (access) token in response
interface ResponseData { interface ResponseData {
@@ -267,12 +270,16 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
await checkUserDevice({ await checkUserDevice({
user, user,
ip: req.ip, ip: req.realIP,
userAgent: req.headers['user-agent'] ?? '' userAgent: req.headers['user-agent'] ?? ''
}); });
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() }); const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie('jid', tokens.refreshToken, {
@@ -330,7 +337,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
userId: user._id, userId: user._id,
actions: [loginAction], actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']), channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip ipAddress: req.realIP
}); });
return res.status(200).send(resObj); return res.status(200).send(resObj);
@@ -297,7 +297,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
actions, actions,
channel, channel,
ipAddress: req.ip, ipAddress: req.realIP,
}); });
} }
@@ -563,7 +563,7 @@ export const createSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
actions: [addAction], actions: [addAction],
channel, channel,
ipAddress: req.ip, ipAddress: req.realIP,
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
@@ -785,7 +785,7 @@ export const getSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId as string), workspaceId: new Types.ObjectId(workspaceId as string),
actions: [readAction], actions: [readAction],
channel, channel,
ipAddress: req.ip, ipAddress: req.realIP,
})); }));
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
@@ -1020,7 +1020,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
actions: [updateAction], actions: [updateAction],
channel, channel,
ipAddress: req.ip, ipAddress: req.realIP,
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
@@ -1158,7 +1158,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(key), workspaceId: new Types.ObjectId(key),
actions: [deleteAction], actions: [deleteAction],
channel, channel,
ipAddress: req.ip, ipAddress: req.realIP,
})); }));
// (EE) take a secret snapshot // (EE) take a secret snapshot
@@ -114,7 +114,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ const tokens = await issueAuthTokens({
userId: user._id.toString() userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
}); });
token = tokens.token; token = tokens.token;
@@ -237,7 +239,9 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ const tokens = await issueAuthTokens({
userId: user._id.toString() userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
}); });
token = tokens.token; token = tokens.token;
@@ -68,7 +68,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
environment, environment,
secrets, secrets,
channel: channel ? channel : 'cli', channel: channel ? channel : 'cli',
ipAddress: req.ip ipAddress: req.realIP
}); });
await pushKeys({ await pushKeys({
@@ -111,6 +111,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string; const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string; const channel: string = req.query.channel as string;
@@ -128,17 +129,16 @@ export const pullSecrets = async (req: Request, res: Response) => {
throw new Error('Failed to validate environment'); throw new Error('Failed to validate environment');
} }
let secrets = await pull({ secrets = await pull({
userId, userId,
workspaceId, workspaceId,
environment, environment,
channel: channel ? channel : 'cli', channel: channel ? channel : 'cli',
ipAddress: req.ip ipAddress: req.realIP
}); });
if (channel !== 'cli') { if (channel !== 'cli') {
// FIX: Fix this any secrets = reformatPullSecrets({ secrets });
secrets = reformatPullSecrets({ secrets }) as any;
} }
if (postHogClient) { if (postHogClient) {
+8 -4
View File
@@ -113,7 +113,7 @@ export const login2 = async (req: Request, res: Response) => {
const user = await User.findOne({ const user = await User.findOne({
email, email,
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag'); }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
@@ -179,12 +179,16 @@ export const login2 = async (req: Request, res: Response) => {
await checkUserDevice({ await checkUserDevice({
user, user,
ip: req.ip, ip: req.realIP,
userAgent: req.headers['user-agent'] ?? '' userAgent: req.headers['user-agent'] ?? ''
}); });
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() }); const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie('jid', tokens.refreshToken, {
@@ -239,7 +243,7 @@ export const login2 = async (req: Request, res: Response) => {
userId: user._id, userId: user._id,
actions: [loginAction], actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']), channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip ipAddress: req.realIP
}); });
return res.status(200).send(response); return res.status(200).send(response);
@@ -137,7 +137,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ const tokens = await issueAuthTokens({
userId: user._id.toString() userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
}); });
token = tokens.token; token = tokens.token;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+76 -33
View File
@@ -6,7 +6,9 @@ import {
User, User,
ServiceTokenData, ServiceTokenData,
ServiceAccount, ServiceAccount,
APIKeyData APIKeyData,
TokenVersion,
ITokenVersion
} from '../models'; } from '../models';
import { import {
AccountNotFoundError, AccountNotFoundError,
@@ -35,7 +37,7 @@ import {
* @param {Object} obj * @param {Object} obj
* @param {Object} obj.headers - HTTP request headers object * @param {Object} obj.headers - HTTP request headers object
*/ */
const validateAuthMode = ({ export const validateAuthMode = ({
headers, headers,
acceptedAuthModes acceptedAuthModes
}: { }: {
@@ -97,7 +99,7 @@ const validateAuthMode = ({
* @param {String} obj.authTokenValue - JWT token value * @param {String} obj.authTokenValue - JWT token value
* @returns {User} user - user corresponding to JWT token * @returns {User} user - user corresponding to JWT token
*/ */
const getAuthUserPayload = async ({ export const getAuthUserPayload = async ({
authTokenValue authTokenValue
}: { }: {
authTokenValue: string; authTokenValue: string;
@@ -107,14 +109,32 @@ const getAuthUserPayload = async ({
); );
const user = await User.findOne({ const user = await User.findOne({
_id: decodedToken.userId _id: new Types.ObjectId(decodedToken.userId)
}).select('+publicKey'); }).select('+publicKey +accessVersion');
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' }); if (!user) throw AccountNotFoundError({ message: 'Failed to find user' });
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' }); if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate user with partially set up account' });
return user; const tokenVersion = await TokenVersion.findOneAndUpdate({
_id: new Types.ObjectId(decodedToken.tokenVersionId),
user: user._id
}, {
lastUsed: new Date()
});
if (!tokenVersion) throw UnauthorizedRequestError({
message: 'Failed to validate access token'
});
if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError({
message: 'Failed to validate access token'
});
return ({
user,
tokenVersionId: tokenVersion._id
});
} }
/** /**
@@ -123,7 +143,7 @@ const getAuthUserPayload = async ({
* @param {String} obj.authTokenValue - service token value * @param {String} obj.authTokenValue - service token value
* @returns {ServiceTokenData} serviceTokenData - service token data * @returns {ServiceTokenData} serviceTokenData - service token data
*/ */
const getAuthSTDPayload = async ({ export const getAuthSTDPayload = async ({
authTokenValue authTokenValue
}: { }: {
authTokenValue: string; authTokenValue: string;
@@ -169,7 +189,7 @@ const getAuthSTDPayload = async ({
* @param {String} obj.authTokenValue - service account access token value * @param {String} obj.authTokenValue - service account access token value
* @returns {ServiceAccount} serviceAccount * @returns {ServiceAccount} serviceAccount
*/ */
const getAuthSAAKPayload = async ({ export const getAuthSAAKPayload = async ({
authTokenValue authTokenValue
}: { }: {
authTokenValue: string; authTokenValue: string;
@@ -198,7 +218,7 @@ const getAuthSAAKPayload = async ({
* @param {String} obj.authTokenValue - API key value * @param {String} obj.authTokenValue - API key value
* @returns {APIKeyData} apiKeyData - API key data * @returns {APIKeyData} apiKeyData - API key data
*/ */
const getAuthAPIKeyPayload = async ({ export const getAuthAPIKeyPayload = async ({
authTokenValue authTokenValue
}: { }: {
authTokenValue: string; authTokenValue: string;
@@ -255,12 +275,43 @@ const getAuthAPIKeyPayload = async ({
* @return {String} obj.token - issued JWT token * @return {String} obj.token - issued JWT token
* @return {String} obj.refreshToken - issued refresh token * @return {String} obj.refreshToken - issued refresh token
*/ */
const issueAuthTokens = async ({ userId }: { userId: string }) => { export const issueAuthTokens = async ({
userId,
ip,
userAgent
}: {
userId: Types.ObjectId;
ip: string;
userAgent: string;
}) => {
let tokenVersion: ITokenVersion | null;
// continue with (session) token version matching existing ip and user agent
tokenVersion = await TokenVersion.findOne({
user: userId,
ip,
userAgent
});
if (!tokenVersion) {
// case: no existing ip and user agent exists
// -> create new (session) token version for ip and user agent
tokenVersion = await new TokenVersion({
user: userId,
refreshVersion: 0,
accessVersion: 0,
ip,
userAgent,
lastUsed: new Date()
}).save();
}
// issue tokens // issue tokens
const token = createToken({ const token = createToken({
payload: { payload: {
userId userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.accessVersion
}, },
expiresIn: await getJwtAuthLifetime(), expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret() secret: await getJwtAuthSecret()
@@ -268,7 +319,9 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
const refreshToken = createToken({ const refreshToken = createToken({
payload: { payload: {
userId userId,
tokenVersionId: tokenVersion._id.toString(),
refreshVersion: tokenVersion.refreshVersion
}, },
expiresIn: await getJwtRefreshLifetime(), expiresIn: await getJwtRefreshLifetime(),
secret: await getJwtRefreshSecret() secret: await getJwtRefreshSecret()
@@ -285,13 +338,15 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
* @param {Object} obj * @param {Object} obj
* @param {String} obj.userId - id of user whose tokens are cleared. * @param {String} obj.userId - id of user whose tokens are cleared.
*/ */
const clearTokens = async ({ userId }: { userId: string }): Promise<void> => { export const clearTokens = async (tokenVersionId: Types.ObjectId): Promise<void> => {
// increment refreshVersion on user by 1 // increment refreshVersion on user by 1
User.findOneAndUpdate({
_id: userId await TokenVersion.findOneAndUpdate({
_id: tokenVersionId
}, { }, {
$inc: { $inc: {
refreshVersion: 1 refreshVersion: 1,
accessVersion: 1
} }
}); });
}; };
@@ -304,7 +359,7 @@ const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
* @param {String} obj.secret - (JWT) secret such as [JWT_AUTH_SECRET] * @param {String} obj.secret - (JWT) secret such as [JWT_AUTH_SECRET]
* @param {String} obj.expiresIn - string describing time span such as '10h' or '7d' * @param {String} obj.expiresIn - string describing time span such as '10h' or '7d'
*/ */
const createToken = ({ export const createToken = ({
payload, payload,
expiresIn, expiresIn,
secret secret
@@ -318,7 +373,7 @@ const createToken = ({
}); });
}; };
const validateProviderAuthToken = async ({ export const validateProviderAuthToken = async ({
email, email,
user, user,
providerAuthToken, providerAuthToken,
@@ -341,16 +396,4 @@ const validateProviderAuthToken = async ({
) { ) {
throw new Error('Invalid authentication credentials.') throw new Error('Invalid authentication credentials.')
} }
} }
export {
validateAuthMode,
validateProviderAuthToken,
getAuthUserPayload,
getAuthSTDPayload,
getAuthSAAKPayload,
getAuthAPIKeyPayload,
createToken,
issueAuthTokens,
clearTokens
};
+6 -13
View File
@@ -31,7 +31,7 @@ import { InternalServerError } from "../utils/errors";
* @param {String} obj.name - name of bot * @param {String} obj.name - name of bot
* @param {String} obj.workspaceId - id of workspace that bot belongs to * @param {String} obj.workspaceId - id of workspace that bot belongs to
*/ */
const createBot = async ({ export const createBot = async ({
name, name,
workspaceId, workspaceId,
}: { }: {
@@ -93,7 +93,7 @@ const createBot = async ({
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment * @param {String} obj.environment - environment
*/ */
const getSecretsHelper = async ({ export const getSecretsBotHelper = async ({
workspaceId, workspaceId,
environment, environment,
}: { }: {
@@ -136,7 +136,7 @@ const getSecretsHelper = async ({
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key * @returns {String} key - decrypted workspace key
*/ */
const getKey = async ({ workspaceId }: { workspaceId: string }) => { export const getKey = async ({ workspaceId }: { workspaceId: string }) => {
const encryptionKey = await getEncryptionKey(); const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey();
@@ -194,7 +194,7 @@ const getKey = async ({ workspaceId }: { workspaceId: string }) => {
* @param {String} obj1.workspaceId - id of workspace * @param {String} obj1.workspaceId - id of workspace
* @param {String} obj1.plaintext - plaintext to encrypt * @param {String} obj1.plaintext - plaintext to encrypt
*/ */
const encryptSymmetricHelper = async ({ export const encryptSymmetricHelper = async ({
workspaceId, workspaceId,
plaintext, plaintext,
}: { }: {
@@ -222,7 +222,7 @@ const encryptSymmetricHelper = async ({
* @param {String} obj.iv - iv * @param {String} obj.iv - iv
* @param {String} obj.tag - tag * @param {String} obj.tag - tag
*/ */
const decryptSymmetricHelper = async ({ export const decryptSymmetricHelper = async ({
workspaceId, workspaceId,
ciphertext, ciphertext,
iv, iv,
@@ -242,11 +242,4 @@ const decryptSymmetricHelper = async ({
}); });
return plaintext; return plaintext;
}; };
export {
createBot,
getSecretsHelper,
encryptSymmetricHelper,
decryptSymmetricHelper
};
+2 -7
View File
@@ -7,7 +7,7 @@ import { getLogger } from '../utils/logger';
* @param {String} obj.mongoURL - mongo connection string * @param {String} obj.mongoURL - mongo connection string
* @returns * @returns
*/ */
const initDatabaseHelper = async ({ export const initDatabaseHelper = async ({
mongoURL mongoURL
}: { }: {
mongoURL: string; mongoURL: string;
@@ -30,7 +30,7 @@ const initDatabaseHelper = async ({
/** /**
* Close database conection * Close database conection
*/ */
const closeDatabaseHelper = async () => { export const closeDatabaseHelper = async () => {
return Promise.all([ return Promise.all([
new Promise((resolve) => { new Promise((resolve) => {
if (mongoose.connection && mongoose.connection.readyState == 1) { if (mongoose.connection && mongoose.connection.readyState == 1) {
@@ -41,9 +41,4 @@ const closeDatabaseHelper = async () => {
} }
}) })
]); ]);
}
export {
initDatabaseHelper,
closeDatabaseHelper
} }
+2 -4
View File
@@ -18,7 +18,7 @@ interface Event {
* @param {String} obj.event.workspaceId - id of workspace that event is part of * @param {String} obj.event.workspaceId - id of workspace that event is part of
* @param {Object} obj.event.payload - payload of event (depends on event) * @param {Object} obj.event.payload - payload of event (depends on event)
*/ */
const handleEventHelper = async ({ event }: { event: Event }) => { export const handleEventHelper = async ({ event }: { event: Event }) => {
const { workspaceId, environment } = event; const { workspaceId, environment } = event;
// TODO: moduralize bot check into separate function // TODO: moduralize bot check into separate function
@@ -37,6 +37,4 @@ const handleEventHelper = async ({ event }: { event: Event }) => {
}); });
break; break;
} }
}; };
export { handleEventHelper };
+17
View File
@@ -0,0 +1,17 @@
export * from './auth';
export * from './bot';
export * from './database';
export * from './event';
export * from './integration';
export * from './key';
export * from './membership';
export * from './membershipOrg';
export * from './nodemailer';
export * from './organization';
export * from './rateLimiter';
export * from './secret';
export * from './secrets';
export * from './signup';
export * from './token';
export * from './user';
export * from './workspace';
+7 -17
View File
@@ -15,7 +15,6 @@ import {
import { import {
UnauthorizedRequestError, UnauthorizedRequestError,
} from '../utils/errors'; } from '../utils/errors';
import RequestError from '../utils/requestError';
interface Update { interface Update {
workspace: string; workspace: string;
@@ -36,7 +35,7 @@ interface Update {
* @param {String} obj.code - code * @param {String} obj.code - code
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange * @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/ */
const handleOAuthExchangeHelper = async ({ export const handleOAuthExchangeHelper = async ({
workspaceId, workspaceId,
integration, integration,
code, code,
@@ -110,7 +109,7 @@ const handleOAuthExchangeHelper = async ({
* @param {Object} obj * @param {Object} obj
* @param {Object} obj.workspaceId - id of workspace * @param {Object} obj.workspaceId - id of workspace
*/ */
const syncIntegrationsHelper = async ({ export const syncIntegrationsHelper = async ({
workspaceId, workspaceId,
environment environment
}: { }: {
@@ -162,7 +161,7 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @param {String} refreshToken - decrypted refresh token * @param {String} refreshToken - decrypted refresh token
*/ */
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
const integrationAuth = await IntegrationAuth const integrationAuth = await IntegrationAuth
.findById(integrationAuthId) .findById(integrationAuthId)
.select('+refreshCiphertext +refreshIV +refreshTag'); .select('+refreshCiphertext +refreshIV +refreshTag');
@@ -187,7 +186,7 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token * @returns {String} accessToken - decrypted access token
*/ */
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let accessId; let accessId;
let accessToken; let accessToken;
const integrationAuth = await IntegrationAuth const integrationAuth = await IntegrationAuth
@@ -240,7 +239,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.refreshToken - refresh token * @param {String} obj.refreshToken - refresh token
*/ */
const setIntegrationAuthRefreshHelper = async ({ export const setIntegrationAuthRefreshHelper = async ({
integrationAuthId, integrationAuthId,
refreshToken refreshToken
}: { }: {
@@ -282,7 +281,7 @@ const setIntegrationAuthRefreshHelper = async ({
* @param {String} obj.accessToken - access token * @param {String} obj.accessToken - access token
* @param {Date} obj.accessExpiresAt - expiration date of access token * @param {Date} obj.accessExpiresAt - expiration date of access token
*/ */
const setIntegrationAuthAccessHelper = async ({ export const setIntegrationAuthAccessHelper = async ({
integrationAuthId, integrationAuthId,
accessId, accessId,
accessToken, accessToken,
@@ -327,13 +326,4 @@ const setIntegrationAuthAccessHelper = async ({
}); });
return integrationAuth; return integrationAuth;
} }
export {
handleOAuthExchangeHelper,
syncIntegrationsHelper,
getIntegrationAuthRefreshHelper,
getIntegrationAuthAccessHelper,
setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper
}
+2 -4
View File
@@ -17,7 +17,7 @@ interface Key {
* @param {String} obj.keys.nonce - nonce for encryption * @param {String} obj.keys.nonce - nonce for encryption
* @param {String} obj.keys.userId - id of receiver user * @param {String} obj.keys.userId - id of receiver user
*/ */
const pushKeys = async ({ export const pushKeys = async ({
userId, userId,
workspaceId, workspaceId,
keys keys
@@ -50,6 +50,4 @@ const pushKeys = async ({
workspace: workspaceId workspace: workspaceId
})) }))
); );
}; };
export { pushKeys };
+14 -16
View File
@@ -10,10 +10,10 @@ import { MembershipNotFoundError, BadRequestError } from "../utils/errors";
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId] * @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
*/ */
const validateMembership = async ({ export const validateMembership = async ({
userId, userId,
workspaceId, workspaceId,
acceptedRoles, acceptedRoles,
}: { }: {
userId: Types.ObjectId | string; userId: Types.ObjectId | string;
workspaceId: Types.ObjectId | string; workspaceId: Types.ObjectId | string;
@@ -46,8 +46,8 @@ const validateMembership = async ({
* @param {Object} queryObj - query object * @param {Object} queryObj - query object
* @return {Object} membership - membership * @return {Object} membership - membership
*/ */
const findMembership = async (queryObj: any) => { export const findMembership = async (queryObj: any) => {
const membership = await Membership.findOne(queryObj); const membership = await Membership.findOne(queryObj);
return membership; return membership;
}; };
@@ -59,10 +59,10 @@ const findMembership = async (queryObj: any) => {
* @param {String} obj.workspaceId - id of workspace. * @param {String} obj.workspaceId - id of workspace.
* @param {String[]} obj.roles - roles of users. * @param {String[]} obj.roles - roles of users.
*/ */
const addMemberships = async ({ export const addMemberships = async ({
userIds, userIds,
workspaceId, workspaceId,
roles, roles
}: { }: {
userIds: string[]; userIds: string[];
workspaceId: string; workspaceId: string;
@@ -93,9 +93,9 @@ const addMemberships = async ({
* @param {Object} obj * @param {Object} obj
* @param {String} obj.membershipId - id of membership to delete * @param {String} obj.membershipId - id of membership to delete
*/ */
const deleteMembership = async ({ membershipId }: { membershipId: string }) => { export const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
const deletedMembership = await Membership.findOneAndDelete({ const deletedMembership = await Membership.findOneAndDelete({
_id: membershipId, _id: membershipId
}); });
// delete keys associated with the membership // delete keys associated with the membership
@@ -107,7 +107,5 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
}); });
} }
return deletedMembership; return deletedMembership;
}; };
export { validateMembership, addMemberships, findMembership, deleteMembership };
+5 -12
View File
@@ -18,7 +18,7 @@ import {
* @param {Types.ObjectId} obj.organizationId * @param {Types.ObjectId} obj.organizationId
* @param {String[]} obj.acceptedRoles * @param {String[]} obj.acceptedRoles
*/ */
const validateMembershipOrg = async ({ export const validateMembershipOrg = async ({
userId, userId,
organizationId, organizationId,
acceptedRoles, acceptedRoles,
@@ -59,7 +59,7 @@ const validateMembershipOrg = async ({
* @param {Object} queryObj - query object * @param {Object} queryObj - query object
* @return {Object} membershipOrg - membership * @return {Object} membershipOrg - membership
*/ */
const findMembershipOrg = (queryObj: any) => { export const findMembershipOrg = (queryObj: any) => {
const membershipOrg = MembershipOrg.findOne(queryObj); const membershipOrg = MembershipOrg.findOne(queryObj);
return membershipOrg; return membershipOrg;
}; };
@@ -72,7 +72,7 @@ const findMembershipOrg = (queryObj: any) => {
* @param {String} obj.organizationId - id of organization. * @param {String} obj.organizationId - id of organization.
* @param {String[]} obj.roles - roles of users. * @param {String[]} obj.roles - roles of users.
*/ */
const addMembershipsOrg = async ({ export const addMembershipsOrg = async ({
userIds, userIds,
organizationId, organizationId,
roles, roles,
@@ -111,7 +111,7 @@ const addMembershipsOrg = async ({
* @param {Object} obj * @param {Object} obj
* @param {String} obj.membershipOrgId - id of organization membership to delete * @param {String} obj.membershipOrgId - id of organization membership to delete
*/ */
const deleteMembershipOrg = async ({ export const deleteMembershipOrg = async ({
membershipOrgId membershipOrgId
}: { }: {
membershipOrgId: string; membershipOrgId: string;
@@ -148,11 +148,4 @@ const deleteMembershipOrg = async ({
} }
return deletedMembershipOrg; return deletedMembershipOrg;
}; };
export {
validateMembershipOrg,
findMembershipOrg,
addMembershipsOrg,
deleteMembershipOrg
};
+3 -5
View File
@@ -13,7 +13,7 @@ let smtpTransporter: nodemailer.Transporter;
* @param {String[]} obj.recipients - email addresses of people to send email to * @param {String[]} obj.recipients - email addresses of people to send email to
* @param {Object} obj.substitutions - object containing template substitutions * @param {Object} obj.substitutions - object containing template substitutions
*/ */
const sendMail = async ({ export const sendMail = async ({
template, template,
subjectLine, subjectLine,
recipients, recipients,
@@ -41,8 +41,6 @@ const sendMail = async ({
} }
}; };
const setTransporter = (transporter: nodemailer.Transporter) => { export const setTransporter = (transporter: nodemailer.Transporter) => {
smtpTransporter = transporter; smtpTransporter = transporter;
}; };
export { sendMail, setTransporter };
+4 -10
View File
@@ -28,7 +28,7 @@ import {
* @param {String} obj.email - POC email that will receive invoice info * @param {String} obj.email - POC email that will receive invoice info
* @param {Object} organization - new organization * @param {Object} organization - new organization
*/ */
const createOrganization = async ({ export const createOrganization = async ({
name, name,
email, email,
}: { }: {
@@ -70,7 +70,7 @@ const createOrganization = async ({
* @return {Object} obj.stripeSubscription - new stripe subscription * @return {Object} obj.stripeSubscription - new stripe subscription
* @return {Subscription} obj.subscription - new subscription * @return {Subscription} obj.subscription - new subscription
*/ */
const initSubscriptionOrg = async ({ export const initSubscriptionOrg = async ({
organizationId, organizationId,
}: { }: {
organizationId: Types.ObjectId; organizationId: Types.ObjectId;
@@ -125,7 +125,7 @@ const initSubscriptionOrg = async ({
* @param {Object} obj * @param {Object} obj
* @param {Number} obj.organizationId - id of subscription's organization * @param {Number} obj.organizationId - id of subscription's organization
*/ */
const updateSubscriptionOrgQuantity = async ({ export const updateSubscriptionOrgQuantity = async ({
organizationId, organizationId,
}: { }: {
organizationId: string; organizationId: string;
@@ -171,10 +171,4 @@ const updateSubscriptionOrgQuantity = async ({
} }
return stripeSubscription; return stripeSubscription;
}; };
export {
createOrganization,
initSubscriptionOrg,
updateSubscriptionOrgQuantity
};
+7 -13
View File
@@ -2,7 +2,7 @@ import rateLimit from 'express-rate-limit';
const MongoStore = require('rate-limit-mongo'); const MongoStore = require('rate-limit-mongo');
// 200 per minute // 200 per minute
const apiLimiter = rateLimit({ export const apiLimiter = rateLimit({
store: new MongoStore({ store: new MongoStore({
uri: process.env.MONGO_URL, uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60, expireTimeMs: 1000 * 60,
@@ -17,7 +17,7 @@ const apiLimiter = rateLimit({
return request.path === '/healthcheck' || request.path === '/api/status' return request.path === '/healthcheck' || request.path === '/api/status'
}, },
keyGenerator: (req, res) => { keyGenerator: (req, res) => {
return req.clientIp return req.realIP
} }
}); });
@@ -34,12 +34,12 @@ const authLimit = rateLimit({
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
keyGenerator: (req, res) => { keyGenerator: (req, res) => {
return req.clientIp return req.realIP
} }
}); });
// 50 requests per 1 hour // 50 requests per 1 hour
const passwordLimiter = rateLimit({ export const passwordLimiter = rateLimit({
store: new MongoStore({ store: new MongoStore({
uri: process.env.MONGO_URL, uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60 * 60, expireTimeMs: 1000 * 60 * 60,
@@ -51,20 +51,14 @@ const passwordLimiter = rateLimit({
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
keyGenerator: (req, res) => { keyGenerator: (req, res) => {
return req.clientIp return req.realIP
} }
}); });
const authLimiter = (req: any, res: any, next: any) => { export const authLimiter = (req: any, res: any, next: any) => {
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
authLimit(req, res, next); authLimit(req, res, next);
} else { } else {
next(); next();
} }
}; };
export {
apiLimiter,
authLimiter,
passwordLimiter
};
+6 -8
View File
@@ -60,7 +60,7 @@ interface Update {
* @param {String} obj.environment - environment for secrets * @param {String} obj.environment - environment for secrets
* @param {Object[]} obj.secrets - secrets to push * @param {Object[]} obj.secrets - secrets to push
*/ */
const v1PushSecrets = async ({ export const v1PushSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment, environment,
@@ -304,7 +304,7 @@ const v1PushSecrets = async ({
* @param {String} obj.channel - channel (web/cli/auto) * @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets * @param {String} obj.ipAddress - ip address of request to push secrets
*/ */
const v2PushSecrets = async ({ export const v2PushSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment, environment,
@@ -530,7 +530,7 @@ const v2PushSecrets = async ({
* @param {String} obj.workspaceId - id of workspace to pull from * @param {String} obj.workspaceId - id of workspace to pull from
* @param {String} obj.environment - environment for secrets * @param {String} obj.environment - environment for secrets
*/ */
const getSecrets = async ({ export const getSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment, environment,
@@ -570,7 +570,7 @@ const getSecrets = async ({
* @param {String} obj.channel - channel (web/cli/auto) * @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets * @param {String} obj.ipAddress - ip address of request to push secrets
*/ */
const pullSecrets = async ({ export const pullSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment, environment,
@@ -614,7 +614,7 @@ const pullSecrets = async ({
* @param {Object} obj * @param {Object} obj
* @param {Object} obj.secrets * @param {Object} obj.secrets
*/ */
const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => { export const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
const reformatedSecrets = secrets.map((s) => ({ const reformatedSecrets = secrets.map((s) => ({
_id: s._id, _id: s._id,
workspace: s.workspace, workspace: s.workspace,
@@ -644,6 +644,4 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
})); }));
return reformatedSecrets; return reformatedSecrets;
}; };
export { v1PushSecrets, v2PushSecrets, pullSecrets, reformatPullSecrets };
+20 -31
View File
@@ -45,8 +45,8 @@ import {
* @param {Object} obj * @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId * @param {Types.ObjectId} obj.workspaceId
*/ */
const createSecretBlindIndexDataHelper = async ({ export const createSecretBlindIndexDataHelper = async ({
workspaceId, workspaceId
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
@@ -98,8 +98,8 @@ const createSecretBlindIndexDataHelper = async ({
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for * @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
* @returns * @returns
*/ */
const getSecretBlindIndexSaltHelper = async ({ export const getSecretBlindIndexSaltHelper = async ({
workspaceId, workspaceId
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
@@ -147,9 +147,9 @@ const getSecretBlindIndexSaltHelper = async ({
* @param {String} obj.secretName - name of secret to generate blind index for * @param {String} obj.secretName - name of secret to generate blind index for
* @param {String} obj.salt - base64-salt * @param {String} obj.salt - base64-salt
*/ */
const generateSecretBlindIndexWithSaltHelper = async ({ export const generateSecretBlindIndexWithSaltHelper = async ({
secretName, secretName,
salt, salt
}: { }: {
secretName: string; secretName: string;
salt: string; salt: string;
@@ -177,9 +177,9 @@ const generateSecretBlindIndexWithSaltHelper = async ({
* @param {Stringj} obj.secretName - name of secret to generate blind index for * @param {Stringj} obj.secretName - name of secret to generate blind index for
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
*/ */
const generateSecretBlindIndexHelper = async ({ export const generateSecretBlindIndexHelper = async ({
secretName, secretName,
workspaceId, workspaceId
}: { }: {
secretName: string; secretName: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
@@ -247,7 +247,7 @@ const generateSecretBlindIndexHelper = async ({
* @param {AuthData} obj.authData - authentication data on request * @param {AuthData} obj.authData - authentication data on request
* @returns * @returns
*/ */
const createSecretHelper = async ({ export const createSecretHelper = async ({
secretName, secretName,
workspaceId, workspaceId,
environment, environment,
@@ -397,10 +397,10 @@ const createSecretHelper = async ({
* @param {AuthData} obj.authData - authentication data on request * @param {AuthData} obj.authData - authentication data on request
* @returns * @returns
*/ */
const getSecretsHelper = async ({ export const getSecretsHelper = async ({
workspaceId, workspaceId,
environment, environment,
authData, authData
}: GetSecretsParams) => { }: GetSecretsParams) => {
let secrets: ISecret[] = []; let secrets: ISecret[] = [];
@@ -472,7 +472,7 @@ const getSecretsHelper = async ({
* @param {AuthData} obj.authData - authentication data on request * @param {AuthData} obj.authData - authentication data on request
* @returns * @returns
*/ */
const getSecretHelper = async ({ export const getSecretHelper = async ({
secretName, secretName,
workspaceId, workspaceId,
environment, environment,
@@ -558,7 +558,8 @@ const getSecretHelper = async ({
* @param {AuthData} obj.authData - authentication data on request * @param {AuthData} obj.authData - authentication data on request
* @returns * @returns
*/ */
const updateSecretHelper = async ({
export const updateSecretHelper = async ({
secretName, secretName,
workspaceId, workspaceId,
environment, environment,
@@ -698,7 +699,7 @@ const updateSecretHelper = async ({
* @param {AuthData} obj.authData - authentication data on request * @param {AuthData} obj.authData - authentication data on request
* @returns * @returns
*/ */
const deleteSecretHelper = async ({ export const deleteSecretHelper = async ({
secretName, secretName,
workspaceId, workspaceId,
environment, environment,
@@ -799,16 +800,4 @@ const deleteSecretHelper = async ({
secrets, secrets,
secret, secret,
}; };
}; };
export {
createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper,
generateSecretBlindIndexHelper,
createSecretHelper,
getSecretsHelper,
getSecretHelper,
updateSecretHelper,
deleteSecretHelper,
};
+8 -10
View File
@@ -13,11 +13,11 @@ import { TOKEN_EMAIL_CONFIRMATION } from '../variables';
* @param {String} obj.email - email * @param {String} obj.email - email
* @returns {Boolean} success - whether or not operation was successful * @returns {Boolean} success - whether or not operation was successful
*/ */
const sendEmailVerification = async ({ email }: { email: string }) => { export const sendEmailVerification = async ({ email }: { email: string }) => {
const token = await TokenService.createToken({ const token = await TokenService.createToken({
type: TOKEN_EMAIL_CONFIRMATION, type: TOKEN_EMAIL_CONFIRMATION,
email email
}); });
// send mail // send mail
await sendMail({ await sendMail({
@@ -36,7 +36,7 @@ const sendEmailVerification = async ({ email }: { email: string }) => {
* @param {String} obj.email - emai * @param {String} obj.email - emai
* @param {String} obj.code - code that was sent to [email] * @param {String} obj.code - code that was sent to [email]
*/ */
const checkEmailVerification = async ({ export const checkEmailVerification = async ({
email, email,
code code
}: { }: {
@@ -57,7 +57,7 @@ const checkEmailVerification = async ({
* @param {String} obj.organizationName - name of organization to initialize * @param {String} obj.organizationName - name of organization to initialize
* @param {IUser} obj.user - user who we are initializing for * @param {IUser} obj.user - user who we are initializing for
*/ */
const initializeDefaultOrg = async ({ export const initializeDefaultOrg = async ({
organizationName, organizationName,
user user
}: { }: {
@@ -81,6 +81,4 @@ const initializeDefaultOrg = async ({
} catch (err) { } catch (err) {
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`); throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
} }
}; };
export { sendEmailVerification, checkEmailVerification, initializeDefaultOrg };
View File
+3 -5
View File
@@ -20,7 +20,7 @@ import { getSaltRounds } from "../config";
* @param {Types.ObjectId} obj.organizationId * @param {Types.ObjectId} obj.organizationId
* @returns {String} token - the created token * @returns {String} token - the created token
*/ */
const createTokenHelper = async ({ export const createTokenHelper = async ({
type, type,
email, email,
phoneNumber, phoneNumber,
@@ -121,7 +121,7 @@ const createTokenHelper = async ({
* @param {String} obj.email - email associated with the token * @param {String} obj.email - email associated with the token
* @param {String} obj.token - value of the token * @param {String} obj.token - value of the token
*/ */
const validateTokenHelper = async ({ export const validateTokenHelper = async ({
type, type,
email, email,
phoneNumber, phoneNumber,
@@ -212,6 +212,4 @@ const validateTokenHelper = async ({
// case: token is valid // case: token is valid
await TokenData.findByIdAndDelete(tokenData._id); await TokenData.findByIdAndDelete(tokenData._id);
}; };
export { createTokenHelper, validateTokenHelper };
+15 -20
View File
@@ -15,7 +15,7 @@ import { SecretService } from '../services';
* @param {String} organizationId - id of organization to create workspace in * @param {String} organizationId - id of organization to create workspace in
* @param {Object} workspace - new workspace * @param {Object} workspace - new workspace
*/ */
const createWorkspace = async ({ export const createWorkspace = async ({
name, name,
organizationId organizationId
}: { }: {
@@ -50,23 +50,18 @@ const createWorkspace = async ({
* @param {Object} obj * @param {Object} obj
* @param {String} obj.id - id of workspace to delete * @param {String} obj.id - id of workspace to delete
*/ */
const deleteWorkspace = async ({ id }: { id: string }) => { export const deleteWorkspace = async ({ id }: { id: string }) => {
await Workspace.deleteOne({ _id: id }); await Workspace.deleteOne({ _id: id });
await Bot.deleteOne({ await Bot.deleteOne({
workspace: id workspace: id
}); });
await Membership.deleteMany({ await Membership.deleteMany({
workspace: id workspace: id
}); });
await Secret.deleteMany({ await Secret.deleteMany({
workspace: id workspace: id
}); });
await Key.deleteMany({ await Key.deleteMany({
workspace: id workspace: id
}); });
};
export {
createWorkspace,
deleteWorkspace
}; };
+7 -3
View File
@@ -13,7 +13,6 @@ import swaggerUi = require("swagger-ui-express");
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require("../spec.json"); const swaggerFile = require("../spec.json");
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const requestIp = require("request-ip");
import { apiLimiter } from "./helpers/rateLimiter"; import { apiLimiter } from "./helpers/rateLimiter";
import { import {
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter,
@@ -86,8 +85,6 @@ const main = async () => {
}) })
); );
app.use(requestIp.mw());
if ((await getNodeEnv()) === "production") { if ((await getNodeEnv()) === "production") {
// enable app-wide rate-limiting + helmet security // enable app-wide rate-limiting + helmet security
// in production // in production
@@ -96,6 +93,13 @@ const main = async () => {
app.use(helmet()); app.use(helmet());
} }
app.use((req, res, next) => {
// default to IP address provided by Cloudflare
const cfIp = req.headers['cf-connecting-ip'];
req.realIP = Array.isArray(cfIp) ? cfIp[0] : (cfIp as string) || req.ip;
next();
});
// (EE) routes // (EE) routes
app.use("/api/v1/secret", eeSecretRouter); app.use("/api/v1/secret", eeSecretRouter);
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter); app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
@@ -1,3 +1,4 @@
import { Types } from 'mongoose';
import { import {
IUser, IUser,
IServiceAccount, IServiceAccount,
@@ -10,4 +11,5 @@ export interface AuthData {
authChannel: string; authChannel: string;
authIP: string; authIP: string;
authUserAgent: string; authUserAgent: string;
tokenVersionId?: Types.ObjectId;
} }
+7 -4
View File
@@ -71,10 +71,12 @@ const requireAuth = ({
req.user = authPayload; req.user = authPayload;
break; break;
default: default:
authPayload = await getAuthUserPayload({ const { user, tokenVersionId } = await getAuthUserPayload({
authTokenValue authTokenValue
}); });
req.user = authPayload; authPayload = user;
req.user = user;
req.tokenVersionId = tokenVersionId;
break; break;
} }
@@ -88,8 +90,9 @@ const requireAuth = ({
authMode, authMode,
authPayload, // User, ServiceAccount, ServiceTokenData authPayload, // User, ServiceAccount, ServiceTokenData
authChannel: getChannelFromUserAgent(req.headers['user-agent']), authChannel: getChannelFromUserAgent(req.headers['user-agent']),
authIP: req.ip, authIP: req.realIP,
authUserAgent: req.headers['user-agent'] ?? 'other' authUserAgent: req.headers['user-agent'] ?? 'other',
tokenVersionId: req.tokenVersionId
} }
return next(); return next();
+4 -1
View File
@@ -22,6 +22,7 @@ import Workspace, { IWorkspace } from './workspace';
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData'; import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
import APIKeyData, { IAPIKeyData } from './apiKeyData'; import APIKeyData, { IAPIKeyData } from './apiKeyData';
import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail'; import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail';
import TokenVersion, { ITokenVersion } from './tokenVersion';
export { export {
AuthProvider, AuthProvider,
@@ -72,5 +73,7 @@ export {
APIKeyData, APIKeyData,
IAPIKeyData, IAPIKeyData,
LoginSRPDetail, LoginSRPDetail,
ILoginSRPDetail ILoginSRPDetail,
TokenVersion,
ITokenVersion
}; };
+47
View File
@@ -0,0 +1,47 @@
import { Schema, model, Types, Document } from 'mongoose';
export interface ITokenVersion extends Document {
user: Types.ObjectId;
ip: string;
userAgent: string;
refreshVersion: number;
accessVersion: number;
lastUsed: Date;
}
const tokenVersionSchema = new Schema<ITokenVersion>(
{
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
ip: {
type: String,
required: true
},
userAgent: {
type: String,
required: true
},
refreshVersion: {
type: Number,
required: true
},
accessVersion: {
type: Number,
required: true
},
lastUsed: {
type: Date,
required: true
}
},
{
timestamps: true
}
);
const TokenVersion = model<ITokenVersion>('TokenVersion', tokenVersionSchema);
export default TokenVersion;
+2 -7
View File
@@ -21,7 +21,6 @@ export interface IUser extends Document {
tag?: string; tag?: string;
salt?: string; salt?: string;
verifier?: string; verifier?: string;
refreshVersion?: number;
isMfaEnabled: boolean; isMfaEnabled: boolean;
mfaMethods: boolean; mfaMethods: boolean;
devices: { devices: {
@@ -91,11 +90,6 @@ const userSchema = new Schema<IUser>(
type: String, type: String,
select: false select: false
}, },
refreshVersion: {
type: Number,
default: 0,
select: false
},
isMfaEnabled: { isMfaEnabled: {
type: Boolean, type: Boolean,
default: false default: false
@@ -108,7 +102,8 @@ const userSchema = new Schema<IUser>(
ip: String, ip: String,
userAgent: String userAgent: String
}], }],
default: [] default: [],
select: false
} }
}, },
{ {
+17 -4
View File
@@ -44,8 +44,6 @@ router.post(
authController.checkAuth authController.checkAuth
); );
router.get( router.get(
'/redirect/google', '/redirect/google',
authLimiter, authLimiter,
@@ -53,12 +51,27 @@ router.get(
scope: ['profile', 'email'], scope: ['profile', 'email'],
session: false, session: false,
}), }),
) );
router.get( router.get(
'/callback/google', '/callback/google',
passport.authenticate('google', { failureRedirect: '/login/provider/error', session: false }), passport.authenticate('google', { failureRedirect: '/login/provider/error', session: false }),
authController.handleAuthProviderCallback, authController.handleAuthProviderCallback,
) );
router.get(
'/common-passwords',
authLimiter,
authController.getCommonPasswords
);
router.delete(
'/sessions',
authLimiter,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
authController.revokeAllSessions
);
export default router; export default router;
+1 -1
View File
@@ -26,7 +26,7 @@ router.post(
router.post( router.post(
'/mfa/send', '/mfa/send',
authLimiter, authLimiter,
body('email').isString().trim().notEmpty(), body('email').isString().trim().notEmpty().isEmail(),
validateRequest, validateRequest,
authController.sendMfaToken authController.sendMfaToken
); );
+2 -2
View File
@@ -1,6 +1,6 @@
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
getSecretsHelper, getSecretsBotHelper,
encryptSymmetricHelper, encryptSymmetricHelper,
decryptSymmetricHelper decryptSymmetricHelper
} from '../helpers/bot'; } from '../helpers/bot';
@@ -25,7 +25,7 @@ class BotService {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
}) { }) {
return await getSecretsHelper({ return await getSecretsBotHelper({
workspaceId, workspaceId,
environment environment
}); });
+3
View File
@@ -1,4 +1,5 @@
import * as express from 'express'; import * as express from 'express';
import { Types } from 'mongoose';
import { import {
IUser, IUser,
IServiceAccount, IServiceAccount,
@@ -39,7 +40,9 @@ declare global {
serviceTokenData: any; serviceTokenData: any;
apiKeyData: any; apiKeyData: any;
query?: any; query?: any;
tokenVersionId?: Types.ObjectId;
authData: AuthData; authData: AuthData;
realIP: string;
requestData: { requestData: {
[key: string]: string [key: string]: string
}; };
+1
View File
@@ -1,3 +1,4 @@
export * from './user';
export * from './workspace'; export * from './workspace';
export * from './bot'; export * from './bot';
export * from './integration'; export * from './integration';
+19 -1
View File
@@ -1,3 +1,5 @@
import fs from 'fs';
import path from 'path';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
IUser, IUser,
@@ -8,7 +10,7 @@ import {
} from '../models'; } from '../models';
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
import _ from 'lodash'; import _ from 'lodash';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../utils/errors';
import { import {
validateMembershipOrg validateMembershipOrg
} from '../helpers/membershipOrg'; } from '../helpers/membershipOrg';
@@ -17,6 +19,22 @@ import {
PERMISSION_WRITE_SECRETS PERMISSION_WRITE_SECRETS
} from '../variables'; } from '../variables';
/**
* Validate that email [email] is not disposable
* @param email - email to validate
*/
export const validateUserEmail = (email: string) => {
const emailDomain = email.split('@')[1];
const disposableEmails = fs.readFileSync(
path.resolve(__dirname, '../data/' + 'disposable_emails.txt'),
'utf8'
).split('\n');
if (disposableEmails.includes(emailDomain)) throw ValidationError({
message: 'Failed to validate email as non-disposable'
});
}
/** /**
* Validate that user (client) can access workspace * Validate that user (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions * with id [workspaceId] and its environment [environment] with required permissions
+47 -52
View File
@@ -2,18 +2,19 @@ import crypto from 'crypto';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { faCheck, faXmark } from '@fortawesome/free-solid-svg-icons'; import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import jsrp from 'jsrp'; import jsrp from 'jsrp';
import nacl from 'tweetnacl'; import nacl from 'tweetnacl';
import { encodeBase64 } from 'tweetnacl-util'; import { encodeBase64 } from 'tweetnacl-util';
import { useGetCommonPasswords } from '@app/hooks/api';
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup'; import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
import getOrganizations from '@app/pages/api/organization/getOrgs'; import getOrganizations from '@app/pages/api/organization/getOrgs';
import ProjectService from '@app/services/ProjectService'; import ProjectService from '@app/services/ProjectService';
import InputField from '../basic/InputField'; import InputField from '../basic/InputField';
import passwordCheck from '../utilities/checks/PasswordCheck'; import checkPassword from '../utilities/checks/checkPassword';
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm'; import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '../utilities/cryptography/crypto'; import { deriveArgonKey } from '../utilities/cryptography/crypto';
import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage'; import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage';
@@ -37,6 +38,15 @@ interface UserInfoStepProps {
providerAuthToken?: string; providerAuthToken?: string;
} }
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
/** /**
* This is the step of the sign up flow where people provife their name/surname and password * This is the step of the sign up flow where people provife their name/surname and password
* @param {object} obj * @param {object} obj
@@ -63,11 +73,11 @@ export default function UserInfoStep({
setAttributionSource, setAttributionSource,
providerAuthToken, providerAuthToken,
}: UserInfoStepProps): JSX.Element { }: UserInfoStepProps): JSX.Element {
const { data: commonPasswords } = useGetCommonPasswords();
const [nameError, setNameError] = useState(false); const [nameError, setNameError] = useState(false);
const [organizationNameError, setOrganizationNameError] = useState(false); const [organizationNameError, setOrganizationNameError] = useState(false);
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); const [errors, setErrors] = useState<Errors>({});
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -89,12 +99,11 @@ export default function UserInfoStep({
} else { } else {
setOrganizationNameError(false); setOrganizationNameError(false);
} }
errorCheck = passwordCheck({
errorCheck = checkPassword({
password, password,
setPasswordErrorLength, commonPasswords,
setPasswordErrorNumber, setErrors
setPasswordErrorLowerCase,
errorCheck
}); });
if (!errorCheck) { if (!errorCheck) {
@@ -248,59 +257,45 @@ export default function UserInfoStep({
label={t('section.password.password')} label={t('section.password.password')}
onChangeHandler={(pass: string) => { onChangeHandler={(pass: string) => {
setPassword(pass); setPassword(pass);
passwordCheck({ checkPassword({
password: pass, password: pass,
setPasswordErrorLength, commonPasswords,
setPasswordErrorNumber, setErrors
setPasswordErrorLowerCase,
errorCheck: false
}); });
}} }}
type="password" type="password"
value={password} value={password}
isRequired isRequired
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase} error={Object.keys(errors).length > 0}
autoComplete="new-password" autoComplete="new-password"
id="new-password" id="new-password"
/> />
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? ( {Object.keys(errors).length > 0 && (
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2"> <div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-1 text-sm text-gray-400">{t('section.password.validate-base')}</div> <div className="mb-2 text-sm text-gray-400">{t('section.password.validate-base')}</div>
<div className="ml-1 flex flex-row items-center justify-start"> {Object.keys(errors).map((key) => {
{passwordErrorLength ? ( if (errors[key as keyof Errors]) {
<FontAwesomeIcon icon={faXmark} className="text-md text-red ml-0.5 mr-2.5" /> return (
) : ( <div
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> className="ml-1 flex flex-row items-top justify-start"
)} key={key}
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}> >
{t('section.password.validate-length')} <div>
</div> <FontAwesomeIcon
</div> icon={faXmark}
<div className="ml-1 flex flex-row items-center justify-start"> className="text-md text-red ml-0.5 mr-2.5"
{passwordErrorLowerCase ? ( />
<FontAwesomeIcon icon={faXmark} className="text-md text-red ml-0.5 mr-2.5" /> </div>
) : ( <p className="text-gray-400 text-sm">
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> {errors[key as keyof Errors]}
)} </p>
<div </div>
className={`${passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'} text-sm`} );
> }
{t('section.password.validate-case')}
</div> return null;
</div> })}
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNumber ? (
<FontAwesomeIcon icon={faXmark} className="text-md text-red ml-0.5 mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
{t('section.password.validate-number')}
</div>
</div>
</div> </div>
) : (
<div className="py-2" />
)} )}
</div> </div>
<div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left"> <div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
@@ -17,6 +17,7 @@ const passwordCheck = ({
setPasswordErrorLowerCase, setPasswordErrorLowerCase,
errorCheck errorCheck
}: PasswordCheckProps) => { }: PasswordCheckProps) => {
if (!password || password.length < 14) { if (!password || password.length < 14) {
setPasswordErrorLength(true); setPasswordErrorLength(true);
errorCheck = true; errorCheck = true;
@@ -0,0 +1,72 @@
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
commonPassword?: string
};
interface CheckPasswordParams {
password: string;
commonPasswords: string[];
setErrors: (value: Errors) => void;
}
/**
* Validate that the password [password] is at least:
* - 8 characters long
* - Contains 1 uppercase character (A-Z)
* - Contains 1 lowercase character (a-z)
* - Contains 1 number (0-9)
* - Does not contain 3 repeat, consecutive characters
*
* The function returns whether or not the password [password]
* passes the minimum requirements above. It sets errors on
* an erorr object via [setErrors].
*
* @param {Object} obj
* @param {String} obj.password - the password to check
* @param {Function} obj.setErrors - set state function to set error object
*/
const checkPassword = ({
password,
commonPasswords,
setErrors
}: CheckPasswordParams): boolean => {
const errors: Errors = {};
if (password.length < 8) {
errors.length = "8 characters";
}
if (!/[A-Z]/.test(password)) {
errors.upperCase = "1 uppercase character (A-Z)";
}
if (!/[a-z]/.test(password)) {
errors.lowerCase = "1 lowercase character (a-z)";
}
if (!/[0-9]/.test(password)) {
errors.number = "1 number (0-9)";
}
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.specialChar = "1 special character (!@#$%^&*(),.?)";
}
if (/([A-Za-z0-9])\1\1\1/.test(password)) {
errors.repeatedChar = "No 3 repeat, consecutive characters";
}
if (commonPasswords.includes(password)) {
errors.commonPassword = "No common passwords";
}
setErrors(errors);
return Object.keys(errors).length > 0;
}
export default checkPassword;
@@ -125,6 +125,10 @@ const changePassword = async (
setPasswordChanged(true); setPasswordChanged(true);
setCurrentPassword(''); setCurrentPassword('');
setNewPassword(''); setNewPassword('');
window.location.href = '/login';
// move to login page
} catch (error) { } catch (error) {
setCurrentPasswordError(true); setCurrentPasswordError(true);
console.log(error); console.log(error);
+4 -1
View File
@@ -1,4 +1,7 @@
export { export {
useGetAuthToken, useGetAuthToken,
useSendMfaToken, useSendMfaToken,
useVerifyMfaToken} from './queries' useVerifyMfaToken,
useRevokeAllSessions,
useGetCommonPasswords
} from './queries'
+19 -1
View File
@@ -10,7 +10,8 @@ import {
VerifyMfaTokenRes} from './types'; VerifyMfaTokenRes} from './types';
const authKeys = { const authKeys = {
getAuthToken: ['token'] as const getAuthToken: ['token'] as const,
commonPasswords: ['common-passwords'] as const
}; };
export const useSendMfaToken = () => { export const useSendMfaToken = () => {
@@ -49,3 +50,20 @@ export const useGetAuthToken = () =>
onSuccess: (data) => setAuthToken(data.token), onSuccess: (data) => setAuthToken(data.token),
retry: 0 retry: 0
}); });
export const useRevokeAllSessions = () => {
return useMutation({
mutationFn: async () => {
const { data } = await apiRequest.delete('/api/v1/auth/sessions');
return data;
}
});
}
const fetchCommonPasswords = async () => {
const { data } = await apiRequest.get('/api/v1/auth/common-passwords');
return data || [];
};
export const useGetCommonPasswords = () =>
useQuery({ queryKey: authKeys.commonPasswords, queryFn: fetchCommonPasswords });
+72 -66
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faCheck, faPlus, faX } from '@fortawesome/free-solid-svg-icons'; import { faBan,faCheck, faPlus, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '@app/components/basic/buttons/Button'; import Button from '@app/components/basic/buttons/Button';
@@ -10,21 +10,31 @@ import InputField from '@app/components/basic/InputField';
import ListBox from '@app/components/basic/Listbox'; import ListBox from '@app/components/basic/Listbox';
import ApiKeyTable from '@app/components/basic/table/ApiKeyTable'; import ApiKeyTable from '@app/components/basic/table/ApiKeyTable';
import NavHeader from '@app/components/navigation/NavHeader'; import NavHeader from '@app/components/navigation/NavHeader';
import passwordCheck from '@app/components/utilities/checks/PasswordCheck'; import checkPassword from '@app/components/utilities/checks/checkPassword';
import changePassword from '@app/components/utilities/cryptography/changePassword'; import changePassword from '@app/components/utilities/cryptography/changePassword';
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey'; import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
import {
useGetCommonPasswords,
useRevokeAllSessions} from '@app/hooks/api';
import { SecuritySection } from '@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection'; import { SecuritySection } from '@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection';
import AddApiKeyDialog from '../../../components/basic/dialog/AddApiKeyDialog'; import AddApiKeyDialog from '../../../components/basic/dialog/AddApiKeyDialog';
import getAPIKeys from '../../api/apiKey/getAPIKeys'; import getAPIKeys from '../../api/apiKey/getAPIKeys';
import getUser from '../../api/user/getUser'; import getUser from '../../api/user/getUser';
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
export default function PersonalSettings() { export default function PersonalSettings() {
const { data: commonPasswords } = useGetCommonPasswords();
const [personalEmail, setPersonalEmail] = useState(''); const [personalEmail, setPersonalEmail] = useState('');
const [personalName, setPersonalName] = useState(''); const [personalName, setPersonalName] = useState('');
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const [currentPasswordError, setCurrentPasswordError] = useState(false); const [currentPasswordError, setCurrentPasswordError] = useState(false);
const [currentPassword, setCurrentPassword] = useState(''); const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState(''); const [newPassword, setNewPassword] = useState('');
@@ -34,6 +44,9 @@ export default function PersonalSettings() {
const [backupKeyError, setBackupKeyError] = useState(false); const [backupKeyError, setBackupKeyError] = useState(false);
const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false); const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false);
const [apiKeys, setApiKeys] = useState<any[]>([]); const [apiKeys, setApiKeys] = useState<any[]>([]);
const [errors, setErrors] = useState<Errors>({});
const revokeAllSessions = useRevokeAllSessions();
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const router = useRouter(); const router = useRouter();
@@ -154,78 +167,54 @@ export default function PersonalSettings() {
label={t('section.password.new') as string} label={t('section.password.new') as string}
onChangeHandler={(password) => { onChangeHandler={(password) => {
setNewPassword(password); setNewPassword(password);
passwordCheck({ checkPassword({
password, password,
setPasswordErrorLength, commonPasswords,
setPasswordErrorNumber, setErrors
setPasswordErrorLowerCase,
errorCheck: false
}); });
}} }}
type="password" type="password"
value={newPassword} value={newPassword}
isRequired isRequired
error={passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber} error={Object.keys(errors).length > 0}
autoComplete="new-password" autoComplete="new-password"
id="new-password" id="new-password"
/> />
</div> </div>
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? ( {Object.keys(errors).length > 0 && (
<div className="mt-3 mb-2 flex w-full max-w-xl flex-col items-start rounded-md bg-white/5 px-2 py-2"> <div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-1 text-sm text-gray-400"> <div className="mb-2 text-sm text-gray-400">{t('section.password.validate-base')}</div>
{t('section.password.validate-base')} {Object.keys(errors).map((key) => {
</div> if (errors[key as keyof Errors]) {
<div className="ml-1 flex flex-row items-center justify-start"> return (
{passwordErrorLength ? ( <div className="ml-1 flex flex-row items-top justify-start" key={key}>
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" /> <div>
) : ( <FontAwesomeIcon
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> icon={faXmark}
)} className="text-md text-red ml-0.5 mr-2.5"
<div />
className={`${ </div>
passwordErrorLength ? 'text-gray-400' : 'text-gray-600' <p className="text-gray-400 text-sm">
} text-sm`} {errors[key as keyof Errors]}
> </p>
{t('section.password.validate-length')} </div>
</div> );
</div> }
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLowerCase ? ( return null;
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" /> })}
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
{t('section.password.validate-case')}
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNumber ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
{t('section.password.validate-number')}
</div>
</div>
</div> </div>
) : (
<div className="py-2" />
)} )}
<div className="mt-3 flex w-52 flex-row items-center pr-3"> <div className="mt-3 flex w-52 flex-row items-center pr-3">
<Button <Button
text={t('section.password.change') as string} text={t('section.password.change') as string}
onButtonPressed={() => { onButtonPressed={() => {
if (!passwordErrorLength && !passwordErrorLowerCase && !passwordErrorNumber) { const errorCheck = checkPassword({
password: newPassword,
commonPasswords,
setErrors
});
if (!errorCheck) {
changePassword( changePassword(
personalEmail, personalEmail,
currentPassword, currentPassword,
@@ -239,11 +228,6 @@ export default function PersonalSettings() {
}} }}
color="mineshaft" color="mineshaft"
size="md" size="md"
active={
newPassword !== '' &&
currentPassword !== '' &&
!(passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber)
}
textDisabled={t('section.password.change') as string} textDisabled={t('section.password.change') as string}
/> />
<FontAwesomeIcon <FontAwesomeIcon
@@ -254,6 +238,28 @@ export default function PersonalSettings() {
/> />
</div> </div>
</div> </div>
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<div className="my-4 flex w-full flex-row justify-between">
<p className="text-xl font-semibold w-full">
Sessions
</p>
<div className="w-40">
<Button
text="Revoke all"
onButtonPressed={async () => {
await revokeAllSessions.mutateAsync();
router.push('/login');
}}
color="mineshaft"
icon={faBan}
size="md"
/>
</div>
</div>
<p className="mb-5 text-sm text-mineshaft-300">
Logging into Infisical via browser or CLI creates a session. Revoking all sessions logs your account out all active sessions across all browsers and CLIs.
</p>
</div>
<div className="mt-2 mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-5 pb-6"> <div className="mt-2 mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-5 pb-6">
<div className="flex w-full max-w-5xl flex-row items-center justify-between"> <div className="flex w-full max-w-5xl flex-row items-center justify-between">
+49 -57
View File
@@ -7,7 +7,7 @@ import Head from 'next/head';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faCheck, faWarning, faX } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faWarning, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import jsrp from 'jsrp'; import jsrp from 'jsrp';
import queryString from 'query-string'; import queryString from 'query-string';
@@ -17,7 +17,9 @@ import { encodeBase64 } from 'tweetnacl-util';
import Button from '@app/components/basic/buttons/Button'; import Button from '@app/components/basic/buttons/Button';
import InputField from '@app/components/basic/InputField'; import InputField from '@app/components/basic/InputField';
import attemptLogin from '@app/components/utilities/attemptLogin'; import attemptLogin from '@app/components/utilities/attemptLogin';
import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
import checkPassword from '@app/components/utilities/checks/checkPassword';
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm'; import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto'; import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey'; import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
@@ -25,28 +27,36 @@ import { saveTokenToLocalStorage } from '@app/components/utilities/saveTokenToLo
import SecurityClient from '@app/components/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
import getOrganizations from '@app/pages/api/organization/getOrgs'; import getOrganizations from '@app/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects'; import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
import {
useGetCommonPasswords
} from '@app/hooks/api';
import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite'; import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite';
import verifySignupInvite from './api/auth/VerifySignupInvite'; import verifySignupInvite from './api/auth/VerifySignupInvite';
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const client = new jsrp.client(); const client = new jsrp.client();
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
export default function SignupInvite() { export default function SignupInvite() {
const { data: commonPasswords } = useGetCommonPasswords();
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [firstName, setFirstName] = useState(''); const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState(''); const [lastName, setLastName] = useState('');
const [firstNameError, setFirstNameError] = useState(false); const [firstNameError, setFirstNameError] = useState(false);
const [lastNameError, setLastNameError] = useState(false); const [lastNameError, setLastNameError] = useState(false);
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const [errorLogin, setErrorLogin] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const [backupKeyError, setBackupKeyError] = useState(false); const [backupKeyError, setBackupKeyError] = useState(false);
const [verificationToken, setVerificationToken] = useState('');
const [backupKeyIssued, setBackupKeyIssued] = useState(false); const [backupKeyIssued, setBackupKeyIssued] = useState(false);
const [errors, setErrors] = useState<Errors>({});
const router = useRouter(); const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]); const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
@@ -70,12 +80,11 @@ export default function SignupInvite() {
} else { } else {
setLastNameError(false); setLastNameError(false);
} }
errorCheck = passwordCheck({
errorCheck = checkPassword({
password, password,
setPasswordErrorLength, commonPasswords,
setPasswordErrorNumber, setErrors
setPasswordErrorLowerCase,
errorCheck
}); });
if (!errorCheck) { if (!errorCheck) {
@@ -252,60 +261,43 @@ export default function SignupInvite() {
label="Password" label="Password"
onChangeHandler={(pass) => { onChangeHandler={(pass) => {
setPassword(pass); setPassword(pass);
passwordCheck({ checkPassword({
password: pass, password: pass,
setPasswordErrorLength, commonPasswords,
setPasswordErrorNumber, setErrors
setPasswordErrorLowerCase,
errorCheck: false
}); });
}} }}
type="password" type="password"
value={password} value={password}
isRequired isRequired
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase} error={Object.keys(errors).length > 0}
autoComplete="new-password" autoComplete="new-password"
id="new-password" id="new-password"
/> />
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? ( {Object.keys(errors).length > 0 && (
<div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md"> <div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="text-gray-400 text-sm mb-1">Password should contain at least:</div> <div className="mb-2 text-sm text-gray-400">Password should contain at least:</div>
<div className="flex flex-row justify-start items-center ml-1"> {Object.keys(errors).map((key) => {
{passwordErrorLength ? ( if (errors[key as keyof Errors]) {
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" /> return (
) : ( <div className="ml-1 flex flex-row items-top justify-start" key={key}>
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" /> <div>
)} <FontAwesomeIcon
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}> icon={faXmark}
14 characters className="text-md text-red ml-0.5 mr-2.5"
</div> />
</div>
<p className="text-gray-400 text-sm">
{errors[key as keyof Errors]}
</p>
</div>
);
}
return null;
})}
</div> </div>
<div className="flex flex-row justify-start items-center ml-1"> )}
{passwordErrorLowerCase ? (
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
)}
<div
className={`${passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'} text-sm`}
>
1 lowercase character
</div>
</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorNumber ? (
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
)}
<div className={`${passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
1 number
</div>
</div>
</div>
) : (
<div className="py-2" />
)}
</div> </div>
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg"> <div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
<Button <Button