mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feature: setup password
This commit is contained in:
@@ -203,7 +203,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
encryptedPrivateKeyIV: z.string().trim(),
|
||||
encryptedPrivateKeyTag: z.string().trim(),
|
||||
salt: z.string().trim(),
|
||||
verifier: z.string().trim()
|
||||
verifier: z.string().trim(),
|
||||
password: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -218,7 +219,69 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
||||
userId: token.userId
|
||||
});
|
||||
|
||||
return { message: "Successfully updated backup private key" };
|
||||
return { message: "Successfully reset password" };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/email/password-setup",
|
||||
config: {
|
||||
rateLimit: authRateLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
await server.services.password.sendPasswordSetupEmail(req.permission);
|
||||
|
||||
return {
|
||||
message: "A password setup link has been sent"
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/password-setup",
|
||||
config: {
|
||||
rateLimit: authRateLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
protectedKey: z.string().trim(),
|
||||
protectedKeyIV: z.string().trim(),
|
||||
protectedKeyTag: z.string().trim(),
|
||||
encryptedPrivateKey: z.string().trim(),
|
||||
encryptedPrivateKeyIV: z.string().trim(),
|
||||
encryptedPrivateKeyTag: z.string().trim(),
|
||||
salt: z.string().trim(),
|
||||
verifier: z.string().trim(),
|
||||
password: z.string().trim(),
|
||||
token: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
await server.services.password.setupPassword(req.body, req.permission);
|
||||
|
||||
const appCfg = getConfig();
|
||||
void res.cookie("jid", "", {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: appCfg.HTTPS_ENABLED
|
||||
});
|
||||
|
||||
return { message: "Successfully setup password" };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -57,6 +57,12 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
||||
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||
return { token, expiresAt };
|
||||
}
|
||||
case TokenType.TOKEN_EMAIL_PASSWORD_SETUP: {
|
||||
// generate random hex
|
||||
const token = crypto.randomBytes(16).toString("hex");
|
||||
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||
return { token, expiresAt };
|
||||
}
|
||||
case TokenType.TOKEN_USER_UNLOCK: {
|
||||
const token = crypto.randomBytes(16).toString("hex");
|
||||
const expiresAt = new Date(new Date().getTime() + 259200000);
|
||||
|
||||
@@ -6,6 +6,7 @@ export enum TokenType {
|
||||
TOKEN_EMAIL_MFA = "emailMfa",
|
||||
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
||||
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
|
||||
TOKEN_EMAIL_PASSWORD_SETUP = "passwordSetup",
|
||||
TOKEN_USER_UNLOCK = "userUnlock"
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import jwt from "jsonwebtoken";
|
||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
@@ -11,8 +13,13 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TAuthDALFactory } from "./auth-dal";
|
||||
import { TChangePasswordDTO, TCreateBackupPrivateKeyDTO, TResetPasswordViaBackupKeyDTO } from "./auth-password-type";
|
||||
import { AuthTokenType } from "./auth-type";
|
||||
import {
|
||||
TChangePasswordDTO,
|
||||
TCreateBackupPrivateKeyDTO,
|
||||
TResetPasswordViaBackupKeyDTO,
|
||||
TSetupPasswordViaBackupKeyDTO
|
||||
} from "./auth-password-type";
|
||||
import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
|
||||
|
||||
type TAuthPasswordServiceFactoryDep = {
|
||||
authDAL: TAuthDALFactory;
|
||||
@@ -169,8 +176,13 @@ export const authPaswordServiceFactory = ({
|
||||
verifier,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
userId
|
||||
userId,
|
||||
password
|
||||
}: TResetPasswordViaBackupKeyDTO) => {
|
||||
const cfg = getConfig();
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
|
||||
|
||||
await userDAL.updateUserEncryptionByUserId(userId, {
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
@@ -180,7 +192,8 @@ export const authPaswordServiceFactory = ({
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
verifier,
|
||||
hashedPassword
|
||||
});
|
||||
|
||||
await userDAL.updateById(userId, {
|
||||
@@ -267,6 +280,106 @@ export const authPaswordServiceFactory = ({
|
||||
return backupKey;
|
||||
};
|
||||
|
||||
const sendPasswordSetupEmail = async (actor: OrgServiceActor) => {
|
||||
if (actor.type !== ActorType.USER)
|
||||
throw new BadRequestError({ message: `Actor of type ${actor.type} cannot set password` });
|
||||
|
||||
const user = await userDAL.findById(actor.id);
|
||||
|
||||
if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` });
|
||||
|
||||
if (!user.isAccepted || !user.authMethods)
|
||||
throw new BadRequestError({ message: `You must complete signup to set a password` });
|
||||
|
||||
const cfg = getConfig();
|
||||
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
const email = user.email ?? user.username;
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.SetupPassword,
|
||||
recipients: [email],
|
||||
subjectLine: "Infisical Password Setup",
|
||||
substitutions: {
|
||||
email,
|
||||
token,
|
||||
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-setup` : ""
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setupPassword = async (
|
||||
{
|
||||
encryptedPrivateKey,
|
||||
protectedKeyTag,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
salt,
|
||||
verifier,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
password,
|
||||
token
|
||||
}: TSetupPasswordViaBackupKeyDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
try {
|
||||
await tokenService.validateTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
|
||||
userId: actor.id,
|
||||
code: token
|
||||
});
|
||||
} catch (e) {
|
||||
throw new BadRequestError({ message: "Expired or invalid token. Please try again." });
|
||||
}
|
||||
|
||||
await userDAL.transaction(async (tx) => {
|
||||
const user = await userDAL.findById(actor.id, tx);
|
||||
|
||||
if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` });
|
||||
|
||||
if (!user.isAccepted || !user.authMethods)
|
||||
throw new BadRequestError({ message: `You must complete signup to set a password` });
|
||||
|
||||
await userDAL.updateById(
|
||||
actor.id,
|
||||
{
|
||||
authMethods: [...user.authMethods, AuthMethod.EMAIL]
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const cfg = getConfig();
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
|
||||
|
||||
await userDAL.updateUserEncryptionByUserId(
|
||||
actor.id,
|
||||
{
|
||||
encryptionVersion: 2,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
hashedPassword,
|
||||
serverPrivateKey: null,
|
||||
clientPublicKey: null
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
await tokenService.revokeAllMySessions(actor.id);
|
||||
};
|
||||
|
||||
return {
|
||||
generateServerPubKey,
|
||||
changePassword,
|
||||
@@ -274,6 +387,8 @@ export const authPaswordServiceFactory = ({
|
||||
sendPasswordResetEmail,
|
||||
verifyPasswordResetEmail,
|
||||
createBackupPrivateKey,
|
||||
getBackupPrivateKeyOfUser
|
||||
getBackupPrivateKeyOfUser,
|
||||
sendPasswordSetupEmail,
|
||||
setupPassword
|
||||
};
|
||||
};
|
||||
|
||||
@@ -23,6 +23,20 @@ export type TResetPasswordViaBackupKeyDTO = {
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type TSetupPasswordViaBackupKeyDTO = {
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
password: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type TCreateBackupPrivateKeyDTO = {
|
||||
|
||||
@@ -30,6 +30,7 @@ export enum SmtpTemplates {
|
||||
NewDeviceJoin = "newDevice.handlebars",
|
||||
OrgInvite = "organizationInvitation.handlebars",
|
||||
ResetPassword = "passwordReset.handlebars",
|
||||
SetupPassword = "passwordSetup.handlebars",
|
||||
SecretLeakIncident = "secretLeakIncident.handlebars",
|
||||
WorkspaceInvite = "workspaceInvitation.handlebars",
|
||||
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
||||
|
||||
16
backend/src/services/smtp/templates/passwordSetup.handlebars
Normal file
16
backend/src/services/smtp/templates/passwordSetup.handlebars
Normal file
@@ -0,0 +1,16 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Password Setup</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Setup your password</h2>
|
||||
<p>Someone requested to set up a password for your account. Make sure you are already logged in to Infisical in the current browser before clicking the link below.</p>
|
||||
<a href="{{callback_url}}?token={{token}}&to={{email}}">Setup password</a>
|
||||
<p>If you didn't initiate this request, please contact
|
||||
{{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user