From 9e9129dd02a7b272fa4f2fb50f922d298c92788b Mon Sep 17 00:00:00 2001 From: quinton11 Date: Wed, 14 Jun 2023 19:12:56 +0000 Subject: [PATCH 01/10] feat: cli login via browser --- cli/go.mod | 6 +- cli/go.sum | 5 + cli/packages/cmd/login.go | 146 ++++++++++++++++-- cli/packages/config/config.go | 1 + cli/packages/util/constants.go | 1 + frontend/next.config.js | 2 +- .../src/components/login/InitialLoginStep.tsx | 111 ++++++++----- .../components/utilities/attemptCliLogin.ts | 124 +++++++++++++++ frontend/src/pages/cli-redirect.tsx | 26 ++++ 9 files changed, 367 insertions(+), 55 deletions(-) create mode 100644 frontend/src/components/utilities/attemptCliLogin.ts create mode 100644 frontend/src/pages/cli-redirect.tsx diff --git a/cli/go.mod b/cli/go.mod index 640e6dc0ca..6a29a182e6 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/99designs/keyring v1.2.2 github.com/charmbracelet/lipgloss v0.5.0 + github.com/denisbrodbeck/machineid v1.0.1 github.com/fatih/semgroup v1.2.0 github.com/gitleaks/go-gitdiff v0.8.0 github.com/h2non/filetype v1.1.3 @@ -14,6 +15,7 @@ require ( github.com/muesli/reflow v0.3.0 github.com/muesli/roff v0.1.0 github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 + github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a github.com/rs/zerolog v1.26.1 github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.8.1 @@ -28,7 +30,6 @@ require ( github.com/chzyer/readline v1.5.1 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/denisbrodbeck/machineid v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.5.0 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/go-openapi/errors v0.20.2 // indirect @@ -47,9 +48,10 @@ require ( github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/pelletier/go-toml v1.9.3 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/rs/cors v1.9.0 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect diff --git a/cli/go.sum b/cli/go.sum index 574452d459..ec126dab29 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -283,6 +283,8 @@ github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5d github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 h1:lL+y4Xv20pVlCGyLzNHRC0I0rIHhIL1lTvHizoS/dU8= github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= @@ -297,6 +299,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= +github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= @@ -520,6 +524,7 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index f20a178539..6c36be36e0 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -6,10 +6,14 @@ package cmd import ( "encoding/base64" "encoding/hex" + "encoding/json" "strings" + "time" "errors" "fmt" + "net" + "net/http" "net/url" "regexp" @@ -22,7 +26,9 @@ import ( "github.com/fatih/color" "github.com/go-resty/resty/v2" "github.com/manifoldco/promptui" + "github.com/pkg/browser" "github.com/posthog/posthog-go" + "github.com/rs/cors" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "golang.org/x/crypto/argon2" @@ -90,16 +96,43 @@ var loginCmd = &cobra.Command{ } } - email, password, err := askForLoginCredentials() + var ( + email string + password string + loginOneResponse *api.GetLoginOneV2Response + loginTwoResponse *api.GetLoginTwoV2Response + ) + + //check for --browser flag + browserLogin, err := cmd.Flags().GetBool("browser") if err != nil { - util.HandleError(err, "Unable to parse email and password for authentication") + util.HandleError(err, "Unable to parse browser flag") } - loginOneResponse, loginTwoResponse, err := getFreshUserCredentials(email, password) - if err != nil { - fmt.Println("Unable to authenticate with the provided credentials, please try again") - log.Debug().Err(err) - return + if browserLogin { + log.Debug().Msg("Login via browser") + //call browser login function + loginResponse, err := browserCliLogin() + if err != nil { + util.HandleError(err, err.Error()) + } + email = loginResponse.Email + password = loginResponse.Password + loginOneResponse = &loginResponse.LoginOneResponse + loginTwoResponse = &loginResponse.LoginTwoResponse + //util.HandleError(errors.New("not implemented"), " login via browser not fully implemented") + } else { + email, password, err = askForLoginCredentials() + if err != nil { + util.HandleError(err, "Unable to parse email and password for authentication") + } + + loginOneResponse, loginTwoResponse, err = getFreshUserCredentials(email, password) + if err != nil { + fmt.Println("Unable to authenticate with the provided credentials, please try again") + log.Debug().Err(err) + return + } } if loginTwoResponse.MfaEnabled { @@ -282,6 +315,7 @@ var loginCmd = &cobra.Command{ func init() { rootCmd.AddCommand(loginCmd) + loginCmd.Flags().BoolP("browser", "b", false, "Login via browser") } func DomainOverridePrompt() (bool, error) { @@ -326,7 +360,8 @@ func askForDomain() error { if selectedHostingOption == INFISICAL_CLOUD { //cloud option - config.INFISICAL_URL = util.INFISICAL_DEFAULT_API_URL + config.INFISICAL_URL = fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_URL) + config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", util.INFISICAL_DEFAULT_URL) return nil } @@ -341,7 +376,7 @@ func askForDomain() error { domainPrompt := promptui.Prompt{ Label: "Domain", Validate: urlValidation, - Default: "Example - https://my-self-hosted-instance.com/api", + Default: "Example - https://my-self-hosted-instance.com", } domain, err := domainPrompt.Run() @@ -349,8 +384,9 @@ func askForDomain() error { return err } - //set api url - config.INFISICAL_URL = domain + //set api and login url + config.INFISICAL_URL = fmt.Sprintf("%s/api", domain) + config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", domain) //return nil return nil } @@ -476,3 +512,91 @@ func askForMFACode() string { return mfaVerifyCode } + +type CliLoginResponse struct { + Email string `json:"email"` + Password string `json:"password"` + LoginOneResponse api.GetLoginOneV2Response `json:"loginOneResponse"` + LoginTwoResponse api.GetLoginTwoV2Response `json:"loginTwoResponse"` +} + +// Manages the browser login flow +// returns a CLILoginResponse on success and an error on failure +func browserCliLogin() (CliLoginResponse, error) { + + //create listener + listener, err := net.Listen("tcp", ":0") + if err != nil { + return CliLoginResponse{}, err + } + + //get callback port + callbackPort := listener.Addr().(*net.TCPAddr).Port + url := fmt.Sprintf("%s?callback_port=%d", config.INFISICAL_LOGIN_URL, callbackPort) + + //open browser and login + err = browser.OpenURL(url) + if err != nil { + return CliLoginResponse{}, err + } + + //flow channels + success := make(chan CliLoginResponse) + failure := make(chan error) + timeout := time.After(time.Second * 60) + + //create handler + c := cors.New(cors.Options{ + AllowedOrigins: []string{strings.ReplaceAll(config.INFISICAL_LOGIN_URL, "/login", "")}, + AllowCredentials: true, + AllowedMethods: []string{"POST", "OPTIONS"}, + AllowedHeaders: []string{"Content-Type"}, + Debug: false, + }) + corsHandler := c.Handler(browserLoginHandler(success, failure)) + + log.Debug().Msgf("Callback server listening on port %d", callbackPort) + go http.Serve(listener, corsHandler) + + for { + select { + case loginResponse := <-success: + err = closeListener(&listener) + return loginResponse, nil + + case err = <-failure: + err = closeListener(&listener) + return CliLoginResponse{}, err + + case _ = <-timeout: + err = closeListener(&listener) + return CliLoginResponse{}, errors.New("server timeout") + } + } +} + +func closeListener(listener *net.Listener) error { + err := (*listener).Close() + if err != nil { + return err + } + log.Debug().Msg("Callback server shutdown successfully") + return nil +} + +func browserLoginHandler(success chan CliLoginResponse, failure chan error) http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + var loginResponse CliLoginResponse + + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&loginResponse) + if err != nil { + failure <- err + } + + w.WriteHeader(http.StatusOK) + success <- loginResponse + + } +} diff --git a/cli/packages/config/config.go b/cli/packages/config/config.go index e27bc8f683..c5e162c922 100644 --- a/cli/packages/config/config.go +++ b/cli/packages/config/config.go @@ -2,3 +2,4 @@ package config var INFISICAL_URL string var INFISICAL_URL_MANUAL_OVERRIDE string +var INFISICAL_LOGIN_URL string diff --git a/cli/packages/util/constants.go b/cli/packages/util/constants.go index 5c169f6d50..ee2532ee8e 100644 --- a/cli/packages/util/constants.go +++ b/cli/packages/util/constants.go @@ -4,6 +4,7 @@ const ( CONFIG_FILE_NAME = "infisical-config.json" CONFIG_FOLDER_NAME = ".infisical" INFISICAL_DEFAULT_API_URL = "https://app.infisical.com/api" + INFISICAL_DEFAULT_URL = "https://app.infisical.com" INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" SECRET_TYPE_PERSONAL = "personal" diff --git a/frontend/next.config.js b/frontend/next.config.js index dbe69eb1db..0fc0bec3e4 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -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; diff --git a/frontend/src/components/login/InitialLoginStep.tsx b/frontend/src/components/login/InitialLoginStep.tsx index 77e83434e5..dffe07be5e 100644 --- a/frontend/src/components/login/InitialLoginStep.tsx +++ b/frontend/src/components/login/InitialLoginStep.tsx @@ -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({
-
- setPassword(e.target.value)} - type="password" - placeholder="Enter your password..." - isRequired - autoComplete="current-password" - id="current-password" - className="h-12 select:-webkit-autofill:focus" - /> -
+
+ setPassword(e.target.value)} + type="password" + placeholder="Enter your password..." + isRequired + autoComplete="current-password" + id="current-password" + className="h-12 select:-webkit-autofill:focus" + /> +
{!isLoading && loginError && }
@@ -116,18 +145,18 @@ export default function InitialLoginStep({ > Login
-
+
or -
+
diff --git a/frontend/src/components/utilities/attemptCliLogin.ts b/frontend/src/components/utilities/attemptCliLogin.ts new file mode 100644 index 0000000000..33935a9e2e --- /dev/null +++ b/frontend/src/components/utilities/attemptCliLogin.ts @@ -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 => { + + 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; diff --git a/frontend/src/pages/cli-redirect.tsx b/frontend/src/pages/cli-redirect.tsx new file mode 100644 index 0000000000..076174b674 --- /dev/null +++ b/frontend/src/pages/cli-redirect.tsx @@ -0,0 +1,26 @@ +import Head from 'next/head'; +import Image from 'next/image'; +import Link from 'next/link'; + +export default function CliRedirect() { + return ( +
+ + Infisical Cli | Login Successful! + + +
+

Head back to your terminal!

+

+ You've successfully logged into infisical-cli +

+ infisical dragon - page not found +
+
+ ); +} From dd0fdea19f4c405c97995273ff9de7c8dfb58f34 Mon Sep 17 00:00:00 2001 From: quinton11 Date: Fri, 16 Jun 2023 12:58:00 +0000 Subject: [PATCH 02/10] fix: included mfa login flow --- cli/go.mod | 4 +- cli/go.sum | 4 + cli/packages/cmd/login.go | 432 +++++++++--------- .../src/components/login/InitialLoginStep.tsx | 10 +- frontend/src/components/login/MFAStep.tsx | 46 +- .../components/utilities/attemptCliLogin.ts | 85 ++-- .../utilities/attemptCliLoginMfa.ts | 122 +++++ 7 files changed, 449 insertions(+), 254 deletions(-) create mode 100644 frontend/src/components/utilities/attemptCliLoginMfa.ts diff --git a/cli/go.mod b/cli/go.mod index 6a29a182e6..ce4cf6808e 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -21,7 +21,7 @@ require ( github.com/spf13/viper v1.8.1 github.com/stretchr/testify v1.8.0 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d - golang.org/x/term v0.5.0 + golang.org/x/term v0.9.0 ) require ( @@ -60,7 +60,7 @@ require ( go.mongodb.org/mongo-driver v1.10.0 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.5.0 // indirect + golang.org/x/sys v0.9.0 // indirect golang.org/x/text v0.7.0 // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/cli/go.sum b/cli/go.sum index ec126dab29..ea16f2d37c 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -531,9 +531,13 @@ golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= +golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index 6c36be36e0..9ce0ea5ea4 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" + "os" "strings" "time" @@ -32,6 +33,7 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" "golang.org/x/crypto/argon2" + "golang.org/x/term" ) type params struct { @@ -45,6 +47,7 @@ type params struct { const ADD_USER = "Add a new account login" const REPLACE_USER = "Override current logged in user" const EXIT_USER_MENU = "Exit" +const QUIT_BROWSER_LOGIN = "q" // loginCmd represents the login command var loginCmd = &cobra.Command{ @@ -96,202 +99,25 @@ var loginCmd = &cobra.Command{ } } - var ( - email string - password string - loginOneResponse *api.GetLoginOneV2Response - loginTwoResponse *api.GetLoginTwoV2Response - ) - - //check for --browser flag - browserLogin, err := cmd.Flags().GetBool("browser") + //call browser login function + fmt.Printf("\nLogging in via browser... Hit '%s' to cancel\n", QUIT_BROWSER_LOGIN) + userCredentialsToBeStored, err := browserCliLogin() if err != nil { - util.HandleError(err, "Unable to parse browser flag") + //default to cli login on error + cliDefaultLogin(&userCredentialsToBeStored) } - if browserLogin { - log.Debug().Msg("Login via browser") - //call browser login function - loginResponse, err := browserCliLogin() - if err != nil { - util.HandleError(err, err.Error()) - } - email = loginResponse.Email - password = loginResponse.Password - loginOneResponse = &loginResponse.LoginOneResponse - loginTwoResponse = &loginResponse.LoginTwoResponse - //util.HandleError(errors.New("not implemented"), " login via browser not fully implemented") - } else { - email, password, err = askForLoginCredentials() - if err != nil { - util.HandleError(err, "Unable to parse email and password for authentication") - } - - loginOneResponse, loginTwoResponse, err = getFreshUserCredentials(email, password) - if err != nil { - fmt.Println("Unable to authenticate with the provided credentials, please try again") - log.Debug().Err(err) - return - } - } - - if loginTwoResponse.MfaEnabled { - i := 1 - for i < 6 { - mfaVerifyCode := askForMFACode() - - httpClient := resty.New() - httpClient.SetAuthToken(loginTwoResponse.Token) - verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{ - Email: email, - MFAToken: mfaVerifyCode, - }) - - if requestError != nil { - util.HandleError(err) - break - } else if mfaErrorResponse != nil { - if mfaErrorResponse.Context.Code == "mfa_invalid" { - msg := fmt.Sprintf("Incorrect, verification code. You have %v attempts left", 5-i) - fmt.Println(msg) - if i == 5 { - util.PrintErrorMessageAndExit("No tries left, please try again in a bit") - break - } - } - - if mfaErrorResponse.Context.Code == "mfa_expired" { - util.PrintErrorMessageAndExit("Your 2FA verification code has expired, please try logging in again") - break - } - i++ - } else { - loginTwoResponse.EncryptedPrivateKey = verifyMFAresponse.EncryptedPrivateKey - loginTwoResponse.EncryptionVersion = verifyMFAresponse.EncryptionVersion - loginTwoResponse.Iv = verifyMFAresponse.Iv - loginTwoResponse.ProtectedKey = verifyMFAresponse.ProtectedKey - loginTwoResponse.ProtectedKeyIV = verifyMFAresponse.ProtectedKeyIV - loginTwoResponse.ProtectedKeyTag = verifyMFAresponse.ProtectedKeyTag - loginTwoResponse.PublicKey = verifyMFAresponse.PublicKey - loginTwoResponse.Tag = verifyMFAresponse.Tag - loginTwoResponse.Token = verifyMFAresponse.Token - loginTwoResponse.EncryptionVersion = verifyMFAresponse.EncryptionVersion - - break - } - } - } - - var decryptedPrivateKey []byte - - if loginTwoResponse.EncryptionVersion == 1 { - log.Debug().Msg("Login version 1") - encryptedPrivateKey, _ := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey) - tag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag) - if err != nil { - util.HandleError(err) - } - - IV, err := base64.StdEncoding.DecodeString(loginTwoResponse.Iv) - if err != nil { - util.HandleError(err) - } - - paddedPassword := fmt.Sprintf("%032s", password) - key := []byte(paddedPassword) - - computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(key, encryptedPrivateKey, tag, IV) - if err != nil || len(computedDecryptedPrivateKey) == 0 { - util.HandleError(err) - } - - decryptedPrivateKey = computedDecryptedPrivateKey - - } else if loginTwoResponse.EncryptionVersion == 2 { - log.Debug().Msg("Login version 2") - protectedKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKey) - if err != nil { - util.HandleError(err) - } - - protectedKeyTag, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKeyTag) - if err != nil { - util.HandleError(err) - } - - protectedKeyIV, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKeyIV) - if err != nil { - util.HandleError(err) - } - - nonProtectedTag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag) - if err != nil { - util.HandleError(err) - } - - nonProtectedIv, err := base64.StdEncoding.DecodeString(loginTwoResponse.Iv) - if err != nil { - util.HandleError(err) - } - - parameters := ¶ms{ - memory: 64 * 1024, - iterations: 3, - parallelism: 1, - keyLength: 32, - } - - derivedKey, err := generateFromPassword(password, []byte(loginOneResponse.Salt), parameters) - if err != nil { - util.HandleError(fmt.Errorf("unable to generate argon hash from password [err=%s]", err)) - } - - decryptedProtectedKey, err := crypto.DecryptSymmetric(derivedKey, protectedKey, protectedKeyTag, protectedKeyIV) - if err != nil { - util.HandleError(fmt.Errorf("unable to get decrypted protected key [err=%s]", err)) - } - - encryptedPrivateKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey) - if err != nil { - util.HandleError(err) - } - - decryptedProtectedKeyInHex, err := hex.DecodeString(string(decryptedProtectedKey)) - if err != nil { - util.HandleError(err) - } - - computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(decryptedProtectedKeyInHex, encryptedPrivateKey, nonProtectedTag, nonProtectedIv) - if err != nil { - util.HandleError(err) - } - - decryptedPrivateKey = computedDecryptedPrivateKey - } else { - util.PrintErrorMessageAndExit("Insufficient details to decrypt private key") - } - - if string(decryptedPrivateKey) == "" || email == "" || loginTwoResponse.Token == "" { - log.Debug().Msgf("[decryptedPrivateKey=%s] [email=%s] [loginTwoResponse.Token=%s]", string(decryptedPrivateKey), email, loginTwoResponse.Token) - util.PrintErrorMessageAndExit("We were unable to fetch required details to complete your login. Run with -d to see more info") - } - - userCredentialsToBeStored := &models.UserCredentials{ - Email: email, - PrivateKey: string(decryptedPrivateKey), - JTWToken: loginTwoResponse.Token, - } - - err = util.StoreUserCredsInKeyRing(userCredentialsToBeStored) + err = util.StoreUserCredsInKeyRing(&userCredentialsToBeStored) if err != nil { currentVault, _ := util.GetCurrentVaultBackend() log.Error().Msgf("Unable to store your credentials in system vault [%s]. Rerun with flag -d to see full logs", currentVault) log.Error().Msgf("\nTo trouble shoot further, read https://infisical.com/docs/cli/faq") log.Debug().Err(err) - return + //return here + util.HandleError(err) } - err = util.WriteInitalConfig(userCredentialsToBeStored) + err = util.WriteInitalConfig(&userCredentialsToBeStored) if err != nil { util.HandleError(err, "Unable to write write to Infisical Config file. Please try again") } @@ -301,8 +127,9 @@ var loginCmd = &cobra.Command{ whilte := color.New(color.FgGreen) boldWhite := whilte.Add(color.Bold) + time.Sleep(time.Second * 1) boldWhite.Printf(">>>> Welcome to Infisical!") - boldWhite.Printf(" You are now logged in as %v <<<< \n", email) + boldWhite.Printf(" You are now logged in as %v <<<< \n", userCredentialsToBeStored.Email) plainBold := color.New(color.Bold) @@ -313,9 +140,169 @@ var loginCmd = &cobra.Command{ }, } +func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) { + email, password, err := askForLoginCredentials() + if err != nil { + util.HandleError(err, "Unable to parse email and password for authentication") + } + + loginOneResponse, loginTwoResponse, err := getFreshUserCredentials(email, password) + if err != nil { + fmt.Println("Unable to authenticate with the provided credentials, please try again") + log.Debug().Err(err) + //return here + util.HandleError(err) + } + + if loginTwoResponse.MfaEnabled { + i := 1 + for i < 6 { + mfaVerifyCode := askForMFACode() + + httpClient := resty.New() + httpClient.SetAuthToken(loginTwoResponse.Token) + verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{ + Email: email, + MFAToken: mfaVerifyCode, + }) + + if requestError != nil { + util.HandleError(err) + break + } else if mfaErrorResponse != nil { + if mfaErrorResponse.Context.Code == "mfa_invalid" { + msg := fmt.Sprintf("Incorrect, verification code. You have %v attempts left", 5-i) + fmt.Println(msg) + if i == 5 { + util.PrintErrorMessageAndExit("No tries left, please try again in a bit") + break + } + } + + if mfaErrorResponse.Context.Code == "mfa_expired" { + util.PrintErrorMessageAndExit("Your 2FA verification code has expired, please try logging in again") + break + } + i++ + } else { + loginTwoResponse.EncryptedPrivateKey = verifyMFAresponse.EncryptedPrivateKey + loginTwoResponse.EncryptionVersion = verifyMFAresponse.EncryptionVersion + loginTwoResponse.Iv = verifyMFAresponse.Iv + loginTwoResponse.ProtectedKey = verifyMFAresponse.ProtectedKey + loginTwoResponse.ProtectedKeyIV = verifyMFAresponse.ProtectedKeyIV + loginTwoResponse.ProtectedKeyTag = verifyMFAresponse.ProtectedKeyTag + loginTwoResponse.PublicKey = verifyMFAresponse.PublicKey + loginTwoResponse.Tag = verifyMFAresponse.Tag + loginTwoResponse.Token = verifyMFAresponse.Token + loginTwoResponse.EncryptionVersion = verifyMFAresponse.EncryptionVersion + + break + } + } + } + + var decryptedPrivateKey []byte + + if loginTwoResponse.EncryptionVersion == 1 { + log.Debug().Msg("Login version 1") + encryptedPrivateKey, _ := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey) + tag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag) + if err != nil { + util.HandleError(err) + } + + IV, err := base64.StdEncoding.DecodeString(loginTwoResponse.Iv) + if err != nil { + util.HandleError(err) + } + + paddedPassword := fmt.Sprintf("%032s", password) + key := []byte(paddedPassword) + + computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(key, encryptedPrivateKey, tag, IV) + if err != nil || len(computedDecryptedPrivateKey) == 0 { + util.HandleError(err) + } + + decryptedPrivateKey = computedDecryptedPrivateKey + + } else if loginTwoResponse.EncryptionVersion == 2 { + log.Debug().Msg("Login version 2") + protectedKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKey) + if err != nil { + util.HandleError(err) + } + + protectedKeyTag, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKeyTag) + if err != nil { + util.HandleError(err) + } + + protectedKeyIV, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKeyIV) + if err != nil { + util.HandleError(err) + } + + nonProtectedTag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag) + if err != nil { + util.HandleError(err) + } + + nonProtectedIv, err := base64.StdEncoding.DecodeString(loginTwoResponse.Iv) + if err != nil { + util.HandleError(err) + } + + parameters := ¶ms{ + memory: 64 * 1024, + iterations: 3, + parallelism: 1, + keyLength: 32, + } + + derivedKey, err := generateFromPassword(password, []byte(loginOneResponse.Salt), parameters) + if err != nil { + util.HandleError(fmt.Errorf("unable to generate argon hash from password [err=%s]", err)) + } + + decryptedProtectedKey, err := crypto.DecryptSymmetric(derivedKey, protectedKey, protectedKeyTag, protectedKeyIV) + if err != nil { + util.HandleError(fmt.Errorf("unable to get decrypted protected key [err=%s]", err)) + } + + encryptedPrivateKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey) + if err != nil { + util.HandleError(err) + } + + decryptedProtectedKeyInHex, err := hex.DecodeString(string(decryptedProtectedKey)) + if err != nil { + util.HandleError(err) + } + + computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(decryptedProtectedKeyInHex, encryptedPrivateKey, nonProtectedTag, nonProtectedIv) + if err != nil { + util.HandleError(err) + } + + decryptedPrivateKey = computedDecryptedPrivateKey + } else { + util.PrintErrorMessageAndExit("Insufficient details to decrypt private key") + } + + if string(decryptedPrivateKey) == "" || email == "" || loginTwoResponse.Token == "" { + log.Debug().Msgf("[decryptedPrivateKey=%s] [email=%s] [loginTwoResponse.Token=%s]", string(decryptedPrivateKey), email, loginTwoResponse.Token) + util.PrintErrorMessageAndExit("We were unable to fetch required details to complete your login. Run with -d to see more info") + } + + //updating usercredentials + userCredentialsToBeStored.Email = email + userCredentialsToBeStored.PrivateKey = string(decryptedPrivateKey) + userCredentialsToBeStored.JTWToken = loginTwoResponse.Token +} + func init() { rootCmd.AddCommand(loginCmd) - loginCmd.Flags().BoolP("browser", "b", false, "Login via browser") } func DomainOverridePrompt() (bool, error) { @@ -400,6 +387,7 @@ func askForLoginCredentials() (email string, password string, err error) { return nil } + fmt.Println("Enter Credentials...") emailPrompt := promptui.Prompt{ Label: "Email", Validate: validateEmail, @@ -513,21 +501,15 @@ func askForMFACode() string { return mfaVerifyCode } -type CliLoginResponse struct { - Email string `json:"email"` - Password string `json:"password"` - LoginOneResponse api.GetLoginOneV2Response `json:"loginOneResponse"` - LoginTwoResponse api.GetLoginTwoV2Response `json:"loginTwoResponse"` -} - -// Manages the browser login flow -// returns a CLILoginResponse on success and an error on failure -func browserCliLogin() (CliLoginResponse, error) { +// Manages the browser login flow. +// Returns a UserCredentials object on success and an error on failure +func browserCliLogin() (models.UserCredentials, error) { + SERVER_TIMEOUT := 60 * 10 //create listener - listener, err := net.Listen("tcp", ":0") + listener, err := net.Listen("tcp", "localhost:0") if err != nil { - return CliLoginResponse{}, err + return models.UserCredentials{}, err } //get callback port @@ -537,13 +519,17 @@ func browserCliLogin() (CliLoginResponse, error) { //open browser and login err = browser.OpenURL(url) if err != nil { - return CliLoginResponse{}, err + return models.UserCredentials{}, err } //flow channels - success := make(chan CliLoginResponse) + success := make(chan models.UserCredentials) failure := make(chan error) - timeout := time.After(time.Second * 60) + timeout := time.After(time.Second * time.Duration(SERVER_TIMEOUT)) + quit := make(chan bool) + + // + done := false //create handler c := cors.New(cors.Options{ @@ -556,21 +542,53 @@ func browserCliLogin() (CliLoginResponse, error) { corsHandler := c.Handler(browserLoginHandler(success, failure)) log.Debug().Msgf("Callback server listening on port %d", callbackPort) + go quitBrowserLogin(quit, &done) go http.Serve(listener, corsHandler) for { select { case loginResponse := <-success: err = closeListener(&listener) + done = true return loginResponse, nil case err = <-failure: err = closeListener(&listener) - return CliLoginResponse{}, err + done = true + return models.UserCredentials{}, err case _ = <-timeout: err = closeListener(&listener) - return CliLoginResponse{}, errors.New("server timeout") + done = true + return models.UserCredentials{}, errors.New("server timeout") + + case _ = <-quit: + return models.UserCredentials{}, errors.New("quitting browser login, defaulting to cli...") + + } + } +} + +// listens to 'q' input on terminal and +// sends 'true' to 'quit' channel +func quitBrowserLogin(quit chan bool, done *bool) { + // + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + fmt.Println(err) + return + } + defer term.Restore(int(os.Stdin.Fd()), oldState) + b := make([]byte, 1) + for { + _, _ = os.Stdin.Read(b) + if string(b) == QUIT_BROWSER_LOGIN { + quit <- true + break + } + + if *done { + break } } } @@ -584,10 +602,10 @@ func closeListener(listener *net.Listener) error { return nil } -func browserLoginHandler(success chan CliLoginResponse, failure chan error) http.HandlerFunc { +func browserLoginHandler(success chan models.UserCredentials, failure chan error) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var loginResponse CliLoginResponse + var loginResponse models.UserCredentials decoder := json.NewDecoder(r.Body) err := decoder.Decode(&loginResponse) diff --git a/frontend/src/components/login/InitialLoginStep.tsx b/frontend/src/components/login/InitialLoginStep.tsx index dffe07be5e..be8b316679 100644 --- a/frontend/src/components/login/InitialLoginStep.tsx +++ b/frontend/src/components/login/InitialLoginStep.tsx @@ -49,12 +49,20 @@ export default function InitialLoginStep({ }) 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,email,password}) + const cliResp = await instance.post(cliUrl,{...isCliLoginSuccessful.loginResponse}) + console.log(cliResp) //cli page router.push("/cli-redirect"); diff --git a/frontend/src/components/login/MFAStep.tsx b/frontend/src/components/login/MFAStep.tsx index a619cf07b9..acf6a2ac9c 100644 --- a/frontend/src/components/login/MFAStep.tsx +++ b/frontend/src/components/login/MFAStep.tsx @@ -3,8 +3,10 @@ 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 attemptLoginMfa from '@app/components/utilities/attemptLoginMfa'; +import attemptCliLoginMfa from '@app/components/utilities/attemptCliLoginMfa' import { useSendMfaToken } from '@app/hooks/api/auth'; import Error from '../basic/Error'; @@ -76,17 +78,43 @@ export default function MFAStep({ } setIsLoading(true); - const isLoginSuccessful = await attemptLoginMfa({ - email, - password, - providerAuthToken, - mfaToken: mfaCode - }); + const queryParams = new URLSearchParams(location.search) + if (queryParams){ + 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() + const cliResp = 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; diff --git a/frontend/src/components/utilities/attemptCliLogin.ts b/frontend/src/components/utilities/attemptCliLogin.ts index 33935a9e2e..f6427e36f3 100644 --- a/frontend/src/components/utilities/attemptCliLogin.ts +++ b/frontend/src/components/utilities/attemptCliLogin.ts @@ -15,23 +15,11 @@ import SecurityClient from './SecurityClient'; 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; - }; + mfaEnabled: boolean; + loginResponse?: { + email: string; + privateKey: string; + JTWToken: string; }; success: boolean; } @@ -92,27 +80,54 @@ const attemptLogin = async ( providerAuthToken, } ); + if (mfaEnabled) { + // case: MFA is enabled - resolve({ - loginResponse: { - loginOneResponse: { serverPublicKey, salt }, - loginTwoResponse: { - mfaEnabled, - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - token, - publicKey, - encryptedPrivateKey, - iv, - tag - } - }, - success: true - }) + // 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 + }); + + resolve({ + mfaEnabled: false, + loginResponse: { + email: email, + privateKey: privateKey, + JTWToken: token + }, + success: true + }) + + } } catch (err) { reject(err); } diff --git a/frontend/src/components/utilities/attemptCliLoginMfa.ts b/frontend/src/components/utilities/attemptCliLoginMfa.ts new file mode 100644 index 0000000000..e4143edbbe --- /dev/null +++ b/frontend/src/components/utilities/attemptCliLoginMfa.ts @@ -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 => { + 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: privateKey, + JTWToken: token + } + }); + } catch (err) { + reject(err); + } + }); + }); +} + +export default attemptLoginMfa; \ No newline at end of file From 5a4a36a06a7f29266f4475ee7f1464f41d7960ae Mon Sep 17 00:00:00 2001 From: quinton11 Date: Fri, 16 Jun 2023 13:20:17 +0000 Subject: [PATCH 03/10] fix: minor change --- cli/packages/cmd/login.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index 9ce0ea5ea4..ef5e9759bb 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -529,7 +529,6 @@ func browserCliLogin() (models.UserCredentials, error) { quit := make(chan bool) // - done := false //create handler c := cors.New(cors.Options{ @@ -542,24 +541,21 @@ func browserCliLogin() (models.UserCredentials, error) { corsHandler := c.Handler(browserLoginHandler(success, failure)) log.Debug().Msgf("Callback server listening on port %d", callbackPort) - go quitBrowserLogin(quit, &done) + go quitBrowserLogin(quit) go http.Serve(listener, corsHandler) for { select { case loginResponse := <-success: err = closeListener(&listener) - done = true return loginResponse, nil case err = <-failure: err = closeListener(&listener) - done = true return models.UserCredentials{}, err case _ = <-timeout: err = closeListener(&listener) - done = true return models.UserCredentials{}, errors.New("server timeout") case _ = <-quit: @@ -571,7 +567,7 @@ func browserCliLogin() (models.UserCredentials, error) { // listens to 'q' input on terminal and // sends 'true' to 'quit' channel -func quitBrowserLogin(quit chan bool, done *bool) { +func quitBrowserLogin(quit chan bool) { // oldState, err := term.MakeRaw(int(os.Stdin.Fd())) if err != nil { @@ -586,10 +582,6 @@ func quitBrowserLogin(quit chan bool, done *bool) { quit <- true break } - - if *done { - break - } } } From 7e8ba077ae97c625d0a07e3f8bae909775124a9b Mon Sep 17 00:00:00 2001 From: quinton11 Date: Mon, 19 Jun 2023 09:32:55 +0000 Subject: [PATCH 04/10] fix: terminal text alignment --- cli/packages/cmd/login.go | 18 +++++++++++++----- .../src/components/login/InitialLoginStep.tsx | 2 +- frontend/src/components/login/MFAStep.tsx | 4 ++-- frontend/src/hooks/api/users/queries.tsx | 2 +- frontend/src/pages/login.tsx | 19 ++++++++++++++++++- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index ef5e9759bb..9133d31015 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -528,7 +528,8 @@ func browserCliLogin() (models.UserCredentials, error) { timeout := time.After(time.Second * time.Duration(SERVER_TIMEOUT)) quit := make(chan bool) - // + //terminal state + var oldState term.State //create handler c := cors.New(cors.Options{ @@ -541,21 +542,24 @@ func browserCliLogin() (models.UserCredentials, error) { corsHandler := c.Handler(browserLoginHandler(success, failure)) log.Debug().Msgf("Callback server listening on port %d", callbackPort) - go quitBrowserLogin(quit) + go quitBrowserLogin(quit, &oldState) go http.Serve(listener, corsHandler) for { select { case loginResponse := <-success: err = closeListener(&listener) + restoreTerminal(&oldState) return loginResponse, nil case err = <-failure: err = closeListener(&listener) + restoreTerminal(&oldState) return models.UserCredentials{}, err case _ = <-timeout: err = closeListener(&listener) + restoreTerminal(&oldState) return models.UserCredentials{}, errors.New("server timeout") case _ = <-quit: @@ -565,16 +569,20 @@ func browserCliLogin() (models.UserCredentials, error) { } } +func restoreTerminal(oldState *term.State) { + term.Restore(int(os.Stdin.Fd()), oldState) +} + // listens to 'q' input on terminal and // sends 'true' to 'quit' channel -func quitBrowserLogin(quit chan bool) { +func quitBrowserLogin(quit chan bool, oState *term.State) { // oldState, err := term.MakeRaw(int(os.Stdin.Fd())) if err != nil { - fmt.Println(err) return } - defer term.Restore(int(os.Stdin.Fd()), oldState) + *oState = *oldState + defer restoreTerminal(oldState) b := make([]byte, 1) for { _, _ = os.Stdin.Read(b) diff --git a/frontend/src/components/login/InitialLoginStep.tsx b/frontend/src/components/login/InitialLoginStep.tsx index be8b316679..42105253c3 100644 --- a/frontend/src/components/login/InitialLoginStep.tsx +++ b/frontend/src/components/login/InitialLoginStep.tsx @@ -39,7 +39,7 @@ export default function InitialLoginStep({ setIsLoading(true); const queryParams = new URLSearchParams(location.search) - if (queryParams) { + if (queryParams && queryParams.get("callback_port")) { const callbackPort = queryParams.get("callback_port") //attemptCliLogin diff --git a/frontend/src/components/login/MFAStep.tsx b/frontend/src/components/login/MFAStep.tsx index acf6a2ac9c..85341620cd 100644 --- a/frontend/src/components/login/MFAStep.tsx +++ b/frontend/src/components/login/MFAStep.tsx @@ -79,7 +79,7 @@ export default function MFAStep({ setIsLoading(true); const queryParams = new URLSearchParams(location.search) - if (queryParams){ + if (queryParams && queryParams.get("callback_port")){ const callbackPort = queryParams.get("callback_port") //attemptCliLogin @@ -94,7 +94,7 @@ export default function MFAStep({ // case: login was successful const cliUrl = `http://localhost:${callbackPort}` - //send request to server endpoint + //send request to server endpoint const instance = axios.create() const cliResp = await instance.post(cliUrl,{...isCliLoginSuccessful.loginResponse,email}) diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index ef6dcc726e..b700374f14 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -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; diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index 61276f35a2..ab28bba447 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -4,13 +4,15 @@ 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 +22,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 +47,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(location.search) + if(queryParams && queryParams.get("callback_port")){ + const callback_port = queryParams.get("callback_port") + + //send post request to cli with details + const cliUrl = `http://localhost:${callback_port}` + const instance = axios.create() + const cliResp = 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'); From c267aee20f2882f3b36bf56295f688c0875af581 Mon Sep 17 00:00:00 2001 From: quinton11 Date: Mon, 19 Jun 2023 22:25:21 +0000 Subject: [PATCH 05/10] feat: interactive login --- cli/packages/cmd/login.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index 9133d31015..56998cbdb8 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -98,13 +98,22 @@ var loginCmd = &cobra.Command{ util.HandleError(err, "Unable to parse domain url") } } + var userCredentialsToBeStored models.UserCredentials + + interactiveLogin := false + if cmd.Flags().Changed("interactive") { + interactiveLogin = true + cliDefaultLogin(&userCredentialsToBeStored) + } //call browser login function - fmt.Printf("\nLogging in via browser... Hit '%s' to cancel\n", QUIT_BROWSER_LOGIN) - userCredentialsToBeStored, err := browserCliLogin() - if err != nil { - //default to cli login on error - cliDefaultLogin(&userCredentialsToBeStored) + if !interactiveLogin { + fmt.Printf("\nLogging in via browser... Hit '%s' to cancel\n", QUIT_BROWSER_LOGIN) + userCredentialsToBeStored, err = browserCliLogin() + if err != nil { + //default to cli login on error + cliDefaultLogin(&userCredentialsToBeStored) + } } err = util.StoreUserCredsInKeyRing(&userCredentialsToBeStored) @@ -303,6 +312,7 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) { func init() { rootCmd.AddCommand(loginCmd) + loginCmd.Flags().BoolP("interactive", "i", false, "login via the command line") } func DomainOverridePrompt() (bool, error) { From 8a5e65512224cfd432e3735042721ff1741dbe74 Mon Sep 17 00:00:00 2001 From: quinton11 Date: Wed, 21 Jun 2023 07:29:05 +0000 Subject: [PATCH 06/10] fix: frontend lint errors --- .../src/components/login/InitialLoginStep.tsx | 50 +++++++++---------- frontend/src/components/login/MFAStep.tsx | 26 +++++----- .../components/utilities/attemptCliLogin.ts | 49 +++++++++++++----- .../utilities/attemptCliLoginMfa.ts | 30 +++++------ frontend/src/hooks/api/users/queries.tsx | 2 +- frontend/src/pages/cli-redirect.tsx | 7 ++- frontend/src/pages/login.tsx | 49 +++++++++--------- 7 files changed, 119 insertions(+), 94 deletions(-) diff --git a/frontend/src/components/login/InitialLoginStep.tsx b/frontend/src/components/login/InitialLoginStep.tsx index 6b39f8eced..5f17b1cf98 100644 --- a/frontend/src/components/login/InitialLoginStep.tsx +++ b/frontend/src/components/login/InitialLoginStep.tsx @@ -1,16 +1,16 @@ -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; +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'; -import attemptCliLogin from '../utilities/attemptCliLogin'; +import { Button, Input } from "../v2"; export default function InitialLoginStep({ setStep, @@ -38,11 +38,11 @@ export default function InitialLoginStep({ } setIsLoading(true); - const queryParams = new URLSearchParams(location.search) + const queryParams = new URLSearchParams(window.location.search) if (queryParams && queryParams.get("callback_port")) { const callbackPort = queryParams.get("callback_port") - //attemptCliLogin + // attemptCliLogin const isCliLoginSuccessful = await attemptCliLogin({ email, password, @@ -59,15 +59,15 @@ export default function InitialLoginStep({ // case: login was successful const cliUrl = `http://localhost:${callbackPort}` - //send request to server endpoint + // send request to server endpoint const instance = axios.create() - const cliResp = await instance.post(cliUrl,{...isCliLoginSuccessful.loginResponse}) + const cliResp = await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse }) console.log(cliResp) - //cli page + // cli page router.push("/cli-redirect"); - //on success, router.push to cli Login Successful page + // on success, router.push to cli Login Successful page } } else { @@ -86,7 +86,7 @@ export default function InitialLoginStep({ } // case: login does not require MFA step - router.push(`/dashboard/${localStorage.getItem('projectData.id')}`); + router.push(`/dashboard/${localStorage.getItem("projectData.id")}`); } } @@ -127,18 +127,18 @@ export default function InitialLoginStep({
-
- setPassword(e.target.value)} - type="password" - placeholder="Enter your password..." - isRequired - autoComplete="current-password" - id="current-password" - className="h-12 select:-webkit-autofill:focus" - /> -
+
+ setPassword(e.target.value)} + type="password" + placeholder="Enter your password..." + isRequired + autoComplete="current-password" + id="current-password" + className="h-12 select:-webkit-autofill:focus" + /> +
{!isLoading && loginError && }
diff --git a/frontend/src/components/login/MFAStep.tsx b/frontend/src/components/login/MFAStep.tsx index 7ae7c77811..2408ad0b10 100644 --- a/frontend/src/components/login/MFAStep.tsx +++ b/frontend/src/components/login/MFAStep.tsx @@ -1,13 +1,13 @@ /* eslint-disable react/jsx-props-no-spreading */ -import React, { useState } from 'react'; -import ReactCodeInput from 'react-code-input'; -import { useTranslation } from 'react-i18next'; -import { useRouter } from 'next/router'; +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 attemptLoginMfa from '@app/components/utilities/attemptLoginMfa'; -import attemptCliLoginMfa from '@app/components/utilities/attemptCliLoginMfa' -import { useSendMfaToken } from '@app/hooks/api/auth'; +import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa" +import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa"; +import { useSendMfaToken } from "@app/hooks/api/auth"; import Error from "../basic/Error"; import { Button } from "../v2"; @@ -78,11 +78,11 @@ export default function MFAStep({ } setIsLoading(true); - const queryParams = new URLSearchParams(location.search) + const queryParams = new URLSearchParams(window.location.search) if (queryParams && queryParams.get("callback_port")){ const callbackPort = queryParams.get("callback_port") - //attemptCliLogin + // attemptCliLogin const isCliLoginSuccessful = await attemptCliLoginMfa({ email, password, @@ -94,11 +94,11 @@ export default function MFAStep({ // case: login was successful const cliUrl = `http://localhost:${callbackPort}` - //send request to server endpoint + // send request to server endpoint const instance = axios.create() - const cliResp = await instance.post(cliUrl,{...isCliLoginSuccessful.loginResponse,email}) + await instance.post(cliUrl,{...isCliLoginSuccessful.loginResponse,email}) - //cli page + // cli page router.push("/cli-redirect"); } }else{ @@ -111,7 +111,7 @@ export default function MFAStep({ if (isLoginSuccessful) { setIsLoading(false); - router.push(`/dashboard/${localStorage.getItem('projectData.id')}`); + router.push(`/dashboard/${localStorage.getItem("projectData.id")}`); } } diff --git a/frontend/src/components/utilities/attemptCliLogin.ts b/frontend/src/components/utilities/attemptCliLogin.ts index f6427e36f3..099351baaf 100644 --- a/frontend/src/components/utilities/attemptCliLogin.ts +++ b/frontend/src/components/utilities/attemptCliLogin.ts @@ -1,15 +1,15 @@ /* eslint-disable prefer-destructuring */ -import jsrp from 'jsrp'; +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 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'; +import Telemetry from "./telemetry/Telemetry"; +import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; +import SecurityClient from "./SecurityClient"; // eslint-disable-next-line new-cap const client = new jsrp.client(); @@ -101,7 +101,7 @@ const attemptLogin = async ( // case: MFA is not enabled // unset provider auth token in case it was used - SecurityClient.setProviderAuthToken(''); + SecurityClient.setProviderAuthToken(""); // set JWT token SecurityClient.setToken(token); @@ -117,11 +117,36 @@ const attemptLogin = async ( 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: email, - privateKey: privateKey, + email, + privateKey, JTWToken: token }, success: true diff --git a/frontend/src/components/utilities/attemptCliLoginMfa.ts b/frontend/src/components/utilities/attemptCliLoginMfa.ts index e4143edbbe..cb33d3bad4 100644 --- a/frontend/src/components/utilities/attemptCliLoginMfa.ts +++ b/frontend/src/components/utilities/attemptCliLoginMfa.ts @@ -1,19 +1,19 @@ /* eslint-disable prefer-destructuring */ -import jsrp from 'jsrp'; +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 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'; +import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; +import SecurityClient from "./SecurityClient"; // eslint-disable-next-line new-cap const client = new jsrp.client(); -interface isMfaLoginSuccessful { +interface IsMfaLoginSuccessful { success: boolean; loginResponse:{ privateKey: string; @@ -39,7 +39,7 @@ const attemptLoginMfa = async ({ password: string; providerAuthToken?: string, mfaToken: string; -}): Promise => { +}): Promise => { return new Promise((resolve, reject) => { client.init({ username: email, @@ -69,9 +69,9 @@ const attemptLoginMfa = async ({ }); // unset temporary (MFA) JWT token and set JWT token - SecurityClient.setMfaToken(''); + SecurityClient.setMfaToken(""); SecurityClient.setToken(token); - SecurityClient.setProviderAuthToken(''); + SecurityClient.setProviderAuthToken(""); const privateKey = await KeyService.decryptPrivateKey({ encryptionVersion, @@ -98,17 +98,17 @@ const attemptLoginMfa = async ({ // and not initializing the login details const userOrgs = await getOrganizations(); const orgId = userOrgs[0]._id; - localStorage.setItem('orgData.id', orgId); + localStorage.setItem("orgData.id", orgId); const orgUserProjects = await getOrganizationUserProjects({ orgId }); - localStorage.setItem('projectData.id', orgUserProjects[0]._id); + localStorage.setItem("projectData.id", orgUserProjects[0]._id); resolve({ success: true, loginResponse:{ - privateKey: privateKey, + privateKey, JTWToken: token } }); diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index d48aa07af0..9baf7b7847 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -25,7 +25,7 @@ const userKeys = { }; export const fetchUserDetails = async () => { - const { data } = await apiRequest.get<{ user: User }>('/api/v1/user'); + const { data } = await apiRequest.get<{ user: User }>("/api/v1/user"); return data.user; }; diff --git a/frontend/src/pages/cli-redirect.tsx b/frontend/src/pages/cli-redirect.tsx index 076174b674..889a31dcc8 100644 --- a/frontend/src/pages/cli-redirect.tsx +++ b/frontend/src/pages/cli-redirect.tsx @@ -1,6 +1,5 @@ -import Head from 'next/head'; -import Image from 'next/image'; -import Link from 'next/link'; +import Head from "next/head"; +import Image from "next/image"; export default function CliRedirect() { return ( @@ -12,7 +11,7 @@ export default function CliRedirect() {

Head back to your terminal!

- You've successfully logged into infisical-cli + You've successfully logged into infisical-cli

Date: Wed, 21 Jun 2023 12:45:56 -0400 Subject: [PATCH 07/10] remove service accounts from k8 docs --- docs/integrations/platforms/kubernetes.mdx | 58 ++-------------------- 1 file changed, 3 insertions(+), 55 deletions(-) diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index efe5297ad9..58a8363057 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -51,16 +51,10 @@ spec: # The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used hostAPI: https://app.infisical.com/api authentication: - serviceToken: # <-- option 1 + serviceToken: serviceTokenSecretReference: secretName: service-token secretNamespace: option - serviceAccount: # <-- method 2 - serviceAccountSecretReference: - secretName: service-account - secretNamespace: default - projectId: "6439ec224cfbf7ea2a95b651" - environmentName: "dev" managedSecretReference: secretName: managed-secret # <-- the name of kubernetes secret that will be created secretNamespace: default # <-- where the kubernetes secret that will be created @@ -86,7 +80,7 @@ spec: - The `authentication` property tells the operator where it should look to find credentials needed to fetch secrets from Infisical. You can authenticate via two methods as described below. + The `authentication` property tells the operator where it should look to find credentials needed to fetch secrets from Infisical. @@ -94,7 +88,7 @@ spec: #### 1. Generate service token - You can generate a service token for an Infisical project by heading over to the Infisical dashboard then to Project Settings. + You can generate a [service token](../../documentation/platform/token) for an Infisical project by heading over to the Infisical dashboard then to Project Settings. #### 2. Create Kubernetes secret containing service token @@ -123,52 +117,6 @@ spec: secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token ... ``` - - - - We recommend authenticating with service account credentials when you have a large number of services. With this method, instead of creating a service token for each Infisical project you'd like to - fetch secrets from, you can fetch secrets from a number of Infisical projects with just one set of credentials. - - #### 1. Generate service account - - You can generate a service account by heading over to the organization settings. Once you create the service account, keep the credentials at hand for the next steps. - - #### 2. Grant service account access to Infisical projects - - Click on the pencil icon on the service account you just created and add the projects you'd like to be accessible via that service account. - - #### 3. Store service account credentials in K8 secret - Next, we'll need to store the service account credentials in a kubernetes secret so that we can reference it in our InfisicalSecret CRD. - - We recommend you create this kubernetes secret in a new namespace since you may need to reference it many times for each InfisicalSecret CRD you create. - - To quickly create a Kubernetes secret containing the service account details, you can execute the command below after replacing it with your own service account credentials. - - ``` - kubectl create secret generic service-token --from-literal=serviceAccountAccessKey=[REPLACE] --from-literal=serviceAccountPrivateKey=[REPLACE] --from-literal=serviceAccountPublicKey=[REPLACE] - ``` - - Regardless of how you create the kubernetes secret containing the service account credentials, you will need to define values for the following keys in the secret: `serviceAccountAccessKey`, `serviceAccountPrivateKey`, and `serviceAccountPublicKey` - - Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceAccount.serviceAccountSecretReference` field in the InfisicalSecret CRD. - - #### 4. Add projectId and environment from which to fetch secrets from - Add the Infisical project id and environment from which to fetch secrets for by providing values under `authentication.serviceAccount.projectId` and `authentication.serviceAccount.environmentName`. - - ## Example - ```yaml - apiVersion: secrets.infisical.com/v1alpha1 - kind: InfisicalSecret - metadata: - name: infisicalsecret-sample-crd - spec: - serviceAccount: - serviceAccountSecretReference: - secretName: service-account - secretNamespace: default - projectId: "6439ec224cfbf7ea2a95b651" - environmentName: "dev" - ``` From f785d62315eaa607354601d51382699b922c6fe8 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 21 Jun 2023 14:46:57 -0400 Subject: [PATCH 08/10] remove img from login by cli --- frontend/src/pages/cli-redirect.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/frontend/src/pages/cli-redirect.tsx b/frontend/src/pages/cli-redirect.tsx index 889a31dcc8..35f4fb04dc 100644 --- a/frontend/src/pages/cli-redirect.tsx +++ b/frontend/src/pages/cli-redirect.tsx @@ -13,12 +13,6 @@ export default function CliRedirect() {

You've successfully logged into infisical-cli

- infisical dragon - page not found
); From afd0c6de087869dee59dbdbcd71180fc56f75d46 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 21 Jun 2023 15:08:47 -0400 Subject: [PATCH 09/10] remove unused import --- frontend/src/pages/cli-redirect.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/pages/cli-redirect.tsx b/frontend/src/pages/cli-redirect.tsx index 35f4fb04dc..e28a9e9dcd 100644 --- a/frontend/src/pages/cli-redirect.tsx +++ b/frontend/src/pages/cli-redirect.tsx @@ -1,5 +1,4 @@ import Head from "next/head"; -import Image from "next/image"; export default function CliRedirect() { return ( From b1f4e17aaf96eebee8d56d981f6dc21ff8e52351 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Thu, 22 Jun 2023 00:40:45 -0400 Subject: [PATCH 10/10] increase limit --- backend/src/helpers/rateLimiter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/helpers/rateLimiter.ts b/backend/src/helpers/rateLimiter.ts index 853e7c2ed7..e639f94d11 100644 --- a/backend/src/helpers/rateLimiter.ts +++ b/backend/src/helpers/rateLimiter.ts @@ -10,7 +10,7 @@ export const apiLimiter = rateLimit({ // errorHandler: console.error.bind(null, 'rate-limit-mongo') // }), windowMs: 60 * 1000, - max: 240, + max: 350, standardHeaders: true, legacyHeaders: false, skip: (request) => { @@ -30,7 +30,7 @@ const authLimit = rateLimit({ // collectionName: "expressRateRecords-authLimit", // }), windowMs: 60 * 1000, - max: 10, + max: 100, standardHeaders: true, legacyHeaders: false, keyGenerator: (req, res) => {