Merge pull request #885 from ragnarbull/ragnarbull-auth-pwd-fixes

Password fixes - enforce max length, add checks (pwd breach, PII, low entropy), improved UX, deprecate common-passwords api
This commit is contained in:
BlackMagiq
2023-08-28 11:33:57 +01:00
committed by GitHub
26 changed files with 1046 additions and 2256 deletions

View File

@@ -1,32 +1,20 @@
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 {
LoginSRPDetail,
TokenVersion,
User,
} from "../../models";
import { LoginSRPDetail, TokenVersion, User } from "../../models";
import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth";
import { checkUserDevice } from "../../helpers/user";
import {
ACTION_LOGIN,
ACTION_LOGOUT,
} from "../../variables";
import {
BadRequestError,
UnauthorizedRequestError,
} from "../../utils/errors";
import { ACTION_LOGIN, ACTION_LOGOUT } from "../../variables";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { EELogService } from "../../ee/services";
import { getUserAgentType } from "../../utils/posthog";
import {
getHttpsEnabled,
getJwtAuthLifetime,
getJwtAuthSecret,
getJwtRefreshSecret,
getJwtRefreshSecret
} from "../../config";
import { ActorType } from "../../ee/models";
@@ -44,13 +32,10 @@ declare module "jsonwebtoken" {
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
const {
email,
clientPublicKey,
}: { email: string; clientPublicKey: string } = req.body;
const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({
email,
email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
@@ -59,21 +44,25 @@ export const login1 = async (req: Request, res: Response) => {
server.init(
{
salt: user.salt,
verifier: user.verifier,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: email }, {
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false })
await LoginSRPDetail.findOneAndReplace(
{ email: email },
{
email: email,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
},
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({
serverPublicKey,
salt: user.salt,
salt: user.salt
});
}
);
@@ -89,15 +78,19 @@ export const login1 = async (req: Request, res: Response) => {
export const login2 = async (req: Request, res: Response) => {
const { email, clientProof } = req.body;
const user = await User.findOne({
email,
email
}).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag");
if (!user) throw new Error("Failed to find user");
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email })
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email });
if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
return BadRequestError(
Error(
"It looks like some details from the first login are not found. Please try login one again"
)
);
}
const server = new jsrp.server();
@@ -105,7 +98,7 @@ export const login2 = async (req: Request, res: Response) => {
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
@@ -117,13 +110,13 @@ export const login2 = async (req: Request, res: Response) => {
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgent: req.headers["user-agent"] ?? ""
});
const tokens = await issueAuthTokens({
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie
@@ -131,20 +124,21 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled(),
secure: await getHttpsEnabled()
});
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP,
});
loginAction &&
(await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP
}));
// return (access) token in response
return res.status(200).send({
@@ -152,12 +146,12 @@ export const login2 = async (req: Request, res: Response) => {
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
tag: user.tag
});
}
return res.status(400).send({
message: "Failed to authenticate. Try again?",
message: "Failed to authenticate. Try again?"
});
}
);
@@ -171,7 +165,7 @@ export const login2 = async (req: Request, res: Response) => {
*/
export const logout = async (req: Request, res: Response) => {
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId)
await clearTokens(req.authData.tokenVersionId);
}
// clear httpOnly cookie
@@ -179,49 +173,44 @@ export const logout = async (req: Request, res: Response) => {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: (await getHttpsEnabled()) as boolean,
secure: (await getHttpsEnabled()) as boolean
});
const logoutAction = await EELogService.createAction({
name: ACTION_LOGOUT,
userId: req.user._id,
userId: req.user._id
});
logoutAction && await EELogService.createLog({
userId: req.user._id,
actions: [logoutAction],
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP,
});
logoutAction &&
(await EELogService.createLog({
userId: req.user._id,
actions: [logoutAction],
channel: getUserAgentType(req.headers["user-agent"]),
ipAddress: req.realIP
}));
return res.status(200).send({
message: "Successfully logged out.",
message: "Successfully logged out."
});
};
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,
await TokenVersion.updateMany(
{
user: req.user._id
},
});
{
$inc: {
refreshVersion: 1,
accessVersion: 1
}
}
);
return res.status(200).send({
message: "Successfully revoked all sessions.",
});
}
message: "Successfully revoked all sessions."
});
};
/**
* Return user is authenticated
@@ -231,9 +220,9 @@ export const revokeAllSessions = async (req: Request, res: Response) => {
*/
export const checkAuth = async (req: Request, res: Response) => {
return res.status(200).send({
message: "Authenticated",
message: "Authenticated"
});
}
};
/**
* Return new JWT access token by first validating the refresh token
@@ -244,47 +233,47 @@ export const checkAuth = async (req: Request, res: Response) => {
export const getNewToken = async (req: Request, res: Response) => {
const refreshToken = req.cookies.jid;
if (!refreshToken) throw BadRequestError({
message: "Failed to find refresh token in request cookies"
});
if (!refreshToken)
throw BadRequestError({
message: "Failed to find refresh token in request cookies"
});
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(refreshToken, await getJwtRefreshSecret())
);
const decodedToken = <jwt.UserIDJwtPayload>jwt.verify(refreshToken, await getJwtRefreshSecret());
const user = await User.findOne({
_id: decodedToken.userId,
_id: decodedToken.userId
}).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");
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 (!tokenVersion)
throw UnauthorizedRequestError({
message: "Failed to validate refresh token"
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({
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,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.refreshVersion,
accessVersion: tokenVersion.refreshVersion
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret(),
secret: await getJwtAuthSecret()
});
return res.status(200).send({
token,
token
});
};
export const handleAuthProviderCallback = (req: Request, res: Response) => {
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ import {
secretSnapshot as eeSecretSnapshotRouter,
users as eeUsersRouter,
workspace as eeWorkspaceRouter,
secretScanning as v1SecretScanningRouter,
secretScanning as v1SecretScanningRouter
} from "./ee/routes/v1";
import {
auth as v1AuthRouter,
@@ -58,7 +58,7 @@ import {
signup as v2SignupRouter,
tags as v2TagsRouter,
users as v2UsersRouter,
workspace as v2WorkspaceRouter,
workspace as v2WorkspaceRouter
} from "./routes/v2";
import {
auth as v3AuthRouter,
@@ -70,14 +70,21 @@ import { healthCheck } from "./routes/status";
import { getLogger } from "./utils/logger";
import { RouteNotFoundError } from "./utils/errors";
import { requestErrorHandler } from "./middleware/requestErrorHandler";
import { getNodeEnv, getPort, getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookProxy, getSecretScanningWebhookSecret, getSiteURL } from "./config";
import {
getNodeEnv,
getPort,
getSecretScanningGitAppId,
getSecretScanningPrivateKey,
getSecretScanningWebhookProxy,
getSecretScanningWebhookSecret,
getSiteURL
} from "./config";
import { setup } from "./utils/setup";
import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices";
import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent";
const SmeeClient = require('smee-client') // eslint-disable-line
const SmeeClient = require("smee-client"); // eslint-disable-line
const main = async () => {
await setup();
await EELicenseService.initGlobalFeatureSet();
@@ -94,11 +101,15 @@ const main = async () => {
})
);
if (await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey()) {
if (
(await getSecretScanningGitAppId()) &&
(await getSecretScanningWebhookSecret()) &&
(await getSecretScanningPrivateKey())
) {
const probot = new Probot({
appId: await getSecretScanningGitAppId(),
privateKey: await getSecretScanningPrivateKey(),
secret: await getSecretScanningWebhookSecret(),
secret: await getSecretScanningWebhookSecret()
});
if ((await getNodeEnv()) != "production") {
@@ -106,12 +117,14 @@ const main = async () => {
source: await getSecretScanningWebhookProxy(),
target: "http://backend:4000/ss-webhook",
logger: console
})
});
smee.start()
smee.start();
}
app.use(createNodeMiddleware(GithubSecretScanningService, { probot, webhooksPath: "/ss-webhook" })); // secret scanning webhook
app.use(
createNodeMiddleware(GithubSecretScanningService, { probot, webhooksPath: "/ss-webhook" })
); // secret scanning webhook
}
if ((await getNodeEnv()) === "production") {
@@ -207,8 +220,8 @@ const main = async () => {
server.on("close", async () => {
await DatabaseService.closeDatabase();
syncSecretsToThirdPartyServices.close()
githubPushEventSecretScan.close()
syncSecretsToThirdPartyServices.close();
githubPushEventSecretScan.close();
});
return server;

View File

@@ -8,7 +8,8 @@ import { AuthMode } from "../../variables";
router.post("/token", validateRequest, authController.getNewToken);
router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login1)
router.post(
// TODO endpoint: deprecate (moved to api/v3/auth/login1)
"/login1",
authLimiter,
body("email").exists().trim().notEmpty().toLowerCase(),
@@ -17,7 +18,8 @@ router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login1)
authController.login1
);
router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login2)
router.post(
// TODO endpoint: deprecate (moved to api/v3/auth/login2)
"/login2",
authLimiter,
body("email").exists().trim().notEmpty().toLowerCase(),
@@ -30,7 +32,7 @@ router.post(
"/logout",
authLimiter,
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
acceptedAuthModes: [AuthMode.JWT]
}),
authController.logout
);
@@ -38,24 +40,19 @@ router.post(
router.post(
"/checkAuth",
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
acceptedAuthModes: [AuthMode.JWT]
}),
authController.checkAuth
);
router.get(
"/common-passwords",
authLimiter,
authController.getCommonPasswords
);
router.delete( // TODO endpoint: deprecate (moved to DELETE v2/users/me/sessions)
router.delete(
// TODO endpoint: deprecate (moved to DELETE v2/users/me/sessions)
"/sessions",
authLimiter,
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
acceptedAuthModes: [AuthMode.JWT]
}),
authController.revokeAllSessions
);
export default router;
export default router;

View File

@@ -3,7 +3,7 @@
/**
* @type {import('next').NextConfig}
**/
const path = require('path');
const path = require("path");
const ContentSecurityPolicy = `
default-src 'self';
@@ -11,7 +11,7 @@ const ContentSecurityPolicy = `
style-src 'self' https://rsms.me 'unsafe-inline';
child-src https://api.stripe.com;
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/;
connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com http://localhost:*;
connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://localhost:*;
img-src 'self' https://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:;
media-src https://js.intercomcdn.com;
font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com;
@@ -21,50 +21,50 @@ const ContentSecurityPolicy = `
// after learning more below.
const securityHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
key: "X-DNS-Prefetch-Control",
value: "on"
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload"
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
key: "X-XSS-Protection",
value: "1; mode=block"
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
key: "X-Frame-Options",
value: "SAMEORIGIN"
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=()'
key: "Permissions-Policy",
value: "camera=(), microphone=()"
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
key: "X-Content-Type-Options",
value: "nosniff"
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin"
},
{
key: 'Content-Security-Policy',
value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
key: "Content-Security-Policy",
value: ContentSecurityPolicy.replace(/\s{2,}/g, " ").trim()
}
];
module.exports = {
output: 'standalone',
output: "standalone",
i18n: {
locales: ['en', 'ko', 'fr', 'pt-BR', 'pt-PT', 'es'],
defaultLocale: 'en'
locales: ["en", "ko", "fr", "pt-BR", "pt-PT", "es"],
defaultLocale: "en"
},
async headers() {
return [
{
// Apply these headers to all routes in your application.
source: '/:path*',
source: "/:path*",
headers: securityHeaders
}
];
@@ -73,15 +73,15 @@ module.exports = {
// config
config.module.rules.push({
test: /\.wasm$/,
loader: 'base64-loader',
type: 'javascript/auto'
loader: "base64-loader",
type: "javascript/auto"
});
config.module.noParse = /\.wasm$/;
config.module.rules.forEach((rule) => {
(rule.oneOf || []).forEach((oneOf) => {
if (oneOf.loader && oneOf.loader.indexOf('file-loader') >= 0) {
if (oneOf.loader && oneOf.loader.indexOf("file-loader") >= 0) {
oneOf.exclude.push(/\.wasm$/);
}
});

View File

@@ -102,7 +102,7 @@
"@storybook/testing-library": "^0.2.0",
"@tailwindcss/typography": "^0.5.4",
"@types/jsrp": "^0.2.4",
"@types/node": "18.11.9",
"@types/node": "^18.11.9",
"@types/react": "^18.0.26",
"@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",

View File

@@ -110,7 +110,7 @@
"@storybook/testing-library": "^0.2.0",
"@tailwindcss/typography": "^0.5.4",
"@types/jsrp": "^0.2.4",
"@types/node": "18.11.9",
"@types/node": "^18.11.9",
"@types/react": "^18.0.26",
"@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",

View File

@@ -231,10 +231,15 @@
"current": "Current password",
"current-wrong": "The current password may be wrong",
"new": "New password",
"validate-base": "Password should contain at least:",
"validate-length": "14 characters",
"validate-case": "1 lowercase character",
"validate-number": "1 number"
"validate-base": "Password should contain:",
"validate-tooShort": "at least 14 characters",
"validate-tooLong": "at most 100 characters",
"validate-noLetterChar": "at least 1 letter character",
"validate-noNumOrSpecialChar": "at least 1 number or special character",
"validate-repeatedChar": "at most 3 repeated, consecutive characters",
"validate-escapeChar": "No escape characters allowed.",
"validate-lowEntropy": "Password contains sensitive data.",
"validate-breached": "Password was found in a data breach."
},
"token": {
"service-tokens": "Service Tokens",

View File

@@ -228,10 +228,15 @@
"current": "Contraseña actual",
"current-wrong": "La contraseña actual puede puede que sea incorrecta",
"new": "Nueva contraseña",
"validate-base": "La contraseña debe contener como mínimo:",
"validate-length": "14 caracteres",
"validate-case": "1 letra en minúsculas",
"validate-number": "1 número"
"validate-base": "La contraseña debe contener:",
"validate-tooShort": "al menos 14 caracteres",
"validate-tooLong": "como máximo 100 caracteres",
"validate-noLetterChar": "al menos 1 carácter alfabético",
"validate-noNumOrSpecialChar": "al menos 1 número o carácter especial",
"validate-repeatedChar": "como máximo 3 caracteres repetidos y consecutivos",
"validate-escapeChar": "No se permiten caracteres de escape.",
"validate-lowEntropy": "La contraseña contiene datos sensibles.",
"validate-breached": "La contraseña se encontró en una violación de datos."
},
"token": {
"service-tokens": "Tokens de servicio",

View File

@@ -215,10 +215,15 @@
"current": "Mot de passe actuel",
"current-wrong": "Le mot de passe actuel peut être érroné",
"new": "Nouveau mot de passe",
"validate-base": "Le mot de passe doit contenir au moins:",
"validate-length": "14 caractères",
"validate-case": "1 caractère miniscule",
"validate-number": "1 chiffre"
"validate-base": "Le mot de passe doit contenir :",
"validate-tooShort": "au moins 14 caractères",
"validate-tooLong": "au plus 100 caractères",
"validate-noLetterChar": "au moins 1 caractère alphabétique",
"validate-noNumOrSpecialChar": "au moins 1 chiffre ou caractère spécial",
"validate-repeatedChar": "au plus 3 caractères consécutifs répétés",
"validate-escapeChar": "Aucun caractère d'échappement autorisé.",
"validate-lowEntropy": "Le mot de passe contient des données sensibles.",
"validate-breached": "Le mot de passe a été trouvé dans une violation de données."
},
"token": {
"service-tokens": "Jetons de service",
@@ -296,4 +301,4 @@
"step5-subtitle": "Infisical a pour but d'être utilisé avec vos coéquipiers. Invitez-les à le tester.",
"step5-skip": "Passer"
}
}
}

View File

@@ -182,10 +182,15 @@
"current": "현재 비밀번호",
"new": "새 비밀번호",
"current-wrong": "현재 비밀번호가 잘못되었어요",
"validate-base": "비밀번호는 다음 조건을 만족해야 합니다:",
"validate-length": "14 글자 이상",
"validate-case": "1개 이상의 소문자",
"validate-number": "1개 이상의 숫자"
"validate-base": "비밀번호는 다음을 포함해야 합니다:",
"validate-tooShort": "최소 14자",
"validate-tooLong": "최대 100자",
"validate-noLetterChar": "최소 1개의 문자를 포함해야 합니다.",
"validate-noNumOrSpecialChar": "최소 1개의 숫자 또는 특수 문자를 포함해야 합니다.",
"validate-repeatedChar": "연속으로 최대 3개의 반복된 문자를 포함할 수 있습니다.",
"validate-escapeChar": "이스케이프 문자는 허용되지 않습니다.",
"validate-lowEntropy": "비밀번호에 민감한 데이터가 포함되어 있습니다.",
"validate-breached": "비밀번호가 데이터 유출에 포함되었습니다."
},
"token": {
"add-dialog": {
@@ -256,4 +261,4 @@
"step4-description3": "분실시 접근하거나 복구할 수 없는 시크릿 키가 포함되어 있어요.",
"step4-download": "PDF 다운로드"
}
}
}

View File

@@ -210,10 +210,15 @@
"current": "Senha atual",
"current-wrong": "A senha atual pode estar errada",
"new": "Nova Senha",
"validate-base": "A senha deve conter pelo menos:",
"validate-length": "14 caracteres",
"validate-case": "1 caractere minúsculo",
"validate-number": "1 número"
"validate-base": "A senha deve conter:",
"validate-tooShort": "pelo menos 14 caracteres",
"validate-tooLong": "no máximo 100 caracteres",
"validate-noLetterChar": "pelo menos 1 caractere alfabético",
"validate-noNumOrSpecialChar": "pelo menos 1 número ou caractere especial",
"validate-repeatedChar": "no máximo 3 caracteres repetidos e consecutivos",
"validate-escapeChar": "Nenhum caractere de escape permitido.",
"validate-lowEntropy": "A senha contém dados sensíveis.",
"validate-breached": "A senha foi encontrada em uma violação de dados."
},
"token": {
"service-tokens": "Tokens de Serviço",
@@ -290,4 +295,4 @@
"step5-subtitle": "Infisical foi feito para ser usado com seus colegas. Convide-os para testar também.",
"step5-skip": "Pular"
}
}
}

View File

@@ -228,10 +228,15 @@
"current": "Mevcut şifre",
"current-wrong": "Mevcut şifre yanlış olabilir",
"new": "Yeni şifre",
"validate-base": "Şifre en az şunları içermelidir:",
"validate-length": "14 karakter",
"validate-case": "1 küçük harf",
"validate-number": "1 rakam"
"validate-base": "Parola içermelidir:",
"validate-tooShort": "en az 14 karakter",
"validate-tooLong": "en fazla 100 karakter",
"validate-noLetterChar": "en az 1 harf karakteri",
"validate-noNumOrSpecialChar": "en az 1 rakam veya özel karakter",
"validate-repeatedChar": "en fazla 3 tekrarlanan, ardışık karakter",
"validate-escapeChar": "Kaçış karakterlerine izin verilmez.",
"validate-lowEntropy": "Parola hassas veriler içeriyor.",
"validate-breached": "Parola veri ihlalinde bulundu."
},
"token": {
"service-tokens": "Servis Belirteçleri",

View File

@@ -8,13 +8,12 @@ import jsrp from "jsrp";
import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util";
import { useGetCommonPasswords } from "@app/hooks/api";
import { completeAccountSignup } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import ProjectService from "@app/services/ProjectService";
import InputField from "../basic/InputField";
import checkPassword from "../utilities/checks/checkPassword";
import checkPassword from "../utilities/checks/password/checkPassword";
import Aes256Gcm from "../utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "../utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "../utilities/saveTokenToLocalStorage";
@@ -39,12 +38,14 @@ interface UserInfoStepProps {
}
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
tooShort?: string;
tooLong?: string;
noLetterChar?: string;
noNumOrSpecialChar?: string;
repeatedChar?: string;
escapeChar?: string;
lowEntropy?: string;
breached?: string;
};
/**
@@ -71,9 +72,8 @@ export default function UserInfoStep({
setOrganizationName,
attributionSource,
setAttributionSource,
providerAuthToken,
providerAuthToken
}: UserInfoStepProps): JSX.Element {
const { data: commonPasswords } = useGetCommonPasswords();
const [nameError, setNameError] = useState(false);
const [organizationNameError, setOrganizationNameError] = useState(false);
@@ -99,10 +99,9 @@ export default function UserInfoStep({
} else {
setOrganizationNameError(false);
}
errorCheck = checkPassword({
errorCheck = await checkPassword({
password,
commonPasswords,
setErrors
});
@@ -174,7 +173,7 @@ export default function UserInfoStep({
salt: result.salt,
verifier: result.verifier,
organizationName,
attributionSource,
attributionSource
});
// unset signup JWT token and set JWT token
@@ -191,7 +190,7 @@ export default function UserInfoStep({
});
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]?._id;
const project = await ProjectService.initProject({
organizationId: orgId,
@@ -215,13 +214,15 @@ export default function UserInfoStep({
};
return (
<div className="h-full mx-auto mb-36 w-max rounded-xl md:px-8 md:mb-16">
<p className="mx-8 mb-6 flex justify-center text-xl font-bold text-medium md:mx-16 text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
<div className="mx-auto mb-36 h-full w-max rounded-xl md:mb-16 md:px-8">
<p className="text-medium mx-8 mb-6 flex justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-xl font-bold text-transparent md:mx-16">
{t("signup.step3-message")}
</p>
<div className="h-full mx-auto mb-36 w-max rounded-xl py-6 md:px-8 md:mb-16 md:border md:border-mineshaft-600 md:bg-mineshaft-800">
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Your Name</p>
<div className="mx-auto mb-36 h-full w-max rounded-xl py-6 md:mb-16 md:border md:border-mineshaft-600 md:bg-mineshaft-800 md:px-8">
<div className="relative z-0 flex w-1/4 w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Your Name
</p>
<Input
placeholder="Jane Doe"
onChange={(e) => setName(e.target.value)}
@@ -230,10 +231,16 @@ export default function UserInfoStep({
autoComplete="given-name"
className="h-12"
/>
{nameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your name</p>}
{nameError && (
<p className="mt-1 ml-1 w-full text-left text-xs text-red-600">
Please, specify your name
</p>
)}
</div>
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Organization Name</p>
<div className="relative z-0 flex w-1/4 w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Organization Name
</p>
<Input
placeholder="Infisical"
onChange={(e) => setOrganizationName(e.target.value)}
@@ -241,10 +248,16 @@ export default function UserInfoStep({
isRequired
className="h-12"
/>
{organizationNameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your organization name</p>}
{organizationNameError && (
<p className="mt-1 ml-1 w-full text-left text-xs text-red-600">
Please, specify your organization name
</p>
)}
</div>
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Where did you hear about us? <span className="font-light">(optional)</span></p>
<div className="relative z-0 flex w-1/4 w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Where did you hear about us? <span className="font-light">(optional)</span>
</p>
<Input
placeholder=""
onChange={(e) => setAttributionSource(e.target.value)}
@@ -252,16 +265,15 @@ export default function UserInfoStep({
className="h-12"
/>
</div>
<div className="mt-2 flex lg:w-1/6 w-1/4 min-w-[20rem] max-h-60 w-full flex-col items-center justify-center rounded-lg py-2">
<div className="mt-2 flex max-h-60 w-1/4 w-full min-w-[20rem] flex-col items-center justify-center rounded-lg py-2 lg:w-1/6">
<InputField
label={t("section.password.password")}
onChangeHandler={(pass: string) => {
setPassword(pass);
checkPassword({
onChangeHandler={async (pass: string) => {
await checkPassword({
password: pass,
commonPasswords,
setErrors
});
setPassword(pass);
}}
type="password"
value={password}
@@ -272,23 +284,20 @@ export default function UserInfoStep({
/>
{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>
<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 className="items-top ml-1 flex flex-row justify-start" key={key}>
<div>
<FontAwesomeIcon
icon={faXmark}
className="text-md text-red ml-0.5 mr-2.5"
<FontAwesomeIcon
icon={faXmark}
className="text-md ml-0.5 mr-2.5 text-red"
/>
</div>
<p className="text-gray-400 text-sm">
{errors[key as keyof Errors]}
</p>
<p className="text-sm text-gray-400">{errors[key as keyof Errors]}</p>
</div>
);
}
@@ -298,18 +307,21 @@ export default function UserInfoStep({
</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="text-l py-1 text-lg w-full">
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
<div className="text-l w-full py-1 text-lg">
<Button
type="submit"
onClick={signupErrorCheck}
size="sm"
isFullWidth
className='h-14'
className="h-14"
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
> {String(t("signup.signup"))} </Button>
>
{" "}
{String(t("signup.signup"))}{" "}
</Button>
</div>
</div>
</div>

View File

@@ -1,69 +0,0 @@
/* eslint-disable no-param-reassign */
interface PasswordCheckProps {
password: string;
errorCheck: boolean;
setPasswordErrorLength: (value: boolean) => void;
setPasswordErrorNumber: (value: boolean) => void;
setPasswordErrorLowerCase: (value: boolean) => void;
}
/**
* This function checks a user password with respect to some criteria.
*/
const passwordCheck = ({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck
}: PasswordCheckProps) => {
if (!password || password.length < 14) {
setPasswordErrorLength(true);
errorCheck = true;
} else {
setPasswordErrorLength(false);
}
if (!/\d/.test(password)) {
setPasswordErrorNumber(true);
errorCheck = true;
} else {
setPasswordErrorNumber(false);
}
if (!/[a-z]/.test(password)) {
setPasswordErrorLowerCase(true);
errorCheck = true;
// } else if (/(.)(?:(?!\1).){1,2}/.test(password)) {
// console.log(111)
// setPasswordError(true);
// setPasswordErrorMessage("Password should not contain repeating characters.");
// errorCheck = true;
// } else if (RegExp(`[${email}]`).test(password)) {
// console.log(222)
// setPasswordError(true);
// setPasswordErrorMessage("Password should not contain your email.");
// errorCheck = true;
} else {
setPasswordErrorLowerCase(false);
}
// if (!/[A-Z]/.test(password)) {
// setPasswordErrorUpperCase(true);
// errorCheck = true;
// } else {
// setPasswordErrorUpperCase(false);
// }
// if (!/(?=.*[!@#$%^&*])/.test(password)) {
// setPasswordErrorSpecialChar(true);
// // "Please add at least 1 special character (*, !, #, %)."
// errorCheck = true;
// } else {
// setPasswordErrorSpecialChar(false);
// }
return errorCheck;
};
export default passwordCheck;

View File

@@ -1,72 +0,0 @@
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

@@ -0,0 +1,89 @@
import { checkIsPasswordBreached } from "./checkIsPasswordBreached";
import { escapeCharRegex, letterCharRegex, lowEntropyRegexes,numAndSpecialCharRegex, repeatedCharRegex } from "./passwordRegexes";
interface PasswordCheckProps {
password: string;
setPasswordErrorTooShort: (value: boolean) => void;
setPasswordErrorTooLong: (value: boolean) => void;
setPasswordErrorNoLetterChar: (value: boolean) => void;
setPasswordErrorNoNumOrSpecialChar: (value: boolean) => void;
setPasswordErrorRepeatedChar: (value: boolean) => void;
setPasswordErrorEscapeChar: (value: boolean) => void;
setPasswordErrorLowEntropy: (value: boolean) => void;
setPasswordErrorBreached: (value: boolean) => void;
}
const passwordCheck = async ({
password,
setPasswordErrorTooShort,
setPasswordErrorTooLong,
setPasswordErrorNoLetterChar,
setPasswordErrorNoNumOrSpecialChar,
setPasswordErrorRepeatedChar,
setPasswordErrorEscapeChar,
setPasswordErrorLowEntropy,
setPasswordErrorBreached
}: PasswordCheckProps) => {
let errorCheck = false;
const tests = [
{
name: "tooShort",
validator: (pwd: string) => pwd.length >= 14,
setError: setPasswordErrorTooShort,
},
{
name: "tooLong",
validator: (pwd: string) => pwd.length < 101,
setError: setPasswordErrorTooLong,
},
{
name: "noLetterChar",
validator: (pwd: string) => letterCharRegex.test(pwd),
setError: setPasswordErrorNoLetterChar,
},
{
name: "noNumOrSpecialChar",
validator: (pwd: string) => numAndSpecialCharRegex.test(pwd),
setError: setPasswordErrorNoNumOrSpecialChar,
},
{
name: "repeatedChar",
validator: (pwd: string) => !repeatedCharRegex.test(pwd),
setError: setPasswordErrorRepeatedChar,
},
{
name: "escapeChar",
validator: (pwd: string) => !escapeCharRegex.test(pwd),
setError: setPasswordErrorEscapeChar,
},
{
name: "lowEntropy",
validator: (pwd: string) => (
!lowEntropyRegexes.some(regex => regex.test(pwd))
),
setError: setPasswordErrorLowEntropy,
},
];
const isBreached = await checkIsPasswordBreached(password);
if (isBreached) {
errorCheck = true;
setPasswordErrorBreached(true);
} else {
setPasswordErrorBreached(false);
}
tests.forEach((test) => {
if (!test.validator(password)) {
errorCheck = true;
test.setError(true);
} else {
test.setError(false);
}
})
return errorCheck;
};
export default passwordCheck;

View File

@@ -0,0 +1,113 @@
import axios from "axios";
// SHA-1 hash the password using the SubtleCrypto API
async function hashPassword(passwordBytes: ArrayBuffer): Promise<ArrayBuffer> {
const buffer = await window.crypto.subtle.digest("SHA-1", passwordBytes);
return buffer;
}
// Convert the hashed password buffer to a hexadecimal string
function bufferToHex(buffer: ArrayBuffer): string {
const byteArray = new Uint8Array(buffer);
const hexParts: string[] = [];
byteArray.forEach((byte) => {
const hex = byte.toString(16).padStart(2, "0");
hexParts.push(hex);
});
return hexParts.join("");
}
// see API details here: https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange
// in short, the pending password is hashed (SHA-1), the first 5 chars are sliced and compared against a ranged hash table
// this hash table is formed from the 5 char hash prefix (ie. 00000-FFFFF) so 16^5 results
// returns a hash table of 800-1000 results
// padding has been added to prevent MitM attacker determining which hash table was called by the response size
// the last 35 chars of the password hash are compared client-side against the table
// if there is a match, that password has been involved in a password breach (ie. pwnd) and should NOT be accepted
// the database consists of ~700 mln breached passwords and is continuously updated, including with law enforcement ingestion
// https://www.troyhunt.com/open-source-pwned-passwords-with-fbi-feed-and-225m-new-nca-passwords-is-now-live/
// The HIBP API follows NIST guidance (pg.14) https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63b.pdf
// "When processing requests to establish and change memorized secrets, verifiers SHALL compare
// the prospective secrets against a list that contains values known to be commonly-used, expected,
// or compromised. For example, the list MAY include, but is not limited to:
// • Passwords obtained from previous breach corpuses.
// • Dictionary words.
// • Repetitive or sequential characters (e.g. ‘aaaaaa’, ‘1234abcd’).
// • Context-specific words, such as the name of the service, the username, and derivatives
// thereof."
export const checkIsPasswordBreached = async (password: string): Promise<boolean> => {
const HAVE_I_BEEN_PWNED_API_URL = "https://api.pwnedpasswords.com";
const maxRetryAttempts = 3;
let encodedPwd: Uint8Array | undefined;
let hashedPwdBuffer: ArrayBuffer | undefined;
try {
// Convert the password to a Uint8Array (UTF-8 encoded bytes)
const textEncoder = new TextEncoder();
encodedPwd = textEncoder.encode(password);
// Hash the password and convert it to a useful format for the HIBP API
hashedPwdBuffer = await hashPassword(encodedPwd!.buffer);
const hashedPwd = bufferToHex(hashedPwdBuffer).toUpperCase();
// ONLY send the first 5 hash chars (over HTTPS)
const hashedPwdToSend = hashedPwd.slice(0, 5);
const safeHashedPwdToSend = encodeURIComponent(hashedPwdToSend); // Ensure URL safety
const rangedHashTableUri = `${HAVE_I_BEEN_PWNED_API_URL}/range/${safeHashedPwdToSend}`;
let response;
let retryAttempt = 0;
/* eslint-disable no-await-in-loop */
while (retryAttempt < maxRetryAttempts) {
try {
response = await axios.get(rangedHashTableUri, {
headers: {
"Add-Padding": "true", // see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/
"Content-Type": "text/plain",
},
});
if (response.status === 200) {
// now we get back one of 16^5 hash prefix tables with random padding
const responseData = response.data.toUpperCase();
// check the last 35 hash chars to see if there's a match
const isBreachedPassword: boolean = responseData.includes(hashedPwd.slice(5, 40));
return isBreachedPassword;
}
retryAttempt += 1;
} catch (err) {
if (!axios.isAxiosError(err)) {
throw err;
}
retryAttempt += 1;
}
}
console.error(
`Received a non-200 response (${response ? response.status : "unknown"}) from the Pwnd Passwords API`
);
return false;
} catch (err: any) {
console.error("An unexpected error has occurred:", err.message);
return false;
} finally {
// Clear the UTF-8 encoded password from memory
if (encodedPwd) {
const zeroEncodedPwdBuffer = new Uint8Array(encodedPwd.length);
encodedPwd.set(zeroEncodedPwdBuffer);
}
// Clear the hashed password buffer from memory
if (hashedPwdBuffer) {
const zeroHashedPwdBuffer = new Uint8Array(hashedPwdBuffer);
zeroHashedPwdBuffer.fill(0);
}
}
};

View File

@@ -0,0 +1,99 @@
import { checkIsPasswordBreached } from "./checkIsPasswordBreached";
import { escapeCharRegex, letterCharRegex, lowEntropyRegexes,numAndSpecialCharRegex, repeatedCharRegex } from "./passwordRegexes";
type Errors = {
tooShort?: string;
tooLong?: string;
noLetterChar?: string;
noNumOrSpecialChar?: string;
repeatedChar?: string;
escapeChar?: string;
lowEntropy?: string;
breached?: string;
};
interface CheckPasswordParams {
password: string;
setErrors: (value: Errors) => void;
}
/**
* Validate that the password [password]:
* - Contains at least 14 characters
* - Contains at most 100 characters
* - Contains at least 1 letter character (many languages supported) (case insensitive)
* - Contains at least 1 number (0-9) or special character (emojis included)
* - Does not contain 3 repeat, consecutive characters
* - Does not contain any escape characters/sequences
* - Does not contain PII and/or low entropy data (eg. email address, URL, phone number, DoB, SSN, driver's license, passport)
* - Is not in a database of breached passwords
*
* 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 = async ({ password, setErrors }: CheckPasswordParams): Promise<boolean> => {
const errors: Errors = {};
const tests = [
{
name: "tooShort",
validator: (pwd: string) => pwd.length >= 14,
errorText: "at least 14 characters",
},
{
name: "tooLong",
validator: (pwd: string) => pwd.length < 101,
errorText: "at most 100 characters",
},
{
name: "noLetterChar",
validator: (pwd: string) => letterCharRegex.test(pwd),
errorText: "at least 1 letter character",
},
{
name: "noNumOrSpecialChar",
validator: (pwd: string) => numAndSpecialCharRegex.test(pwd),
errorText: "at least 1 number or special character",
},
{
name: "repeatedChar",
validator: (pwd: string) => !repeatedCharRegex.test(pwd),
errorText: "at most 3 repeated, consecutive characters",
},
{
name: "escapeChar",
validator: (pwd: string) => !escapeCharRegex.test(pwd),
errorText: "No escape characters allowed.",
},
{
name: "lowEntropy",
validator: (pwd: string) => (
!lowEntropyRegexes.some(regex => regex.test(pwd))
),
errorText: "Password contains sensitive data.",
},
];
const isBreached = await checkIsPasswordBreached(password);
if (isBreached) {
errors.breached = "Password was found in a data breach.";
}
tests.forEach((test) => {
if (test.validator && !test.validator(password)) {
errors[test.name as keyof Errors] = test.errorText;
}
});
setErrors(errors);
return Object.keys(errors).length > 0;
};
export default checkPassword;

View File

@@ -0,0 +1,36 @@
// This regex covers letters (case insensitive) for the top 50 most spoken languages
/* eslint-disable no-misleading-character-class */
export const letterCharRegex = /[A-Za-z\u00C0-\u00D6\u00D8-\u00DE\u00DF-\u00F6\u00F8-\u00FF\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u0600-\u06FF\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u05B0-\u05FF\u0980-\u09FF\u1F00-\u1FFF\u0130\u015E\u011E\u00C7\u00FC\u00FB\u00EB\u00E7]/u;
// This regex covers digits, special characters, symbols, and emojis.
export const numAndSpecialCharRegex = /[\d!@#$%^&*(),.?":{}|<>]|[^\p{L}\p{N}\s]/u;
// This regex covers 3 repeated consecutive chars (incl. spaces)
export const repeatedCharRegex = /(.)\1\1\1|\s{4,}/;
// This regex covers the escape sequences as a precaution
export const escapeCharRegex = /[\n\t\r\\]/;
// This regex covers some PII and/or low entropy data
export const lowEntropyRegexes = [
// Email address
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/,
// URL (incl. subdomains, paths, top-level domains & query params)
/^(?:(?:https?|ftp):\/\/)?(?:\w+\.)?[a-zA-Z0-9.-]+\.(?:com|org|net|edu)(?:\/\S*)?(?:\?\S*)?$/,
// Date in various formats
/(\b\d{1,4}[-/.]?\d{1,2}[-/.]?\d{1,4}\b)|(\b\d{1,4}[-/.]?\w{3}[-/.]?\d{1,4}\b)/,
// Phone numbers (generalized)
/(?:\+(?:[1-9]\d{0,2})\s?)?(?:\(\d{1,4}\)\s?)?(?:\d[-.\s]?){5,}\d/,
// Passport numbers (generalized)
/\b(?:[A-Z0-9]{6,9}|[A-Z0-9]{8,9}|[A-Z0-9]{9}|[A-Z0-9]{10,11})\b/,
// Driver's license numbers (generalized)
/\b(?:[A-Z0-9]{7,10}|[A-Z0-9]{10,11}|[A-Z0-9]{7,10})\b/,
// US social security number
/\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/,
];

View File

@@ -1,10 +1,10 @@
export {
useGetAuthToken,
useGetCommonPasswords,
useResetPassword,
useSendMfaToken,
useSendMfaToken,
useSendPasswordResetEmail,
useSendVerificationEmail,
useVerifyEmailVerificationCode,
useVerifyMfaToken,
useVerifyPasswordResetCode} from "./queries"
useVerifyPasswordResetCode
} from "./queries";

View File

@@ -20,22 +20,22 @@ import {
SRPR1Res,
VerifyMfaTokenDTO,
VerifyMfaTokenRes,
VerifySignupInviteDTO} from "./types";
VerifySignupInviteDTO
} from "./types";
const authKeys = {
getAuthToken: ["token"] as const,
commonPasswords: ["common-passwords"] as const
getAuthToken: ["token"] as const
};
export const login1 = async (loginDetails: Login1DTO) => {
const { data } = await apiRequest.post<Login1Res>("/api/v3/auth/login1", loginDetails);
return data;
}
};
export const login2 = async (loginDetails: Login2DTO) => {
const { data } = await apiRequest.post<Login2Res>("/api/v3/auth/login2", loginDetails);
return data;
}
};
export const useLogin1 = () => {
return useMutation({
@@ -47,7 +47,7 @@ export const useLogin1 = () => {
return login1(details);
}
});
}
};
export const useLogin2 = () => {
return useMutation({
@@ -59,22 +59,22 @@ export const useLogin2 = () => {
return login2(details);
}
});
}
};
export const srp1 = async (details: SRP1DTO) => {
const { data } = await apiRequest.post<SRPR1Res>("/api/v1/password/srp1", details);
return data;
}
return data;
};
export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => {
const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details);
return data;
}
return data;
};
export const completeAccountSignupInvite = async (details: CompleteAccountDTO) => {
const { data } = await apiRequest.post("/api/v2/signup/complete-account/invite", details);
return data;
}
return data;
};
export const useCompleteAccountSignup = () => {
return useMutation({
@@ -82,7 +82,7 @@ export const useCompleteAccountSignup = () => {
return completeAccountSignup(details);
}
});
}
};
export const useSendMfaToken = () => {
return useMutation<{}, {}, SendMfaTokenDTO>({
@@ -91,22 +91,16 @@ export const useSendMfaToken = () => {
return data;
}
});
}
};
export const verifyMfaToken = async ({
email,
mfaCode
}: {
email: string;
mfaCode: string;
}) => {
export const verifyMfaToken = async ({ email, mfaCode }: { email: string; mfaCode: string }) => {
const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", {
email,
mfaToken: mfaCode
});
return data;
}
};
export const useVerifyMfaToken = () => {
return useMutation<VerifyMfaTokenRes, {}, VerifyMfaTokenDTO>({
@@ -117,87 +111,67 @@ export const useVerifyMfaToken = () => {
});
}
});
}
};
export const verifySignupInvite = async (details: VerifySignupInviteDTO) => {
const { data } = await apiRequest.post("/api/v1/invite-org/verify", details);
return data;
}
};
export const useSendVerificationEmail = () => {
return useMutation({
mutationFn: async ({
email
}: {
email: string;
}) => {
mutationFn: async ({ email }: { email: string }) => {
const { data } = await apiRequest.post("/api/v1/signup/email/signup", {
email
});
return data;
}
});
}
};
export const useVerifyEmailVerificationCode = () => {
return useMutation({
mutationFn: async ({
email,
code
}: {
email: string;
code: string;
}) => {
mutationFn: async ({ email, code }: { email: string; code: string }) => {
const { data } = await apiRequest.post("/api/v1/signup/email/verify", {
email,
code
});
return data;
}
});
}
};
export const useSendPasswordResetEmail = () => {
return useMutation({
mutationFn: async ({
email
}: {
email: string;
}) => {
mutationFn: async ({ email }: { email: string }) => {
const { data } = await apiRequest.post("/api/v1/password/email/password-reset", {
email
});
return data;
}
});
}
};
export const useVerifyPasswordResetCode = () => {
return useMutation({
mutationFn: async ({
email,
code
}: {
email: string;
code: string;
}) => {
mutationFn: async ({ email, code }: { email: string; code: string }) => {
const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", {
email,
code
});
return data;
}
});
}
};
export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => {
const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details);
return data;
}
};
export const getBackupEncryptedPrivateKey = async ({
verificationToken
@@ -207,37 +181,41 @@ export const getBackupEncryptedPrivateKey = async ({
Authorization: `Bearer ${verificationToken}`
}
});
return data.backupPrivateKey;
}
};
export const useResetPassword = () => {
return useMutation({
mutationFn: async (details: ResetPasswordDTO) => {
const { data } = await apiRequest.post("/api/v1/password/password-reset", {
protectedKey: details.protectedKey,
protectedKeyIV: details.protectedKeyIV,
protectedKeyTag: details.protectedKeyTag,
encryptedPrivateKey: details.encryptedPrivateKey,
encryptedPrivateKeyIV: details.encryptedPrivateKeyIV,
encryptedPrivateKeyTag: details.encryptedPrivateKeyTag,
salt: details.salt,
verifier: details.verifier
}, {
headers: {
Authorization: `Bearer ${details.verificationToken}`
const { data } = await apiRequest.post(
"/api/v1/password/password-reset",
{
protectedKey: details.protectedKey,
protectedKeyIV: details.protectedKeyIV,
protectedKeyTag: details.protectedKeyTag,
encryptedPrivateKey: details.encryptedPrivateKey,
encryptedPrivateKeyIV: details.encryptedPrivateKeyIV,
encryptedPrivateKeyTag: details.encryptedPrivateKeyTag,
salt: details.salt,
verifier: details.verifier
},
{
headers: {
Authorization: `Bearer ${details.verificationToken}`
}
}
});
);
return data;
}
});
}
};
export const changePassword = async (details: ChangePasswordDTO) => {
const { data } = await apiRequest.post("/api/v1/password/change-password", details);
return data;
}
};
export const useChangePassword = () => {
// note: use after srp1
@@ -246,7 +224,7 @@ export const useChangePassword = () => {
return changePassword(details);
}
});
}
};
// Refresh token is set as cookie when logged in
// Using that we fetch the auth bearer token needed for auth calls
@@ -263,11 +241,3 @@ export const useGetAuthToken = () =>
onSuccess: (data) => setAuthToken(data.token),
retry: 0
});
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

@@ -10,9 +10,9 @@ import queryString from "query-string";
import Button from "@app/components/basic/buttons/Button";
import InputField from "@app/components/basic/InputField";
import passwordCheck from "@app/components/utilities/checks/PasswordCheck";
import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { useResetPassword,useVerifyPasswordResetCode } from "@app/hooks/api";
import { useResetPassword, useVerifyPasswordResetCode } from "@app/hooks/api";
import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries";
import { deriveArgonKey } from "../components/utilities/cryptography/crypto";
@@ -28,25 +28,30 @@ export default function PasswordReset() {
const [privateKey, setPrivateKey] = useState("");
const [newPassword, setNewPassword] = useState("");
const [backupKeyError, setBackupKeyError] = useState(false);
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(false);
const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false);
const [passwordErrorNoLetterChar, setPasswordErrorNoLetterChar] = useState(false);
const [passwordErrorNoNumOrSpecialChar, setPasswordErrorNoNumOrSpecialChar] = useState(false);
const [passwordErrorRepeatedChar, setPasswordErrorRepeatedChar] = useState(false);
const [passwordErrorEscapeChar, setPasswordErrorEscapeChar] = useState(false);
const [passwordErrorLowEntropy, setPasswordErrorLowEntropy] = useState(false);
const [passwordErrorBreached, setPasswordErrorBreached] = useState(false);
const router = useRouter();
const { mutateAsync: verifyPasswordResetCodeMutateAsync } = useVerifyPasswordResetCode();
const { mutateAsync: resetPasswordMutateAsync } = useResetPassword();
const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
const token = parsedUrl.token as string;
const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
// Unencrypt the private key with a backup key
// Decrypt the private key with a backup key
const getEncryptedKeyHandler = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const result = await getBackupEncryptedPrivateKey({ verificationToken });
setPrivateKey(
Aes256Gcm.decrypt({
ciphertext: result.encryptedPrivateKey,
@@ -56,7 +61,7 @@ export default function PasswordReset() {
})
);
setStep(3);
} catch(err) {
} catch (err) {
console.error(err);
setBackupKeyError(true);
}
@@ -65,12 +70,16 @@ export default function PasswordReset() {
// If everything is correct, reset the password
const resetPasswordHandler = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const errorCheck = passwordCheck({
const errorCheck = await passwordCheck({
password: newPassword,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false
setPasswordErrorTooShort,
setPasswordErrorTooLong,
setPasswordErrorNoLetterChar,
setPasswordErrorNoNumOrSpecialChar,
setPasswordErrorRepeatedChar,
setPasswordErrorEscapeChar,
setPasswordErrorLowEntropy,
setPasswordErrorBreached
});
if (!errorCheck) {
@@ -127,10 +136,10 @@ export default function PasswordReset() {
verifier: result.verifier,
verificationToken
});
router.push("/login");
setLoading(false)
setLoading(false);
});
}
);
@@ -169,13 +178,17 @@ export default function PasswordReset() {
// Input backup key
const stepInputBackupKey = (
<form onSubmit={getEncryptedKeyHandler} className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
<form
onSubmit={getEncryptedKeyHandler}
className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6"
>
<p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
Enter your backup key
</p>
<div className="flex flex-row items-center justify-center md:pb-4 mt-4 md:mx-2">
<p className="text-sm flex justify-center text-gray-400 w-max max-w-md">
You can find it in your emergency kit. You had to download the emergency kit during signup.
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
<p className="flex w-max max-w-md justify-center text-sm text-gray-400">
You can find it in your emergency kit. You had to download the emergency kit during
signup.
</p>
</div>
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
@@ -192,12 +205,7 @@ export default function PasswordReset() {
</div>
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button
type="submit"
text="Submit Backup Key"
onButtonPressed={() => {}}
size="lg"
/>
<Button type="submit" text="Submit Backup Key" onButtonPressed={() => {}} size="lg" />
</div>
</div>
</form>
@@ -205,13 +213,16 @@ export default function PasswordReset() {
// Enter new password
const stepEnterNewPassword = (
<form onSubmit={resetPasswordHandler} className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
<form
onSubmit={resetPasswordHandler}
className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6"
>
<p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
Enter new password
</p>
<div className="mt-1 flex flex-row items-center justify-center md:mx-2 md:pb-4">
<p className="flex w-max max-w-md justify-center text-sm text-gray-400">
Make sure you save it somewhere save.
Make sure you save it somewhere safe.
</p>
</div>
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
@@ -221,55 +232,141 @@ export default function PasswordReset() {
setNewPassword(password);
passwordCheck({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false
setPasswordErrorTooShort,
setPasswordErrorTooLong,
setPasswordErrorNoLetterChar,
setPasswordErrorNoNumOrSpecialChar,
setPasswordErrorRepeatedChar,
setPasswordErrorEscapeChar,
setPasswordErrorLowEntropy,
setPasswordErrorBreached
});
}}
type="password"
value={newPassword}
isRequired
error={passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber}
error={
passwordErrorTooShort &&
passwordErrorTooLong &&
passwordErrorNoLetterChar &&
passwordErrorNoNumOrSpecialChar &&
passwordErrorRepeatedChar &&
passwordErrorEscapeChar &&
passwordErrorLowEntropy &&
passwordErrorBreached
}
autoComplete="new-password"
id="new-password"
/>
</div>
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
{passwordErrorTooShort ||
passwordErrorTooLong ||
passwordErrorNoLetterChar ||
passwordErrorNoNumOrSpecialChar ||
passwordErrorRepeatedChar ||
passwordErrorEscapeChar ||
passwordErrorLowEntropy ||
passwordErrorBreached ? (
<div className="mx-2 mt-3 mb-2 flex w-full max-w-md flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-1 text-sm text-gray-400">Password should contain at least:</div>
<div className="mb-1 text-sm text-gray-400">Password should contain:</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLength ? (
{passwordErrorTooShort ? (
<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`}>
14 characters
<div className={`${passwordErrorTooShort ? "text-gray-400" : "text-gray-600"} text-sm`}>
at least 14 characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLowerCase ? (
{passwordErrorTooLong ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorTooLong ? "text-gray-400" : "text-gray-600"} text-sm`}>
at most 100 characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNoLetterChar ? (
<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`}
className={`${passwordErrorNoLetterChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
1 lowercase character
at least 1 letter character
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNumber ? (
{passwordErrorNoNumOrSpecialChar ? (
<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`}>
1 number
<div
className={`${passwordErrorNoNumOrSpecialChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
at least 1 number or special character
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorRepeatedChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorRepeatedChar ? "text-gray-400" : "text-gray-600"
} text-sm`}
>
at most 3 repeated, consecutive characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorEscapeChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorEscapeChar ? "text-gray-400" : "text-gray-600"
} text-sm`}
>
No escape characters allowed.
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLowEntropy ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorLowEntropy ? "text-gray-400" : "text-gray-600"} text-sm`}
>
Password contains sensitive data.
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorBreached ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorBreached ? "text-gray-400" : "text-gray-600"
} text-sm`}
>
Password was found in a data breach.
</div>
</div>
</div>
) : (
<div className="py-2" />

View File

@@ -16,36 +16,30 @@ import { encodeBase64 } from "tweetnacl-util";
import Button from "@app/components/basic/buttons/Button";
import InputField from "@app/components/basic/InputField";
import checkPassword from "@app/components/utilities/checks/checkPassword";
import checkPassword from "@app/components/utilities/checks/password/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";
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient";
import {
useGetCommonPasswords
} from "@app/hooks/api";
import {
completeAccountSignupInvite,
verifySignupInvite
} from "@app/hooks/api/auth/queries";
import { completeAccountSignupInvite, verifySignupInvite } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
// 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,
tooShort?: string;
tooLong?: string;
noLetterChar?: string;
noNumOrSpecialChar?: string;
repeatedChar?: string;
escapeChar?: string;
lowEntropy?: string;
breached?: string;
};
export default function SignupInvite() {
const { data: commonPasswords } = useGetCommonPasswords();
const [password, setPassword] = useState("");
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
@@ -79,10 +73,9 @@ export default function SignupInvite() {
} else {
setLastNameError(false);
}
errorCheck = checkPassword({
errorCheck = await checkPassword({
password,
commonPasswords,
setErrors
});
@@ -116,7 +109,7 @@ export default function SignupInvite() {
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
@@ -127,7 +120,7 @@ export default function SignupInvite() {
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
@@ -138,10 +131,8 @@ export default function SignupInvite() {
text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash)
});
const {
token: jwtToken
} = await completeAccountSignupInvite({
const { token: jwtToken } = await completeAccountSignupInvite({
email,
firstName,
lastName,
@@ -155,20 +146,20 @@ export default function SignupInvite() {
salt: result.salt,
verifier: result.verifier
});
// unset temporary signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(jwtToken);
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey
});
const userOrgs = await fetchOrganizations();
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem("orgData.id", orgId);
@@ -188,12 +179,12 @@ export default function SignupInvite() {
// Step 4 of the sign up process (download the emergency kit pdf)
const stepConfirmEmail = (
<div className="border border-mineshaft-600 bg-mineshaft-800 flex flex-col items-center w-full max-w-xs md:max-w-lg h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
<p className="text-4xl text-center font-semibold mb-6 flex justify-center text-primary-100">
<div className="h-7/12 mx-1 mb-36 flex w-full max-w-xs flex-col items-center rounded-xl border border-mineshaft-600 bg-mineshaft-800 py-8 px-4 drop-shadow-xl md:mb-16 md:max-w-lg md:px-6">
<p className="mb-6 flex justify-center text-center text-4xl font-semibold text-primary-100">
Confirm your email
</p>
<Image src="/images/dragon-signupinvite.svg" height={262} width={410} alt="verify email" />
<div className="flex flex-col items-center justify-center md:p-2 max-h-24 max-w-md mx-auto text-lg px-4 mt-10 mb-2">
<div className="mx-auto mt-10 mb-2 flex max-h-24 max-w-md flex-col items-center justify-center px-4 text-lg md:p-2">
<Button
text="Confirm Email"
onButtonPressed={async () => {
@@ -229,11 +220,11 @@ export default function SignupInvite() {
// Because this is the invite signup - we directly go to the last step of signup (email is already verified)
const main = (
<div className="border border-mineshaft-600 bg-mineshaft-800 w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-32 md:mb-16">
<p className="text-4xl font-bold flex justify-center mb-6 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-tr from-mineshaft-300 to-white">
<div className="h-7/12 mx-auto mb-32 w-max rounded-xl border border-mineshaft-600 bg-mineshaft-800 py-10 px-8 drop-shadow-xl md:mb-16">
<p className="mx-8 mb-6 flex justify-center bg-gradient-to-tr from-mineshaft-300 to-white bg-clip-text text-4xl font-bold text-transparent md:mx-16">
Almost there!
</p>
<div className="relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24">
<div className="relative z-0 flex max-h-24 w-full items-center justify-end rounded-lg md:p-2">
<InputField
label="First Name"
onChangeHandler={setFirstName}
@@ -245,7 +236,7 @@ export default function SignupInvite() {
autoComplete="given-name"
/>
</div>
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24">
<div className="flex max-h-24 w-full items-center justify-center rounded-lg md:p-2">
<InputField
label="Last Name"
onChangeHandler={setLastName}
@@ -257,14 +248,13 @@ export default function SignupInvite() {
autoComplete="family-name"
/>
</div>
<div className="mt-2 flex flex-col items-center justify-center w-full md:p-2 rounded-lg max-h-60">
<div className="mt-2 flex max-h-60 w-full flex-col items-center justify-center rounded-lg md:p-2">
<InputField
label="Password"
onChangeHandler={(pass) => {
setPassword(pass);
checkPassword({
password: pass,
commonPasswords,
setErrors
});
}}
@@ -276,31 +266,26 @@ export default function SignupInvite() {
id="new-password"
/>
{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 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="items-top ml-1 flex flex-row justify-start" key={key}>
<div>
<FontAwesomeIcon icon={faXmark} className="text-md ml-0.5 mr-2.5 text-red" />
</div>
);
}
<p className="text-sm text-gray-400">{errors[key as keyof Errors]}</p>
</div>
);
}
return null;
})}
</div>
)}
return null;
})}
</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="mx-auto mt-2 flex max-h-24 max-w-max flex-col items-center justify-center px-2 py-3 text-lg md:px-4 md:py-5">
<Button
text="Sign Up"
onButtonPressed={() => {
@@ -315,21 +300,21 @@ export default function SignupInvite() {
// Step 4 of the sign up process (download the emergency kit pdf)
const step4 = (
<div className="border border-mineshaft-600 bg-mineshaft-800 flex flex-col items-center w-full max-w-xs md:max-w-lg h-7/12 pt-8 pb-6 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
<p className="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-white to-mineshaft-300">
<div className="h-7/12 mx-1 mb-36 flex w-full max-w-xs flex-col items-center rounded-xl border border-mineshaft-600 bg-mineshaft-800 px-4 pt-8 pb-6 drop-shadow-xl md:mb-16 md:max-w-lg md:px-6">
<p className="flex justify-center bg-gradient-to-br from-white to-mineshaft-300 bg-clip-text text-center text-4xl font-semibold text-transparent">
Save your Emergency Kit
</p>
<div className="flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2">
<div className="text-md mt-4 flex w-full max-w-md flex-col items-center justify-center rounded-md px-2 text-gray-400 md:mt-8">
<div>
If you get locked out of your account, your Emergency Kit is the only way to sign in.
</div>
<div className="mt-3">We recommend you download it and keep it somewhere safe.</div>
</div>
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-400 rounded-md max-w-xs md:max-w-md mx-auto mt-4">
<div className="mx-auto mt-4 flex w-full max-w-xs flex-row items-center rounded-md bg-white/10 p-2 text-gray-400 md:max-w-md">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
It contains your Secret Key which we cannot access or recover for you if you lose it.
</div>
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-4 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
<div className="mx-auto mt-4 flex max-h-24 max-w-max flex-col items-center justify-center px-2 py-3 text-lg md:px-4 md:py-5">
<Button
text="Download PDF"
onButtonPressed={async () => {
@@ -349,7 +334,7 @@ export default function SignupInvite() {
);
return (
<div className="bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 h-screen flex flex-col items-center justify-center">
<div className="flex h-screen flex-col items-center justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Head>
<title>Sign Up</title>
<link rel="icon" href="/infisical.ico" />

View File

@@ -1,5 +1,5 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -8,159 +8,142 @@ import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import attemptChangePassword from "@app/components/utilities/attemptChangePassword";
import checkPassword from "@app/components/utilities/checks/checkPassword";
import {
Button,
FormControl,
Input
} from "@app/components/v2";
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
import { Button, FormControl, Input } from "@app/components/v2";
import { useUser } from "@app/context";
import { useGetCommonPasswords } from "@app/hooks/api";
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
tooShort?: string;
tooLong?: string;
noLetterChar?: string;
noNumOrSpecialChar?: string;
repeatedChar?: string;
escapeChar?: string;
lowEntropy?: string;
breached?: string;
};
const schema = yup.object({
const schema = yup
.object({
oldPassword: yup.string().required("Old password is required"),
newPassword: yup.string().required("New password is required")
}).required();
})
.required();
export type FormData = yup.InferType<typeof schema>;
export const ChangePasswordSection = () => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const { user } = useUser();
const { data: commonPasswords } = useGetCommonPasswords();
const { reset, control, handleSubmit } = useForm({
defaultValues: {
oldPassword: "",
newPassword: ""
},
resolver: yupResolver(schema)
});
const [errors, setErrors] = useState<Errors>({});
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const { user } = useUser();
const { reset, control, handleSubmit } = useForm({
defaultValues: {
oldPassword: "",
newPassword: ""
},
resolver: yupResolver(schema)
});
const [errors, setErrors] = useState<Errors>({});
const [isLoading, setIsLoading] = useState(false);
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
try {
if (!user?.email) return;
if (!commonPasswords) return;
const errorCheck = checkPassword({
password: newPassword,
commonPasswords,
setErrors
});
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
try {
if (!user?.email) return;
if (errorCheck) return;
setIsLoading(true);
await attemptChangePassword({
email: user.email,
currentPassword: oldPassword,
newPassword
});
setIsLoading(false);
createNotification({
text: "Successfully changed password",
type: "success"
});
const errorCheck = await checkPassword({
password: newPassword,
setErrors
});
reset();
window.location.href = "/login";
} catch (err) {
console.error(err);
setIsLoading(false);
createNotification({
text: "Failed to change password",
type: "error"
});
}
if (errorCheck) return;
setIsLoading(true);
await attemptChangePassword({
email: user.email,
currentPassword: oldPassword,
newPassword
});
setIsLoading(false);
createNotification({
text: "Successfully changed password",
type: "success"
});
reset();
window.location.href = "/login";
} catch (err) {
console.error(err);
setIsLoading(false);
createNotification({
text: "Failed to change password",
type: "error"
});
}
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
Change password
</h2>
<div className="max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
placeholder="Old password"
type="password"
{...field}
className="bg-mineshaft-800"
/>
</FormControl>
)}
control={control}
name="oldPassword"
/>
</div>
<div className="max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
placeholder="New password"
type="password"
{...field}
className="bg-mineshaft-800"
/>
</FormControl>
)}
control={control}
name="newPassword"
/>
</div>
{Object.keys(errors).length > 0 && (
<div className="my-4 max-w-md flex 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;
})}
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Change password</h2>
<div className="max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
placeholder="Old password"
type="password"
{...field}
className="bg-mineshaft-800"
/>
</FormControl>
)}
control={control}
name="oldPassword"
/>
</div>
<div className="max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
placeholder="New password"
type="password"
{...field}
className="bg-mineshaft-800"
/>
</FormControl>
)}
control={control}
name="newPassword"
/>
</div>
{Object.keys(errors).length > 0 && (
<div className="my-4 flex max-w-md 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="items-top ml-1 flex flex-row justify-start" key={key}>
<div>
<FontAwesomeIcon icon={faXmark} className="text-md ml-0.5 mr-2.5 text-red" />
</div>
<p className="text-sm text-gray-400">{errors[key as keyof Errors]}</p>
</div>
)}
<Button
type="submit"
colorSchema="secondary"
isLoading={isLoading}
isDisabled={isLoading}
>
Save
</Button>
</form>
);
}
);
}
return null;
})}
</div>
)}
<Button type="submit" colorSchema="secondary" isLoading={isLoading} isDisabled={isLoading}>
Save
</Button>
</form>
);
};

View File

@@ -1,4 +1,3 @@
import crypto from "crypto";
import React, { useEffect, useState } from "react";
@@ -10,13 +9,12 @@ import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util";
import InputField from "@app/components/basic/InputField";
import checkPassword from "@app/components/utilities/checks/checkPassword";
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input } from "@app/components/v2";
import { useGetCommonPasswords } from "@app/hooks/api";
import { completeAccountSignup } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import ProjectService from "@app/services/ProjectService";
@@ -25,22 +23,24 @@ import ProjectService from "@app/services/ProjectService";
const client = new jsrp.client();
type Props = {
setStep: (step: number) => void;
email: string;
password: string;
setPassword: (value: string) => void;
name: string;
providerOrganizationName: string;
providerAuthToken?: string;
}
setStep: (step: number) => void;
email: string;
password: string;
setPassword: (value: string) => void;
name: string;
providerOrganizationName: string;
providerAuthToken?: string;
};
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
tooShort?: string;
tooLong?: string;
noLetterChar?: string;
noNumOrSpecialChar?: string;
repeatedChar?: string;
escapeChar?: string;
lowEntropy?: string;
breached?: string;
};
/**
@@ -63,9 +63,8 @@ export const UserInfoSSOStep = ({
password,
setPassword,
setStep,
providerAuthToken,
providerAuthToken
}: Props) => {
const { data: commonPasswords } = useGetCommonPasswords();
const [nameError, setNameError] = useState(false);
const [organizationName, setOrganizationName] = useState("");
const [organizationNameError, setOrganizationNameError] = useState(false);
@@ -97,10 +96,9 @@ export const UserInfoSSOStep = ({
} else {
setOrganizationNameError(false);
}
errorCheck = checkPassword({
errorCheck = await checkPassword({
password,
commonPasswords,
setErrors
});
@@ -210,15 +208,17 @@ export const UserInfoSSOStep = ({
setIsLoading(false);
}
};
return (
<div className="h-full mx-auto mb-36 w-max rounded-xl md:px-8 md:mb-16">
<p className="mx-8 mb-6 flex justify-center text-xl font-bold text-medium md:mx-16 text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200">
<div className="mx-auto mb-36 h-full w-max rounded-xl md:mb-16 md:px-8">
<p className="text-medium mx-8 mb-6 flex justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-xl font-bold text-transparent md:mx-16">
{t("signup.step3-message")}
</p>
<div className="h-full mx-auto mb-36 w-max rounded-xl py-6 md:px-8 md:mb-16 md:border md:border-mineshaft-600 md:bg-mineshaft-800">
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Your Name</p>
<div className="mx-auto mb-36 h-full w-max rounded-xl py-6 md:mb-16 md:border md:border-mineshaft-600 md:bg-mineshaft-800 md:px-8">
<div className="relative z-0 flex w-1/4 w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Your Name
</p>
<Input
placeholder="Jane Doe"
value={name}
@@ -227,11 +227,17 @@ export const UserInfoSSOStep = ({
autoComplete="given-name"
className="h-12"
/>
{nameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your name</p>}
{nameError && (
<p className="mt-1 ml-1 w-full text-left text-xs text-red-600">
Please, specify your name
</p>
)}
</div>
{providerOrganizationName === undefined && (
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Organization Name</p>
<div className="relative z-0 flex w-1/4 w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Organization Name
</p>
<Input
placeholder="Infisical"
value={organizationName}
@@ -240,12 +246,18 @@ export const UserInfoSSOStep = ({
className="h-12"
disabled
/>
{organizationNameError && <p className='text-left w-full text-xs text-red-600 mt-1 ml-1'>Please, specify your organization name</p>}
{organizationNameError && (
<p className="mt-1 ml-1 w-full text-left text-xs text-red-600">
Please, specify your organization name
</p>
)}
</div>
)}
{providerOrganizationName === undefined && (
<div className="relative z-0 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg">
<p className='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Where did you hear about us? <span className="font-light">(optional)</span></p>
<div className="relative z-0 flex w-1/4 w-full min-w-[20rem] flex-col items-center justify-end rounded-lg py-2 lg:w-1/6">
<p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Where did you hear about us? <span className="font-light">(optional)</span>
</p>
<Input
placeholder=""
onChange={(e) => setAttributionSource(e.target.value)}
@@ -254,14 +266,13 @@ export const UserInfoSSOStep = ({
/>
</div>
)}
<div className="mt-2 flex lg:w-1/6 w-1/4 min-w-[20rem] max-h-60 w-full flex-col items-center justify-center rounded-lg py-2">
<div className="mt-2 flex max-h-60 w-1/4 w-full min-w-[20rem] flex-col items-center justify-center rounded-lg py-2 lg:w-1/6">
<InputField
label="Infisical Password"
onChangeHandler={(pass: string) => {
onChangeHandler={async (pass: string) => {
setPassword(pass);
checkPassword({
await checkPassword({
password: pass,
commonPasswords,
setErrors
});
}}
@@ -272,26 +283,27 @@ export const UserInfoSSOStep = ({
autoComplete="new-password"
id="new-password"
/>
<div className="mt-2 w-min min-w-[20rem] max-h-60 flex-col items-center justify-center rounded-md px-1.5 bg-mineshaft-500 text-mineshaft-300 text-xs p-1.5"><FontAwesomeIcon icon={faInfoCircle} className="mr-1.5" />Infisical Password is used as part of the encryption mechanism so that even the authentication provider is not able to access your secrets.</div>
<div className="mt-2 max-h-60 w-min min-w-[20rem] flex-col items-center justify-center rounded-md bg-mineshaft-500 p-1.5 px-1.5 text-xs text-mineshaft-300">
<FontAwesomeIcon icon={faInfoCircle} className="mr-1.5" />
Infisical Password is used as part of the encryption mechanism so that even the
authentication provider is not able to access your secrets.
</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>
<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 className="items-top ml-1 flex flex-row justify-start" key={key}>
<div>
<FontAwesomeIcon
icon={faXmark}
className="text-md text-red ml-0.5 mr-2.5"
<FontAwesomeIcon
icon={faXmark}
className="text-md ml-0.5 mr-2.5 text-red"
/>
</div>
<p className="text-gray-400 text-sm">
{errors[key as keyof Errors]}
</p>
<p className="text-sm text-gray-400">{errors[key as keyof Errors]}</p>
</div>
);
}
@@ -301,21 +313,24 @@ export const UserInfoSSOStep = ({
</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="text-l py-1 text-lg w-full">
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
<div className="text-l w-full py-1 text-lg">
<Button
type="submit"
onClick={signupErrorCheck}
size="sm"
isFullWidth
className='h-12'
className="h-12"
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
> {String(t("signup.signup"))} </Button>
>
{" "}
{String(t("signup.signup"))}{" "}
</Button>
</div>
</div>
</div>
</div>
);
}
};