Skip to content

Fix session token expiry with automatic re-authentication#27

Open
c1-dev-bot[bot] wants to merge 3 commits intomainfrom
fix/cxh-1183-session-token-refresh
Open

Fix session token expiry with automatic re-authentication#27
c1-dev-bot[bot] wants to merge 3 commits intomainfrom
fix/cxh-1183-session-token-refresh

Conversation

@c1-dev-bot
Copy link
Copy Markdown

@c1-dev-bot c1-dev-bot bot commented Mar 13, 2026

Summary

The op CLI session token expires after 30 minutes of inactivity, causing subsequent syncs to fail with "no active session found". The token was obtained once at startup and stored as a static string — there was no mechanism to detect expiry or re-authenticate.

Changes:

  • Store AccountDetails in OnePasswordClient so re-authentication is possible
  • Add isSessionExpiredError() to detect session expiry from op CLI stderr output (matches "no active session", "session expired", "not currently signed in", "Invalid session token")
  • Refactor executeCommand to extract runCommand helper, and on session expiry errors for user auth type, automatically call refreshToken() (which re-signs in via GetUserToken) and retry the failed command once
  • Update NewCli to accept accountDetails parameter, and thread it through from connector.New

Test plan

  • Verify go build ./... passes
  • Verify go test ./... passes
  • Manual testing: configure baton-1password with user auth, wait >30 minutes, trigger sync — should now automatically re-authenticate instead of failing
  • Verify service auth type is unaffected (no account details needed, no retry logic triggered)

Fixes: CXH-1183


Automated PR Notice

This PR was automatically created by c1-dev-bot as a potential implementation.

This code requires:

  • Human review of the implementation approach
  • Manual testing to verify correctness
  • Approval from the appropriate team before merging

The op CLI session token expires after 30 minutes of inactivity, causing
subsequent syncs to fail with "no active session". Store account details
in OnePasswordClient and automatically re-authenticate when a session
expiry error is detected, then retry the failed command.

Fixes: CXH-1183
@c1-dev-bot c1-dev-bot bot requested a review from a team March 13, 2026 20:55
@linear
Copy link
Copy Markdown

linear bot commented Mar 13, 2026

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 13, 2026

Claude finished @c1-dev-bot[bot]'s task in 1m 22s —— View job


PR Review: Fix session token expiry with automatic re-authentication

  • Read changed files and gather context
  • Analyze implementation for correctness and security
  • Post review feedback

Overall Assessment

This is a well-structured change that solves a real operational problem — session tokens expiring during long syncs. The approach of retry-once on session expiry is sound and appropriately scoped.

What's Good

  • Clean refactoring: Extracting runCommand from executeCommand is the right decomposition. It also fixes a subtle pre-existing bug where args was mutated in-place via append, which could corrupt the caller's slice on retry.
  • Conservative retry: Only retrying once on detected session expiry (not on all errors) is the right call.
  • Auth-type gating: The retry logic correctly gates on authType == "user" — service tokens don't have the same expiry semantics.
  • Minimal connector.go change: Just threading providedAccountDetails through to NewCli — clean and minimal.

Issues & Suggestions

1. Token logged in error output (security concern)
In runCommand (line 449), zap.Strings("command_args", cmd.Args) will log the session token since it's appended as --session <token>. Consider redacting it before logging.

2. Case sensitivity in error matching (minor)
isSessionExpiredError does exact-case matching. If the op CLI ever changes casing (e.g., "No active session"), it would miss. Consider strings.ToLower for robustness. Low risk given known CLI output, but defensive.

3. Error wrapping (minor)
Line 465 uses %v for the refresh error, making it un-unwrappable. Go 1.20+ supports multiple %w verbs — consider fmt.Errorf("...: %w (refresh failed: %w)", err, refreshErr) for better error inspection.

4. Concurrency note (informational)
c.token = token in refreshToken isn't concurrency-safe. This is fine today since the SDK drives sync sequentially, but worth a code comment if this assumption matters for the future.

5. No tests for the new logic
There are no unit tests for isSessionExpiredError or the retry behavior. Given this is error-path logic that's hard to exercise manually, even a simple table-driven test for isSessionExpiredError would add confidence.

Verdict

Looks good overall — the core logic is correct and the refactoring is clean. The token-in-logs issue (point 1) is worth addressing before merge. The other items are minor suggestions.

Comment on lines +26 to +29
return strings.Contains(stderr, "no active session") ||
strings.Contains(stderr, "session expired") ||
strings.Contains(stderr, "not currently signed in") ||
strings.Contains(stderr, "Invalid session token")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: case sensitivity could be an issue. The op CLI error messages may vary across versions. Consider using strings.Contains(strings.ToLower(stderr), ...) with lowercase patterns for more robust matching. However, this is low-risk given you're matching known 1Password CLI output.

}

c.token = token
return nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good: the refreshToken method correctly stores credentials in the client struct, but note that AccountDetails contains sensitive fields (password, secret). This is acceptable since they were already being stored in the OnePassword connector struct — now they're also referenced in the client. Just be mindful that OnePasswordClient now holds credential material for its entire lifetime.

Also, c.token = token is not concurrency-safe. If executeCommand were ever called from multiple goroutines, this would be a data race. Currently the SDK drives execution sequentially so this is fine, but worth noting with a comment if this assumption is important.

Comment on lines +429 to +430
cmdArgs := make([]string, len(args))
copy(cmdArgs, args)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good fix here — the original code had a subtle bug where args was mutated in place (args = append(args, ...)) which could modify the caller's slice. The new runCommand correctly copies the args before modifying them. This is a nice side-effect of the refactoring.

// If the session token has expired and we have account details to re-authenticate, retry once.
if c.authType == "user" && c.accountDetails != nil && isSessionExpiredError(err) {
if refreshErr := c.refreshToken(ctx); refreshErr != nil {
return fmt.Errorf("error: %w (token refresh also failed: %v)", err, refreshErr)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This error message includes the refresh failure via %v but wraps the original error via %w. That's fine, but the refreshErr won't be unwrappable. Consider also using errors.Join or fmt.Errorf with %w for both if callers might want to inspect the refresh error. Not blocking, but a suggestion for better error ergonomics:

Suggested change
return fmt.Errorf("error: %w (token refresh also failed: %v)", err, refreshErr)
return fmt.Errorf("session expired error: %w (token refresh also failed: %w)", err, refreshErr)

Note: Go 1.20+ supports multiple %w verbs in a single fmt.Errorf.

- Use %w instead of %v for refreshErr to satisfy errorlint
- Use exec.CommandContext in runCommand to satisfy noctx
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 13, 2026

Claude finished @c1-dev-bot[bot]'s task in 1m 57s —— View job


PR Review: Fix session token expiry with automatic re-authentication

  • Read changed files and gather context
  • Analyze implementation for correctness and security
  • Post review feedback

Blocking Issues: 1 | Suggestions: 2

Security Issues

  • Token in logs (pkg/client/client.go:449): zap.Strings("command_args", cmd.Args) logs the full command including --session <token>. Session tokens will be written to logs on every command failure. Remove the field or redact the token value. Fix this →

Correctness Issues

None found.

Suggestions

  • Case-insensitive matching (pkg/client/client.go:25-29): isSessionExpiredError uses mixed-case patterns ("no active session" vs "Invalid session token"). Use strings.ToLower(stderr) with all-lowercase patterns for robustness against CLI output changes. Fix this →
  • Unit tests: isSessionExpiredError and the retry path have no test coverage. A table-driven test for isSessionExpiredError would be low-effort and high-value for this error-path logic.

Notes

The overall approach is sound — retry-once on session expiry with auth-type gating is well-scoped. The runCommand extraction is clean and fixes a pre-existing slice mutation bug. Using exec.CommandContext instead of exec.Command is a good improvement for context cancellation support.


return strings.Contains(stderr, "no active session") ||
strings.Contains(stderr, "session expired") ||
strings.Contains(stderr, "not currently signed in") ||
strings.Contains(stderr, "Invalid session token")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Suggestion: The string patterns use mixed casing — "no active session" (lowercase) vs "Invalid session token" (capitalized). If the op CLI ever changes casing across versions, some patterns could silently stop matching. Consider normalizing with strings.ToLower:

Suggested change
strings.Contains(stderr, "Invalid session token")
stderr := strings.ToLower(string(exitErr.Stderr))
return strings.Contains(stderr, "no active session") ||
strings.Contains(stderr, "session expired") ||
strings.Contains(stderr, "not currently signed in") ||
strings.Contains(stderr, "invalid session token")


if c.authType == "user" {
args = append(args, []string{"--session", c.token}...)
cmdArgs = append(cmdArgs, "--session", c.token)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Security (H5): In runCommand, on error, zap.Strings("command_args", cmd.Args) (line 449) will log the session token since --session <token> is appended to cmdArgs on line 433. Consider removing the command_args log field entirely, or redacting the --session value before logging.

This is the most important finding — credentials in logs can propagate to log aggregators and persist indefinitely.

Use exec.CommandContext instead of exec.Command in GetLocalAccounts,
AddLocalAccount, and SignIn to respect context cancellation.
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 13, 2026

Claude finished @c1-dev-bot[bot]'s task in 1m 18s —— View job


PR Review: Fix session token expiry with automatic re-authentication

  • Read changed files and gather context
  • Analyze implementation for correctness and security
  • Post review feedback

Overall Assessment

This is a well-scoped fix for a real operational issue — the op CLI session token expiring after 30 minutes of inactivity. The approach (detect expiry, re-authenticate once, retry) is sound and the refactoring is clean.

What's Good

  • Clean decomposition: Extracting runCommand from executeCommand is the right refactor. It also fixes a pre-existing subtle bug where args = append(args, ...) could mutate the caller's slice — the new code uses make + copy to avoid this.
  • Conservative retry: Only retries once on session expiry, not on all errors. This prevents retry storms.
  • Auth-type gating: Retry logic correctly gates on authType == "user" — service tokens don't support this flow.
  • Context propagation: The switch from exec.Command to exec.CommandContext across all call sites (commits 2 & 3) is a good improvement for cancellation support.
  • Minimal connector.go change: Just threading providedAccountDetails through to NewCli — clean and minimal.

Issues

1. Session token logged in error outputpkg/client/client.go:449

zap.Strings("command_args", cmd.Args) logs the full command including --session <token>. This means session tokens will be written to logs on every command failure, where they can propagate to log aggregators and persist indefinitely.

Fix: Remove the command_args field entirely, or redact the --session value before logging. Fix this →

2. Case sensitivity in error matchingpkg/client/client.go:25-29

isSessionExpiredError uses mixed-case patterns ("no active session" lowercase vs "Invalid session token" capitalized). If the op CLI changes casing across versions, matches could silently break. Consider normalizing:

stderr := strings.ToLower(string(exitErr.Stderr))
return strings.Contains(stderr, "no active session") ||
    strings.Contains(stderr, "session expired") ||
    strings.Contains(stderr, "not currently signed in") ||
    strings.Contains(stderr, "invalid session token")

Low risk given known CLI output, but a simple defensive improvement. Fix this →

Suggestions (non-blocking)

  • Unit tests: isSessionExpiredError is a pure function — a simple table-driven test would be low-effort and high-value for this error-path logic that's hard to exercise manually.
  • Concurrency: c.token = token in refreshToken (line 422) isn't concurrency-safe. Fine today since the SDK drives sync sequentially, but worth a brief comment if this assumption matters for the future.

Summary

The core logic is correct and the refactoring is well done. The token-in-logs issue (point 1) should be addressed before merge as it's a security concern. The case-sensitivity fix (point 2) is low-effort and worth doing. The rest are minor suggestions.


@c1-dev-bot
Copy link
Copy Markdown
Author

c1-dev-bot bot commented Mar 13, 2026

CI Note: The go-lint check failure is caused by a pre-existing gosec G101 false positive in pkg/connector/vault.go:42 (a map of vault permission display names flagged as "hardcoded credentials"). This same issue exists on the main branch — the lint CI was already broken before this PR.

This PR actually fixes 3 pre-existing noctx lint issues in pkg/client/client.go by switching exec.Command to exec.CommandContext. The go-test check passes.

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.

0 participants