Skip to main content

Python SDK for Keychains.dev — credential proxy for API calls

Project description

keychains

Minimal Python SDK for making authenticated API calls through the Keychains.dev proxy. Your code never touches real credentials — the proxy injects them at runtime.

Quickstart

1. Get a permission token

npx -y keychains permissions create --name "Testing keychains" --env >> .env

This registers your machine (if needed), creates a wildcard permission, and appends KEYCHAINS_PERMISSION_ID to your .env file.

2. Use keychains.get() as a drop-in replacement for requests.get()

The only difference? You can replace any credential with a template variable.

pip install keychains
import keychains

# Gmail — get last 10 emails from my inbox
response = keychains.get(
    "https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=10",
    headers={
        "Authorization": "Bearer {{OAUTH2_ACCESS_TOKEN}}",
    },
)

emails = response.json()
print(emails)

That's it. The proxy resolves {{OAUTH2_ACCESS_TOKEN}} with the user's real Google OAuth token — your code never sees it.


Template Variables

How to write them

Template variables use the {{VARIABLE_NAME}} syntax. The variable name tells the proxy which type of credential to inject:

Prefix Type Supported Variables
OAUTH2_ OAuth 2.0 token {{OAUTH2_ACCESS_TOKEN}}, {{OAUTH2_REFRESH_TOKEN}}
OAUTH1_ OAuth 1.0 token {{OAUTH1_ACCESS_TOKEN}}, {{OAUTH1_REQUEST_TOKEN}}
Anything else API key {{LIFX_PERSONAL_ACCESS_TOKEN}}, {{OPENAI_API_KEY}}, etc.

Where to put them

Place them exactly where you'd normally put the real credential — headers, body, or query parameters:

import keychains

# In a header (most common)
response = keychains.get(
    "https://api.lifx.com/v1/lights/all",
    headers={"Authorization": "Bearer {{LIFX_PERSONAL_ACCESS_TOKEN}}"},
)

# In the request body
response = keychains.post(
    "https://slack.com/api/chat.postMessage",
    headers={
        "Authorization": "Bearer {{OAUTH2_ACCESS_TOKEN}}",
        "Content-Type": "application/json",
    },
    json={"channel": "#general", "text": "Hello!"},
)

# In query parameters
response = keychains.get(
    "https://api.example.com/data?api_key={{MY_API_KEY}}&format=json",
)

What Happens Next

When you call keychains.get():

  1. URL rewritinghttps://api.lifx.com/v1/lights/all becomes https://keychains.dev/api.lifx.com/v1/lights/all
  2. Token injection — your permission token is sent via X-Proxy-Authorization so the proxy knows who you are
  3. Scope check — the proxy verifies the user has approved the required credentials for this API
  4. Credential resolution — the proxy replaces {{LIFX_PERSONAL_ACCESS_TOKEN}} with the real API key stored in the user's vault
  5. Request forwarding — the proxy forwards the request to the upstream API with real credentials injected
  6. Response passthrough — the upstream response is returned to you as-is

Handling missing approvals

With wildcard permissions, users approve scopes on demand. The first time your code hits a new API, the user may not have approved it yet. When that happens, the SDK raises an ApprovalRequired exception containing an approval_url — share it with the user so they can grant access:

import keychains
from keychains.exceptions import ApprovalRequired

try:
    response = keychains.get(
        "https://api.github.com/user",
        headers={"Authorization": "Bearer {{OAUTH2_ACCESS_TOKEN}}"},
    )
    print(response.json())
except ApprovalRequired as err:
    # The user hasn't approved GitHub yet — show them the link
    print("Please approve access:", err.approval_url)
    # Once approved, retry the same call and it will succeed

The exception includes useful details:

Property Type Description
approval_url str | None URL the user should visit to approve the missing scopes
missing_scopes list[str] | None Scopes that need approval
refused_scopes list[str] | None Scopes explicitly refused by the user
code str Error code (insufficient_scope, scope_refused, permission_denied, etc.)

Session (Connection Pooling)

For multiple requests, use Session to reuse connections — just like requests.Session:

import keychains

with keychains.Session() as s:
    s.headers.update({"Authorization": "Bearer {{OAUTH2_ACCESS_TOKEN}}"})

    repos = s.get("https://api.github.com/user/repos")
    for repo in repos.json():
        issues = s.get(f"https://api.github.com/repos/{repo['full_name']}/issues")
        print(f"{repo['name']}: {len(issues.json())} issues")

Async

For asyncio codebases, use AsyncClient:

import keychains

async with keychains.AsyncClient() as client:
    response = await client.get("https://api.github.com/user/repos")
    repos = response.json()

Configuration

The SDK automatically loads variables from a .env file in your working directory (via python-dotenv), so the quickstart command just works out of the box.

Environment variable Description
KEYCHAINS_PERMISSION_ID Permission token (required)

Tokens can also be passed explicitly:

keychains.get(url, token="ey...")
keychains.Session(token="ey...")
keychains.AsyncClient(token="ey...")

Security benefits

  • Secrets never leave the Keychains.dev servers — your code, logs, and environment stay clean
  • Users approve exactly which scopes and APIs an agent can access
  • Credentials can only be sent to the APIs of the providers they belong to
  • Every proxied request is audited with full traceability
  • Permissions can be revoked instantly from the dashboard

Bug Reports & Feedback

Found a bug or have a suggestion? Submit it straight from your terminal:

# Report a bug
npx -y keychains feedback "The proxy returns 502 on large POST bodies"

# Send feedback
npx -y keychains feedback --type feedback "Love the wildcard permissions!"

# With more detail
npx -y keychains feedback --type bug --title "502 on large POST" --description "When sending >1MB body to Slack API..." --contact you@example.com

The keychains feedback command (alias: keychains bug) sends your report directly to the engineering team.


More Info

Let's meet on keychains.dev!

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

keychains-0.1.5.tar.gz (9.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

keychains-0.1.5-py3-none-any.whl (8.9 kB view details)

Uploaded Python 3

File details

Details for the file keychains-0.1.5.tar.gz.

File metadata

  • Download URL: keychains-0.1.5.tar.gz
  • Upload date:
  • Size: 9.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.12

File hashes

Hashes for keychains-0.1.5.tar.gz
Algorithm Hash digest
SHA256 b87d5718d74cb9ab3e5ad41191421a835ecc80bbcb9daa77e6465ac213702d36
MD5 53834064a5712a1355ae50aad50661f3
BLAKE2b-256 bdf18822a1067bdbed0cc6db272dd905f4f2a05670c1abe71093faf64e2b889a

See more details on using hashes here.

File details

Details for the file keychains-0.1.5-py3-none-any.whl.

File metadata

  • Download URL: keychains-0.1.5-py3-none-any.whl
  • Upload date:
  • Size: 8.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.12

File hashes

Hashes for keychains-0.1.5-py3-none-any.whl
Algorithm Hash digest
SHA256 23681006b644e53097bd895cd01a1f85c3e0440fe070dde55ad3f1e43fbe435e
MD5 b8e65c421ccf714c50272a26fe2ea367
BLAKE2b-256 7b97ae14d24b546a755683b3faa13c8308c4ab1a08f9956b5d3925fe80b3eb31

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page