Skip to content

Add named login profiles and directory-scoped tenant selection - #372

Open
scott-ray-wilson wants to merge 6 commits into
mainfrom
feat/named-profiles
Open

Add named login profiles and directory-scoped tenant selection#372
scott-ray-wilson wants to merge 6 commits into
mainfrom
feat/named-profiles

Conversation

@scott-ray-wilson

Copy link
Copy Markdown
Contributor

Problem

Switching between tenants in the CLI requires a full re-login each time, and working in parallel across projects in different tenants is only possible by dumping tokens into env vars or .env files. Root cause: the keyring holds exactly one session per email (the keyring key is the bare email), and the session JWT is scoped to a single organization, so logging into tenant B destroys the session for tenant A. Community ask: Infisical/infisical#2191.

What this PR does

Named profiles (phase 1). A profile is one login session: account + instance + organization (+ sub-org), with its own keyring entry keyed by the profile name. Sessions for any number of tenants now coexist.

Per-invocation selection precedence: --profile flag > INFISICAL_PROFILE env var > directory scope > global default. Parallel terminals pin different tenants with one export INFISICAL_PROFILE=... each; tokens never leave the keyring.

Directory scoping (phase 2). A user-level map in ~/.infisical/infisical-config.json binds a directory tree to a profile (nearest ancestor wins, same upward walk as .infisical.json discovery). cd into a project and the right tenant is selected automatically. Nothing is written to the repo.

New commands

  • infisical profile list | current [--plain] | use <name> [--scope <dir>] | unlink [path] | delete <name>
  • infisical org list and infisical org switch [--org-id <id>] [--save-as <profile>]: re-scopes the session via the existing select-organization exchange (with MFA handling) without re-authenticating; --save-as stores the result as a new profile and leaves the current one untouched.

Behavior fixes that came with the rework

  • Expired sessions renew via the stored refresh token when present (wires up the previously dead CallGetNewAccessTokenWithRefreshToken TODO in credentials.go), re-scoping to the profile's org; falls back to the interactive flow on any failure, incl. MFA.
  • An explicit --domain/INFISICAL_DOMAIN now beats the saved login domain instead of being silently overridden; a one-time warning surfaces mismatches.
  • infisical reset deletes the keyring entries of all stored sessions (previously it orphaned every account except the active one).
  • infisical vault set clears all profiles, since sessions in the old backend are unreachable after the switch.
  • infisical init persists its org re-scope on the profile the invocation resolved to (it previously rewrote the single session as a side effect) and offers a directory binding when multiple profiles exist. Commands that resolve a profile via env var, flag, or directory scope never move the global default, so pinned terminals cannot affect other terminals.
  • infisical user switch now operates on profiles (same picker UX, kept for compatibility).

Migration and back-compat

  • Lazy, transparent migration on first read: legacy loggedInUserEmail/loggedInUsers become profiles named after the account email. That name is also the legacy keyring key, so existing keyring entries keep working with no rewrite and no re-login.
  • The legacy config fields stay synced with the active profile, so older CLI binaries and scripts that read them keep working. If an older binary runs user switch, the migration reconciles the divergence in favor of the legacy pointer.
  • Single-profile users see no change: no new prompts, no notices. The "Using profile X via ..." stderr notice only appears when selection came from a flag, env var, or directory scope (and is suppressed by --silent, structured output, and for token-based invocations).
  • Machine identities, INFISICAL_TOKEN, and service tokens are untouched: token-based auth still outranks the login session.

Deliberately out of scope

The file-vault passphrase being stored base64-encoded in the plaintext config is a pre-existing issue and is not addressed here; it should get its own PR, ideally sequenced with this migration.

The parallel-orgs story for org switch --save-as depends on the server keeping the prior session token valid after select-organization; if the server invalidates it, the old profile simply shows as expired and can be refreshed by one login. One login per org is always sufficient to get fully parallel profiles.

Testing

  • Unit tests for migration (incl. idempotency and legacy-divergence reconciliation), resolution precedence, directory walk-up, default profile naming/dedup/suffixing, active-profile legacy sync, and profile removal (packages/util/profile_test.go).
  • go build ./..., go vet (clean for changed files; run.go/pam/gateway-v2 findings are pre-existing on main), go test ./packages/... (cmd package needs -vet=off due to those pre-existing findings), e2e module builds.
  • Smoke-tested against an isolated $HOME: legacy config migration and persistence, profile use --scope, nearest-ancestor resolution from subdirectories, env var selection, the stderr notice on a real command, unlink from a subdirectory, delete, and error paths.

🤖 Generated with Claude Code

Switching tenants previously required a full re-login because the CLI
held exactly one session per email (keyring key = email) and the session
JWT is scoped to a single organization. Working across tenants in
parallel meant exporting tokens into .env files.

Sessions are now stored as named profiles (account + instance + org),
each with its own keyring entry. Selection precedence per invocation:
--profile flag > INFISICAL_PROFILE env var > directory scope > global
default, so parallel terminals can pin different tenants and directories
can be bound to the tenant they belong to.

New commands:
- infisical profile list/current/use/unlink/delete
  (profile use --scope binds a directory tree to a profile)
- infisical org list / org switch (re-scopes the session via
  select-organization without re-authenticating; --save-as stores the
  result as a new profile)

Also:
- infisical init persists the org re-scope on the resolved profile and
  offers a directory binding when multiple profiles exist
- expired sessions now renew via the stored refresh token when present
  (previously dead code), falling back to interactive login
- an explicit --domain/INFISICAL_DOMAIN now beats the saved login
  domain instead of being silently overridden
- infisical reset removes all profile keyring entries instead of only
  the active one
- infisical user switch operates on profiles (behavior preserved)

Migration is lazy and transparent: legacy config fields become profiles
named after the account email, which is also the legacy keyring key, so
existing sessions keep working without re-login. Legacy fields stay
synced with the active profile for older binaries and scripts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@infisical-review-police

Copy link
Copy Markdown

💬 Discussion in Slack: #pr-review-cli-372-add-named-login-profiles-and-directory-scoped-tenant-selection

Posted by Review Police — reviews, comments, new commits, and CI failures will stream into this channel.

@greptile-apps

greptile-apps Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds named login profiles, directory-scoped profile resolution, organization switching, migration of legacy login state, and automatic session refresh. It also updates reset, vault, init, login, and user compatibility flows to operate on the new profile model.

  • Adds profile and organization management commands with flag, environment, directory, and default selection precedence.
  • Migrates legacy account configuration into named, profile-keyed keyring sessions.
  • Adds refresh-token renewal and organization re-scoping for expired sessions.
  • Updates reset, vault switching, initialization, and legacy user commands for profile-aware state.

Confidence Score: 1/5

This PR is not safe to merge until cross-instance domain rewriting, unreachable automatic refresh for SRP sessions, and concurrent profile-config lost updates are addressed.

Same-email profiles can be routed to the wrong instance with their bearer token, standard SRP sessions cannot use the new renewal path, and parallel profile operations can overwrite one another's durable state.

Files Needing Attention: packages/cmd/user.go, packages/cmd/login.go, packages/util/credentials.go, packages/util/profile.go, packages/util/config.go

Security Review

The legacy domain-update compatibility command rewrites every same-email profile, so a session belonging to another instance can subsequently be sent to the newly selected endpoint. How this was verified: Profile loading uses each rewritten domain as the API base while retrieving the bearer token from that profile's distinct keyring entry.

Important Files Changed

Filename Overview
packages/util/profile.go Implements profile migration, resolution, naming, persistence, and directory scope handling, but whole-file persistence can lose concurrent updates.
packages/util/credentials.go Loads profile-keyed credentials and adds automatic renewal, but the renewal path is unreachable for normal SRP sessions because login does not persist their refresh token.
packages/cmd/user.go Adapts legacy user commands to profiles, but domain updates collapse the instance boundary across all profiles sharing an email.
packages/cmd/profile.go Adds profile list, current, use, unlink, and delete commands; its mutations participate in unlocked shared-config updates.
packages/cmd/org.go Adds organization listing and token re-scoping, including MFA and optional profile creation.
packages/cmd/root.go Adds profile override resolution and notices while preserving explicit domain precedence.
packages/cmd/login.go Persists logins as named profiles but leaves refresh tokens returned by SRP and MFA authentication out of stored credentials.
packages/util/profile_test.go Covers migration, resolution precedence, naming, active-profile synchronization, deletion, and validation, but not concurrent persistence or refresh integration.

Comments Outside Diff (1)

  1. packages/util/profile.go, line 3411-3423 (link)

    P1 Concurrent profile writes lose state

    If two terminals perform profile-affecting operations concurrently, each rewrites the complete config from its own stale snapshot without locking or merge semantics. The later write drops profiles or directory bindings created by the other terminal, leaving keyring sessions unreachable or causing commands to resolve the wrong profile.

    Knowledge Base Used: Core Shared Packages

Reviews (1): Last reviewed commit: "Add named login profiles and directory-s..." | Re-trigger Greptile

Comment thread packages/cmd/user.go Outdated
Comment on lines +247 to +252
// keep profile entries for this account in sync with the new domain
for idx := range configFile.Profiles {
if configFile.Profiles[idx].Email == profile {
configFile.Profiles[idx].Domain = domain
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Domain updates cross profile boundaries

When the same email has profiles on different Infisical instances, this loop rewrites every matching profile's domain. Loading an unrelated profile then sends its profile-specific bearer token to the newly selected server, causing authentication failure or disclosing the token to the wrong instance.

How this was verified: Profile loading retrieves credentials by profile name while using the rewritten Profile.Domain as the API base URL.

Context Used: Flag SSRF risks (source)

Knowledge Base Used: Core Shared Packages

Comment thread packages/util/credentials.go Outdated
Comment on lines +136 to +140
// The session expired: try the stored refresh token before falling back to
// an interactive re-login. Only on setConfigVariables paths, so read-only
// probes (e.g. the root pre-run warning) never mutate the keyring, and so
// the refresh request targets the profile's own domain (set above).
if !isAuthenticated && setConfigVariables && userCreds.RefreshToken != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Refresh path lacks stored tokens

When an email/SRP session expires, this guard skips automatic renewal because the login and MFA responses capture the jid refresh token but the login flow never copies it into the persisted UserCredentials. These sessions therefore fall back to interactive login at every access-token expiry.

Knowledge Base Used: CLI Authentication: Login, Token Storage, and Consumption

Comment thread packages/cmd/user.go Outdated
@veria-ai

veria-ai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

PR overview

This pull request adds named CLI login profiles and directory-scoped tenant selection, including commands and output for selecting and pinning profiles and organizations.

Three security issues remain open, while one has already been addressed. The most significant issue allows a compromised server to inject shell commands into the documented profile-pinning workflow when a user evaluates generated output. The remaining issues could expose a token through legacy profile-state confusion or allow backend-controlled organization names to manipulate terminal output.

Open issues (3)

Fixed/addressed: 1 · PR risk: 7/10

…es, wire SRP refresh token

Three fixes from PR review and CI root-causing:

1. Derived profile names (raw emails) are no longer validated in
   PersistLoginProfile. The name pattern rejected characters like '+'
   that are legal in emails, which made login fail after successful
   authentication for plus-addressed accounts and hung the CI pty
   harness (init auto-triggered an interactive login whose prompts the
   harness does not answer). Validation now applies only to user-typed
   names (--profile, --save-as), and the allowed charset includes '+'
   so email-named profiles can be targeted explicitly.

2. `user update domain` only repoints profiles whose domain matched the
   roster entry's previous domain. The same email can be a different
   account on another instance, and its session token must never be
   sent to the new domain. (greptile/veria review finding)

3. The password/SRP login path now stores the refresh session scraped
   from the `jid` cookie (login2/MFA responses, and login v3 which now
   scrapes it too), so expired sessions renew silently instead of
   always falling back to interactive re-login. (greptile review finding)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@scott-ray-wilson

Copy link
Copy Markdown
Contributor Author

Addressed the review findings and root-caused the CI failures in c0a430e:

CI: Go CLI Tests hang (caused by this PR, now fixed). The harness drives login --interactive then init through a pty and never checks login's exit code. On this branch, login authenticated successfully and then died locally: the derived profile name is the raw account email, and ValidateProfileName rejected characters like + that are legal in emails (the CI test account's email evidently contains one). With no session stored, init's RequireLogin auto-spawned login --silent, whose "Select your hosting option" prompt the harness has no answer for, deadlocking until the 11m test timeout. Reproduced locally under a pty. Fix: derived names (raw emails) are stored as-is and never validated; validation applies only to user-typed names (--profile, --save-as), whose charset now also allows +. Regression tests added.

greptile/veria on user update domain (valid): the sync loop now repoints only profiles whose domain matched the roster entry's previous domain, so a same-email account on a different instance never has its token redirected to the new domain.

greptile on the refresh path (valid): the password/SRP login path now stores the refresh session scraped from the jid cookie (login2/MFA responses, and v3 login now scrapes it too), so the refresh wiring works for those sessions instead of only browser logins.

Other failing checks, not related to this PR:

  • govulncheck: fails on GO-2026-6163 (panic in github.com/pion/stun/v3), a new advisory not in the workflow allowlist; go.mod is untouched here, so this fails for any PR scanned now. Needs a stun bump or allowlist entry in its own PR (the job also warns GO-2026-5942 can be dropped from the allowlist).
  • Agent End-to-End Testing: transient proxy.golang.org download errors (stream error: INTERNAL_ERROR); CLI and PAM E2E in the same run passed. Should clear on re-run.

🤖 Generated with Claude Code

Comment thread packages/util/profile.go

func syncLegacyLoginFields(configFile *models.ConfigFile, profile models.Profile) {
configFile.LoggedInUserEmail = profile.Email
configFile.LoggedInUserDomain = profile.Domain

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low: Legacy profile state can route credentials to another instance

For a named profile, credentials are stored under profile.Name, while older CLI versions load the keyring entry identified by LoggedInUserEmail. If an email-named profile for instance A already exists and a differently named profile for the same email on instance B becomes active, these fields point the older CLI at B while it loads A's token; an operator controlling B can capture that token. Only publish legacy login fields when the profile's credentials are actually available under the legacy email key, or clear the legacy selection so older clients require a fresh login.

`login --profile <name>` (or with INFISICAL_PROFILE set) is a scoped
write to that profile. Making it the global default yanked every
unpinned terminal onto the new tenant, which is exactly the
cross-terminal interference profiles exist to prevent, and it made
expired-session renewals (which re-exec login with --profile) steal the
default as a side effect.

Explicitly targeted logins now only create/update their profile, in line
with the source-aware rule org switch and init already follow. Untargeted
logins keep the familiar last-login-wins behavior, and a targeted login
prints which profile remains the default and how to switch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@scott-ray-wilson

Copy link
Copy Markdown
Contributor Author

One more behavior fix from demo testing: infisical login --profile <name> (or with INFISICAL_PROFILE set) no longer moves the global default profile. It previously did, which yanked every unpinned terminal onto the freshly logged-in tenant, and meant an expired-session auto-renewal (which re-execs login --profile <resolved>) could steal the default as a side effect. Targeted logins now only write their own profile, matching the source-aware rule org switch and init already follow; plain infisical login keeps last-login-wins. The CLI prints which profile remains the default after a targeted login.

🤖 Generated with Claude Code

Comment thread packages/cmd/root.go Outdated

detail := ""
if profile.OrganizationName != "" {
detail = fmt.Sprintf(" (org %s)", profile.OrganizationName)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low: Terminal control injection via organization names

A backend can return an organization name containing newlines or ANSI/OSC control sequences, which this notice writes directly to the terminal on routine commands. This lets an attacker forge CLI output or manipulate supported terminal features such as the clipboard; strip control characters from backend-derived display values before rendering them, including the new organization and profile list outputs.

The legacy loggedInUsers roster is kept in sync with profiles for
old-binary compatibility. The migration treated every roster entry
without a same-named profile as a legacy session and synthesized a
profile for it, so a targeted first login (login --profile x) produced a
phantom email-named profile with no org and no keyring session behind it.

Migration now only synthesizes a profile when no existing profile covers
that account's email, and the legacy-switch reconciliation falls back to
any profile for the account when no email-named one exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@scott-ray-wilson

Copy link
Copy Markdown
Contributor Author

Demo testing surfaced one more edge: after a targeted first login (login --profile x), the migration synthesized a phantom email-named profile from the legacy roster mirror (no org, no keyring session behind it). Fixed in the latest commit: migration only synthesizes a profile when no existing profile covers that account's email, with regression tests for the mirror case and for legacy-switch reconciliation onto named profiles.

🤖 Generated with Claude Code

Team review of the profiles PR surfaced a consistent complaint: `org switch`
and profiles read as unrelated features. The help text was part of it (org
commands talked about the "login session" while profile commands talked about
"profiles"), but the deeper cause was that the organization was baked into a
profile's identity, so the only way to change organization was to mutate or
fork a profile.

The organization is now a field of the profile, the way a namespace is a field
of a kubectl context:

- --org / INFISICAL_ORG override the organization for a single command without
  touching the profile. Selectors accept an organization name, slug, or id.
- Switching organizations no longer requires re-authenticating. The session is
  exchanged for an organization-scoped one and cached per organization in the
  keyring, so repeat use costs nothing and tokens never touch the config file.
- `infisical profile set-org` is the canonical way to change a profile's
  default organization; `infisical org switch` remains as an alias since that
  is the name people reach for. Both say which profile they changed.
- `infisical profile current` reports the organization in effect and where it
  came from; `infisical org list` marks the current organization and now
  enumerates sub-organizations with their slugs.
- Help text consistently describes the organization as profile state.

`infisical init` no longer re-asks for an organization when the profile
already has one, which was the sharpest symptom of the old model. It reports
the organization it is using, and warns when a transient --org would leave the
linked project unreachable on later runs.

Also adds `infisical profile use <name> --shell`, which prints an eval-able
export so a single terminal can be pinned without knowing the env var name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@scott-ray-wilson

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback, all of it landed. Pushed a change that reworks the org/profile relationship, plus answers to the specific questions.

On "I didn't understand that org switch and profiles were connected" (@akhil, @thiago). You were right, and it was more than wording. The organization was baked into a profile's identity, so the only way to change it was to mutate or fork a profile, while org switch sat in its own command namespace and its help text talked about the "login session" rather than the profile. Two nouns, one object.

The organization is now a field of the profile, the way a namespace is a field of a kubectl context (@thiago, this is your framing, and it fit better than the terminology change):

  • --org / INFISICAL_ORG retarget a single command without touching the profile, accepting an org name, slug, or id: infisical secrets --org globex
  • No re-authentication. The session is exchanged for an org-scoped one and cached per organization in the keyring, so repeat use is free and tokens still never touch the config file.
  • infisical profile set-org is the canonical way to change a profile's default. infisical org switch stays as an alias because it's what people type. Both now state which profile they changed.
  • infisical profile current reports the org in effect and where it came from; infisical org list marks the current org and points at both ways to change it.

I kept the name "profile" rather than moving to "context": two of you reached for the AWS model unprompted, and AWS/gcloud/Azure/1Password all use profile-shaped language. The kubectl idea we adopted is the conceptual separation, not the vocabulary.

infisical init no longer re-asks for an organization when the profile already has one. That redundant prompt was the sharpest symptom of the old model (and it used to silently re-scope your profile as a side effect). It now reports the org it's using, and warns if a transient --org would leave the linked project unreachable later.

@victor, on --profile at the end of a command: that already works today and applies to every command including secrets, where it's orthogonal to --projectId/--env. Same for INFISICAL_PROFILE (@akhil). Two of you asking for features that already shipped is a strong signal the PR description and docs buried them, so I've reordered the story to lead with per-command and per-shell selection.

@victor, on sub-orgs: yes, and the gaps you'd have hit are closed. The pickers were already two-level, and profiles stored the sub-org, but org list only showed root orgs so you couldn't discover a sub-org's id. It now enumerates sub-orgs with slugs, and --org resolves against them.

@thiago, on session vs directory scoping: both work, but you were right that pinning a session was second-class, since it meant knowing the env var name. Added infisical profile use <name> --shell, so eval "$(infisical profile use globex --shell)" pins one terminal, nvm-style, leaving the default and other terminals alone.

Verified end to end against a local instance: one login, then --org by name, slug, and env var all fetch the other tenant's secrets with no re-login, and the profile's default stays put.

🤖 Generated with Claude Code

Comment thread packages/cmd/profile.go
// Pin only this shell, leaving the default and other terminals alone.
// Printed for eval so the export lands in the caller's shell:
// eval "$(infisical profile use globex --shell)"
util.PrintlnStdout(fmt.Sprintf("export %s=%s", util.INFISICAL_PROFILE_ENV_NAME, profileName))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Shell command injection

Derived profile names can come from the raw email in a login response, but this line inserts the name directly into output intended for eval. A compromised server can return a name such as work$(command), causing that command to execute when the user follows the documented shell-pinning workflow. Encode the value with robust shell-safe quoting, such as POSIX single-quote escaping, before emitting the export statement.

Two related changes to how long credentials live on a machine and how they
are ended.

Stop renewing sessions with the refresh token. The wiring added earlier was
also incorrect: the server rotates the refresh token on every refresh and
returns the replacement in the response body, keeping the old one valid for
only a 10 second grace window, after which reuse is treated as theft and the
session is deleted. The client discarded the rotated token, so refreshing
worked exactly once per login and the next attempt would have revoked the
user's session. Rotating correctly needs an atomic read-rotate-write across
independent CLI processes sharing one vault entry, which is not something the
CLI can do safely today, and the fix that matters more is the one this makes
possible: a session now lives at most JWT_AUTH_LIFETIME and expiry sends the
user back through login, so forgotten sessions age out on their own.

Refresh tokens are no longer written to the vault at all, since nothing uses
them and storing one means a stolen vault yields a long-lived rotating
credential rather than a short-lived access token. Existing stored tokens are
cleared on the next credential write.

Add `infisical logout`, which revokes the session on the server and removes it
from the machine, with --all for every profile and --local-only to skip
revocation. The server keys sessions by user, IP, and user agent, so several
profiles for one account on one machine share a single session; logging out of
one leaves a session another profile still uses intact and removes only the
local credentials, instead of silently signing the user out of their other
tenants. `profile delete` and `reset` revoke as well, so credentials that look
gone locally are actually gone, and `profile list` gained a SESSION column so
stored sessions are visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@scott-ray-wilson

Copy link
Copy Markdown
Contributor Author

Session lifetime and logout, following the question about credentials staying alive unnoticed.

Reverted refresh-token renewal, and it was hiding a bug. While checking whether to keep it I found the earlier wiring was incorrect. The server rotates the refresh token on every refresh, returns the replacement in the response body, and keeps the old one valid for only a 10 second grace window; reuse after that is treated as theft and the session is deleted:

// Reuse detection: old token used outside grace window — potential theft
await tokenDAL.deleteTokenSession({ id: tokenVersion.id, userId: decodedToken.userId });

Our client read only token from the response and discarded the rotated refreshToken, so refresh worked exactly once per login and the next attempt would have force-revoked the user's session with a security error, taking every terminal on that profile with it. Doing rotation properly needs an atomic read-rotate-write across independent CLI processes sharing one vault entry, inside a 10 second budget, which we have no primitive for.

So sessions are now bounded by JWT_AUTH_LIFETIME again and expiry sends you back through login, which is the pre-PR behavior. The cost of that dropped a lot with the previous commit: N organizations is now one profile rather than N, and expired org-scoped tokens re-mint themselves from the profile session, so a multi-org user re-authenticates once per window instead of N times. Refresh tokens are also no longer persisted at all, so a stolen vault yields a short-lived access token instead of a 90-day rotating credential; existing ones are cleared on the next write.

Added infisical logout, with --all and --local-only. One subtlety worth flagging for review: the server keys sessions by user, IP, and user agent, so several profiles for the same account on one machine share a single session row. I verified this on a local instance, two profiles, one tokenVersionId. Naively revoking on logout would sign you out of your other tenants. So logout revokes only sessions no remaining profile still uses, and otherwise removes just the local credentials and says so:

Removed the stored session for profile 'globex'. The server session is still
used by profile 'demo@infisical.local', so it was left active.

profile delete and reset revoke too, since credentials that look gone locally should actually be gone, and profile list gained a SESSION column (active / expired / none) so stored sessions are visible rather than implicit.

Verified end to end against a local instance: logging out of a shared-session profile leaves the other one working, logging out the last one drops the server session row to zero, and a token captured beforehand is rejected afterwards.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant