Skip to content

feat(auth): inject an externally-supplied Graph access token (sandbox/CI mode) - #1

Merged
ajay-bhargava merged 8 commits into
mainfrom
access-token-injection
Jul 27, 2026
Merged

feat(auth): inject an externally-supplied Graph access token (sandbox/CI mode)#1
ajay-bhargava merged 8 commits into
mainfrom
access-token-injection

Conversation

@ajay-bhargava

@ajay-bhargava ajay-bhargava commented Jul 26, 2026

Copy link
Copy Markdown
Member

Summary

Adds an externally-supplied access-token mode to olk: when OLK_ACCESS_TOKEN is set, olk builds its Graph credential from that token and never touches the OS keyring, the stored account files, or the default-account config. It never refreshes and never persists the token — the token's lifetime is the process's lifetime.

This is the seam a sandboxed/CI caller needs: an external system (a token broker, a Convex backend, a CI secret) owns the OAuth flow and hands olk a short-lived delegated token, so no refresh token ever enters a disposable environment and concurrent containers can't race Microsoft's refresh-token rotation.

Implements docs/OLK_FORK_STAGE2.md (added in the first commit).

Contract

Variable Flag Meaning
OLK_ACCESS_TOKEN --access-token Delegated Graph access token (bearer). Flag wins over env; env is preferred because a command line is visible to other processes.
OLK_ACCESS_TOKEN_EXPIRES_AT --access-token-expires-at Optional RFC3339 expiry. A past value fails before any network call with exit code 77.
OLK_ACCOUNT_EMAIL --account-email Optional identity hint (UPN) for display. whoami still resolves from Graph /me.

Behavior

  • No stored-credential access. The branch sits above r.Store() in GraphClient(), so the keyring, accounts/*.json, and GetDefaultAccount() are unreachable in token mode. No keyring password prompt is possible.
  • No refresh, no persistence. Nothing is written to the config dir; there is no cache file.
  • Fail closed on expiry, with a distinct exit code (77) so a caller can mint a fresh token and retry. With no expiry supplied, the credential reports a nominal 30-minute lifetime (a zero ExpiresOn makes the azcore pipeline refuse to send) and Graph's 401 is the authority.
  • Guards compose unchanged. newGraphClient() is extracted so every credential source shares one construction path — SetGuards() (--no-write / --no-send) can't be bypassed by a new auth path. --no-input, --wrap-untrusted, and the command allow/deny lists are untouched.
  • --account is rejected (it selects among stored accounts); --mailbox delegated access keeps working.
  • auth login|logout|clean|list refuse to run in token mode, each above its first Config()/Store() call. auth status reports the injected token, hint, and expiry.
  • olk mcp needs no new plumbing — each tool call re-parses argv and therefore re-reads the environment, then rebuilds the credential per call. buildArgv never carries the token, and the new flags are global, so they never appear in a tool's JSON schema.
  • No token in output. Not in stdout/stderr, errors (a bad expiry reports only the expected format), verbose HTTP logs (Authorization: <redacted>), or MCP results.

Files

internal/cmd/token.go        new: tokenMode, expiry handling, exit code, refusal helper
internal/cmd/root.go         3 flags, token branch, newGraphClient() extraction, exitCodeFor
internal/cmd/auth.go         5 subcommand guards
internal/cmd/token_test.go   new: 18 tests
README.md / SKILL.md         docs
docs/OLK_FORK_STAGE2.md      the implementation spec this PR follows

No changes under internal/graphapi, internal/msauth, internal/secrets, or internal/config — the credential interface and StaticTokenCredential already existed.

Testing

  • go build ./..., go vet ./..., go mod tidy (no drift) — clean.
  • go test -race -count=1 ./... — all packages pass.
  • golangci-lint run at CI's pinned v2.11.40 issues.
  • 18 new tests: keyring-that-fails-if-touched, fail-closed expiry (asserts no client is built), verbatim vs nominal expiry, malformed expiry without echoing the token, exit-code mapping (incl. wrapped sentinel), empty config dir after a token-mode run, --account rejection, --mailbox passthrough, flag-over-env precedence, guard composition (ErrNoWrite), no-leak check on captured stdout/stderr, all five auth subcommands, and an in-memory MCP session proving per-call token mode plus a stable expired-token message.
  • Live smoke test with the built binary (OLK_CONFIG_DIR pointed at a temp dir): expired token → exit 77 with zero requests; valid-shaped injection reaches Graph and comes back InvalidAuthenticationToken (proving the keyring path was bypassed rather than erroring with "no account configured"); config dir never created; --verbose shows Authorization: <redacted> and zero occurrences of the sentinel token; account mode unchanged.

View with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is enabled.

…ction

Detailed, file-and-line spec for the injected access-token mode, plus the four
open decisions resolved (config reads for display only, OLK_ACCOUNT_EMAIL scope,
exit code 77, env preferred over the flag).
Add OLK_ACCESS_TOKEN (plus --access-token), OLK_ACCESS_TOKEN_EXPIRES_AT, and
OLK_ACCOUNT_EMAIL. When a token is supplied, GraphClient() builds a
StaticTokenCredential from it and returns before touching the keyring, the
account files, or the default-account config. olk never refreshes and never
persists the token: its lifetime is the process's lifetime.

An expired supplied expiry fails closed before any request with exit code 77 so
an orchestrator can mint a fresh token and retry. With no expiry supplied, the
credential reports a nominal lifetime (a zero expiry makes the azcore pipeline
refuse to send) and Graph's 401 is the authority.

Extract newGraphClient() so every credential source shares one construction
path, keeping SetGuards (--no-write/--no-send) unbypassable. --account is
rejected in token mode; --mailbox is unaffected.
auth login/logout/clean/list manage stored accounts, so they refuse to run while
OLK_ACCESS_TOKEN is set. Each guard sits above the first Config()/Store() call so
a refusal never opens the keyring or prompts for its password. auth status
reports the injected token, its identity hint, and its expiry instead.

Tests cover credential selection against a keyring that fails the test if
touched, fail-closed expiry (no client is ever constructed), the exit-code
mapping, an empty config dir after a token-mode run, --account rejection,
--mailbox passthrough, flag-over-env precedence, guard composition, and that the
token never reaches stdout, stderr, or an MCP tool result. The MCP test also
pins the expired-token message, which orchestrators match on because exit codes
are invisible over stdio.
Add an Access-Token Injection section under Authentication covering the three
variables, the no-refresh/no-persistence semantics, exit code 77, guard
composition, and the MCP lifetime caveat. List the new flags in both global-flag
tables.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c0f018d7-76be-463b-b3ab-daff0a37cd56

📥 Commits

Reviewing files that changed from the base of the PR and between fd84034 and ef71228.

📒 Files selected for processing (3)
  • README.md
  • docs/OLK_FORK.md
  • internal/cmd/token.go
💤 Files with no reviewable changes (1)
  • docs/OLK_FORK.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/cmd/token.go
  • README.md

📝 Walkthrough

Walkthrough

The CLI adds injected Microsoft Graph access-token mode through flags and environment variables, validates optional expiry, bypasses stored authentication state, preserves capability guards, restricts stored-account commands, reports token status, and returns token-specific exit codes. Documentation and comprehensive command/MCP tests are included.

Changes

Access token injection

Layer / File(s) Summary
Token mode inputs and documentation
internal/cmd/root.go, README.md, SKILL.md
Adds token, expiry, and account-email inputs and documents injected-token behavior, restrictions, and exit semantics.
Token parsing and credential construction
internal/cmd/token.go
Validates token settings and RFC3339 expiry values, creates static credentials, formats status labels, and maps errors to exit codes.
Graph client and command execution wiring
internal/cmd/root.go
Routes token mode before account/keyring resolution, centralizes guarded Graph client creation and caching, and uses derived process exit codes.
Authentication command behavior
internal/cmd/auth.go
Rejects stored-account management commands in token mode and reports injected-token authentication status and expiry.
Token mode command and integration coverage
internal/cmd/token_test.go
Tests credential selection, expiry handling, persistence isolation, flag precedence, mailbox resolution, auth behavior, redaction, exit codes, and MCP execution.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant RunContext
  participant GraphClient
  participant GraphAPI
  CLI->>RunContext: provide injected access token and optional expiry
  RunContext->>RunContext: validate token mode and bypass stored accounts
  RunContext->>GraphClient: create guarded cached client
  GraphClient->>GraphAPI: issue request with static token
  GraphAPI-->>CLI: return command response or token error
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
SKILL.md (1)

339-339: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Warn agents not to place bearer tokens in argv.

Add prefer OLK_ACCESS_TOKEN; command-line arguments are visible to other processes, matching README.md, so agent workflows do not expose tokens unnecessarily.

internal/cmd/token_test.go (1)

298-327: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing test coverage: AuthStatusCmd with an already-expired token.

Both AuthStatus tests here use a future expiry or no expiry hint; neither exercises an already-expired injected token. This is precisely the scenario where the current implementation (see auth.go Lines 274-284) always reports Authenticated regardless of actual expiry. Once that's addressed, add a case asserting auth status surfaces an invalid/expired status for a past AccessTokenExpiresAt.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 50949a89-7a13-41e2-ad36-8771475fdebb

📥 Commits

Reviewing files that changed from the base of the PR and between 3398752 and fd84034.

📒 Files selected for processing (8)
  • README.md
  • SKILL.md
  • docs/OLK_FORK.md
  • docs/OLK_FORK_STAGE2.md
  • internal/cmd/auth.go
  • internal/cmd/root.go
  • internal/cmd/token.go
  • internal/cmd/token_test.go

Comment thread docs/OLK_FORK_STAGE2.md Outdated
Comment on lines +67 to +88
1. Add the four fields below.
2. Keep the same style as the other fields. Each flag has an `env` tag.
3. Do not change the existing fields.

```go
AccessToken string `help:"Delegated Graph access token; bypasses the keyring (prefer OLK_ACCESS_TOKEN)" env:"OLK_ACCESS_TOKEN" name:"access-token"`
AccessTokenExpiresAt string `help:"RFC3339 expiry of --access-token" env:"OLK_ACCESS_TOKEN_EXPIRES_AT" name:"access-token-expires-at"`
AccountEmail string `help:"Account identity hint (UPN) for use with --access-token" env:"OLK_ACCOUNT_EMAIL" name:"account-email"`
```

Notes:

- kong reads the `env` tag only when the command line does not give the flag. The flag wins. You write no code for this.
- `OLK_ACCOUNT` continues to set `--account`. `OLK_ACCOUNT_EMAIL` is a different variable with a different function.

---

## 6 Task 2 — Add the token mode helper

Make a new file `internal/cmd/token.go`. Put all the new logic in this file.

The file holds one type and three functions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the declared counts in the implementation plan.

Line [67] says to add four fields, but the block lists only three. Line [88] says the file holds three functions, while later tasks require refuseInTokenMode and exitCodeFor plus the other helpers implemented in internal/cmd/token.go. Update these counts so the plan matches the implementation.

Comment thread README.md Outdated
Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
@ajay-bhargava
ajay-bhargava merged commit ce801a0 into main Jul 27, 2026
4 checks passed
@ajay-bhargava
ajay-bhargava deleted the access-token-injection branch July 27, 2026 20:52
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.

2 participants