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

1657
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,6 @@
"posthog-node": "^2.6.0",
"query-string": "^7.1.3",
"rate-limit-mongo": "^2.3.2",
"request-ip": "^3.3.0",
"rimraf": "^3.0.2",
"stripe": "^10.7.0",
"swagger-autogen": "^2.22.0",

View File

@@ -1,18 +1,28 @@
import { Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import jwt from 'jsonwebtoken';
import * as bigintConversion from 'bigint-conversion';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models';
import {
User,
LoginSRPDetail,
TokenVersion
} from '../../models';
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
import { checkUserDevice } from '../../helpers/user';
import {
ACTION_LOGIN,
ACTION_LOGOUT
ACTION_LOGOUT,
AUTH_MODE_JWT
} from '../../variables';
import { BadRequestError } from '../../utils/errors';
import {
BadRequestError,
UnauthorizedRequestError
} from '../../utils/errors';
import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import { getChannelFromUserAgent } from '../../utils/posthog';
import {
getJwtRefreshSecret,
getJwtAuthLifetime,
@@ -23,6 +33,7 @@ import {
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string;
refreshVersion?: number;
}
}
@@ -105,11 +116,15 @@ export const login2 = async (req: Request, res: Response) => {
await checkUserDevice({
user,
ip: req.ip,
ip: req.realIP,
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
res.cookie('jid', tokens.refreshToken, {
@@ -128,7 +143,7 @@ export const login2 = async (req: Request, res: Response) => {
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
ipAddress: req.realIP
});
// return (access) token in response
@@ -155,9 +170,9 @@ export const login2 = async (req: Request, res: Response) => {
* @returns
*/
export const logout = async (req: Request, res: Response) => {
await clearTokens({
userId: req.user._id.toString()
});
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', '', {
@@ -176,7 +191,7 @@ export const logout = async (req: Request, res: Response) => {
userId: req.user._id,
actions: [logoutAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
ipAddress: req.realIP
});
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
* @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 res
* @returns
@@ -206,7 +245,7 @@ export const getNewToken = async (req: Request, res: Response) => {
const refreshToken = req.cookies.jid;
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>(
@@ -215,15 +254,27 @@ export const getNewToken = async (req: Request, res: Response) => {
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
}).select('+publicKey +refreshVersion +accessVersion');
if (!user) throw new Error('Failed to authenticate unfound user');
if (!user?.publicKey)
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({
payload: {
userId: decodedToken.userId
userId: decodedToken.userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.refreshVersion
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret()

View File

@@ -6,8 +6,10 @@ import { createToken } from '../../helpers/auth';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services';
import { EELicenseService } from '../../ee/services';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
import { validateUserEmail } from '../../validation';
/**
* Delete organization membership with id [membershipOrgId] from organization
@@ -96,6 +98,19 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
if (!membershipOrg) {
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({
email: inviteeEmail
@@ -134,6 +149,9 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
if (!inviteeMembershipOrg) {
// case: invitee has never been invited before
// validate that email is not disposable
validateUserEmail(inviteeEmail);
await new MembershipOrg({
inviteEmail: inviteeEmail,

View File

@@ -3,12 +3,23 @@ import { Request, Response } from 'express';
const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion';
import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
import { createToken } from '../../helpers/auth';
import { sendMail } from '../../helpers/nodemailer';
import {
createToken,
sendMail,
clearTokens
} from '../../helpers';
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 { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config';
import {
getSiteURL,
getJwtSignupLifetime,
getJwtSignupSecret,
getHttpsEnabled
} from '../../config';
/**
* Password reset step 1: Send email verification link to email [email]
@@ -18,14 +29,15 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../conf
* @returns
*/
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');
if (!user || !user?.publicKey) {
// case: user has already completed account
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({
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) => {
// return salt, serverPublicKey as part of first step of SRP protocol
const { clientPublicKey } = req.body;
const user = await User.findOne({
email: req.user.email
@@ -127,7 +140,8 @@ export const srp1 = async (req: Request, res: Response) => {
});
}
);
};
}
/**
* Change account SRP authentication information for user
@@ -193,6 +207,19 @@ export const changePassword = async (req: Request, res: Response) => {
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({
message: 'Successfully changed password'

View File

@@ -100,50 +100,52 @@ export const pushSecrets = async (req: Request, res: Response) => {
* @returns
*/
export const pullSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
let secrets;
let key;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
let secrets = await pull({
userId: req.user._id.toString(),
workspaceId,
environment,
channel: channel ? channel : 'cli',
ipAddress: req.ip
});
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
const key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.populate('sender', '+publicKey');
if (channel !== 'cli') {
// FIX: Fix this any
secrets = reformatPullSecrets({ secrets }) as any;
}
secrets = await pull({
userId: req.user._id.toString(),
workspaceId,
environment,
channel: channel ? channel : 'cli',
ipAddress: req.realIP
});
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'
}
});
}
key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.populate('sender', '+publicKey');
if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets });
}
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({
secrets,
@@ -160,48 +162,51 @@ export const pullSecrets = async (req: Request, res: Response) => {
* @returns
*/
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
let secrets;
let key;
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
const secrets = await pull({
userId: req.serviceToken.user._id.toString(),
workspaceId,
environment,
channel: 'cli',
ipAddress: req.ip
});
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
const key = {
encryptedKey: req.serviceToken.encryptedKey,
nonce: req.serviceToken.nonce,
sender: {
publicKey: req.serviceToken.publicKey
},
receiver: req.serviceToken.user,
workspace: req.serviceToken.workspace
};
secrets = await pull({
userId: req.serviceToken.user._id.toString(),
workspaceId,
environment,
channel: 'cli',
ipAddress: req.realIP
});
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'
}
});
}
key = {
encryptedKey: req.serviceToken.encryptedKey,
nonce: req.serviceToken.nonce,
sender: {
publicKey: req.serviceToken.publicKey
},
receiver: req.serviceToken.user,
workspace: req.serviceToken.workspace
};
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({
secrets: reformatPullSecrets({ secrets }),

View File

@@ -7,6 +7,7 @@ import {
import { createToken } from '../../helpers/auth';
import { BadRequestError } from '../../utils/errors';
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
@@ -16,7 +17,11 @@ import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpC
* @returns
*/
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');
if (user && user?.publicKey) {

View File

@@ -13,6 +13,7 @@ import {
createWorkspace as create,
deleteWorkspace as deleteWork,
} from "../../helpers/workspace";
import { EELicenseService } from '../../ee/services';
import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables";
@@ -115,6 +116,18 @@ export const createWorkspace = async (req: Request, res: Response) => {
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) {
throw new Error("Workspace names must be at least 1-character long");
}

View File

@@ -83,7 +83,7 @@ export const login2 = async (req: Request, res: Response) => {
const { email, clientProof } = req.body;
const user = await User.findOne({
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');
@@ -105,7 +105,6 @@ export const login2 = async (req: Request, res: Response) => {
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
@@ -141,12 +140,16 @@ export const login2 = async (req: Request, res: Response) => {
await checkUserDevice({
user,
ip: req.ip,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// 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
res.cookie('jid', tokens.refreshToken, {
@@ -156,7 +159,7 @@ export const login2 = async (req: Request, res: Response) => {
secure: await getHttpsEnabled()
});
// case: user does not have MFA enablgged
// case: user does not have MFA enabled
// return (access) token in response
interface ResponseData {
@@ -267,12 +270,16 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
await checkUserDevice({
user,
ip: req.ip,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// 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
res.cookie('jid', tokens.refreshToken, {
@@ -330,7 +337,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
ipAddress: req.realIP
});
return res.status(200).send(resObj);

View File

@@ -297,7 +297,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
actions,
channel,
ipAddress: req.ip,
ipAddress: req.realIP,
});
}
@@ -563,7 +563,7 @@ export const createSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
actions: [addAction],
channel,
ipAddress: req.ip,
ipAddress: req.realIP,
}));
// (EE) take a secret snapshot
@@ -785,7 +785,7 @@ export const getSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId as string),
actions: [readAction],
channel,
ipAddress: req.ip,
ipAddress: req.realIP,
}));
const postHogClient = await TelemetryService.getPostHogClient();
@@ -1020,7 +1020,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(key),
actions: [updateAction],
channel,
ipAddress: req.ip,
ipAddress: req.realIP,
}));
// (EE) take a secret snapshot
@@ -1158,7 +1158,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(key),
actions: [deleteAction],
channel,
ipAddress: req.ip,
ipAddress: req.realIP,
}));
// (EE) take a secret snapshot

View File

@@ -114,7 +114,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id.toString()
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
token = tokens.token;
@@ -237,7 +239,9 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id.toString()
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
token = tokens.token;

View File

@@ -68,7 +68,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
environment,
secrets,
channel: channel ? channel : 'cli',
ipAddress: req.ip
ipAddress: req.realIP
});
await pushKeys({
@@ -111,6 +111,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
* @returns
*/
export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment 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');
}
let secrets = await pull({
secrets = await pull({
userId,
workspaceId,
environment,
channel: channel ? channel : 'cli',
ipAddress: req.ip
ipAddress: req.realIP
});
if (channel !== 'cli') {
// FIX: Fix this any
secrets = reformatPullSecrets({ secrets }) as any;
secrets = reformatPullSecrets({ secrets });
}
if (postHogClient) {

View File

@@ -113,7 +113,7 @@ export const login2 = async (req: Request, res: Response) => {
const user = await User.findOne({
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');
@@ -179,12 +179,16 @@ export const login2 = async (req: Request, res: Response) => {
await checkUserDevice({
user,
ip: req.ip,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// 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
res.cookie('jid', tokens.refreshToken, {
@@ -239,7 +243,7 @@ export const login2 = async (req: Request, res: Response) => {
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
ipAddress: req.realIP
});
return res.status(200).send(response);

View File

@@ -137,7 +137,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id.toString()
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
token = tokens.token;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,9 @@ import {
User,
ServiceTokenData,
ServiceAccount,
APIKeyData
APIKeyData,
TokenVersion,
ITokenVersion
} from '../models';
import {
AccountNotFoundError,
@@ -35,7 +37,7 @@ import {
* @param {Object} obj
* @param {Object} obj.headers - HTTP request headers object
*/
const validateAuthMode = ({
export const validateAuthMode = ({
headers,
acceptedAuthModes
}: {
@@ -97,7 +99,7 @@ const validateAuthMode = ({
* @param {String} obj.authTokenValue - JWT token value
* @returns {User} user - user corresponding to JWT token
*/
const getAuthUserPayload = async ({
export const getAuthUserPayload = async ({
authTokenValue
}: {
authTokenValue: string;
@@ -107,14 +109,32 @@ const getAuthUserPayload = async ({
);
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
_id: new Types.ObjectId(decodedToken.userId)
}).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
* @returns {ServiceTokenData} serviceTokenData - service token data
*/
const getAuthSTDPayload = async ({
export const getAuthSTDPayload = async ({
authTokenValue
}: {
authTokenValue: string;
@@ -169,7 +189,7 @@ const getAuthSTDPayload = async ({
* @param {String} obj.authTokenValue - service account access token value
* @returns {ServiceAccount} serviceAccount
*/
const getAuthSAAKPayload = async ({
export const getAuthSAAKPayload = async ({
authTokenValue
}: {
authTokenValue: string;
@@ -198,7 +218,7 @@ const getAuthSAAKPayload = async ({
* @param {String} obj.authTokenValue - API key value
* @returns {APIKeyData} apiKeyData - API key data
*/
const getAuthAPIKeyPayload = async ({
export const getAuthAPIKeyPayload = async ({
authTokenValue
}: {
authTokenValue: string;
@@ -255,12 +275,43 @@ const getAuthAPIKeyPayload = async ({
* @return {String} obj.token - issued JWT 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
const token = createToken({
payload: {
userId
userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.accessVersion
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret()
@@ -268,7 +319,9 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
const refreshToken = createToken({
payload: {
userId
userId,
tokenVersionId: tokenVersion._id.toString(),
refreshVersion: tokenVersion.refreshVersion
},
expiresIn: await getJwtRefreshLifetime(),
secret: await getJwtRefreshSecret()
@@ -285,13 +338,15 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
* @param {Object} obj
* @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
User.findOneAndUpdate({
_id: userId
await TokenVersion.findOneAndUpdate({
_id: tokenVersionId
}, {
$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.expiresIn - string describing time span such as '10h' or '7d'
*/
const createToken = ({
export const createToken = ({
payload,
expiresIn,
secret
@@ -318,7 +373,7 @@ const createToken = ({
});
};
const validateProviderAuthToken = async ({
export const validateProviderAuthToken = async ({
email,
user,
providerAuthToken,
@@ -341,16 +396,4 @@ const validateProviderAuthToken = async ({
) {
throw new Error('Invalid authentication credentials.')
}
}
export {
validateAuthMode,
validateProviderAuthToken,
getAuthUserPayload,
getAuthSTDPayload,
getAuthSAAKPayload,
getAuthAPIKeyPayload,
createToken,
issueAuthTokens,
clearTokens
};
}

View File

@@ -31,7 +31,7 @@ import { InternalServerError } from "../utils/errors";
* @param {String} obj.name - name of bot
* @param {String} obj.workspaceId - id of workspace that bot belongs to
*/
const createBot = async ({
export const createBot = async ({
name,
workspaceId,
}: {
@@ -93,7 +93,7 @@ const createBot = async ({
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment
*/
const getSecretsHelper = async ({
export const getSecretsBotHelper = async ({
workspaceId,
environment,
}: {
@@ -136,7 +136,7 @@ const getSecretsHelper = async ({
* @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key
*/
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
export const getKey = async ({ workspaceId }: { workspaceId: string }) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
@@ -194,7 +194,7 @@ const getKey = async ({ workspaceId }: { workspaceId: string }) => {
* @param {String} obj1.workspaceId - id of workspace
* @param {String} obj1.plaintext - plaintext to encrypt
*/
const encryptSymmetricHelper = async ({
export const encryptSymmetricHelper = async ({
workspaceId,
plaintext,
}: {
@@ -222,7 +222,7 @@ const encryptSymmetricHelper = async ({
* @param {String} obj.iv - iv
* @param {String} obj.tag - tag
*/
const decryptSymmetricHelper = async ({
export const decryptSymmetricHelper = async ({
workspaceId,
ciphertext,
iv,
@@ -242,11 +242,4 @@ const decryptSymmetricHelper = async ({
});
return plaintext;
};
export {
createBot,
getSecretsHelper,
encryptSymmetricHelper,
decryptSymmetricHelper
};
};

View File

@@ -7,7 +7,7 @@ import { getLogger } from '../utils/logger';
* @param {String} obj.mongoURL - mongo connection string
* @returns
*/
const initDatabaseHelper = async ({
export const initDatabaseHelper = async ({
mongoURL
}: {
mongoURL: string;
@@ -30,7 +30,7 @@ const initDatabaseHelper = async ({
/**
* Close database conection
*/
const closeDatabaseHelper = async () => {
export const closeDatabaseHelper = async () => {
return Promise.all([
new Promise((resolve) => {
if (mongoose.connection && mongoose.connection.readyState == 1) {
@@ -41,9 +41,4 @@ const closeDatabaseHelper = async () => {
}
})
]);
}
export {
initDatabaseHelper,
closeDatabaseHelper
}

View File

@@ -18,7 +18,7 @@ interface Event {
* @param {String} obj.event.workspaceId - id of workspace that event is part of
* @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;
// TODO: moduralize bot check into separate function
@@ -37,6 +37,4 @@ const handleEventHelper = async ({ event }: { event: Event }) => {
});
break;
}
};
export { handleEventHelper };
};

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';

View File

@@ -15,7 +15,6 @@ import {
import {
UnauthorizedRequestError,
} from '../utils/errors';
import RequestError from '../utils/requestError';
interface Update {
workspace: string;
@@ -36,7 +35,7 @@ interface Update {
* @param {String} obj.code - code
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/
const handleOAuthExchangeHelper = async ({
export const handleOAuthExchangeHelper = async ({
workspaceId,
integration,
code,
@@ -110,7 +109,7 @@ const handleOAuthExchangeHelper = async ({
* @param {Object} obj
* @param {Object} obj.workspaceId - id of workspace
*/
const syncIntegrationsHelper = async ({
export const syncIntegrationsHelper = async ({
workspaceId,
environment
}: {
@@ -162,7 +161,7 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth
* @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
.findById(integrationAuthId)
.select('+refreshCiphertext +refreshIV +refreshTag');
@@ -187,7 +186,7 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token
*/
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let accessId;
let accessToken;
const integrationAuth = await IntegrationAuth
@@ -240,7 +239,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.refreshToken - refresh token
*/
const setIntegrationAuthRefreshHelper = async ({
export const setIntegrationAuthRefreshHelper = async ({
integrationAuthId,
refreshToken
}: {
@@ -282,7 +281,7 @@ const setIntegrationAuthRefreshHelper = async ({
* @param {String} obj.accessToken - access token
* @param {Date} obj.accessExpiresAt - expiration date of access token
*/
const setIntegrationAuthAccessHelper = async ({
export const setIntegrationAuthAccessHelper = async ({
integrationAuthId,
accessId,
accessToken,
@@ -327,13 +326,4 @@ const setIntegrationAuthAccessHelper = async ({
});
return integrationAuth;
}
export {
handleOAuthExchangeHelper,
syncIntegrationsHelper,
getIntegrationAuthRefreshHelper,
getIntegrationAuthAccessHelper,
setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper
}
}

View File

@@ -17,7 +17,7 @@ interface Key {
* @param {String} obj.keys.nonce - nonce for encryption
* @param {String} obj.keys.userId - id of receiver user
*/
const pushKeys = async ({
export const pushKeys = async ({
userId,
workspaceId,
keys
@@ -50,6 +50,4 @@ const pushKeys = async ({
workspace: workspaceId
}))
);
};
export { pushKeys };
};

View File

@@ -10,10 +10,10 @@ import { MembershipNotFoundError, BadRequestError } from "../utils/errors";
* @param {String} obj.workspaceId - id of workspace
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
*/
const validateMembership = async ({
userId,
workspaceId,
acceptedRoles,
export const validateMembership = async ({
userId,
workspaceId,
acceptedRoles,
}: {
userId: Types.ObjectId | string;
workspaceId: Types.ObjectId | string;
@@ -46,8 +46,8 @@ const validateMembership = async ({
* @param {Object} queryObj - query object
* @return {Object} membership - membership
*/
const findMembership = async (queryObj: any) => {
const membership = await Membership.findOne(queryObj);
export const findMembership = async (queryObj: any) => {
const membership = await Membership.findOne(queryObj);
return membership;
};
@@ -59,10 +59,10 @@ const findMembership = async (queryObj: any) => {
* @param {String} obj.workspaceId - id of workspace.
* @param {String[]} obj.roles - roles of users.
*/
const addMemberships = async ({
userIds,
workspaceId,
roles,
export const addMemberships = async ({
userIds,
workspaceId,
roles
}: {
userIds: string[];
workspaceId: string;
@@ -93,9 +93,9 @@ const addMemberships = async ({
* @param {Object} obj
* @param {String} obj.membershipId - id of membership to delete
*/
const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
const deletedMembership = await Membership.findOneAndDelete({
_id: membershipId,
export const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
const deletedMembership = await Membership.findOneAndDelete({
_id: membershipId
});
// 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 };

View File

@@ -18,7 +18,7 @@ import {
* @param {Types.ObjectId} obj.organizationId
* @param {String[]} obj.acceptedRoles
*/
const validateMembershipOrg = async ({
export const validateMembershipOrg = async ({
userId,
organizationId,
acceptedRoles,
@@ -59,7 +59,7 @@ const validateMembershipOrg = async ({
* @param {Object} queryObj - query object
* @return {Object} membershipOrg - membership
*/
const findMembershipOrg = (queryObj: any) => {
export const findMembershipOrg = (queryObj: any) => {
const membershipOrg = MembershipOrg.findOne(queryObj);
return membershipOrg;
};
@@ -72,7 +72,7 @@ const findMembershipOrg = (queryObj: any) => {
* @param {String} obj.organizationId - id of organization.
* @param {String[]} obj.roles - roles of users.
*/
const addMembershipsOrg = async ({
export const addMembershipsOrg = async ({
userIds,
organizationId,
roles,
@@ -111,7 +111,7 @@ const addMembershipsOrg = async ({
* @param {Object} obj
* @param {String} obj.membershipOrgId - id of organization membership to delete
*/
const deleteMembershipOrg = async ({
export const deleteMembershipOrg = async ({
membershipOrgId
}: {
membershipOrgId: string;
@@ -148,11 +148,4 @@ const deleteMembershipOrg = async ({
}
return deletedMembershipOrg;
};
export {
validateMembershipOrg,
findMembershipOrg,
addMembershipsOrg,
deleteMembershipOrg
};
};

View File

@@ -13,7 +13,7 @@ let smtpTransporter: nodemailer.Transporter;
* @param {String[]} obj.recipients - email addresses of people to send email to
* @param {Object} obj.substitutions - object containing template substitutions
*/
const sendMail = async ({
export const sendMail = async ({
template,
subjectLine,
recipients,
@@ -41,8 +41,6 @@ const sendMail = async ({
}
};
const setTransporter = (transporter: nodemailer.Transporter) => {
export const setTransporter = (transporter: nodemailer.Transporter) => {
smtpTransporter = transporter;
};
export { sendMail, setTransporter };
};

View File

@@ -28,7 +28,7 @@ import {
* @param {String} obj.email - POC email that will receive invoice info
* @param {Object} organization - new organization
*/
const createOrganization = async ({
export const createOrganization = async ({
name,
email,
}: {
@@ -70,7 +70,7 @@ const createOrganization = async ({
* @return {Object} obj.stripeSubscription - new stripe subscription
* @return {Subscription} obj.subscription - new subscription
*/
const initSubscriptionOrg = async ({
export const initSubscriptionOrg = async ({
organizationId,
}: {
organizationId: Types.ObjectId;
@@ -125,7 +125,7 @@ const initSubscriptionOrg = async ({
* @param {Object} obj
* @param {Number} obj.organizationId - id of subscription's organization
*/
const updateSubscriptionOrgQuantity = async ({
export const updateSubscriptionOrgQuantity = async ({
organizationId,
}: {
organizationId: string;
@@ -171,10 +171,4 @@ const updateSubscriptionOrgQuantity = async ({
}
return stripeSubscription;
};
export {
createOrganization,
initSubscriptionOrg,
updateSubscriptionOrgQuantity
};
};

View File

@@ -2,7 +2,7 @@ import rateLimit from 'express-rate-limit';
const MongoStore = require('rate-limit-mongo');
// 200 per minute
const apiLimiter = rateLimit({
export const apiLimiter = rateLimit({
store: new MongoStore({
uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60,
@@ -17,7 +17,7 @@ const apiLimiter = rateLimit({
return request.path === '/healthcheck' || request.path === '/api/status'
},
keyGenerator: (req, res) => {
return req.clientIp
return req.realIP
}
});
@@ -34,12 +34,12 @@ const authLimit = rateLimit({
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => {
return req.clientIp
return req.realIP
}
});
// 50 requests per 1 hour
const passwordLimiter = rateLimit({
export const passwordLimiter = rateLimit({
store: new MongoStore({
uri: process.env.MONGO_URL,
expireTimeMs: 1000 * 60 * 60,
@@ -51,20 +51,14 @@ const passwordLimiter = rateLimit({
standardHeaders: true,
legacyHeaders: false,
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') {
authLimit(req, res, next);
} else {
next();
}
};
export {
apiLimiter,
authLimiter,
passwordLimiter
};
};

View File

@@ -60,7 +60,7 @@ interface Update {
* @param {String} obj.environment - environment for secrets
* @param {Object[]} obj.secrets - secrets to push
*/
const v1PushSecrets = async ({
export const v1PushSecrets = async ({
userId,
workspaceId,
environment,
@@ -304,7 +304,7 @@ const v1PushSecrets = async ({
* @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets
*/
const v2PushSecrets = async ({
export const v2PushSecrets = async ({
userId,
workspaceId,
environment,
@@ -530,7 +530,7 @@ const v2PushSecrets = async ({
* @param {String} obj.workspaceId - id of workspace to pull from
* @param {String} obj.environment - environment for secrets
*/
const getSecrets = async ({
export const getSecrets = async ({
userId,
workspaceId,
environment,
@@ -570,7 +570,7 @@ const getSecrets = async ({
* @param {String} obj.channel - channel (web/cli/auto)
* @param {String} obj.ipAddress - ip address of request to push secrets
*/
const pullSecrets = async ({
export const pullSecrets = async ({
userId,
workspaceId,
environment,
@@ -614,7 +614,7 @@ const pullSecrets = async ({
* @param {Object} obj
* @param {Object} obj.secrets
*/
const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
export const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
const reformatedSecrets = secrets.map((s) => ({
_id: s._id,
workspace: s.workspace,
@@ -644,6 +644,4 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
}));
return reformatedSecrets;
};
export { v1PushSecrets, v2PushSecrets, pullSecrets, reformatPullSecrets };
};

View File

@@ -45,8 +45,8 @@ import {
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId
*/
const createSecretBlindIndexDataHelper = async ({
workspaceId,
export const createSecretBlindIndexDataHelper = async ({
workspaceId
}: {
workspaceId: Types.ObjectId;
}) => {
@@ -98,8 +98,8 @@ const createSecretBlindIndexDataHelper = async ({
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
* @returns
*/
const getSecretBlindIndexSaltHelper = async ({
workspaceId,
export const getSecretBlindIndexSaltHelper = async ({
workspaceId
}: {
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.salt - base64-salt
*/
const generateSecretBlindIndexWithSaltHelper = async ({
secretName,
salt,
export const generateSecretBlindIndexWithSaltHelper = async ({
secretName,
salt
}: {
secretName: string;
salt: string;
@@ -177,9 +177,9 @@ const generateSecretBlindIndexWithSaltHelper = async ({
* @param {Stringj} obj.secretName - name of secret to generate blind index for
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
*/
const generateSecretBlindIndexHelper = async ({
secretName,
workspaceId,
export const generateSecretBlindIndexHelper = async ({
secretName,
workspaceId
}: {
secretName: string;
workspaceId: Types.ObjectId;
@@ -247,7 +247,7 @@ const generateSecretBlindIndexHelper = async ({
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const createSecretHelper = async ({
export const createSecretHelper = async ({
secretName,
workspaceId,
environment,
@@ -397,10 +397,10 @@ const createSecretHelper = async ({
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const getSecretsHelper = async ({
workspaceId,
environment,
authData,
export const getSecretsHelper = async ({
workspaceId,
environment,
authData
}: GetSecretsParams) => {
let secrets: ISecret[] = [];
@@ -472,7 +472,7 @@ const getSecretsHelper = async ({
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const getSecretHelper = async ({
export const getSecretHelper = async ({
secretName,
workspaceId,
environment,
@@ -558,7 +558,8 @@ const getSecretHelper = async ({
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const updateSecretHelper = async ({
export const updateSecretHelper = async ({
secretName,
workspaceId,
environment,
@@ -698,7 +699,7 @@ const updateSecretHelper = async ({
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const deleteSecretHelper = async ({
export const deleteSecretHelper = async ({
secretName,
workspaceId,
environment,
@@ -799,16 +800,4 @@ const deleteSecretHelper = async ({
secrets,
secret,
};
};
export {
createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper,
generateSecretBlindIndexHelper,
createSecretHelper,
getSecretsHelper,
getSecretHelper,
updateSecretHelper,
deleteSecretHelper,
};
};

View File

@@ -13,11 +13,11 @@ import { TOKEN_EMAIL_CONFIRMATION } from '../variables';
* @param {String} obj.email - email
* @returns {Boolean} success - whether or not operation was successful
*/
const sendEmailVerification = async ({ email }: { email: string }) => {
const token = await TokenService.createToken({
type: TOKEN_EMAIL_CONFIRMATION,
email
});
export const sendEmailVerification = async ({ email }: { email: string }) => {
const token = await TokenService.createToken({
type: TOKEN_EMAIL_CONFIRMATION,
email
});
// send mail
await sendMail({
@@ -36,7 +36,7 @@ const sendEmailVerification = async ({ email }: { email: string }) => {
* @param {String} obj.email - emai
* @param {String} obj.code - code that was sent to [email]
*/
const checkEmailVerification = async ({
export const checkEmailVerification = async ({
email,
code
}: {
@@ -57,7 +57,7 @@ const checkEmailVerification = async ({
* @param {String} obj.organizationName - name of organization to initialize
* @param {IUser} obj.user - user who we are initializing for
*/
const initializeDefaultOrg = async ({
export const initializeDefaultOrg = async ({
organizationName,
user
}: {
@@ -81,6 +81,4 @@ const initializeDefaultOrg = async ({
} catch (err) {
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
}
};
export { sendEmailVerification, checkEmailVerification, initializeDefaultOrg };
};

View File

@@ -20,7 +20,7 @@ import { getSaltRounds } from "../config";
* @param {Types.ObjectId} obj.organizationId
* @returns {String} token - the created token
*/
const createTokenHelper = async ({
export const createTokenHelper = async ({
type,
email,
phoneNumber,
@@ -121,7 +121,7 @@ const createTokenHelper = async ({
* @param {String} obj.email - email associated with the token
* @param {String} obj.token - value of the token
*/
const validateTokenHelper = async ({
export const validateTokenHelper = async ({
type,
email,
phoneNumber,
@@ -212,6 +212,4 @@ const validateTokenHelper = async ({
// case: token is valid
await TokenData.findByIdAndDelete(tokenData._id);
};
export { createTokenHelper, validateTokenHelper };
};

View File

@@ -15,7 +15,7 @@ import { SecretService } from '../services';
* @param {String} organizationId - id of organization to create workspace in
* @param {Object} workspace - new workspace
*/
const createWorkspace = async ({
export const createWorkspace = async ({
name,
organizationId
}: {
@@ -50,23 +50,18 @@ const createWorkspace = async ({
* @param {Object} obj
* @param {String} obj.id - id of workspace to delete
*/
const deleteWorkspace = async ({ id }: { id: string }) => {
await Workspace.deleteOne({ _id: id });
await Bot.deleteOne({
workspace: id
});
await Membership.deleteMany({
workspace: id
});
await Secret.deleteMany({
workspace: id
});
await Key.deleteMany({
workspace: id
});
};
export {
createWorkspace,
deleteWorkspace
export const deleteWorkspace = async ({ id }: { id: string }) => {
await Workspace.deleteOne({ _id: id });
await Bot.deleteOne({
workspace: id
});
await Membership.deleteMany({
workspace: id
});
await Secret.deleteMany({
workspace: id
});
await Key.deleteMany({
workspace: id
});
};

View File

@@ -13,7 +13,6 @@ import swaggerUi = require("swagger-ui-express");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const swaggerFile = require("../spec.json");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const requestIp = require("request-ip");
import { apiLimiter } from "./helpers/rateLimiter";
import {
workspace as eeWorkspaceRouter,
@@ -86,8 +85,6 @@ const main = async () => {
})
);
app.use(requestIp.mw());
if ((await getNodeEnv()) === "production") {
// enable app-wide rate-limiting + helmet security
// in production
@@ -96,6 +93,13 @@ const main = async () => {
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
app.use("/api/v1/secret", eeSecretRouter);
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);

View File

@@ -1,3 +1,4 @@
import { Types } from 'mongoose';
import {
IUser,
IServiceAccount,
@@ -10,4 +11,5 @@ export interface AuthData {
authChannel: string;
authIP: string;
authUserAgent: string;
tokenVersionId?: Types.ObjectId;
}

View File

@@ -71,10 +71,12 @@ const requireAuth = ({
req.user = authPayload;
break;
default:
authPayload = await getAuthUserPayload({
const { user, tokenVersionId } = await getAuthUserPayload({
authTokenValue
});
req.user = authPayload;
authPayload = user;
req.user = user;
req.tokenVersionId = tokenVersionId;
break;
}
@@ -88,8 +90,9 @@ const requireAuth = ({
authMode,
authPayload, // User, ServiceAccount, ServiceTokenData
authChannel: getChannelFromUserAgent(req.headers['user-agent']),
authIP: req.ip,
authUserAgent: req.headers['user-agent'] ?? 'other'
authIP: req.realIP,
authUserAgent: req.headers['user-agent'] ?? 'other',
tokenVersionId: req.tokenVersionId
}
return next();

View File

@@ -22,6 +22,7 @@ import Workspace, { IWorkspace } from './workspace';
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
import APIKeyData, { IAPIKeyData } from './apiKeyData';
import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail';
import TokenVersion, { ITokenVersion } from './tokenVersion';
export {
AuthProvider,
@@ -72,5 +73,7 @@ export {
APIKeyData,
IAPIKeyData,
LoginSRPDetail,
ILoginSRPDetail
ILoginSRPDetail,
TokenVersion,
ITokenVersion
};

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;

View File

@@ -21,7 +21,6 @@ export interface IUser extends Document {
tag?: string;
salt?: string;
verifier?: string;
refreshVersion?: number;
isMfaEnabled: boolean;
mfaMethods: boolean;
devices: {
@@ -91,11 +90,6 @@ const userSchema = new Schema<IUser>(
type: String,
select: false
},
refreshVersion: {
type: Number,
default: 0,
select: false
},
isMfaEnabled: {
type: Boolean,
default: false
@@ -108,7 +102,8 @@ const userSchema = new Schema<IUser>(
ip: String,
userAgent: String
}],
default: []
default: [],
select: false
}
},
{

View File

@@ -44,8 +44,6 @@ router.post(
authController.checkAuth
);
router.get(
'/redirect/google',
authLimiter,
@@ -53,12 +51,27 @@ router.get(
scope: ['profile', 'email'],
session: false,
}),
)
);
router.get(
'/callback/google',
passport.authenticate('google', { failureRedirect: '/login/provider/error', session: false }),
authController.handleAuthProviderCallback,
)
);
router.get(
'/common-passwords',
authLimiter,
authController.getCommonPasswords
);
router.delete(
'/sessions',
authLimiter,
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
authController.revokeAllSessions
);
export default router;

View File

@@ -26,7 +26,7 @@ router.post(
router.post(
'/mfa/send',
authLimiter,
body('email').isString().trim().notEmpty(),
body('email').isString().trim().notEmpty().isEmail(),
validateRequest,
authController.sendMfaToken
);

View File

@@ -1,6 +1,6 @@
import { Types } from 'mongoose';
import {
getSecretsHelper,
getSecretsBotHelper,
encryptSymmetricHelper,
decryptSymmetricHelper
} from '../helpers/bot';
@@ -25,7 +25,7 @@ class BotService {
workspaceId: Types.ObjectId;
environment: string;
}) {
return await getSecretsHelper({
return await getSecretsBotHelper({
workspaceId,
environment
});

View File

@@ -1,4 +1,5 @@
import * as express from 'express';
import { Types } from 'mongoose';
import {
IUser,
IServiceAccount,
@@ -39,7 +40,9 @@ declare global {
serviceTokenData: any;
apiKeyData: any;
query?: any;
tokenVersionId?: Types.ObjectId;
authData: AuthData;
realIP: string;
requestData: {
[key: string]: string
};

View File

@@ -1,3 +1,4 @@
export * from './user';
export * from './workspace';
export * from './bot';
export * from './integration';

View File

@@ -1,3 +1,5 @@
import fs from 'fs';
import path from 'path';
import { Types } from 'mongoose';
import {
IUser,
@@ -8,7 +10,7 @@ import {
} from '../models';
import { validateMembership } from '../helpers/membership';
import _ from 'lodash';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../utils/errors';
import {
validateMembershipOrg
} from '../helpers/membershipOrg';
@@ -17,6 +19,22 @@ import {
PERMISSION_WRITE_SECRETS
} 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
* with id [workspaceId] and its environment [environment] with required permissions

View File

@@ -2,18 +2,19 @@ import crypto from 'crypto';
import React, { useState } from 'react';
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 jsrp from 'jsrp';
import nacl from 'tweetnacl';
import { encodeBase64 } from 'tweetnacl-util';
import { useGetCommonPasswords } from '@app/hooks/api';
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import ProjectService from '@app/services/ProjectService';
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 { deriveArgonKey } from '../utilities/cryptography/crypto';
import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage';
@@ -37,6 +38,15 @@ interface UserInfoStepProps {
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
* @param {object} obj
@@ -63,11 +73,11 @@ export default function UserInfoStep({
setAttributionSource,
providerAuthToken,
}: UserInfoStepProps): JSX.Element {
const { data: commonPasswords } = useGetCommonPasswords();
const [nameError, setNameError] = useState(false);
const [organizationNameError, setOrganizationNameError] = useState(false);
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const [errors, setErrors] = useState<Errors>({});
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
@@ -89,12 +99,11 @@ export default function UserInfoStep({
} else {
setOrganizationNameError(false);
}
errorCheck = passwordCheck({
errorCheck = checkPassword({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck
commonPasswords,
setErrors
});
if (!errorCheck) {
@@ -248,59 +257,45 @@ export default function UserInfoStep({
label={t('section.password.password')}
onChangeHandler={(pass: string) => {
setPassword(pass);
passwordCheck({
checkPassword({
password: pass,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false
commonPasswords,
setErrors
});
}}
type="password"
value={password}
isRequired
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
error={Object.keys(errors).length > 0}
autoComplete="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="mb-1 text-sm text-gray-400">{t('section.password.validate-base')}</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLength ? (
<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={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
{t('section.password.validate-length')}
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLowerCase ? (
<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={`${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={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 className="mb-2 text-sm text-gray-400">{t('section.password.validate-base')}</div>
{Object.keys(errors).map((key) => {
if (errors[key as keyof Errors]) {
return (
<div
className="ml-1 flex flex-row items-top justify-start"
key={key}
>
<div>
<FontAwesomeIcon
icon={faXmark}
className="text-md text-red ml-0.5 mr-2.5"
/>
</div>
<p className="text-gray-400 text-sm">
{errors[key as keyof Errors]}
</p>
</div>
);
}
return null;
})}
</div>
) : (
<div className="py-2" />
)}
</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">

View File

@@ -17,6 +17,7 @@ const passwordCheck = ({
setPasswordErrorLowerCase,
errorCheck
}: PasswordCheckProps) => {
if (!password || password.length < 14) {
setPasswordErrorLength(true);
errorCheck = true;

View File

@@ -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;

View File

@@ -125,6 +125,10 @@ const changePassword = async (
setPasswordChanged(true);
setCurrentPassword('');
setNewPassword('');
window.location.href = '/login';
// move to login page
} catch (error) {
setCurrentPasswordError(true);
console.log(error);

View File

@@ -1,4 +1,7 @@
export {
useGetAuthToken,
useSendMfaToken,
useVerifyMfaToken} from './queries'
useVerifyMfaToken,
useRevokeAllSessions,
useGetCommonPasswords
} from './queries'

View File

@@ -10,7 +10,8 @@ import {
VerifyMfaTokenRes} from './types';
const authKeys = {
getAuthToken: ['token'] as const
getAuthToken: ['token'] as const,
commonPasswords: ['common-passwords'] as const
};
export const useSendMfaToken = () => {
@@ -49,3 +50,20 @@ export const useGetAuthToken = () =>
onSuccess: (data) => setAuthToken(data.token),
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 });

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Head from 'next/head';
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 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 ApiKeyTable from '@app/components/basic/table/ApiKeyTable';
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 issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
import {
useGetCommonPasswords,
useRevokeAllSessions} from '@app/hooks/api';
import { SecuritySection } from '@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection';
import AddApiKeyDialog from '../../../components/basic/dialog/AddApiKeyDialog';
import getAPIKeys from '../../api/apiKey/getAPIKeys';
import getUser from '../../api/user/getUser';
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
export default function PersonalSettings() {
const { data: commonPasswords } = useGetCommonPasswords();
const [personalEmail, setPersonalEmail] = 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 [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
@@ -34,6 +44,9 @@ export default function PersonalSettings() {
const [backupKeyError, setBackupKeyError] = useState(false);
const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false);
const [apiKeys, setApiKeys] = useState<any[]>([]);
const [errors, setErrors] = useState<Errors>({});
const revokeAllSessions = useRevokeAllSessions();
const { t, i18n } = useTranslation();
const router = useRouter();
@@ -154,78 +167,54 @@ export default function PersonalSettings() {
label={t('section.password.new') as string}
onChangeHandler={(password) => {
setNewPassword(password);
passwordCheck({
checkPassword({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false
commonPasswords,
setErrors
});
}}
type="password"
value={newPassword}
isRequired
error={passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber}
error={Object.keys(errors).length > 0}
autoComplete="new-password"
id="new-password"
/>
</div>
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
<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="mb-1 text-sm text-gray-400">
{t('section.password.validate-base')}
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLength ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorLength ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
{t('section.password.validate-length')}
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLowerCase ? (
<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>
{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="mb-2 text-sm text-gray-400">{t('section.password.validate-base')}</div>
{Object.keys(errors).map((key) => {
if (errors[key as keyof Errors]) {
return (
<div className="ml-1 flex flex-row items-top justify-start" key={key}>
<div>
<FontAwesomeIcon
icon={faXmark}
className="text-md text-red ml-0.5 mr-2.5"
/>
</div>
<p className="text-gray-400 text-sm">
{errors[key as keyof Errors]}
</p>
</div>
);
}
return null;
})}
</div>
) : (
<div className="py-2" />
)}
<div className="mt-3 flex w-52 flex-row items-center pr-3">
<Button
text={t('section.password.change') as string}
onButtonPressed={() => {
if (!passwordErrorLength && !passwordErrorLowerCase && !passwordErrorNumber) {
const errorCheck = checkPassword({
password: newPassword,
commonPasswords,
setErrors
});
if (!errorCheck) {
changePassword(
personalEmail,
currentPassword,
@@ -239,11 +228,6 @@ export default function PersonalSettings() {
}}
color="mineshaft"
size="md"
active={
newPassword !== '' &&
currentPassword !== '' &&
!(passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber)
}
textDisabled={t('section.password.change') as string}
/>
<FontAwesomeIcon
@@ -254,6 +238,28 @@ export default function PersonalSettings() {
/>
</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="flex w-full max-w-5xl flex-row items-center justify-between">

View File

@@ -7,7 +7,7 @@ import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';
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 jsrp from 'jsrp';
import queryString from 'query-string';
@@ -17,7 +17,9 @@ import { encodeBase64 } from 'tweetnacl-util';
import Button from '@app/components/basic/buttons/Button';
import InputField from '@app/components/basic/InputField';
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 { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
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 getOrganizations from '@app/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
import {
useGetCommonPasswords
} from '@app/hooks/api';
import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite';
import verifySignupInvite from './api/auth/VerifySignupInvite';
// eslint-disable-next-line new-cap
const client = new jsrp.client();
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
export default function SignupInvite() {
const { data: commonPasswords } = useGetCommonPasswords();
const [password, setPassword] = useState('');
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [firstNameError, setFirstNameError] = 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 [step, setStep] = useState(1);
const [backupKeyError, setBackupKeyError] = useState(false);
const [verificationToken, setVerificationToken] = useState('');
const [backupKeyIssued, setBackupKeyIssued] = useState(false);
const [errors, setErrors] = useState<Errors>({});
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
@@ -70,12 +80,11 @@ export default function SignupInvite() {
} else {
setLastNameError(false);
}
errorCheck = passwordCheck({
errorCheck = checkPassword({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck
commonPasswords,
setErrors
});
if (!errorCheck) {
@@ -252,60 +261,43 @@ export default function SignupInvite() {
label="Password"
onChangeHandler={(pass) => {
setPassword(pass);
passwordCheck({
checkPassword({
password: pass,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false
commonPasswords,
setErrors
});
}}
type="password"
value={password}
isRequired
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
error={Object.keys(errors).length > 0}
autoComplete="new-password"
id="new-password"
/>
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
<div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md">
<div className="text-gray-400 text-sm mb-1">Password should contain at least:</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorLength ? (
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
)}
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
14 characters
</div>
{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="mb-2 text-sm text-gray-400">Password should contain at least:</div>
{Object.keys(errors).map((key) => {
if (errors[key as keyof Errors]) {
return (
<div className="ml-1 flex flex-row items-top justify-start" key={key}>
<div>
<FontAwesomeIcon
icon={faXmark}
className="text-md text-red ml-0.5 mr-2.5"
/>
</div>
<p className="text-gray-400 text-sm">
{errors[key as keyof Errors]}
</p>
</div>
);
}
return null;
})}
</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 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