diff --git a/cli/go.mod b/cli/go.mod index 6a29a182e..ce4cf6808 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 ec126dab2..ea16f2d37 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 6c36be36e..9ce0ea5ea 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 dffe07be5..be8b31667 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 a619cf07b..acf6a2ac9 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 33935a9e2..f6427e36f 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 000000000..e4143edbb --- /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