Merge remote-tracking branch 'origin' into stripe-error

This commit is contained in:
Tuan Dang
2023-06-22 16:38:46 +07:00
15 changed files with 777 additions and 278 deletions

View File

@@ -2,10 +2,12 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import axios from "axios"
import attemptLogin from "@app/components/utilities/attemptLogin";
import Error from "../basic/Error";
import attemptCliLogin from "../utilities/attemptCliLogin";
// import { faGoogle } from '@fortawesome/free-brands-svg-icons';
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, Input } from "../v2";
@@ -31,33 +33,68 @@ export default function InitialLoginStep({
const handleLogin = async () => {
try {
if (!email || !password) {
return;
}
setIsLoading(true);
const isLoginSuccessful = await attemptLogin({
email,
password,
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
if (isLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
if (!email || !password) {
return;
}
// case: login does not require MFA step
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
}
setIsLoading(true);
const queryParams = new URLSearchParams(window.location.search)
if (queryParams && queryParams.get("callback_port")) {
const callbackPort = queryParams.get("callback_port")
// attemptCliLogin
const isCliLoginSuccessful = await attemptCliLogin({
email,
password,
})
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
if (isCliLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
}
// case: login was successful
const cliUrl = `http://localhost:${callbackPort}`
// send request to server endpoint
const instance = axios.create()
const cliResp = await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse })
console.log(cliResp)
// cli page
router.push("/cli-redirect");
// on success, router.push to cli Login Successful page
}
} else {
const isLoginSuccessful = await attemptLogin({
email,
password,
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
if (isLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
}
// case: login does not require MFA step
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
}
}
} catch (err) {
setLoginError(true);
setLoginError(true);
}
setIsLoading(false);
}
@@ -90,18 +127,18 @@ export default function InitialLoginStep({
</div>
</div>
<div className="relative pt-2 md:pt-0 md:px-1.5 flex items-center justify-center w-1/4 lg:w-1/6 min-w-[21.3rem] md:min-w-[22rem] mx-auto rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="h-12 select:-webkit-autofill:focus"
/>
</div>
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="h-12 select:-webkit-autofill:focus"
/>
</div>
</div>
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
@@ -116,18 +153,18 @@ export default function InitialLoginStep({
> Login </Button>
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center mt-4 py-2'>
<div className='w-1/2 border-t border-mineshaft-500'/>
<div className='w-1/2 border-t border-mineshaft-500' />
<span className='px-4 text-sm text-bunker-400'>or</span>
<div className='w-1/2 border-t border-mineshaft-500'/>
<div className='w-1/2 border-t border-mineshaft-500' />
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
colorSchema="primary"
variant="outline_bg"
onClick={() => router.push("/saml-sso")}
onClick={() => router.push("/saml-sso")}
isFullWidth
className="h-14 w-full mx-0"
>
>
Continue with SAML SSO
</Button>
</div>

View File

@@ -3,7 +3,9 @@ import React, { useState } from "react";
import ReactCodeInput from "react-code-input";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import axios from "axios"
import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa"
import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
import { useSendMfaToken } from "@app/hooks/api/auth";
@@ -76,17 +78,43 @@ export default function MFAStep({
}
setIsLoading(true);
const isLoginSuccessful = await attemptLoginMfa({
email,
password,
providerAuthToken,
mfaToken: mfaCode
});
const queryParams = new URLSearchParams(window.location.search)
if (queryParams && queryParams.get("callback_port")){
const callbackPort = queryParams.get("callback_port")
if (isLoginSuccessful) {
setIsLoading(false);
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
// attemptCliLogin
const isCliLoginSuccessful = await attemptCliLoginMfa({
email,
password,
providerAuthToken,
mfaToken: mfaCode
})
if (isCliLoginSuccessful && isCliLoginSuccessful.success){
// case: login was successful
const cliUrl = `http://localhost:${callbackPort}`
// send request to server endpoint
const instance = axios.create()
await instance.post(cliUrl,{...isCliLoginSuccessful.loginResponse,email})
// cli page
router.push("/cli-redirect");
}
}else{
const isLoginSuccessful = await attemptLoginMfa({
email,
password,
providerAuthToken,
mfaToken: mfaCode
});
if (isLoginSuccessful) {
setIsLoading(false);
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
}
}
} catch (err) {
const error = err as VerifyMfaTokenError;

View File

@@ -0,0 +1,164 @@
/* eslint-disable prefer-destructuring */
import jsrp from "jsrp";
import login1 from "@app/pages/api/auth/Login1";
import login2 from "@app/pages/api/auth/Login2";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects";
import KeyService from "@app/services/KeyService";
import Telemetry from "./telemetry/Telemetry";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
import SecurityClient from "./SecurityClient";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
interface IsCliLoginSuccessful {
mfaEnabled: boolean;
loginResponse?: {
email: string;
privateKey: string;
JTWToken: string;
};
success: boolean;
}
/**
* Return whether or not login is successful for user with email [email]
* and password [password]
* @param {string} email - email of user to log in
* @param {string} password - password of user to log in
*/
const attemptLogin = async (
{
email,
password,
providerAuthToken,
}: {
email: string;
password: string;
providerAuthToken?: string;
}
): Promise<IsCliLoginSuccessful> => {
const telemetry = new Telemetry().getInstance();
return new Promise((resolve, reject) => {
client.init(
{
username: email,
password
},
async () => {
try {
const clientPublicKey = client.getPublicKey();
const { serverPublicKey, salt } = await login1({
email,
clientPublicKey,
providerAuthToken,
});
client.setSalt(salt);
client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1
const {
mfaEnabled,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await login2(
{
email,
clientProof,
providerAuthToken,
}
);
if (mfaEnabled) {
// case: MFA is enabled
// set temporary (MFA) JWT token
SecurityClient.setMfaToken(token);
resolve({
mfaEnabled,
success: true
});
} else if (
!mfaEnabled &&
encryptionVersion &&
encryptedPrivateKey &&
iv &&
tag &&
token
) {
// case: MFA is not enabled
// unset provider auth token in case it was used
SecurityClient.setProviderAuthToken("");
// set JWT token
SecurityClient.setToken(token);
const privateKey = await KeyService.decryptPrivateKey({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
const userOrgs = await getOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem("orgData.id", orgId);
const orgUserProjects = await getOrganizationUserProjects({
orgId
});
if (orgUserProjects.length > 0) {
localStorage.setItem("projectData.id", orgUserProjects[0]._id);
}
if (email) {
telemetry.identify(email, email);
telemetry.capture("User Logged In");
}
resolve({
mfaEnabled: false,
loginResponse: {
email,
privateKey,
JTWToken: token
},
success: true
})
}
} catch (err) {
reject(err);
}
}
);
});
};
export default attemptLogin;

View File

@@ -0,0 +1,122 @@
/* eslint-disable prefer-destructuring */
import jsrp from "jsrp";
import login1 from "@app/pages/api/auth/Login1";
import verifyMfaToken from "@app/pages/api/auth/verifyMfaToken";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects";
import KeyService from "@app/services/KeyService";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
import SecurityClient from "./SecurityClient";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
interface IsMfaLoginSuccessful {
success: boolean;
loginResponse:{
privateKey: string;
JTWToken: string;
}
}
/**
* Return whether or not MFA-login is successful for user with email [email]
* and MFA token [mfaToken]
* @param {Object} obj
* @param {String} obj.email - email of user
* @param {String} obj.mfaToken - MFA code/token
*/
const attemptLoginMfa = async ({
email,
password,
providerAuthToken,
mfaToken
}: {
email: string;
password: string;
providerAuthToken?: string,
mfaToken: string;
}): Promise<IsMfaLoginSuccessful> => {
return new Promise((resolve, reject) => {
client.init({
username: email,
password
}, async () => {
try {
const clientPublicKey = client.getPublicKey();
const { salt } = await login1({
email,
clientPublicKey,
providerAuthToken,
});
const {
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await verifyMfaToken({
email,
mfaToken
});
// unset temporary (MFA) JWT token and set JWT token
SecurityClient.setMfaToken("");
SecurityClient.setToken(token);
SecurityClient.setProviderAuthToken("");
const privateKey = await KeyService.decryptPrivateKey({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
// TODO: in the future - move this logic elsewhere
// because this function is about logging the user in
// and not initializing the login details
const userOrgs = await getOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem("orgData.id", orgId);
const orgUserProjects = await getOrganizationUserProjects({
orgId
});
localStorage.setItem("projectData.id", orgUserProjects[0]._id);
resolve({
success: true,
loginResponse:{
privateKey,
JTWToken: token
}
});
} catch (err) {
reject(err);
}
});
});
}
export default attemptLoginMfa;

View File

@@ -24,7 +24,7 @@ const userKeys = {
getOrgUsers: (orgId: string) => [{ orgId }, "user"]
};
const fetchUserDetails = async () => {
export const fetchUserDetails = async () => {
const { data } = await apiRequest.get<{ user: User }>("/api/v1/user");
return data.user;

View File

@@ -0,0 +1,18 @@
import Head from "next/head";
export default function CliRedirect() {
return (
<div className='bg-bunker-800 md:h-screen flex flex-col justify-between'>
<Head>
<title>Infisical Cli | Login Successful!</title>
<link rel='icon' href='/infisical.ico' />
</Head>
<div className='flex flex-col items-center justify-center text-gray-200 h-screen w-screen'>
<p className='text-4xl mt-32'>Head back to your terminal!</p>
<p className='mt-2 mb-1 text-lg'>
You&apos;ve successfully logged into infisical-cli
</p>
</div>
</div>
);
}

View File

@@ -4,13 +4,16 @@ import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import axios from "axios"
// import ListBox from '@app/components/basic/Listbox';
import InitialLoginStep from "@app/components/login/InitialLoginStep";
import MFAStep from "@app/components/login/MFAStep";
import PasswordInputStep from "@app/components/login/PasswordInputStep";
import { useProviderAuth } from "@app/hooks/useProviderAuth";
import { isLoggedIn } from "@app/reactQuery";
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
import { fetchUserDetails } from "~/hooks/api/users/queries";
import getWorkspaces from "./api/workspace/getWorkspaces";
@@ -20,6 +23,7 @@ export default function Login() {
const [password, setPassword] = useState("");
const [step, setStep] = useState(1);
const { t } = useTranslation();
// const lang = router.locale ?? 'en';
const {
providerAuthToken,
@@ -44,6 +48,20 @@ export default function Login() {
try {
const userWorkspaces = await getWorkspaces();
userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
// user details
const userDetails = await fetchUserDetails()
// send details back to client
const queryParams = new URLSearchParams(window.location.search)
if (queryParams && queryParams.get("callback_port")) {
const callbackPort = queryParams.get("callback_port")
// send post request to cli with details
const cliUrl = `http://localhost:${callbackPort}`
const instance = axios.create()
await instance.post(cliUrl, { email: userDetails.email, privateKey: localStorage.getItem("PRIVATE_KEY"), JTWToken: getAuthToken() })
}
router.push(`/dashboard/${userWorkspace}`);
} catch (error) {
console.log("Error - Not logged in yet");
@@ -69,7 +87,7 @@ export default function Login() {
}
if (loginStep === 1) {
return <InitialLoginStep
return <InitialLoginStep
setStep={setStep}
email={email}
setEmail={setEmail}