diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index effc26060a..42a41bd515 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -5,7 +5,7 @@ import "time" type UserCredentials struct { Email string `json:"email"` PrivateKey string `json:"privateKey"` - JTWToken string `json:"JTWToken"` + JWTToken string `json:"JWTToken"` RefreshToken string `json:"RefreshToken"` } diff --git a/cli/packages/util/constants.go b/cli/packages/util/constants.go index d826f2418e..a460ce2cb8 100644 --- a/cli/packages/util/constants.go +++ b/cli/packages/util/constants.go @@ -11,6 +11,12 @@ const ( INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN" INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase. + // JWT configuration + INFISICAL_JWT_EXPIRATION_NAME = "INFISICAL_JWT_EXPIRATION" + DEFAULT_JWT_EXPIRATION = 86400 // 24 hours in seconds + MIN_JWT_EXPIRATION = 3600 // 1 hour in seconds + MAX_JWT_EXPIRATION = 2592000 // 30 days in seconds + VAULT_BACKEND_AUTO_MODE = "auto" VAULT_BACKEND_FILE_MODE = "file" diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index 03722dc414..8f2402c8a8 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -6,10 +6,8 @@ import ( "fmt" "strings" - "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/models" - "github.com/go-resty/resty/v2" "github.com/zalando/go-keyring" ) @@ -77,36 +75,16 @@ func GetCurrentLoggedInUserDetails(setConfigVariables bool) (LoggedInUserDetails if setConfigVariables { config.INFISICAL_URL_MANUAL_OVERRIDE = config.INFISICAL_URL - //configFile.LoggedInUserDomain - //if not empty set as infisical url if configFile.LoggedInUserDomain != "" { config.INFISICAL_URL = AppendAPIEndpoint(configFile.LoggedInUserDomain) } } - // check to to see if the JWT is still valid - httpClient := resty.New(). - SetAuthToken(userCreds.JTWToken). - SetHeader("Accept", "application/json") - - isAuthenticated := api.CallIsAuthenticated(httpClient) - // TODO: add refresh token - // if !isAuthenticated { - // accessTokenResponse, err := api.CallGetNewAccessTokenWithRefreshToken(httpClient, userCreds.RefreshToken) - // if err == nil && accessTokenResponse.Token != "" { - // isAuthenticated = true - // userCreds.JTWToken = accessTokenResponse.Token - // } - // } - - // err = StoreUserCredsInKeyRing(&userCreds) - // if err != nil { - // log.Debug().Msg("unable to store your user credentials with new access token") - // } - - if !isAuthenticated { + // Attempt to refresh token if needed + err = HandleTokenRefresh(&userCreds) + if err != nil { return LoggedInUserDetails{ - IsUserLoggedIn: true, // was logged in + IsUserLoggedIn: true, LoginExpired: true, UserCredentials: userCreds, }, nil @@ -117,7 +95,7 @@ func GetCurrentLoggedInUserDetails(setConfigVariables bool) (LoggedInUserDetails LoginExpired: false, UserCredentials: userCreds, }, nil - } else { - return LoggedInUserDetails{}, nil } + + return LoggedInUserDetails{}, nil } diff --git a/cli/packages/util/jwt.go b/cli/packages/util/jwt.go new file mode 100644 index 0000000000..1e12053e06 --- /dev/null +++ b/cli/packages/util/jwt.go @@ -0,0 +1,56 @@ +package util + +import ( + "fmt" + + "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/models" + "github.com/go-resty/resty/v2" + "github.com/rs/zerolog/log" +) + +// RefreshJWTToken attempts to refresh an expired JWT token using the refresh token +func RefreshJWTToken(httpClient *resty.Client, refreshToken string) (string, error) { + if refreshToken == "" { + return "", fmt.Errorf("no refresh token available") + } + + accessTokenResponse, err := api.CallGetNewAccessTokenWithRefreshToken(httpClient, refreshToken) + if err != nil { + return "", fmt.Errorf("failed to refresh token: %w", err) + } + + if accessTokenResponse.Token == "" { + return "", fmt.Errorf("received empty token from refresh attempt") + } + + return accessTokenResponse.Token, nil +} + +// IsTokenExpired checks if the given token is expired by validating with the server +func IsTokenExpired(httpClient *resty.Client) bool { + return !api.CallIsAuthenticated(httpClient) +} + +// HandleTokenRefresh handles the complete token refresh flow +func HandleTokenRefresh(userCreds *models.UserCredentials) error { + httpClient := resty.New(). + SetAuthToken(userCreds.JWTToken). + SetHeader("Accept", "application/json") + + if IsTokenExpired(httpClient) && userCreds.RefreshToken != "" { + newToken, err := RefreshJWTToken(httpClient, userCreds.RefreshToken) + if err != nil { + return err + } + + userCreds.JWTToken = newToken + err = StoreUserCredsInKeyRing(userCreds) + if err != nil { + log.Debug().Msg("unable to store refreshed credentials in keyring") + return err + } + } + + return nil +} \ No newline at end of file