diff --git a/cli/go.mod b/cli/go.mod index 640e6dc0c..6a29a182e 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 574452d45..ec126dab2 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 f20a17853..6c36be36e 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 e27bc8f68..c5e162c92 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 5c169f6d5..ee2532ee8 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 dbe69eb1d..0fc0bec3e 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 77e83434e..dffe07be5 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 000000000..33935a9e2 --- /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 000000000..076174b67 --- /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 +
+
+ ); +}