feat: cli login via browser

This commit is contained in:
quinton11
2023-06-14 19:12:56 +00:00
parent 4b99a9ea93
commit 9e9129dd02
9 changed files with 367 additions and 55 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ const ContentSecurityPolicy = `
style-src 'self' https://rsms.me 'unsafe-inline';
child-src https://api.stripe.com;
frame-src https://js.stripe.com/ https://api.stripe.com;
connect-src 'self' 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;
connect-src 'self' 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:*;
img-src 'self' https://*.stripe.com https://i.ytimg.com/ data:;
media-src;
font-src 'self' https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com;
@@ -2,6 +2,7 @@ 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';
@@ -9,6 +10,7 @@ import Error from '../basic/Error';
// import { faGoogle } from '@fortawesome/free-brands-svg-icons';
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, Input } from '../v2';
import attemptCliLogin from '../utilities/attemptCliLogin';
export default function InitialLoginStep({
setStep,
@@ -31,33 +33,60 @@ 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(location.search)
if (queryParams) {
const callbackPort = queryParams.get("callback_port")
//attemptCliLogin
const isCliLoginSuccessful = await attemptCliLogin({
email,
password,
})
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
// 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,email,password})
//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 +119,18 @@ export default function InitialLoginStep({
</div>
</div>
<div className="relative pt-2 md:pt-0 md:px-1.5 flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full 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 +145,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>
@@ -0,0 +1,124 @@
/* 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 {
loginResponse: {
loginOneResponse: {
serverPublicKey: string;
salt: string;
};
loginTwoResponse: {
mfaEnabled: boolean;
token: string;
encryptionVersion?: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: 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,
}
);
resolve({
loginResponse: {
loginOneResponse: { serverPublicKey, salt },
loginTwoResponse: {
mfaEnabled,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
}
},
success: true
})
} catch (err) {
reject(err);
}
}
);
});
};
export default attemptLogin;
+26
View File
@@ -0,0 +1,26 @@
import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';
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've successfully logged into infisical-cli
</p>
<Image
src='/images/dragon-404.svg'
height={554}
width={942}
alt='infisical dragon - page not found'
/>
</div>
</div>
);
}