Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,21 @@ func CallLoginV3(httpClient *resty.Client, request GetLoginV3Request) (GetLoginV
SetBody(request).
Post(fmt.Sprintf("%v/v3/auth/login", config.INFISICAL_URL))

cookies := response.Cookies()
// Find a cookie by name
cookieName := "jid"
var refreshToken *http.Cookie
for _, cookie := range cookies {
if cookie.Name == cookieName {
refreshToken = cookie
break
}
}

if refreshToken != nil {
loginV3Response.RefreshToken = refreshToken.Value
}

if err != nil {
return GetLoginV3Response{}, NewGenericRequestError(operationCallLoginV3, err)
}
Expand Down
3 changes: 3 additions & 0 deletions packages/api/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,9 @@ type GetLoginV3Request struct {

type GetLoginV3Response struct {
AccessToken string `json:"accessToken"`
// RefreshToken is not part of the JSON body; it is populated from the
// `jid` response cookie, mirroring GetLoginTwoV2Response.
RefreshToken string `json:"-"`
}

type GetSecretsV4Request struct {
Expand Down
104 changes: 58 additions & 46 deletions packages/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package cmd
import (
"encoding/json"
"fmt"
"os"

"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/config"
Expand Down Expand Up @@ -62,56 +63,30 @@ var initCmd = &cobra.Command{
util.HandleError(err, "Unable to select organization")
}

tokenResponse, err := api.CallSelectOrganization(httpClient, api.SelectOrganizationRequest{OrganizationId: selectedOrgID})
if tokenResponse.MfaEnabled {
i := 1
for i < 6 {
mfaVerifyCode := askForMFACode(tokenResponse.MfaMethod)

httpClient, err := util.GetRestyClientWithCustomHeaders()
if err != nil {
util.HandleError(err, "Unable to get resty client with custom headers")
}
httpClient.SetAuthToken(tokenResponse.Token)
verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{
Email: userCreds.UserCredentials.Email,
MFAToken: mfaVerifyCode,
MFAMethod: tokenResponse.MfaMethod,
})
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)
util.PrintlnStderr(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 {
httpClient.SetAuthToken(verifyMFAresponse.Token)
tokenResponse, err = api.CallSelectOrganization(httpClient, api.SelectOrganizationRequest{OrganizationId: selectedOrgID})
break
}
}
}

newSessionToken, err := selectOrganizationToken(userCreds.UserCredentials.JTWToken, userCreds.UserCredentials.Email, selectedOrgID)
if err != nil {
util.HandleError(err, "Unable to select organization")
}

// set the config jwt token to the new token
userCreds.UserCredentials.JTWToken = tokenResponse.Token
err = util.StoreUserCredsInKeyRing(&userCreds.UserCredentials)
httpClient.SetAuthToken(tokenResponse.Token)
// The session token is now scoped to the selected organization; persist
// it on the profile this invocation resolved to.
userCreds.UserCredentials.JTWToken = newSessionToken
orgID, subOrgID := util.ParseTokenOrgClaims(newSessionToken)
if orgID == "" {
orgID = selectedOrgID
}

updatedProfile := userCreds.Profile
updatedProfile.OrganizationID = orgID
updatedProfile.SubOrganizationID = subOrgID
updatedProfile.OrganizationName = util.FetchOrganizationName(newSessionToken, orgID)

// Only move the global default when this invocation was using it; a
// terminal pinned via env var, flag, or directory scope must not switch
// other terminals.
makeActive := userCreds.ProfileSource == util.ProfileSourceDefault
err = util.PersistLoginProfile(updatedProfile, &userCreds.UserCredentials, makeActive)
httpClient.SetAuthToken(newSessionToken)

if err != nil {
util.HandleError(err, "Unable to store your user credentials")
Expand Down Expand Up @@ -140,11 +115,48 @@ var initCmd = &cobra.Command{
util.HandleError(err)
}

offerDirectoryProfileBinding(userCreds.ProfileName)

Telemetry.CaptureEvent("cli-command:init", posthog.NewProperties().Set("version", util.CLI_VERSION))

},
}

// offerDirectoryProfileBinding asks (only when multiple profiles exist)
// whether this directory should always use the profile init just ran with, so
// commands run here pick the right tenant without flags or env vars.
func offerDirectoryProfileBinding(profileName string) {
configFile, err := util.GetMigratedConfigFile()
if err != nil || profileName == "" || len(configFile.Profiles) < 2 {
return
}

cwd, err := os.Getwd()
if err != nil {
return
}

if boundProfile, _, ok := util.FindGoverningDirectoryProfile(configFile, cwd); ok && boundProfile == profileName {
return
}

prompt := promptui.Select{
Label: fmt.Sprintf("Always use profile '%s' in this directory? Commands run here will select it automatically. Select[Yes/No]", profileName),
Items: []string{"No", "Yes"},
}
_, result, err := prompt.Run()
if err != nil || result != "Yes" {
return
}

util.SetDirectoryProfile(&configFile, cwd, profileName)
if err := util.WriteConfigFile(&configFile); err != nil {
util.PrintWarning(fmt.Sprintf("Unable to save the directory profile binding [err=%s]", err))
return
}
util.PrintlnStderr(fmt.Sprintf("Directory %s now uses profile '%s'. Manage bindings with [infisical profile use --scope] and [infisical profile unlink].", cwd, profileName))
}

func init() {
RootCmd.AddCommand(initCmd)
}
Expand Down
67 changes: 58 additions & 9 deletions packages/cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,17 @@ var loginCmd = &cobra.Command{
}

currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
// if the key can't be found or there is an error getting current credentials from key ring, allow them to override
if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) {
// if the key can't be found, the selected profile doesn't exist yet, or
// there is an error getting current credentials from key ring, allow them to override
if err != nil && (errors.Is(err, util.ErrProfileNotFound) || strings.Contains(err.Error(), "we couldn't find your logged in details")) {
log.Debug().Err(err)
} else if err != nil {
util.HandleError(err)
}

if currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired && len(currentLoggedInUserDetails.UserCredentials.PrivateKey) != 0 {
// When a profile is explicitly targeted (flag or env var), the login is
// a deliberate write to that profile; skip the add/override menu.
if config.INFISICAL_PROFILE_OVERRIDE == "" && currentLoggedInUserDetails.IsUserLoggedIn && !currentLoggedInUserDetails.LoginExpired && len(currentLoggedInUserDetails.UserCredentials.PrivateKey) != 0 {
shouldOverride, err := userLoginMenu(currentLoggedInUserDetails.UserCredentials.Email)
if err != nil {
util.HandleError(err)
Expand Down Expand Up @@ -227,7 +230,40 @@ var loginCmd = &cobra.Command{
cliDefaultLogin(&userCredentialsToBeStored, email, password, organizationId)
}

err = util.StoreUserCredsInKeyRing(&userCredentialsToBeStored)
orgID, subOrgID := util.ParseTokenOrgClaims(userCredentialsToBeStored.JTWToken)
orgName := util.FetchOrganizationName(userCredentialsToBeStored.JTWToken, orgID)

existingConfig, err := util.GetMigratedConfigFile()
if err != nil {
util.HandleError(err, "Unable to read the Infisical config file")
}

profileName := config.INFISICAL_PROFILE_OVERRIDE
if profileName == "" {
profileName = util.DeriveProfileName(existingConfig, userCredentialsToBeStored.Email, config.INFISICAL_URL, orgID, orgName)
} else if err := util.ValidateProfileName(profileName); err != nil {
util.HandleError(err)
}

if existingProfile, found := util.FindProfile(existingConfig, profileName); found && existingProfile.Email != userCredentialsToBeStored.Email {
util.PrintWarning(fmt.Sprintf("Profile '%s' previously stored the session for %s and now stores the session for %s.", profileName, existingProfile.Email, userCredentialsToBeStored.Email))
}

// An explicitly targeted login (--profile flag or INFISICAL_PROFILE) is a
// scoped write: it must not move the global default out from under other
// terminals that rely on it. This also keeps expired-session renewals
// (which re-exec login with --profile) from stealing the default.
// Untargeted logins keep the familiar "last login wins" behavior.
makeActive := config.INFISICAL_PROFILE_OVERRIDE == ""

err = util.PersistLoginProfile(models.Profile{
Name: profileName,
Email: userCredentialsToBeStored.Email,
Domain: config.INFISICAL_URL,
OrganizationID: orgID,
OrganizationName: orgName,
SubOrganizationID: subOrgID,
}, &userCredentialsToBeStored, makeActive)
if err != nil {
log.Error().Msgf("Unable to store your credentials in system vault")
log.Error().Msgf("\nTo trouble shoot further, read https://infisical.com/docs/cli/faq")
Expand All @@ -236,11 +272,6 @@ var loginCmd = &cobra.Command{
util.HandleError(err)
}

err = util.WriteInitalConfig(&userCredentialsToBeStored)
if err != nil {
util.HandleError(err, "Unable to write write to Infisical Config file. Please try again")
}

// Identify the user in PostHog and alias the anonymous machine ID
// so that pre-login CLI events are merged into the same person record.
// This call is idempotent (gated on LastIdentifiedEmail in the config),
Expand All @@ -267,6 +298,17 @@ var loginCmd = &cobra.Command{
boldWhite.Printf(">>>> Welcome to Infisical!")
boldWhite.Printf(" You are now logged in as %v <<<< \n", userCredentialsToBeStored.Email)

if profileName != userCredentialsToBeStored.Email {
orgDetail := ""
if orgName != "" {
orgDetail = fmt.Sprintf(" (org %s)", orgName)
}
util.PrintlnStderr(fmt.Sprintf("Session saved to profile '%s'%s. Select it with --profile %s or INFISICAL_PROFILE=%s.", profileName, orgDetail, profileName, profileName))
}
if configAfterLogin, err := util.GetConfigFile(); err == nil && configAfterLogin.ActiveProfile != "" && configAfterLogin.ActiveProfile != profileName {
util.PrintlnStderr(fmt.Sprintf("Your default profile remains '%s'; terminals using it are unaffected. Run [infisical profile use %s] to make '%s' the default.", configAfterLogin.ActiveProfile, profileName, profileName))
}

plainBold := color.New(color.Bold)

plainBold.Println("\nQuick links")
Expand Down Expand Up @@ -321,9 +363,11 @@ var loginCmd = &cobra.Command{
func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials, email string, password string, organizationId string) {
loginV3Response, err := getFreshUserCredentials(email, password)
var getOrganizationIdAccessToken string
var refreshToken string

if err == nil {
getOrganizationIdAccessToken = loginV3Response.AccessToken
refreshToken = loginV3Response.RefreshToken
} else {
log.Info().Msg("Unable to authenticate with the provided credentials, falling back to SRP authentication")

Expand Down Expand Up @@ -379,13 +423,15 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials, email st
loginTwoResponse.Tag = verifyMFAresponse.Tag
loginTwoResponse.Token = verifyMFAresponse.Token
loginTwoResponse.EncryptionVersion = verifyMFAresponse.EncryptionVersion
loginTwoResponse.RefreshToken = verifyMFAresponse.RefreshToken

break
}
}
}

getOrganizationIdAccessToken = loginTwoResponse.Token
refreshToken = loginTwoResponse.RefreshToken
}

// TODO(daniel): At a later time we should re-add this check, but we don't want to break older Infisical instances that doesn't have the latest SRP removal initiative on them.
Expand All @@ -400,6 +446,9 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials, email st
userCredentialsToBeStored.Email = email
userCredentialsToBeStored.PrivateKey = ""
userCredentialsToBeStored.JTWToken = newJwtToken
// Store the refresh session (scraped from the `jid` cookie) so expired
// access tokens can be renewed without an interactive re-login.
userCredentialsToBeStored.RefreshToken = refreshToken
}

func setDomainConfig(domain string) {
Expand Down
Loading
Loading