Skip to content

Add keyring credential storage and keyring:// URI support#55

Merged
Michel Edkrantz (MichelEdkrantz) merged 20 commits intomasterfrom
credentials-keyring
Feb 20, 2026
Merged

Add keyring credential storage and keyring:// URI support#55
Michel Edkrantz (MichelEdkrantz) merged 20 commits intomasterfrom
credentials-keyring

Conversation

@MichelEdkrantz
Copy link
Copy Markdown
Member

@MichelEdkrantz Michel Edkrantz (MichelEdkrantz) commented Feb 20, 2026

Summary

  • Adds kognic-auth credentials load <file> [--env ENV] and credentials clear [--env ENV] subcommands to store/remove full credentials in the system keyring (macOS Keychain, GNOME Keyring, Windows Credential Manager, etc.)
  • Adds keyring://profile URI support in resolve_credentials and env_config, enabling per-environment keyring lookups via "credentials": "keyring://production" in environments.json
  • Adds system keyring as an automatic fallback in get_credentials_from_env (checked after env vars)
  • Stores the full credentials object (all five fields) rather than just client_id/client_secret
  • Documents the full keyring workflow in README
  • Fix README synopsis showing --profile instead of --env
  • Fix fragile circular import between credentials_parser.py and credentials_store.py by moving parse_credentials import inside load_credentials() function body
  • Rename credential_store.pycredentials_store.py (and test file) for consistency

Test plan

  • uv run python -m pytest — all 235 tests pass
  • kognic-auth credentials load ~/Downloads/credentials.json stores credentials in system keyring
  • kognic-auth credentials load ~/Downloads/prod.json --env production + "credentials": "keyring://production" in environments.json resolves correctly with kog get https://app.kognic.com/...
  • kognic-auth credentials clear removes credentials from keyring
  • BaseApiClient(auth="keyring://production") resolves credentials from keyring

🤖 Generated with Claude Code

Claude (claude) and others added 16 commits February 19, 2026 07:35
Store OAuth tokens in the system keyring so subsequent CLI invocations
(kognic-auth get-access-token, kog) can skip the token fetch when a
valid cached token exists. Tokens are scoped per auth_server:client_id.

- New module: cli/token_cache.py with load/save/clear functions
- Add --no-cache flag to both CLI commands to bypass caching
- keyring is an optional dependency (graceful degradation if absent)
- Library API (RequestsAuthSession, BaseApiClient, etc.) is unaffected

https://claude.ai/code/session_015vu8mYThdaZcoJXS4xWvDQ
Separate token management from HTTP session configuration:
- RequestsAuthSession gains ensure_token() and invalidate_token() as
  first-class methods
- _KognicBearerAuth (requests.AuthBase) injects Bearer tokens per request
  and handles 401 by invalidating and retrying once
- create_session builds a plain requests.Session per call, eliminating
  monkey-patch stacking when multiple clients share credentials
- Module-level WeakValueDictionary pool keyed on (client_id, auth_host,
  token_endpoint) ensures BaseApiClient instances with the same credentials
  share one token provider by default
- BaseApiClient accepts explicit token_provider= for opt-in sharing or
  test injection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TokenCache and its implementations (KeyringTokenCache, FileTokenCache,
make_cache) move from cli/ to internal/ so they can be used by both
CLI commands and library clients.

BaseApiClient gains a token_cache parameter that enables cross-process
token persistence. The shared provider pool key now includes the cache
type so cached and uncached clients for the same credentials get
separate providers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_base.py    – TokenCache ABC + helpers (make_key, is_valid, constants)
_keyring.py – KeyringTokenCache
_file.py    – FileTokenCache
__init__.py – re-exports + make_cache factory

All existing import paths are unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers:
- Same credentials share one provider instance
- Different credentials, auth hosts, or cache types get separate providers
- Explicit token_provider bypasses the pool entirely
- Pool entries are held weakly and GC'd when no client references them

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both CLI commands shared duplicated credential+cache wiring. The new
make_token_provider() in base_client.py centralises this: it resolves
credentials and wires up initial_token/on_token_updated from the
cache in one place.

get_access_token now calls make_token_provider() + ensure_token()
instead of constructing RequestsAuthSession directly.

api_request's _create_authenticated_session now calls
make_token_provider() + create_session(token_provider=).

test_cli.py updated to match the current interface (--token-cache
choices, make_token_provider/make_cache patch targets).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces `kognic-auth credentials load/clear` subcommands to store full
credentials in the system keyring. Adds `keyring://profile` URI support in
resolve_credentials and env_config for per-environment keyring lookups.
The keyring is also checked as a fallback in get_credentials_from_env.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@MichelEdkrantz Michel Edkrantz (MichelEdkrantz) changed the base branch from master to claude/keyring-token-storage-ACjSc February 20, 2026 09:55
Base automatically changed from claude/keyring-token-storage-ACjSc to master February 20, 2026 12:16
# Conflicts:
#	src/kognic/auth/__init__.py
#	src/kognic/auth/cli/__init__.py
#	src/kognic/auth/cli/api_request.py
#	src/kognic/auth/cli/get_access_token.py
#	src/kognic/auth/credentials_parser.py
#	src/kognic/auth/requests/base_client.py
#	tests/test_cli.py
#	tests/test_credentials_parser.py
- README: fix synopsis showing --profile instead of --env
- credentials_store.py: move parse_credentials import inside load_credentials
  to break circular dependency with credentials_parser.py
- credentials_parser.py: add missing credentials_store import
- Rename credential_store.py -> credentials_store.py (and test file)
- Update all references and mock patch paths accordingly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@MichelEdkrantz Michel Edkrantz (MichelEdkrantz) marked this pull request as ready for review February 20, 2026 15:48
@MichelEdkrantz Michel Edkrantz (MichelEdkrantz) merged commit ddce655 into master Feb 20, 2026
10 checks passed
@MichelEdkrantz Michel Edkrantz (MichelEdkrantz) deleted the credentials-keyring branch February 20, 2026 15:48
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