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
+85 -96
View File
@@ -1,32 +1,20 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import fs from "fs";
import path from "path";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import * as bigintConversion from "bigint-conversion"; import * as bigintConversion from "bigint-conversion";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require("jsrp"); const jsrp = require("jsrp");
import { import { LoginSRPDetail, TokenVersion, User } from "../../models";
LoginSRPDetail,
TokenVersion,
User,
} from "../../models";
import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth"; import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth";
import { checkUserDevice } from "../../helpers/user"; import { checkUserDevice } from "../../helpers/user";
import { import { ACTION_LOGIN, ACTION_LOGOUT } from "../../variables";
ACTION_LOGIN, import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
ACTION_LOGOUT,
} from "../../variables";
import {
BadRequestError,
UnauthorizedRequestError,
} from "../../utils/errors";
import { EELogService } from "../../ee/services"; import { EELogService } from "../../ee/services";
import { getUserAgentType } from "../../utils/posthog"; import { getUserAgentType } from "../../utils/posthog";
import { import {
getHttpsEnabled, getHttpsEnabled,
getJwtAuthLifetime, getJwtAuthLifetime,
getJwtAuthSecret, getJwtAuthSecret,
getJwtRefreshSecret, getJwtRefreshSecret
} from "../../config"; } from "../../config";
import { ActorType } from "../../ee/models"; import { ActorType } from "../../ee/models";
@@ -44,13 +32,10 @@ declare module "jsonwebtoken" {
* @returns * @returns
*/ */
export const login1 = async (req: Request, res: Response) => { export const login1 = async (req: Request, res: Response) => {
const { const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body;
email,
clientPublicKey,
}: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email, email
}).select("+salt +verifier"); }).select("+salt +verifier");
if (!user) throw new Error("Failed to find user"); if (!user) throw new Error("Failed to find user");
@@ -59,21 +44,25 @@ export const login1 = async (req: Request, res: Response) => {
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier, verifier: user.verifier
}, },
async () => { async () => {
// generate server-side public key // generate server-side public key
const serverPublicKey = server.getPublicKey(); const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: email }, { await LoginSRPDetail.findOneAndReplace(
email: email, { email: email },
clientPublicKey: clientPublicKey, {
serverBInt: bigintConversion.bigintToBuf(server.bInt), email: email,
}, { upsert: true, returnNewDocument: false }) clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
},
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({ return res.status(200).send({
serverPublicKey, 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) => { export const login2 = async (req: Request, res: Response) => {
const { email, clientProof } = req.body; const { email, clientProof } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email, email
}).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag"); }).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag");
if (!user) throw new Error("Failed to find user"); 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) { 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(); const server = new jsrp.server();
@@ -105,7 +98,7 @@ export const login2 = async (req: Request, res: Response) => {
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier, verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt, b: loginSRPDetailFromDB.serverBInt
}, },
async () => { async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
@@ -117,13 +110,13 @@ export const login2 = async (req: Request, res: Response) => {
await checkUserDevice({ await checkUserDevice({
user, user,
ip: req.realIP, ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "", userAgent: req.headers["user-agent"] ?? ""
}); });
const tokens = await issueAuthTokens({ const tokens = await issueAuthTokens({
userId: user._id, userId: user._id,
ip: req.realIP, ip: req.realIP,
userAgent: req.headers["user-agent"] ?? "", userAgent: req.headers["user-agent"] ?? ""
}); });
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
@@ -131,20 +124,21 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",
secure: await getHttpsEnabled(), secure: await getHttpsEnabled()
}); });
const loginAction = await EELogService.createAction({ const loginAction = await EELogService.createAction({
name: ACTION_LOGIN, name: ACTION_LOGIN,
userId: user._id, userId: user._id
}); });
loginAction && await EELogService.createLog({ loginAction &&
userId: user._id, (await EELogService.createLog({
actions: [loginAction], userId: user._id,
channel: getUserAgentType(req.headers["user-agent"]), actions: [loginAction],
ipAddress: req.realIP, channel: getUserAgentType(req.headers["user-agent"]),
}); ipAddress: req.realIP
}));
// return (access) token in response // return (access) token in response
return res.status(200).send({ return res.status(200).send({
@@ -152,12 +146,12 @@ export const login2 = async (req: Request, res: Response) => {
publicKey: user.publicKey, publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey, encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv, iv: user.iv,
tag: user.tag, tag: user.tag
}); });
} }
return res.status(400).send({ 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) => { export const logout = async (req: Request, res: Response) => {
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) { if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId) await clearTokens(req.authData.tokenVersionId);
} }
// clear httpOnly cookie // clear httpOnly cookie
@@ -179,49 +173,44 @@ export const logout = async (req: Request, res: Response) => {
httpOnly: true, httpOnly: true,
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",
secure: (await getHttpsEnabled()) as boolean, secure: (await getHttpsEnabled()) as boolean
}); });
const logoutAction = await EELogService.createAction({ const logoutAction = await EELogService.createAction({
name: ACTION_LOGOUT, name: ACTION_LOGOUT,
userId: req.user._id, userId: req.user._id
}); });
logoutAction && await EELogService.createLog({ logoutAction &&
userId: req.user._id, (await EELogService.createLog({
actions: [logoutAction], userId: req.user._id,
channel: getUserAgentType(req.headers["user-agent"]), actions: [logoutAction],
ipAddress: req.realIP, channel: getUserAgentType(req.headers["user-agent"]),
}); ipAddress: req.realIP
}));
return res.status(200).send({ 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) => { export const revokeAllSessions = async (req: Request, res: Response) => {
await TokenVersion.updateMany({ await TokenVersion.updateMany(
user: req.user._id, {
}, { user: req.user._id
$inc: {
refreshVersion: 1,
accessVersion: 1,
}, },
}); {
$inc: {
refreshVersion: 1,
accessVersion: 1
}
}
);
return res.status(200).send({ return res.status(200).send({
message: "Successfully revoked all sessions.", message: "Successfully revoked all sessions."
}); });
} };
/** /**
* Return user is authenticated * Return user is authenticated
@@ -231,9 +220,9 @@ export const revokeAllSessions = async (req: Request, res: Response) => {
*/ */
export const checkAuth = async (req: Request, res: Response) => { export const checkAuth = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
message: "Authenticated", message: "Authenticated"
}); });
} };
/** /**
* Return new JWT access token by first validating the refresh token * 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) => { export const getNewToken = async (req: Request, res: Response) => {
const refreshToken = req.cookies.jid; const refreshToken = req.cookies.jid;
if (!refreshToken) throw BadRequestError({ if (!refreshToken)
message: "Failed to find refresh token in request cookies" throw BadRequestError({
}); message: "Failed to find refresh token in request cookies"
});
const decodedToken = <jwt.UserIDJwtPayload>( const decodedToken = <jwt.UserIDJwtPayload>jwt.verify(refreshToken, await getJwtRefreshSecret());
jwt.verify(refreshToken, await getJwtRefreshSecret())
);
const user = await User.findOne({ const user = await User.findOne({
_id: decodedToken.userId, _id: decodedToken.userId
}).select("+publicKey +refreshVersion +accessVersion"); }).select("+publicKey +refreshVersion +accessVersion");
if (!user) throw new Error("Failed to authenticate unfound user"); if (!user) throw new Error("Failed to authenticate unfound user");
if (!user?.publicKey) if (!user?.publicKey) throw new Error("Failed to authenticate not fully set up account");
throw new Error("Failed to authenticate not fully set up account");
const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId); const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId);
if (!tokenVersion) throw UnauthorizedRequestError({ if (!tokenVersion)
message: "Failed to validate refresh token", throw UnauthorizedRequestError({
}); message: "Failed to validate refresh token"
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({ if (decodedToken.refreshVersion !== tokenVersion.refreshVersion)
message: "Failed to validate refresh token", throw BadRequestError({
}); message: "Failed to validate refresh token"
});
const token = createToken({ const token = createToken({
payload: { payload: {
userId: decodedToken.userId, userId: decodedToken.userId,
tokenVersionId: tokenVersion._id.toString(), tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.refreshVersion, accessVersion: tokenVersion.refreshVersion
}, },
expiresIn: await getJwtAuthLifetime(), expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret(), secret: await getJwtAuthSecret()
}); });
return res.status(200).send({ return res.status(200).send({
token, token
}); });
}; };
export const handleAuthProviderCallback = (req: Request, res: Response) => { export const handleAuthProviderCallback = (req: Request, res: Response) => {
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`); res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
} };
File diff suppressed because it is too large Load Diff
+25 -12
View File
@@ -24,7 +24,7 @@ import {
secretSnapshot as eeSecretSnapshotRouter, secretSnapshot as eeSecretSnapshotRouter,
users as eeUsersRouter, users as eeUsersRouter,
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter,
secretScanning as v1SecretScanningRouter, secretScanning as v1SecretScanningRouter
} from "./ee/routes/v1"; } from "./ee/routes/v1";
import { import {
auth as v1AuthRouter, auth as v1AuthRouter,
@@ -58,7 +58,7 @@ import {
signup as v2SignupRouter, signup as v2SignupRouter,
tags as v2TagsRouter, tags as v2TagsRouter,
users as v2UsersRouter, users as v2UsersRouter,
workspace as v2WorkspaceRouter, workspace as v2WorkspaceRouter
} from "./routes/v2"; } from "./routes/v2";
import { import {
auth as v3AuthRouter, auth as v3AuthRouter,
@@ -70,14 +70,21 @@ import { healthCheck } from "./routes/status";
import { getLogger } from "./utils/logger"; import { getLogger } from "./utils/logger";
import { RouteNotFoundError } from "./utils/errors"; import { RouteNotFoundError } from "./utils/errors";
import { requestErrorHandler } from "./middleware/requestErrorHandler"; 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 { setup } from "./utils/setup";
import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices"; import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices";
import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent"; 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 () => { const main = async () => {
await setup(); await setup();
await EELicenseService.initGlobalFeatureSet(); 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({ const probot = new Probot({
appId: await getSecretScanningGitAppId(), appId: await getSecretScanningGitAppId(),
privateKey: await getSecretScanningPrivateKey(), privateKey: await getSecretScanningPrivateKey(),
secret: await getSecretScanningWebhookSecret(), secret: await getSecretScanningWebhookSecret()
}); });
if ((await getNodeEnv()) != "production") { if ((await getNodeEnv()) != "production") {
@@ -106,12 +117,14 @@ const main = async () => {
source: await getSecretScanningWebhookProxy(), source: await getSecretScanningWebhookProxy(),
target: "http://backend:4000/ss-webhook", target: "http://backend:4000/ss-webhook",
logger: console 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") { if ((await getNodeEnv()) === "production") {
@@ -207,8 +220,8 @@ const main = async () => {
server.on("close", async () => { server.on("close", async () => {
await DatabaseService.closeDatabase(); await DatabaseService.closeDatabase();
syncSecretsToThirdPartyServices.close() syncSecretsToThirdPartyServices.close();
githubPushEventSecretScan.close() githubPushEventSecretScan.close();
}); });
return server; return server;
+11 -14
View File
@@ -8,7 +8,8 @@ import { AuthMode } from "../../variables";
router.post("/token", validateRequest, authController.getNewToken); 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", "/login1",
authLimiter, authLimiter,
body("email").exists().trim().notEmpty().toLowerCase(), body("email").exists().trim().notEmpty().toLowerCase(),
@@ -17,7 +18,8 @@ router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login1)
authController.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", "/login2",
authLimiter, authLimiter,
body("email").exists().trim().notEmpty().toLowerCase(), body("email").exists().trim().notEmpty().toLowerCase(),
@@ -30,7 +32,7 @@ router.post(
"/logout", "/logout",
authLimiter, authLimiter,
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
authController.logout authController.logout
); );
@@ -38,24 +40,19 @@ router.post(
router.post( router.post(
"/checkAuth", "/checkAuth",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
authController.checkAuth authController.checkAuth
); );
router.get( router.delete(
"/common-passwords", // TODO endpoint: deprecate (moved to DELETE v2/users/me/sessions)
authLimiter,
authController.getCommonPasswords
);
router.delete( // TODO endpoint: deprecate (moved to DELETE v2/users/me/sessions)
"/sessions", "/sessions",
authLimiter, authLimiter,
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT], acceptedAuthModes: [AuthMode.JWT]
}), }),
authController.revokeAllSessions authController.revokeAllSessions
); );
export default router; export default router;
+25 -25
View File
@@ -3,7 +3,7 @@
/** /**
* @type {import('next').NextConfig} * @type {import('next').NextConfig}
**/ **/
const path = require('path'); const path = require("path");
const ContentSecurityPolicy = ` const ContentSecurityPolicy = `
default-src 'self'; default-src 'self';
@@ -11,7 +11,7 @@ const ContentSecurityPolicy = `
style-src 'self' https://rsms.me 'unsafe-inline'; style-src 'self' https://rsms.me 'unsafe-inline';
child-src https://api.stripe.com; child-src https://api.stripe.com;
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.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:; 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; media-src https://js.intercomcdn.com;
font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.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. // after learning more below.
const securityHeaders = [ const securityHeaders = [
{ {
key: 'X-DNS-Prefetch-Control', key: "X-DNS-Prefetch-Control",
value: 'on' value: "on"
}, },
{ {
key: 'Strict-Transport-Security', key: "Strict-Transport-Security",
value: 'max-age=63072000; includeSubDomains; preload' value: "max-age=63072000; includeSubDomains; preload"
}, },
{ {
key: 'X-XSS-Protection', key: "X-XSS-Protection",
value: '1; mode=block' value: "1; mode=block"
}, },
{ {
key: 'X-Frame-Options', key: "X-Frame-Options",
value: 'SAMEORIGIN' value: "SAMEORIGIN"
}, },
{ {
key: 'Permissions-Policy', key: "Permissions-Policy",
value: 'camera=(), microphone=()' value: "camera=(), microphone=()"
}, },
{ {
key: 'X-Content-Type-Options', key: "X-Content-Type-Options",
value: 'nosniff' value: "nosniff"
}, },
{ {
key: 'Referrer-Policy', key: "Referrer-Policy",
value: 'strict-origin-when-cross-origin' value: "strict-origin-when-cross-origin"
}, },
{ {
key: 'Content-Security-Policy', key: "Content-Security-Policy",
value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim() value: ContentSecurityPolicy.replace(/\s{2,}/g, " ").trim()
} }
]; ];
module.exports = { module.exports = {
output: 'standalone', output: "standalone",
i18n: { i18n: {
locales: ['en', 'ko', 'fr', 'pt-BR', 'pt-PT', 'es'], locales: ["en", "ko", "fr", "pt-BR", "pt-PT", "es"],
defaultLocale: 'en' defaultLocale: "en"
}, },
async headers() { async headers() {
return [ return [
{ {
// Apply these headers to all routes in your application. // Apply these headers to all routes in your application.
source: '/:path*', source: "/:path*",
headers: securityHeaders headers: securityHeaders
} }
]; ];
@@ -73,15 +73,15 @@ module.exports = {
// config // config
config.module.rules.push({ config.module.rules.push({
test: /\.wasm$/, test: /\.wasm$/,
loader: 'base64-loader', loader: "base64-loader",
type: 'javascript/auto' type: "javascript/auto"
}); });
config.module.noParse = /\.wasm$/; config.module.noParse = /\.wasm$/;
config.module.rules.forEach((rule) => { config.module.rules.forEach((rule) => {
(rule.oneOf || []).forEach((oneOf) => { (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$/); oneOf.exclude.push(/\.wasm$/);
} }
}); });
+1 -1
View File
@@ -102,7 +102,7 @@
"@storybook/testing-library": "^0.2.0", "@storybook/testing-library": "^0.2.0",
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/typography": "^0.5.4",
"@types/jsrp": "^0.2.4", "@types/jsrp": "^0.2.4",
"@types/node": "18.11.9", "@types/node": "^18.11.9",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/sanitize-html": "^2.9.0", "@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/eslint-plugin": "^5.48.1",
+1 -1
View File
@@ -110,7 +110,7 @@
"@storybook/testing-library": "^0.2.0", "@storybook/testing-library": "^0.2.0",
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/typography": "^0.5.4",
"@types/jsrp": "^0.2.4", "@types/jsrp": "^0.2.4",
"@types/node": "18.11.9", "@types/node": "^18.11.9",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/sanitize-html": "^2.9.0", "@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/eslint-plugin": "^5.48.1",
+9 -4
View File
@@ -231,10 +231,15 @@
"current": "Current password", "current": "Current password",
"current-wrong": "The current password may be wrong", "current-wrong": "The current password may be wrong",
"new": "New password", "new": "New password",
"validate-base": "Password should contain at least:", "validate-base": "Password should contain:",
"validate-length": "14 characters", "validate-tooShort": "at least 14 characters",
"validate-case": "1 lowercase character", "validate-tooLong": "at most 100 characters",
"validate-number": "1 number" "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": { "token": {
"service-tokens": "Service Tokens", "service-tokens": "Service Tokens",
+9 -4
View File
@@ -228,10 +228,15 @@
"current": "Contraseña actual", "current": "Contraseña actual",
"current-wrong": "La contraseña actual puede puede que sea incorrecta", "current-wrong": "La contraseña actual puede puede que sea incorrecta",
"new": "Nueva contraseña", "new": "Nueva contraseña",
"validate-base": "La contraseña debe contener como mínimo:", "validate-base": "La contraseña debe contener:",
"validate-length": "14 caracteres", "validate-tooShort": "al menos 14 caracteres",
"validate-case": "1 letra en minúsculas", "validate-tooLong": "como máximo 100 caracteres",
"validate-number": "1 número" "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": { "token": {
"service-tokens": "Tokens de servicio", "service-tokens": "Tokens de servicio",
+10 -5
View File
@@ -215,10 +215,15 @@
"current": "Mot de passe actuel", "current": "Mot de passe actuel",
"current-wrong": "Le mot de passe actuel peut être érroné", "current-wrong": "Le mot de passe actuel peut être érroné",
"new": "Nouveau mot de passe", "new": "Nouveau mot de passe",
"validate-base": "Le mot de passe doit contenir au moins:", "validate-base": "Le mot de passe doit contenir :",
"validate-length": "14 caractères", "validate-tooShort": "au moins 14 caractères",
"validate-case": "1 caractère miniscule", "validate-tooLong": "au plus 100 caractères",
"validate-number": "1 chiffre" "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": { "token": {
"service-tokens": "Jetons de service", "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-subtitle": "Infisical a pour but d'être utilisé avec vos coéquipiers. Invitez-les à le tester.",
"step5-skip": "Passer" "step5-skip": "Passer"
} }
} }
+10 -5
View File
@@ -182,10 +182,15 @@
"current": "현재 비밀번호", "current": "현재 비밀번호",
"new": "새 비밀번호", "new": "새 비밀번호",
"current-wrong": "현재 비밀번호가 잘못되었어요", "current-wrong": "현재 비밀번호가 잘못되었어요",
"validate-base": "비밀번호는 다음 조건을 만족해야 합니다:", "validate-base": "비밀번호는 다음을 포함해야 합니다:",
"validate-length": "14 글자 이상", "validate-tooShort": "최소 14자",
"validate-case": "1개 이상의 소문자", "validate-tooLong": "최대 100자",
"validate-number": "1개 이상의 숫자" "validate-noLetterChar": "최소 1개의 문자를 포함해야 합니다.",
"validate-noNumOrSpecialChar": "최소 1개의 숫자 또는 특수 문자를 포함해야 합니다.",
"validate-repeatedChar": "연속으로 최대 3개의 반복된 문자를 포함할 수 있습니다.",
"validate-escapeChar": "이스케이프 문자는 허용되지 않습니다.",
"validate-lowEntropy": "비밀번호에 민감한 데이터가 포함되어 있습니다.",
"validate-breached": "비밀번호가 데이터 유출에 포함되었습니다."
}, },
"token": { "token": {
"add-dialog": { "add-dialog": {
@@ -256,4 +261,4 @@
"step4-description3": "분실시 접근하거나 복구할 수 없는 시크릿 키가 포함되어 있어요.", "step4-description3": "분실시 접근하거나 복구할 수 없는 시크릿 키가 포함되어 있어요.",
"step4-download": "PDF 다운로드" "step4-download": "PDF 다운로드"
} }
} }
@@ -210,10 +210,15 @@
"current": "Senha atual", "current": "Senha atual",
"current-wrong": "A senha atual pode estar errada", "current-wrong": "A senha atual pode estar errada",
"new": "Nova Senha", "new": "Nova Senha",
"validate-base": "A senha deve conter pelo menos:", "validate-base": "A senha deve conter:",
"validate-length": "14 caracteres", "validate-tooShort": "pelo menos 14 caracteres",
"validate-case": "1 caractere minúsculo", "validate-tooLong": "no máximo 100 caracteres",
"validate-number": "1 número" "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": { "token": {
"service-tokens": "Tokens de Serviço", "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-subtitle": "Infisical foi feito para ser usado com seus colegas. Convide-os para testar também.",
"step5-skip": "Pular" "step5-skip": "Pular"
} }
} }
+9 -4
View File
@@ -228,10 +228,15 @@
"current": "Mevcut şifre", "current": "Mevcut şifre",
"current-wrong": "Mevcut şifre yanlış olabilir", "current-wrong": "Mevcut şifre yanlış olabilir",
"new": "Yeni şifre", "new": "Yeni şifre",
"validate-base": "Şifre en az şunları içermelidir:", "validate-base": "Parola içermelidir:",
"validate-length": "14 karakter", "validate-tooShort": "en az 14 karakter",
"validate-case": "1 küçük harf", "validate-tooLong": "en fazla 100 karakter",
"validate-number": "1 rakam" "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": { "token": {
"service-tokens": "Servis Belirteçleri", "service-tokens": "Servis Belirteçleri",
+58 -46
View File
@@ -8,13 +8,12 @@ import jsrp from "jsrp";
import nacl from "tweetnacl"; import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util"; import { encodeBase64 } from "tweetnacl-util";
import { useGetCommonPasswords } from "@app/hooks/api";
import { completeAccountSignup } from "@app/hooks/api/auth/queries"; import { completeAccountSignup } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import ProjectService from "@app/services/ProjectService"; import ProjectService from "@app/services/ProjectService";
import InputField from "../basic/InputField"; 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 Aes256Gcm from "../utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "../utilities/cryptography/crypto"; import { deriveArgonKey } from "../utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "../utilities/saveTokenToLocalStorage"; import { saveTokenToLocalStorage } from "../utilities/saveTokenToLocalStorage";
@@ -39,12 +38,14 @@ interface UserInfoStepProps {
} }
type Errors = { type Errors = {
length?: string, tooShort?: string;
upperCase?: string, tooLong?: string;
lowerCase?: string, noLetterChar?: string;
number?: string, noNumOrSpecialChar?: string;
specialChar?: string, repeatedChar?: string;
repeatedChar?: string, escapeChar?: string;
lowEntropy?: string;
breached?: string;
}; };
/** /**
@@ -71,9 +72,8 @@ export default function UserInfoStep({
setOrganizationName, setOrganizationName,
attributionSource, attributionSource,
setAttributionSource, setAttributionSource,
providerAuthToken, providerAuthToken
}: UserInfoStepProps): JSX.Element { }: UserInfoStepProps): JSX.Element {
const { data: commonPasswords } = useGetCommonPasswords();
const [nameError, setNameError] = useState(false); const [nameError, setNameError] = useState(false);
const [organizationNameError, setOrganizationNameError] = useState(false); const [organizationNameError, setOrganizationNameError] = useState(false);
@@ -99,10 +99,9 @@ export default function UserInfoStep({
} else { } else {
setOrganizationNameError(false); setOrganizationNameError(false);
} }
errorCheck = checkPassword({ errorCheck = await checkPassword({
password, password,
commonPasswords,
setErrors setErrors
}); });
@@ -174,7 +173,7 @@ export default function UserInfoStep({
salt: result.salt, salt: result.salt,
verifier: result.verifier, verifier: result.verifier,
organizationName, organizationName,
attributionSource, attributionSource
}); });
// unset signup JWT token and set JWT token // unset signup JWT token and set JWT token
@@ -191,7 +190,7 @@ export default function UserInfoStep({
}); });
const userOrgs = await fetchOrganizations(); const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]?._id; const orgId = userOrgs[0]?._id;
const project = await ProjectService.initProject({ const project = await ProjectService.initProject({
organizationId: orgId, organizationId: orgId,
@@ -215,13 +214,15 @@ export default function UserInfoStep({
}; };
return ( return (
<div className="h-full mx-auto mb-36 w-max rounded-xl md:px-8 md:mb-16"> <div className="mx-auto mb-36 h-full w-max rounded-xl md:mb-16 md:px-8">
<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"> <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")} {t("signup.step3-message")}
</p> </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="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 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg"> <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='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Your Name</p> <p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Your Name
</p>
<Input <Input
placeholder="Jane Doe" placeholder="Jane Doe"
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
@@ -230,10 +231,16 @@ export default function UserInfoStep({
autoComplete="given-name" autoComplete="given-name"
className="h-12" 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>
<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"> <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='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Organization Name</p> <p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Organization Name
</p>
<Input <Input
placeholder="Infisical" placeholder="Infisical"
onChange={(e) => setOrganizationName(e.target.value)} onChange={(e) => setOrganizationName(e.target.value)}
@@ -241,10 +248,16 @@ export default function UserInfoStep({
isRequired isRequired
className="h-12" 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>
<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"> <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='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> <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 <Input
placeholder="" placeholder=""
onChange={(e) => setAttributionSource(e.target.value)} onChange={(e) => setAttributionSource(e.target.value)}
@@ -252,16 +265,15 @@ export default function UserInfoStep({
className="h-12" className="h-12"
/> />
</div> </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 <InputField
label={t("section.password.password")} label={t("section.password.password")}
onChangeHandler={(pass: string) => { onChangeHandler={async (pass: string) => {
setPassword(pass); await checkPassword({
checkPassword({
password: pass, password: pass,
commonPasswords,
setErrors setErrors
}); });
setPassword(pass);
}} }}
type="password" type="password"
value={password} value={password}
@@ -272,23 +284,20 @@ export default function UserInfoStep({
/> />
{Object.keys(errors).length > 0 && ( {Object.keys(errors).length > 0 && (
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2"> <div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-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) => { {Object.keys(errors).map((key) => {
if (errors[key as keyof Errors]) { if (errors[key as keyof Errors]) {
return ( return (
<div <div className="items-top ml-1 flex flex-row justify-start" key={key}>
className="ml-1 flex flex-row items-top justify-start"
key={key}
>
<div> <div>
<FontAwesomeIcon <FontAwesomeIcon
icon={faXmark} icon={faXmark}
className="text-md text-red ml-0.5 mr-2.5" className="text-md ml-0.5 mr-2.5 text-red"
/> />
</div> </div>
<p className="text-gray-400 text-sm"> <p className="text-sm text-gray-400">{errors[key as keyof Errors]}</p>
{errors[key as keyof Errors]}
</p>
</div> </div>
); );
} }
@@ -298,18 +307,21 @@ export default function UserInfoStep({
</div> </div>
)} )}
</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="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 py-1 text-lg w-full"> <div className="text-l w-full py-1 text-lg">
<Button <Button
type="submit" type="submit"
onClick={signupErrorCheck} onClick={signupErrorCheck}
size="sm" size="sm"
isFullWidth isFullWidth
className='h-14' className="h-14"
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
isLoading={isLoading} isLoading={isLoading}
> {String(t("signup.signup"))} </Button> >
{" "}
{String(t("signup.signup"))}{" "}
</Button>
</div> </div>
</div> </div>
</div> </div>
@@ -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;
@@ -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;
@@ -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;
@@ -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);
}
}
};
@@ -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;
@@ -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/,
];
+3 -3
View File
@@ -1,10 +1,10 @@
export { export {
useGetAuthToken, useGetAuthToken,
useGetCommonPasswords,
useResetPassword, useResetPassword,
useSendMfaToken, useSendMfaToken,
useSendPasswordResetEmail, useSendPasswordResetEmail,
useSendVerificationEmail, useSendVerificationEmail,
useVerifyEmailVerificationCode, useVerifyEmailVerificationCode,
useVerifyMfaToken, useVerifyMfaToken,
useVerifyPasswordResetCode} from "./queries" useVerifyPasswordResetCode
} from "./queries";
+55 -85
View File
@@ -20,22 +20,22 @@ import {
SRPR1Res, SRPR1Res,
VerifyMfaTokenDTO, VerifyMfaTokenDTO,
VerifyMfaTokenRes, VerifyMfaTokenRes,
VerifySignupInviteDTO} from "./types"; VerifySignupInviteDTO
} from "./types";
const authKeys = { const authKeys = {
getAuthToken: ["token"] as const, getAuthToken: ["token"] as const
commonPasswords: ["common-passwords"] as const
}; };
export const login1 = async (loginDetails: Login1DTO) => { export const login1 = async (loginDetails: Login1DTO) => {
const { data } = await apiRequest.post<Login1Res>("/api/v3/auth/login1", loginDetails); const { data } = await apiRequest.post<Login1Res>("/api/v3/auth/login1", loginDetails);
return data; return data;
} };
export const login2 = async (loginDetails: Login2DTO) => { export const login2 = async (loginDetails: Login2DTO) => {
const { data } = await apiRequest.post<Login2Res>("/api/v3/auth/login2", loginDetails); const { data } = await apiRequest.post<Login2Res>("/api/v3/auth/login2", loginDetails);
return data; return data;
} };
export const useLogin1 = () => { export const useLogin1 = () => {
return useMutation({ return useMutation({
@@ -47,7 +47,7 @@ export const useLogin1 = () => {
return login1(details); return login1(details);
} }
}); });
} };
export const useLogin2 = () => { export const useLogin2 = () => {
return useMutation({ return useMutation({
@@ -59,22 +59,22 @@ export const useLogin2 = () => {
return login2(details); return login2(details);
} }
}); });
} };
export const srp1 = async (details: SRP1DTO) => { export const srp1 = async (details: SRP1DTO) => {
const { data } = await apiRequest.post<SRPR1Res>("/api/v1/password/srp1", details); const { data } = await apiRequest.post<SRPR1Res>("/api/v1/password/srp1", details);
return data; return data;
} };
export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => { export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => {
const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details); const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details);
return data; return data;
} };
export const completeAccountSignupInvite = async (details: CompleteAccountDTO) => { export const completeAccountSignupInvite = async (details: CompleteAccountDTO) => {
const { data } = await apiRequest.post("/api/v2/signup/complete-account/invite", details); const { data } = await apiRequest.post("/api/v2/signup/complete-account/invite", details);
return data; return data;
} };
export const useCompleteAccountSignup = () => { export const useCompleteAccountSignup = () => {
return useMutation({ return useMutation({
@@ -82,7 +82,7 @@ export const useCompleteAccountSignup = () => {
return completeAccountSignup(details); return completeAccountSignup(details);
} }
}); });
} };
export const useSendMfaToken = () => { export const useSendMfaToken = () => {
return useMutation<{}, {}, SendMfaTokenDTO>({ return useMutation<{}, {}, SendMfaTokenDTO>({
@@ -91,22 +91,16 @@ export const useSendMfaToken = () => {
return data; return data;
} }
}); });
} };
export const verifyMfaToken = async ({ export const verifyMfaToken = async ({ email, mfaCode }: { email: string; mfaCode: string }) => {
email,
mfaCode
}: {
email: string;
mfaCode: string;
}) => {
const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", { const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", {
email, email,
mfaToken: mfaCode mfaToken: mfaCode
}); });
return data; return data;
} };
export const useVerifyMfaToken = () => { export const useVerifyMfaToken = () => {
return useMutation<VerifyMfaTokenRes, {}, VerifyMfaTokenDTO>({ return useMutation<VerifyMfaTokenRes, {}, VerifyMfaTokenDTO>({
@@ -117,87 +111,67 @@ export const useVerifyMfaToken = () => {
}); });
} }
}); });
} };
export const verifySignupInvite = async (details: VerifySignupInviteDTO) => { export const verifySignupInvite = async (details: VerifySignupInviteDTO) => {
const { data } = await apiRequest.post("/api/v1/invite-org/verify", details); const { data } = await apiRequest.post("/api/v1/invite-org/verify", details);
return data; return data;
} };
export const useSendVerificationEmail = () => { export const useSendVerificationEmail = () => {
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({ email }: { email: string }) => {
email
}: {
email: string;
}) => {
const { data } = await apiRequest.post("/api/v1/signup/email/signup", { const { data } = await apiRequest.post("/api/v1/signup/email/signup", {
email email
}); });
return data; return data;
} }
}); });
} };
export const useVerifyEmailVerificationCode = () => { export const useVerifyEmailVerificationCode = () => {
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({ email, code }: { email: string; code: string }) => {
email,
code
}: {
email: string;
code: string;
}) => {
const { data } = await apiRequest.post("/api/v1/signup/email/verify", { const { data } = await apiRequest.post("/api/v1/signup/email/verify", {
email, email,
code code
}); });
return data; return data;
} }
}); });
} };
export const useSendPasswordResetEmail = () => { export const useSendPasswordResetEmail = () => {
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({ email }: { email: string }) => {
email
}: {
email: string;
}) => {
const { data } = await apiRequest.post("/api/v1/password/email/password-reset", { const { data } = await apiRequest.post("/api/v1/password/email/password-reset", {
email email
}); });
return data; return data;
} }
}); });
} };
export const useVerifyPasswordResetCode = () => { export const useVerifyPasswordResetCode = () => {
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({ email, code }: { email: string; code: string }) => {
email,
code
}: {
email: string;
code: string;
}) => {
const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", { const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", {
email, email,
code code
}); });
return data; return data;
} }
}); });
} };
export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => { export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => {
const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details); const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details);
return data; return data;
} };
export const getBackupEncryptedPrivateKey = async ({ export const getBackupEncryptedPrivateKey = async ({
verificationToken verificationToken
@@ -207,37 +181,41 @@ export const getBackupEncryptedPrivateKey = async ({
Authorization: `Bearer ${verificationToken}` Authorization: `Bearer ${verificationToken}`
} }
}); });
return data.backupPrivateKey; return data.backupPrivateKey;
} };
export const useResetPassword = () => { export const useResetPassword = () => {
return useMutation({ return useMutation({
mutationFn: async (details: ResetPasswordDTO) => { mutationFn: async (details: ResetPasswordDTO) => {
const { data } = await apiRequest.post("/api/v1/password/password-reset", { const { data } = await apiRequest.post(
protectedKey: details.protectedKey, "/api/v1/password/password-reset",
protectedKeyIV: details.protectedKeyIV, {
protectedKeyTag: details.protectedKeyTag, protectedKey: details.protectedKey,
encryptedPrivateKey: details.encryptedPrivateKey, protectedKeyIV: details.protectedKeyIV,
encryptedPrivateKeyIV: details.encryptedPrivateKeyIV, protectedKeyTag: details.protectedKeyTag,
encryptedPrivateKeyTag: details.encryptedPrivateKeyTag, encryptedPrivateKey: details.encryptedPrivateKey,
salt: details.salt, encryptedPrivateKeyIV: details.encryptedPrivateKeyIV,
verifier: details.verifier encryptedPrivateKeyTag: details.encryptedPrivateKeyTag,
}, { salt: details.salt,
headers: { verifier: details.verifier
Authorization: `Bearer ${details.verificationToken}` },
{
headers: {
Authorization: `Bearer ${details.verificationToken}`
}
} }
}); );
return data; return data;
} }
}); });
} };
export const changePassword = async (details: ChangePasswordDTO) => { export const changePassword = async (details: ChangePasswordDTO) => {
const { data } = await apiRequest.post("/api/v1/password/change-password", details); const { data } = await apiRequest.post("/api/v1/password/change-password", details);
return data; return data;
} };
export const useChangePassword = () => { export const useChangePassword = () => {
// note: use after srp1 // note: use after srp1
@@ -246,7 +224,7 @@ export const useChangePassword = () => {
return changePassword(details); return changePassword(details);
} }
}); });
} };
// Refresh token is set as cookie when logged in // Refresh token is set as cookie when logged in
// Using that we fetch the auth bearer token needed for auth calls // Using that we fetch the auth bearer token needed for auth calls
@@ -263,11 +241,3 @@ export const useGetAuthToken = () =>
onSuccess: (data) => setAuthToken(data.token), onSuccess: (data) => setAuthToken(data.token),
retry: 0 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 });
+141 -44
View File
@@ -10,9 +10,9 @@ import queryString from "query-string";
import Button from "@app/components/basic/buttons/Button"; import Button from "@app/components/basic/buttons/Button";
import InputField from "@app/components/basic/InputField"; import InputField from "@app/components/basic/InputField";
import 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 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 { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries";
import { deriveArgonKey } from "../components/utilities/cryptography/crypto"; import { deriveArgonKey } from "../components/utilities/cryptography/crypto";
@@ -28,25 +28,30 @@ export default function PasswordReset() {
const [privateKey, setPrivateKey] = useState(""); const [privateKey, setPrivateKey] = useState("");
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
const [backupKeyError, setBackupKeyError] = useState(false); const [backupKeyError, setBackupKeyError] = useState(false);
const [passwordErrorLength, setPasswordErrorLength] = useState(false); const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = 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 router = useRouter();
const { mutateAsync: verifyPasswordResetCodeMutateAsync } = useVerifyPasswordResetCode(); const { mutateAsync: verifyPasswordResetCodeMutateAsync } = useVerifyPasswordResetCode();
const { mutateAsync: resetPasswordMutateAsync } = useResetPassword(); const { mutateAsync: resetPasswordMutateAsync } = useResetPassword();
const parsedUrl = queryString.parse(router.asPath.split("?")[1]); const parsedUrl = queryString.parse(router.asPath.split("?")[1]);
const token = parsedUrl.token as string; const token = parsedUrl.token as string;
const email = (parsedUrl.to as string)?.replace(" ", "+").trim(); 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>) => { const getEncryptedKeyHandler = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
try { try {
const result = await getBackupEncryptedPrivateKey({ verificationToken }); const result = await getBackupEncryptedPrivateKey({ verificationToken });
setPrivateKey( setPrivateKey(
Aes256Gcm.decrypt({ Aes256Gcm.decrypt({
ciphertext: result.encryptedPrivateKey, ciphertext: result.encryptedPrivateKey,
@@ -56,7 +61,7 @@ export default function PasswordReset() {
}) })
); );
setStep(3); setStep(3);
} catch(err) { } catch (err) {
console.error(err); console.error(err);
setBackupKeyError(true); setBackupKeyError(true);
} }
@@ -65,12 +70,16 @@ export default function PasswordReset() {
// If everything is correct, reset the password // If everything is correct, reset the password
const resetPasswordHandler = async (e: FormEvent<HTMLFormElement>) => { const resetPasswordHandler = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
const errorCheck = passwordCheck({ const errorCheck = await passwordCheck({
password: newPassword, password: newPassword,
setPasswordErrorLength, setPasswordErrorTooShort,
setPasswordErrorNumber, setPasswordErrorTooLong,
setPasswordErrorLowerCase, setPasswordErrorNoLetterChar,
errorCheck: false setPasswordErrorNoNumOrSpecialChar,
setPasswordErrorRepeatedChar,
setPasswordErrorEscapeChar,
setPasswordErrorLowEntropy,
setPasswordErrorBreached
}); });
if (!errorCheck) { if (!errorCheck) {
@@ -127,10 +136,10 @@ export default function PasswordReset() {
verifier: result.verifier, verifier: result.verifier,
verificationToken verificationToken
}); });
router.push("/login"); router.push("/login");
setLoading(false) setLoading(false);
}); });
} }
); );
@@ -169,13 +178,17 @@ export default function PasswordReset() {
// Input backup key // Input backup key
const stepInputBackupKey = ( 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"> <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 Enter your backup key
</p> </p>
<div className="flex flex-row items-center justify-center md:pb-4 mt-4 md:mx-2"> <div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
<p className="text-sm flex justify-center text-gray-400 w-max max-w-md"> <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. You can find it in your emergency kit. You had to download the emergency kit during
signup.
</p> </p>
</div> </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"> <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>
<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="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"> <div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button <Button type="submit" text="Submit Backup Key" onButtonPressed={() => {}} size="lg" />
type="submit"
text="Submit Backup Key"
onButtonPressed={() => {}}
size="lg"
/>
</div> </div>
</div> </div>
</form> </form>
@@ -205,13 +213,16 @@ export default function PasswordReset() {
// Enter new password // Enter new password
const stepEnterNewPassword = ( 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"> <p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
Enter new password Enter new password
</p> </p>
<div className="mt-1 flex flex-row items-center justify-center md:mx-2 md:pb-4"> <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"> <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> </p>
</div> </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"> <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); setNewPassword(password);
passwordCheck({ passwordCheck({
password, password,
setPasswordErrorLength, setPasswordErrorTooShort,
setPasswordErrorNumber, setPasswordErrorTooLong,
setPasswordErrorLowerCase, setPasswordErrorNoLetterChar,
errorCheck: false setPasswordErrorNoNumOrSpecialChar,
setPasswordErrorRepeatedChar,
setPasswordErrorEscapeChar,
setPasswordErrorLowEntropy,
setPasswordErrorBreached
}); });
}} }}
type="password" type="password"
value={newPassword} value={newPassword}
isRequired isRequired
error={passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber} error={
passwordErrorTooShort &&
passwordErrorTooLong &&
passwordErrorNoLetterChar &&
passwordErrorNoNumOrSpecialChar &&
passwordErrorRepeatedChar &&
passwordErrorEscapeChar &&
passwordErrorLowEntropy &&
passwordErrorBreached
}
autoComplete="new-password" autoComplete="new-password"
id="new-password" id="new-password"
/> />
</div> </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="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"> <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={faX} className="text-md mr-2.5 text-red" />
) : ( ) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)} )}
<div className={`${passwordErrorLength ? "text-gray-400" : "text-gray-600"} text-sm`}> <div className={`${passwordErrorTooShort ? "text-gray-400" : "text-gray-600"} text-sm`}>
14 characters at least 14 characters
</div> </div>
</div> </div>
<div className="ml-1 flex flex-row items-center justify-start"> <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={faX} className="text-md mr-2.5 text-red" />
) : ( ) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)} )}
<div <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> </div>
<div className="ml-1 flex flex-row items-center justify-start"> <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={faX} className="text-md mr-2.5 text-red" />
) : ( ) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> <FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)} )}
<div className={`${passwordErrorNumber ? "text-gray-400" : "text-gray-600"} text-sm`}> <div
1 number className={`${passwordErrorNoNumOrSpecialChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
at least 1 number or special character
</div> </div>
</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>
) : ( ) : (
<div className="py-2" /> <div className="py-2" />
+54 -69
View File
@@ -16,36 +16,30 @@ import { encodeBase64 } from "tweetnacl-util";
import Button from "@app/components/basic/buttons/Button"; import Button from "@app/components/basic/buttons/Button";
import InputField from "@app/components/basic/InputField"; import InputField from "@app/components/basic/InputField";
import 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 Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey"; import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { import { completeAccountSignupInvite, verifySignupInvite } from "@app/hooks/api/auth/queries";
useGetCommonPasswords
} from "@app/hooks/api";
import {
completeAccountSignupInvite,
verifySignupInvite
} from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const client = new jsrp.client(); const client = new jsrp.client();
type Errors = { type Errors = {
length?: string, tooShort?: string;
upperCase?: string, tooLong?: string;
lowerCase?: string, noLetterChar?: string;
number?: string, noNumOrSpecialChar?: string;
specialChar?: string, repeatedChar?: string;
repeatedChar?: string, escapeChar?: string;
lowEntropy?: string;
breached?: string;
}; };
export default function SignupInvite() { export default function SignupInvite() {
const { data: commonPasswords } = useGetCommonPasswords();
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [firstName, setFirstName] = useState(""); const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState(""); const [lastName, setLastName] = useState("");
@@ -79,10 +73,9 @@ export default function SignupInvite() {
} else { } else {
setLastNameError(false); setLastNameError(false);
} }
errorCheck = checkPassword({ errorCheck = await checkPassword({
password, password,
commonPasswords,
setErrors setErrors
}); });
@@ -116,7 +109,7 @@ export default function SignupInvite() {
if (!derivedKey) throw new Error("Failed to derive key from password"); if (!derivedKey) throw new Error("Failed to derive key from password");
const key = crypto.randomBytes(32); const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private // create encrypted private key by encrypting the private
// key with the symmetric key [key] // key with the symmetric key [key]
const { const {
@@ -127,7 +120,7 @@ export default function SignupInvite() {
text: privateKey, text: privateKey,
secret: key secret: key
}); });
// create the protected key by encrypting the symmetric key // create the protected key by encrypting the symmetric key
// [key] with the derived key // [key] with the derived key
const { const {
@@ -138,10 +131,8 @@ export default function SignupInvite() {
text: key.toString("hex"), text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash) secret: Buffer.from(derivedKey.hash)
}); });
const { const { token: jwtToken } = await completeAccountSignupInvite({
token: jwtToken
} = await completeAccountSignupInvite({
email, email,
firstName, firstName,
lastName, lastName,
@@ -155,20 +146,20 @@ export default function SignupInvite() {
salt: result.salt, salt: result.salt,
verifier: result.verifier verifier: result.verifier
}); });
// unset temporary signup JWT token and set JWT token // unset temporary signup JWT token and set JWT token
SecurityClient.setSignupToken(""); SecurityClient.setSignupToken("");
SecurityClient.setToken(jwtToken); SecurityClient.setToken(jwtToken);
saveTokenToLocalStorage({ saveTokenToLocalStorage({
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag, tag: encryptedPrivateKeyTag,
privateKey privateKey
}); });
const userOrgs = await fetchOrganizations(); const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]._id; const orgId = userOrgs[0]._id;
localStorage.setItem("orgData.id", orgId); 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) // Step 4 of the sign up process (download the emergency kit pdf)
const stepConfirmEmail = ( 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"> <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="text-4xl text-center font-semibold mb-6 flex justify-center text-primary-100"> <p className="mb-6 flex justify-center text-center text-4xl font-semibold text-primary-100">
Confirm your email Confirm your email
</p> </p>
<Image src="/images/dragon-signupinvite.svg" height={262} width={410} alt="verify email" /> <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 <Button
text="Confirm Email" text="Confirm Email"
onButtonPressed={async () => { 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) // Because this is the invite signup - we directly go to the last step of signup (email is already verified)
const main = ( 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"> <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="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"> <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! Almost there!
</p> </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 <InputField
label="First Name" label="First Name"
onChangeHandler={setFirstName} onChangeHandler={setFirstName}
@@ -245,7 +236,7 @@ export default function SignupInvite() {
autoComplete="given-name" autoComplete="given-name"
/> />
</div> </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 <InputField
label="Last Name" label="Last Name"
onChangeHandler={setLastName} onChangeHandler={setLastName}
@@ -257,14 +248,13 @@ export default function SignupInvite() {
autoComplete="family-name" autoComplete="family-name"
/> />
</div> </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 <InputField
label="Password" label="Password"
onChangeHandler={(pass) => { onChangeHandler={(pass) => {
setPassword(pass); setPassword(pass);
checkPassword({ checkPassword({
password: pass, password: pass,
commonPasswords,
setErrors setErrors
}); });
}} }}
@@ -276,31 +266,26 @@ export default function SignupInvite() {
id="new-password" id="new-password"
/> />
{Object.keys(errors).length > 0 && ( {Object.keys(errors).length > 0 && (
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2"> <div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-2 text-sm text-gray-400">Password should contain at least:</div> <div className="mb-2 text-sm text-gray-400">Password should contain at least:</div>
{Object.keys(errors).map((key) => { {Object.keys(errors).map((key) => {
if (errors[key as keyof Errors]) { if (errors[key as keyof Errors]) {
return ( 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> <div>
<FontAwesomeIcon <FontAwesomeIcon icon={faXmark} className="text-md ml-0.5 mr-2.5 text-red" />
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> </div>
); <p className="text-sm text-gray-400">{errors[key as keyof Errors]}</p>
} </div>
);
}
return null; return null;
})} })}
</div> </div>
)} )}
</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 <Button
text="Sign Up" text="Sign Up"
onButtonPressed={() => { onButtonPressed={() => {
@@ -315,21 +300,21 @@ export default function SignupInvite() {
// Step 4 of the sign up process (download the emergency kit pdf) // Step 4 of the sign up process (download the emergency kit pdf)
const step4 = ( 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"> <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="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-white to-mineshaft-300"> <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 Save your Emergency Kit
</p> </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> <div>
If you get locked out of your account, your Emergency Kit is the only way to sign in. If you get locked out of your account, your Emergency Kit is the only way to sign in.
</div> </div>
<div className="mt-3">We recommend you download it and keep it somewhere safe.</div> <div className="mt-3">We recommend you download it and keep it somewhere safe.</div>
</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" /> <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. It contains your Secret Key which we cannot access or recover for you if you lose it.
</div> </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 <Button
text="Download PDF" text="Download PDF"
onButtonPressed={async () => { onButtonPressed={async () => {
@@ -349,7 +334,7 @@ export default function SignupInvite() {
); );
return ( 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> <Head>
<title>Sign Up</title> <title>Sign Up</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
@@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -8,159 +8,142 @@ import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import attemptChangePassword from "@app/components/utilities/attemptChangePassword"; import attemptChangePassword from "@app/components/utilities/attemptChangePassword";
import checkPassword from "@app/components/utilities/checks/checkPassword"; import checkPassword from "@app/components/utilities/checks/password/checkPassword";
import { import { Button, FormControl, Input } from "@app/components/v2";
Button,
FormControl,
Input
} from "@app/components/v2";
import { useUser } from "@app/context"; import { useUser } from "@app/context";
import { useGetCommonPasswords } from "@app/hooks/api";
type Errors = { type Errors = {
length?: string, tooShort?: string;
upperCase?: string, tooLong?: string;
lowerCase?: string, noLetterChar?: string;
number?: string, noNumOrSpecialChar?: string;
specialChar?: string, repeatedChar?: 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"), oldPassword: yup.string().required("Old password is required"),
newPassword: yup.string().required("New password is required") newPassword: yup.string().required("New password is required")
}).required(); })
.required();
export type FormData = yup.InferType<typeof schema>; export type FormData = yup.InferType<typeof schema>;
export const ChangePasswordSection = () => { export const ChangePasswordSection = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { user } = useUser(); const { user } = useUser();
const { data: commonPasswords } = useGetCommonPasswords(); const { reset, control, handleSubmit } = useForm({
const { reset, control, handleSubmit } = useForm({ defaultValues: {
defaultValues: { oldPassword: "",
oldPassword: "", newPassword: ""
newPassword: "" },
}, resolver: yupResolver(schema)
resolver: yupResolver(schema) });
}); const [errors, setErrors] = useState<Errors>({});
const [errors, setErrors] = useState<Errors>({}); const [isLoading, setIsLoading] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => { const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
try { try {
if (!user?.email) return; if (!user?.email) return;
if (!commonPasswords) return;
const errorCheck = checkPassword({
password: newPassword,
commonPasswords,
setErrors
});
if (errorCheck) return; const errorCheck = await checkPassword({
password: newPassword,
setIsLoading(true); setErrors
await attemptChangePassword({ });
email: user.email,
currentPassword: oldPassword,
newPassword
});
setIsLoading(false);
createNotification({
text: "Successfully changed password",
type: "success"
});
reset(); if (errorCheck) return;
window.location.href = "/login";
setIsLoading(true);
} catch (err) { await attemptChangePassword({
console.error(err); email: user.email,
setIsLoading(false); currentPassword: oldPassword,
createNotification({ newPassword
text: "Failed to change password", });
type: "error"
}); 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> </div>
)} );
<Button }
type="submit"
colorSchema="secondary" return null;
isLoading={isLoading} })}
isDisabled={isLoading} </div>
> )}
Save <Button type="submit" colorSchema="secondary" isLoading={isLoading} isDisabled={isLoading}>
</Button> Save
</form> </Button>
); </form>
} );
};
@@ -1,4 +1,3 @@
import crypto from "crypto"; import crypto from "crypto";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
@@ -10,13 +9,12 @@ import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util"; import { encodeBase64 } from "tweetnacl-util";
import InputField from "@app/components/basic/InputField"; 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 Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input } from "@app/components/v2"; import { Button, Input } from "@app/components/v2";
import { useGetCommonPasswords } from "@app/hooks/api";
import { completeAccountSignup } from "@app/hooks/api/auth/queries"; import { completeAccountSignup } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import ProjectService from "@app/services/ProjectService"; import ProjectService from "@app/services/ProjectService";
@@ -25,22 +23,24 @@ import ProjectService from "@app/services/ProjectService";
const client = new jsrp.client(); const client = new jsrp.client();
type Props = { type Props = {
setStep: (step: number) => void; setStep: (step: number) => void;
email: string; email: string;
password: string; password: string;
setPassword: (value: string) => void; setPassword: (value: string) => void;
name: string; name: string;
providerOrganizationName: string; providerOrganizationName: string;
providerAuthToken?: string; providerAuthToken?: string;
} };
type Errors = { type Errors = {
length?: string, tooShort?: string;
upperCase?: string, tooLong?: string;
lowerCase?: string, noLetterChar?: string;
number?: string, noNumOrSpecialChar?: string;
specialChar?: string, repeatedChar?: string;
repeatedChar?: string, escapeChar?: string;
lowEntropy?: string;
breached?: string;
}; };
/** /**
@@ -63,9 +63,8 @@ export const UserInfoSSOStep = ({
password, password,
setPassword, setPassword,
setStep, setStep,
providerAuthToken, providerAuthToken
}: Props) => { }: Props) => {
const { data: commonPasswords } = useGetCommonPasswords();
const [nameError, setNameError] = useState(false); const [nameError, setNameError] = useState(false);
const [organizationName, setOrganizationName] = useState(""); const [organizationName, setOrganizationName] = useState("");
const [organizationNameError, setOrganizationNameError] = useState(false); const [organizationNameError, setOrganizationNameError] = useState(false);
@@ -97,10 +96,9 @@ export const UserInfoSSOStep = ({
} else { } else {
setOrganizationNameError(false); setOrganizationNameError(false);
} }
errorCheck = checkPassword({ errorCheck = await checkPassword({
password, password,
commonPasswords,
setErrors setErrors
}); });
@@ -210,15 +208,17 @@ export const UserInfoSSOStep = ({
setIsLoading(false); setIsLoading(false);
} }
}; };
return ( return (
<div className="h-full mx-auto mb-36 w-max rounded-xl md:px-8 md:mb-16"> <div className="mx-auto mb-36 h-full w-max rounded-xl md:mb-16 md:px-8">
<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"> <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")} {t("signup.step3-message")}
</p> </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="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 lg:w-1/6 w-1/4 min-w-[20rem] flex flex-col items-center justify-end w-full py-2 rounded-lg"> <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='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Your Name</p> <p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Your Name
</p>
<Input <Input
placeholder="Jane Doe" placeholder="Jane Doe"
value={name} value={name}
@@ -227,11 +227,17 @@ export const UserInfoSSOStep = ({
autoComplete="given-name" autoComplete="given-name"
className="h-12" 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>
{providerOrganizationName === undefined && ( {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"> <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='text-left w-full text-sm text-bunker-300 mb-1 ml-1 font-medium'>Organization Name</p> <p className="mb-1 ml-1 w-full text-left text-sm font-medium text-bunker-300">
Organization Name
</p>
<Input <Input
placeholder="Infisical" placeholder="Infisical"
value={organizationName} value={organizationName}
@@ -240,12 +246,18 @@ export const UserInfoSSOStep = ({
className="h-12" className="h-12"
disabled 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> </div>
)} )}
{providerOrganizationName === undefined && ( {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"> <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='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> <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 <Input
placeholder="" placeholder=""
onChange={(e) => setAttributionSource(e.target.value)} onChange={(e) => setAttributionSource(e.target.value)}
@@ -254,14 +266,13 @@ export const UserInfoSSOStep = ({
/> />
</div> </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 <InputField
label="Infisical Password" label="Infisical Password"
onChangeHandler={(pass: string) => { onChangeHandler={async (pass: string) => {
setPassword(pass); setPassword(pass);
checkPassword({ await checkPassword({
password: pass, password: pass,
commonPasswords,
setErrors setErrors
}); });
}} }}
@@ -272,26 +283,27 @@ export const UserInfoSSOStep = ({
autoComplete="new-password" autoComplete="new-password"
id="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 && ( {Object.keys(errors).length > 0 && (
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2"> <div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-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) => { {Object.keys(errors).map((key) => {
if (errors[key as keyof Errors]) { if (errors[key as keyof Errors]) {
return ( return (
<div <div className="items-top ml-1 flex flex-row justify-start" key={key}>
className="ml-1 flex flex-row items-top justify-start"
key={key}
>
<div> <div>
<FontAwesomeIcon <FontAwesomeIcon
icon={faXmark} icon={faXmark}
className="text-md text-red ml-0.5 mr-2.5" className="text-md ml-0.5 mr-2.5 text-red"
/> />
</div> </div>
<p className="text-gray-400 text-sm"> <p className="text-sm text-gray-400">{errors[key as keyof Errors]}</p>
{errors[key as keyof Errors]}
</p>
</div> </div>
); );
} }
@@ -301,21 +313,24 @@ export const UserInfoSSOStep = ({
</div> </div>
)} )}
</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="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 py-1 text-lg w-full"> <div className="text-l w-full py-1 text-lg">
<Button <Button
type="submit" type="submit"
onClick={signupErrorCheck} onClick={signupErrorCheck}
size="sm" size="sm"
isFullWidth isFullWidth
className='h-12' className="h-12"
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
isLoading={isLoading} isLoading={isLoading}
> {String(t("signup.signup"))} </Button> >
{" "}
{String(t("signup.signup"))}{" "}
</Button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
} };