Skip to content

Commit

Permalink
Merge pull request #3052 from Infisical/set-password-feature
Browse files Browse the repository at this point in the history
Feature: Setup Password
  • Loading branch information
scott-ray-wilson authored Jan 28, 2025
2 parents 657aca5 + d74b819 commit 955cf93
Show file tree
Hide file tree
Showing 19 changed files with 702 additions and 23 deletions.
67 changes: 65 additions & 2 deletions backend/src/server/routes/v1/password-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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" };
}
});
};
6 changes: 6 additions & 0 deletions backend/src/services/auth-token/auth-token-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions backend/src/services/auth-token/auth-token-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

Expand Down
127 changes: 122 additions & 5 deletions backend/src/services/auth/auth-password-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ 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";
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;
Expand Down Expand Up @@ -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,
Expand All @@ -180,7 +192,8 @@ export const authPaswordServiceFactory = ({
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
verifier,
hashedPassword
});

await userDAL.updateById(userId, {
Expand Down Expand Up @@ -267,13 +280,117 @@ 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` });

if (!user.authMethods.includes(AuthMethod.EMAIL)) {
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,
resetPasswordByBackupKey,
sendPasswordResetEmail,
verifyPasswordResetEmail,
createBackupPrivateKey,
getBackupPrivateKeyOfUser
getBackupPrivateKeyOfUser,
sendPasswordSetupEmail,
setupPassword
};
};
14 changes: 14 additions & 0 deletions backend/src/services/auth/auth-password-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
1 change: 1 addition & 0 deletions backend/src/services/smtp/smtp-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions backend/src/services/smtp/templates/passwordSetup.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<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.</p>
<p><strong>Make sure you are already logged in to Infisical in the current browser before clicking the link below.</strong></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 [email protected].{{else}}your administrator immediately.{{/if}}</p>

{{emailFooter}}
</body>
</html>
3 changes: 2 additions & 1 deletion frontend/src/const/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export const ROUTE_PATHS = Object.freeze({
"/_restrict-login-signup/login/provider/success"
),
SignUpSsoPage: setRoute("/signup/sso", "/_restrict-login-signup/signup/sso"),
PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset")
PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset"),
PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup")
},
Organization: {
SecretScanning: setRoute(
Expand Down
24 changes: 23 additions & 1 deletion frontend/src/hooks/api/auth/queries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
MfaMethod,
ResetPasswordDTO,
SendMfaTokenDTO,
SetupPasswordDTO,
SRP1DTO,
SRPR1Res,
TOauthTokenExchangeDTO,
Expand Down Expand Up @@ -286,7 +287,8 @@ export const useResetPassword = () => {
encryptedPrivateKeyIV: details.encryptedPrivateKeyIV,
encryptedPrivateKeyTag: details.encryptedPrivateKeyTag,
salt: details.salt,
verifier: details.verifier
verifier: details.verifier,
password: details.password
},
{
headers: {
Expand Down Expand Up @@ -336,3 +338,23 @@ export const checkUserTotpMfa = async () => {

return data.isVerified;
};

export const useSendPasswordSetupEmail = () => {
return useMutation({
mutationFn: async () => {
const { data } = await apiRequest.post("/api/v1/password/email/password-setup");

return data;
}
});
};

export const useSetupPassword = () => {
return useMutation({
mutationFn: async (payload: SetupPasswordDTO) => {
const { data } = await apiRequest.post("/api/v1/password/password-setup", payload);

return data;
}
});
};
14 changes: 14 additions & 0 deletions frontend/src/hooks/api/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,20 @@ export type ResetPasswordDTO = {
salt: string;
verifier: string;
verificationToken: string;
password: string;
};

export type SetupPasswordDTO = {
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
token: string;
password: string;
};

export type IssueBackupPrivateKeyDTO = {
Expand Down
Loading

0 comments on commit 955cf93

Please sign in to comment.