feat: srp removal

This commit is contained in:
Daniel Hougaard
2025-07-30 04:25:27 +04:00
parent 75622ed03e
commit d5dd2e8bfd
68 changed files with 817 additions and 2410 deletions
@@ -18,7 +18,7 @@ import { useToggle } from "@app/hooks";
import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyPrivateKey, fetchUserDuplicateAccounts } from "@app/hooks/api/users/queries";
import { fetchUserDuplicateAccounts } from "@app/hooks/api/users/queries";
import { EmailDuplicationConfirmation } from "@app/pages/auth/SelectOrgPage/EmailDuplicationConfirmation";
import { navigateUserToOrg, useNavigateToSelectOrganization } from "../../Login.utils";
@@ -70,9 +70,6 @@ export const PasswordStep = ({
// set JWT token
SecurityClient.setToken(oauthLogin.token);
const privateKey = await fetchMyPrivateKey();
localStorage.setItem("PRIVATE_KEY", privateKey);
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
if (organizationId) {
const finishWithOrgWorkflow = async () => {
@@ -92,7 +89,7 @@ export const PasswordStep = ({
console.log("organization id was present. new JWT token to be used in CLI:", token);
const instance = axios.create();
const payload = {
privateKey,
privateKey: "", // note(daniel): no longer needed by the CLI, because the CLI only uses the private key to create service tokens, and the private key isn't used anymore when creating service tokens.
email,
JTWToken: token
};
@@ -1,22 +1,14 @@
import crypto from "crypto";
import { FormEvent, useState } from "react";
import { faCheck, faEye, faEyeSlash, faKey, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import jsrp from "jsrp";
import { createNotification } from "@app/components/notifications";
import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useSetupPassword } from "@app/hooks/api/auth/queries";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
export const PasswordSetupPage = () => {
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
@@ -65,83 +57,31 @@ export const PasswordSetupPage = () => {
setPasswordsMatch(true);
if (!errorCheck) {
client.init(
{
username: email,
try {
await setupPassword.mutateAsync({
email,
token,
password
},
async () => {
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
const derivedKey = await deriveArgonKey({
password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
});
if (!derivedKey) throw new Error("Failed to derive key from password");
setIsRedirecting(true);
const key = crypto.randomBytes(32);
createNotification({
type: "success",
title: "Password successfully set",
text: "Redirecting to login..."
});
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: localStorage.getItem("PRIVATE_KEY") as string,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash)
});
try {
await setupPassword.mutateAsync({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier,
token,
password
});
setIsRedirecting(true);
createNotification({
type: "success",
title: "Password successfully set",
text: "Redirecting to login..."
});
setTimeout(() => {
window.location.href = "/login";
}, 3000);
} catch (error) {
createNotification({
type: "error",
text: (error as Error).message ?? "Error setting password"
});
navigate({ to: "/personal-settings" });
}
});
}
);
setTimeout(() => {
window.location.href = "/login";
}, 3000);
} catch (error) {
createNotification({
type: "error",
text: (error as Error).message ?? "Error setting password"
});
navigate({ to: "/personal-settings" });
}
}
};
@@ -132,11 +132,8 @@ export const SelectOrganizationSection = () => {
}
if (callbackPort) {
const privateKey = localStorage.getItem("PRIVATE_KEY");
let error: string | null = null;
if (!privateKey) error = "Private key not found";
if (!user?.email) error = "User email not found";
if (!token) error = "No token found";
@@ -151,7 +148,7 @@ export const SelectOrganizationSection = () => {
const payload = {
JTWToken: token,
email: user?.email,
privateKey
privateKey: ""
} as IsCliLoginSuccessful["loginResponse"];
// send request to server endpoint
@@ -1,23 +1,17 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable @typescript-eslint/no-unused-vars */
import crypto from "crypto";
import { useState } from "react";
import { Helmet } from "react-helmet";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useSearch } from "@tanstack/react-router";
import jsrp from "jsrp";
import { Mfa } from "@app/components/auth/Mfa";
import InputField from "@app/components/basic/InputField";
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { useToggle } from "@app/hooks";
import {
completeAccountSignupInvite,
@@ -28,9 +22,6 @@ import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { isLoggedIn } from "@app/hooks/api/reactQuery";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
type Errors = {
tooShort?: string;
tooLong?: string;
@@ -67,7 +58,6 @@ export const SignupInvitePage = () => {
const metadata = queryParams.get("metadata") || undefined;
const { mutateAsync: selectOrganization } = useSelectOrganization();
const { config } = useServerConfig();
const loggedIn = isLoggedIn();
@@ -94,123 +84,56 @@ export const SignupInvitePage = () => {
}
if (!errorCheck) {
// Generate a random pair of a public and a private key
const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled);
try {
const { token: jwtToken } = await completeAccountSignupInvite({
email,
password,
firstName,
lastName,
tokenMetadata: metadata
});
localStorage.setItem("PRIVATE_KEY", privateKey);
// unset temporary signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(jwtToken);
client.init(
{
username: email,
password
},
async () => {
client.createVerifier(async (_err, result) => {
try {
const derivedKey = await deriveArgonKey({
password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
const userOrgs = await fetchOrganizations();
if (!derivedKey) throw new Error("Failed to derive key from password");
const orgId = userOrgs[0].id;
const key = crypto.randomBytes(32);
if (!orgId) throw new Error("You are not part of any organization");
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash)
});
const { token: jwtToken } = await completeAccountSignupInvite({
email,
password,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier,
tokenMetadata: metadata
});
// unset temporary signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(jwtToken);
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey
});
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0].id;
if (!orgId) throw new Error("You are not part of any organization");
const completeSignupFlow = async () => {
const {
token: mfaToken,
isMfaEnabled,
mfaMethod
} = await selectOrganization({
organizationId: orgId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(mfaToken);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => completeSignupFlow);
return;
}
localStorage.setItem("orgData.id", orgId);
navigate({
to: "/organization/projects"
});
};
await completeSignupFlow();
} catch (error) {
setIsLoading(false);
console.error(error);
}
const completeSignupFlow = async () => {
const {
token: mfaToken,
isMfaEnabled,
mfaMethod
} = await selectOrganization({
organizationId: orgId
});
}
);
if (isMfaEnabled) {
SecurityClient.setMfaToken(mfaToken);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => completeSignupFlow);
return;
}
localStorage.setItem("orgData.id", orgId);
navigate({
to: "/organization/projects"
});
};
await completeSignupFlow();
} catch (error) {
setIsLoading(false);
console.error(error);
}
} else {
setIsLoading(false);
}
@@ -3,24 +3,16 @@ import crypto from "crypto";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "@tanstack/react-router";
import jsrp from "jsrp";
import { Mfa } from "@app/components/auth/Mfa";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { initProjectHelper } from "@app/helpers/project";
import { useToggle } from "@app/hooks";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
type Props = {
username: string;
password: string;
@@ -64,7 +56,6 @@ export const UserInfoSSOStep = ({
const { mutateAsync: selectOrganization } = useSelectOrganization();
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const navigate = useNavigate();
const { config } = useServerConfig();
useEffect(() => {
const randomPassword = crypto.randomBytes(32).toString("hex");
@@ -93,131 +84,64 @@ export const UserInfoSSOStep = ({
}
if (!errorCheck) {
// Generate a random pair of a public and a private key
const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled);
localStorage.setItem("PRIVATE_KEY", privateKey);
try {
const response = await completeAccountSignup({
email: username,
password,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),
providerAuthToken,
organizationName,
attributionSource,
useDefaultOrg: forceDefaultOrg
});
client.init(
{
username,
password
},
async () => {
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
try {
// TODO: moduralize into KeyService
const derivedKey = await deriveArgonKey({
password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
// unset signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(response.token);
SecurityClient.setProviderAuthToken("");
if (!derivedKey) throw new Error("Failed to derive key from password");
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]?.id;
const key = crypto.randomBytes(32);
const completeSignupFlow = async () => {
try {
const { isMfaEnabled, token, mfaMethod } = await selectOrganization({
organizationId: orgId
});
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash)
});
const response = await completeAccountSignup({
email: username,
password,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
providerAuthToken,
salt: result.salt,
verifier: result.verifier,
organizationName,
attributionSource,
useDefaultOrg: forceDefaultOrg
});
// unset signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(response.token);
SecurityClient.setProviderAuthToken("");
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey
});
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]?.id;
const completeSignupFlow = async () => {
try {
const { isMfaEnabled, token, mfaMethod } = await selectOrganization({
organizationId: orgId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => completeSignupFlow);
return;
}
// only create example project if not joining existing org
if (!providerOrganizationName) {
await initProjectHelper({
projectName: "Example Project"
});
}
localStorage.setItem("orgData.id", orgId);
navigate({
to: "/organization/projects"
});
} catch (error) {
setIsLoading(false);
console.error(error);
}
};
await completeSignupFlow();
} catch (error) {
setIsLoading(false);
console.error(error);
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => completeSignupFlow);
return;
}
});
}
);
// only create example project if not joining existing org
if (!providerOrganizationName) {
await initProjectHelper({
projectName: "Example Project"
});
}
localStorage.setItem("orgData.id", orgId);
navigate({
to: "/organization/projects"
});
} catch (error) {
setIsLoading(false);
console.error(error);
}
};
await completeSignupFlow();
} catch (error) {
setIsLoading(false);
console.error(error);
}
} else {
setIsLoading(false);
}