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) => { diff --git a/cli/go.mod b/cli/go.mod index 640e6dc0ca..ce4cf6808e 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,12 +15,13 @@ 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 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 ( @@ -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 @@ -58,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 574452d459..ea16f2d37c 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,15 +524,20 @@ 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= 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 1329b1d2a0..c629130a44 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -6,10 +6,15 @@ package cmd import ( "encoding/base64" "encoding/hex" + "encoding/json" + "os" "strings" + "time" "errors" "fmt" + "net" + "net/http" "net/url" "regexp" @@ -22,10 +27,13 @@ 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" + "golang.org/x/term" ) type params struct { @@ -39,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{ @@ -89,177 +98,35 @@ var loginCmd = &cobra.Command{ util.HandleError(err, "Unable to parse domain url") } } + var userCredentialsToBeStored models.UserCredentials - email, password, err := askForLoginCredentials() - if err != nil { - util.HandleError(err, "Unable to parse email and password for authentication") + interactiveLogin := false + if cmd.Flags().Changed("interactive") { + interactiveLogin = true + cliDefaultLogin(&userCredentialsToBeStored) } - loginOneResponse, loginTwoResponse, err := getFreshUserCredentials(email, password) - if err != nil { - log.Warn().Msg("Unable to authenticate with the provided credentials, please ensure your email and password are correct") - 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 - loginTwoResponse.RefreshToken = verifyMFAresponse.RefreshToken - break - } + //call browser login function + 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) } } - 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, - RefreshToken: loginTwoResponse.RefreshToken, - } - - 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") } @@ -269,8 +136,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) @@ -281,8 +149,170 @@ 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("interactive", "i", false, "login via the command line") } func DomainOverridePrompt() (bool, error) { @@ -327,7 +357,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 } @@ -342,7 +373,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() @@ -350,8 +381,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 } @@ -365,6 +397,7 @@ func askForLoginCredentials() (email string, password string, err error) { return nil } + fmt.Println("Enter Credentials...") emailPrompt := promptui.Prompt{ Label: "Email", Validate: validateEmail, @@ -477,3 +510,121 @@ func askForMFACode() string { return mfaVerifyCode } + +// 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", "localhost:0") + if err != nil { + return models.UserCredentials{}, 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 models.UserCredentials{}, err + } + + //flow channels + success := make(chan models.UserCredentials) + failure := make(chan 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{ + 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 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: + return models.UserCredentials{}, errors.New("quitting browser login, defaulting to cli...") + + } + } +} + +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, oState *term.State) { + // + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return + } + *oState = *oldState + defer restoreTerminal(oldState) + b := make([]byte, 1) + for { + _, _ = os.Stdin.Read(b) + if string(b) == QUIT_BROWSER_LOGIN { + quit <- true + break + } + } +} + +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 models.UserCredentials, failure chan error) http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + var loginResponse models.UserCredentials + + 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/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" - ``` diff --git a/frontend/next.config.js b/frontend/next.config.js index 0816161ff3..b133818bdb 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 https://www.youtube.com/; - connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io 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' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io 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://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:; media-src https://js.intercomcdn.com; font-src 'self' https://fonts.intercomcdn.com/ 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 50d184f540..5f17b1cf98 100644 --- a/frontend/src/components/login/InitialLoginStep.tsx +++ b/frontend/src/components/login/InitialLoginStep.tsx @@ -2,10 +2,12 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; +import axios from "axios" import attemptLogin from "@app/components/utilities/attemptLogin"; import Error from "../basic/Error"; +import attemptCliLogin from "../utilities/attemptCliLogin"; // import { faGoogle } from '@fortawesome/free-brands-svg-icons'; // import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, Input } from "../v2"; @@ -31,33 +33,68 @@ export default function InitialLoginStep({ const handleLogin = async () => { try { - if (!email || !password) { - return; - } - - setIsLoading(true); - const isLoginSuccessful = await attemptLogin({ - email, - password, - }); - if (isLoginSuccessful && isLoginSuccessful.success) { - // case: login was successful - - if (isLoginSuccessful.mfaEnabled) { - // case: login requires MFA step - setStep(2); - setIsLoading(false); - return; + if (!email || !password) { + return; } - - // case: login does not require MFA step - router.push(`/dashboard/${localStorage.getItem("projectData.id")}`); - } - + + setIsLoading(true); + const queryParams = new URLSearchParams(window.location.search) + if (queryParams && queryParams.get("callback_port")) { + const callbackPort = queryParams.get("callback_port") + + // attemptCliLogin + const isCliLoginSuccessful = await attemptCliLogin({ + email, + password, + }) + + if (isCliLoginSuccessful && isCliLoginSuccessful.success) { + + if (isCliLoginSuccessful.mfaEnabled) { + // case: login requires MFA step + setStep(2); + setIsLoading(false); + return; + } + // case: login was successful + const cliUrl = `http://localhost:${callbackPort}` + + // send request to server endpoint + const instance = axios.create() + const cliResp = await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse }) + console.log(cliResp) + + // cli page + router.push("/cli-redirect"); + + // on success, router.push to cli Login Successful page + + } + } else { + const isLoginSuccessful = await attemptLogin({ + email, + password, + }); + if (isLoginSuccessful && isLoginSuccessful.success) { + // case: login was successful + + if (isLoginSuccessful.mfaEnabled) { + // case: login requires MFA step + setStep(2); + setIsLoading(false); + return; + } + + // case: login does not require MFA step + router.push(`/dashboard/${localStorage.getItem("projectData.id")}`); + } + } + + } catch (err) { - setLoginError(true); + setLoginError(true); } - + setIsLoading(false); } @@ -90,18 +127,18 @@ export default function InitialLoginStep({
-
- 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 +153,18 @@ export default function InitialLoginStep({ > Login
-
+
or -
+
diff --git a/frontend/src/components/login/MFAStep.tsx b/frontend/src/components/login/MFAStep.tsx index 25955b1ad5..2408ad0b10 100644 --- a/frontend/src/components/login/MFAStep.tsx +++ b/frontend/src/components/login/MFAStep.tsx @@ -3,7 +3,9 @@ import React, { useState } from "react"; import ReactCodeInput from "react-code-input"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/router"; +import axios from "axios" +import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa" import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa"; import { useSendMfaToken } from "@app/hooks/api/auth"; @@ -76,17 +78,43 @@ export default function MFAStep({ } setIsLoading(true); - const isLoginSuccessful = await attemptLoginMfa({ - email, - password, - providerAuthToken, - mfaToken: mfaCode - }); + const queryParams = new URLSearchParams(window.location.search) + if (queryParams && queryParams.get("callback_port")){ + const callbackPort = queryParams.get("callback_port") - if (isLoginSuccessful) { - setIsLoading(false); - router.push(`/dashboard/${localStorage.getItem("projectData.id")}`); + // attemptCliLogin + const isCliLoginSuccessful = await attemptCliLoginMfa({ + email, + password, + providerAuthToken, + mfaToken: mfaCode + }) + + if (isCliLoginSuccessful && isCliLoginSuccessful.success){ + // case: login was successful + const cliUrl = `http://localhost:${callbackPort}` + + // send request to server endpoint + const instance = axios.create() + await instance.post(cliUrl,{...isCliLoginSuccessful.loginResponse,email}) + + // cli page + router.push("/cli-redirect"); + } + }else{ + const isLoginSuccessful = await attemptLoginMfa({ + email, + password, + providerAuthToken, + mfaToken: mfaCode + }); + + if (isLoginSuccessful) { + setIsLoading(false); + router.push(`/dashboard/${localStorage.getItem("projectData.id")}`); + } } + } catch (err) { const error = err as VerifyMfaTokenError; diff --git a/frontend/src/components/utilities/attemptCliLogin.ts b/frontend/src/components/utilities/attemptCliLogin.ts new file mode 100644 index 0000000000..099351baaf --- /dev/null +++ b/frontend/src/components/utilities/attemptCliLogin.ts @@ -0,0 +1,164 @@ +/* eslint-disable prefer-destructuring */ +import jsrp from "jsrp"; + +import login1 from "@app/pages/api/auth/Login1"; +import login2 from "@app/pages/api/auth/Login2"; +import getOrganizations from "@app/pages/api/organization/getOrgs"; +import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects"; +import KeyService from "@app/services/KeyService"; + +import Telemetry from "./telemetry/Telemetry"; +import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; +import SecurityClient from "./SecurityClient"; + +// eslint-disable-next-line new-cap +const client = new jsrp.client(); + +interface IsCliLoginSuccessful { + mfaEnabled: boolean; + loginResponse?: { + email: string; + privateKey: string; + JTWToken: string; + }; + success: boolean; +} + +/** + * Return whether or not login is successful for user with email [email] + * and password [password] + * @param {string} email - email of user to log in + * @param {string} password - password of user to log in + */ +const attemptLogin = async ( + { + email, + password, + providerAuthToken, + }: { + email: string; + password: string; + providerAuthToken?: string; + } +): Promise => { + + const telemetry = new Telemetry().getInstance(); + return new Promise((resolve, reject) => { + client.init( + { + username: email, + password + }, + async () => { + try { + const clientPublicKey = client.getPublicKey(); + const { serverPublicKey, salt } = await login1({ + email, + clientPublicKey, + providerAuthToken, + }); + + client.setSalt(salt); + client.setServerPublicKey(serverPublicKey); + const clientProof = client.getProof(); // called M1 + + const { + mfaEnabled, + encryptionVersion, + protectedKey, + protectedKeyIV, + protectedKeyTag, + token, + publicKey, + encryptedPrivateKey, + iv, + tag + } = await login2( + { + email, + clientProof, + providerAuthToken, + } + ); + if (mfaEnabled) { + // case: MFA is enabled + + // set temporary (MFA) JWT token + SecurityClient.setMfaToken(token); + + resolve({ + mfaEnabled, + success: true + }); + } else if ( + !mfaEnabled && + encryptionVersion && + encryptedPrivateKey && + iv && + tag && + token + ) { + // case: MFA is not enabled + + // unset provider auth token in case it was used + SecurityClient.setProviderAuthToken(""); + // set JWT token + SecurityClient.setToken(token); + + const privateKey = await KeyService.decryptPrivateKey({ + encryptionVersion, + encryptedPrivateKey, + iv, + tag, + password, + salt, + protectedKey, + protectedKeyIV, + protectedKeyTag + }); + + saveTokenToLocalStorage({ + publicKey, + encryptedPrivateKey, + iv, + tag, + privateKey + }); + + const userOrgs = await getOrganizations(); + const orgId = userOrgs[0]._id; + localStorage.setItem("orgData.id", orgId); + + const orgUserProjects = await getOrganizationUserProjects({ + orgId + }); + + if (orgUserProjects.length > 0) { + localStorage.setItem("projectData.id", orgUserProjects[0]._id); + } + + if (email) { + telemetry.identify(email, email); + telemetry.capture("User Logged In"); + } + + resolve({ + mfaEnabled: false, + loginResponse: { + email, + privateKey, + JTWToken: token + }, + success: true + }) + + } + } catch (err) { + reject(err); + } + } + ); + }); +}; + +export default attemptLogin; diff --git a/frontend/src/components/utilities/attemptCliLoginMfa.ts b/frontend/src/components/utilities/attemptCliLoginMfa.ts new file mode 100644 index 0000000000..cb33d3bad4 --- /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, + JTWToken: token + } + }); + } catch (err) { + reject(err); + } + }); + }); +} + +export default attemptLoginMfa; \ No newline at end of file diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index d9189d6a81..9baf7b7847 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/cli-redirect.tsx b/frontend/src/pages/cli-redirect.tsx new file mode 100644 index 0000000000..e28a9e9dcd --- /dev/null +++ b/frontend/src/pages/cli-redirect.tsx @@ -0,0 +1,18 @@ +import Head from "next/head"; + +export default function CliRedirect() { + return ( +
+ + Infisical Cli | Login Successful! + + +
+

Head back to your terminal!

+

+ You've successfully logged into infisical-cli +

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