oidc-token-cli

module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 9, 2026 License: Apache-2.0

README

oidc-token-cli

A small CLI that fetches an OIDC token and prints it as a bare string — built as a drop-in exec credential provider for frpc, kubectl, curl, or CI pipelines.

Nothing is hardcoded to a specific issuer or client: every endpoint is resolved at runtime from /.well-known/openid-configuration, and the interactive grant (authcode+PKCE or device-code) is auto-selected based on what the issuer supports and what the environment can do (browser vs. attended terminal). Tokens are cached per (issuer, client_id) and silently refreshed; a browser or device-code prompt is only shown when the cache is cold.

Contract

  • Success: exactly the token bytes on stdout (no trailing newline), exit 0.
  • Failure: non-zero exit, message on stderr, empty stdout — never a partial or placeholder token.
  • Public client + PKCE only — no client secret handling.

Install

Download a release binary (darwin/linux, amd64/arm64) from the releases page.

Usage

oidc-token \
  --issuer https://id.example.com/ \
  --client-id my-public-client \
  [--scope "openid offline_access"] \
  [--audience my-api] \
  [--grant-type auto|authcode|device-code] \
  [--token-type access_token|id_token] \
  [--cache-dir DIR] \
  [--token-store auto|keychain|file] \
  [--redirect PORT] \
  [--non-interactive] \
  [--all] \
  [--logout] \
  [--config FILE]
Flag Default Description
--issuer (required) OIDC issuer URL (HTTPS, except loopback in tests). Discovery's issuer field must match exactly.
--client-id (required) OAuth2/OIDC public client ID.
--scope openid offline_access Space-separated scopes.
--audience (empty) Expected aud claim — required if the relying party checks audience (e.g. frp's auth.oidc.audience).
--grant-type auto auto, authcode, or device-code. See Grant selection.
--token-type access_token Which field bare mode prints.
--cache-dir $XDG_CACHE_HOME/oidc-token Override the token cache directory (used by the file backend and, in auto mode, as the fallback store).
--token-store auto auto, keychain, or file. See Cache.
--redirect 0 (ephemeral) Fixed loopback port for the authcode callback, if your IdP requires an exact redirect URI.
--non-interactive false Never emit a device-code prompt; authcode+browser is still allowed if a display is available.
--all false Print a JSON document instead of a bare token.
--logout false Clear the cached entry for --issuer/--client-id and exit; no login or refresh is attempted.
--config (none) Optional JSON config file.

Every flag except --config also has an env var: OIDC_TOKEN_ISSUER, OIDC_TOKEN_CLIENT_ID, OIDC_TOKEN_SCOPE, OIDC_TOKEN_AUDIENCE, OIDC_TOKEN_GRANT_TYPE, OIDC_TOKEN_TOKEN_TYPE, OIDC_TOKEN_CACHE_DIR, OIDC_TOKEN_STORE, OIDC_TOKEN_NON_INTERACTIVE, OIDC_TOKEN_LOGOUT. Precedence: defaults < env < --config file < explicit flags.

Grant selection

auto picks the best viable grant based on what the issuer advertises and what the environment supports:

Grant Advertised when Viable when
authorization_code default per OIDC Discovery, or explicitly listed a browser can be launched
device-code issuer has a device_authorization_endpoint a TTY is attached and --non-interactive was not passed

Authcode is tried first when both are viable. If the client is rejected for the grant it tried (unauthorized_client/invalid_grant at the authorization endpoint), it falls back to the other viable grant once — capped at 2 attempts total, no cycling back. --grant-type=authcode or =device-code forces one grant and fails fast if it isn't viable, with a diagnostic describing what the IdP offers and why browser/terminal viability failed.

Cache

Tokens are cached per (issuer, client_id) profile, addressed by a SHA-256 hash of the pair so those values don't leak into keys/filenames. --token-store selects where:

--token-store Behavior
auto (default) Try the OS keychain (macOS Keychain, Linux Secret Service over D-Bus) first; fall back to the plaintext file store only when the keychain backend is unavailable (no daemon reachable) — not on a plain cache miss. Logs a one-line notice to stderr the first time it falls back.
keychain OS keychain only, no fallback. Fails fast at startup if no keychain backend is reachable.
file Plaintext JSON file only — this CLI's original behavior, no cgo, works anywhere.

The file store lives at --cache-dir (default $XDG_CACHE_HOME/oidc-token or ~/.cache/oidc-token); files are 0600, written atomically, and refreshes run under an advisory flock so concurrent invocations converge on one winner instead of each re-authenticating. auto mode reuses that same flock for cross-process coordination even when the token payload lives in the keychain; keychain-enforce mode has no cross-process lock (OS keychains expose no such primitive), so concurrent invocations against the same profile aren't fully serialized in that mode.

There's no migration between backends: switching an existing profile from file to auto/keychain on a machine with a working keychain triggers one fresh login, since a keychain miss isn't treated as "check the file next." Use --logout to explicitly clear a cached entry (keychain items can't be removed with rm the way file entries can).

No cgo dependency either way — the keychain backend shells out to security on macOS and speaks D-Bus directly on Linux.

Bootstrap model

First run (cache empty) opens a browser or prints a device code and blocks until login completes. Every run after that is served from cache or silently refreshed. If the cache is cold and no grant is viable (headless CI with --non-interactive), it fails fast — it will never print a device-code prompt nobody can see. Warm the cache once, interactively, before wiring oidc-token into a non-interactive job.

Recipes

frpc (auth.oidc.tokenSource.exec)

frp's client sends the OAuth2 access_token (not id_token) and trims all whitespace from the exec output, so use --token-type access_token:

# frpc.toml
[auth]
method = "oidc"

[auth.oidc.tokenSource.exec]
command = "oidc-token"
args = [
  "--issuer", "https://id.example.com/",
  "--client-id", "frpc-client",
  "--token-type", "access_token",
  "--audience", "frps",        # match frps's auth.oidc.audience exactly
  "--non-interactive",
]

--non-interactive here just suppresses the device-code prompt in frpc's log — a cold cache still opens a browser, since frpc's exec still has a display available even without a TTY. Bootstrap once before first use to avoid that popup coming from inside frpc's process tree:

oidc-token --issuer https://id.example.com/ --client-id frpc-client \
  --token-type access_token --audience frps
kubectl ExecCredential-style (--all)
# ~/.kube/config (users[].user.exec)
exec:
  apiVersion: client.authentication.k8s.io/v1
  command: oidc-token
  args:
    - --issuer=https://id.example.com/
    - --client-id=my-k8s-client
    - --token-type=id_token
    - --all

--all's JSON has access_token/id_token/refresh_token/expiry but no apiVersion/kind/status envelope — wrap it if your consumer needs the literal k8s schema.

CI (--non-interactive)
# One-time, interactive, with a TTY:
oidc-token --issuer https://id.example.com/ --client-id ci-client

# In the actual CI job, against the warmed cache:
export OIDC_TOKEN_ISSUER=https://id.example.com/
export OIDC_TOKEN_CLIENT_ID=ci-client
oidc-token --non-interactive

Reuse the same --cache-dir between the bootstrap step and later non-interactive runs — an ephemeral runner has no warm cache and will fail fast by design.

Build & release

go build ./...
go vet ./...
go test ./...

Every push to main runs CI; if it's green, svu derives the next semver from conventional commits and, on a version bump, goreleaser release --clean builds darwin/linux × amd64/arm64 archives plus a GitHub release.

License

Apache-2.0. See LICENSE.

Directories

Path Synopsis
cmd
oidc-token command
Command oidc-token-cli mints and prints an OIDC token for a generic public client, using cached credentials and silent refresh where possible.
Command oidc-token-cli mints and prints an OIDC token for a generic public client, using cached credentials and silent refresh where possible.
internal
authflow
Package authflow selects a grant type at runtime (auto|authcode|device-code) based on discovery metadata and the local environment, and runs the loopback callback server for the authorization-code flow.
Package authflow selects a grant type at runtime (auto|authcode|device-code) based on discovery metadata and the local environment, and runs the loopback callback server for the authorization-code flow.
cache
Package cache persists tokens keyed by (issuer, client_id) behind the Store interface.
Package cache persists tokens keyed by (issuer, client_id) behind the Store interface.
config
Package config parses CLI flags and optional config-file settings that describe an OIDC issuer, public client, and token request (issuer, client-id, scope, audience, grant-type, token-type, cache-dir, redirect).
Package config parses CLI flags and optional config-file settings that describe an OIDC issuer, public client, and token request (issuer, client-id, scope, audience, grant-type, token-type, cache-dir, redirect).
oidc
Package oidc implements runtime OIDC discovery, id_token verification, authorization-code+PKCE, device-code, and refresh-token flows using golang.org/x/oauth2 and github.com/coreos/go-oidc/v3.
Package oidc implements runtime OIDC discovery, id_token verification, authorization-code+PKCE, device-code, and refresh-token flows using golang.org/x/oauth2 and github.com/coreos/go-oidc/v3.
oidctest
Package oidctest provides a minimal, configurable OIDC issuer for tests (discovery, jwks, device authorization, and token endpoints backed by a real RSA-signed JWT), shared across internal/oidc and internal/authflow.
Package oidctest provides a minimal, configurable OIDC issuer for tests (discovery, jwks, device authorization, and token endpoints backed by a real RSA-signed JWT), shared across internal/oidc and internal/authflow.
output
Package output renders the final token to stdout: bare token bytes, or an ExecCredential-style JSON document when requested.
Package output renders the final token to stdout: bare token bytes, or an ExecCredential-style JSON document when requested.
runner
Package runner orchestrates the cache -> refresh (locked) -> interactive login pipeline and maps typed errors to process exit codes.
Package runner orchestrates the cache -> refresh (locked) -> interactive login pipeline and maps typed errors to process exit codes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL