Microsoft SSO + admin-broker credential client for Sanoptis Claude Code plugins.
Project description
sanoptis-auth
Microsoft SSO + Postgres-broker credential management for Sanoptis Claude Code plugins.
A single library that any Sanoptis plugin can use to fetch its API secrets, gated by the user's normal Microsoft sign-in. No per-plugin dotfiles, no chmod gymnastics, no hand-rolled credential bootstrap docs.
Install
pip install sanoptis-auth
What it does
- Fetches secrets from the Sanoptis admin broker (a Reflex/FastAPI app with a Postgres backend that stores Fernet-encrypted secret values).
- Authenticates the user via Microsoft SSO on first use: reuses an existing
az loginsession if present, otherwise opens a browser tab for one-click sign-in. Refresh token cached in the OS-native secure store (DPAPI/Keychain/libsecret) for ~90 days. - Enforces access policy at the broker, per-plugin:
publicplugins — any authenticated Sanoptis user may read.restrictedplugins — only users added as members (in the admin UI) may read.
Use it in a plugin
Add to your plugin's requirements.txt:
sanoptis-auth>=0.5.0,<0.6
Replace any per-plugin credential loading with:
from sanoptis_auth import get_secret
api_key = get_secret("yokoy/api-key")
api_secret = get_secret("yokoy/api-secret")
Do not register the sanoptis-auth MCP server in your own plugin's .mcp.json. The separate sanoptis-auth plugin in the Sanoptis marketplace registers it once for every Claude Code session; your plugin just imports the Python library.
First-time login (end user)
- Install the plugin in Claude Code.
- Use any tool that needs a secret. If no MSAL cache exists, Claude will call
start_sanoptis_loginfor you (or you can call it directly). - A browser tab opens at the Microsoft sign-in page. If your browser already holds an M365 cookie (the typical case inside Sanoptis), the redirect is instant.
- Done — the next ~90 days of plugin usage are automatic.
Headless environments (SSH, Docker, CI) have no browser, so the SDK's credential chain falls through to an explicit error pointing at az login. Run az login --tenant <tenant-id> once and the AzureCliCredential step picks it up on the next call.
Local dev override
For working offline or before your admin-broker membership is set up:
export SANOPTIS_AUTH_LOCAL_OVERRIDE_FILE=~/.sanoptis-auth-overrides.env
cat > ~/.sanoptis-auth-overrides.env <<'EOF'
yokoy/api-key=dev-key
yokoy/api-secret=dev-secret
EOF
chmod 600 ~/.sanoptis-auth-overrides.env
get_secret() checks this file first when the env var is set.
Admin app
The sanoptis_admin/ package is a Reflex web app that is the primary tool for managing plugins, secrets, and memberships. Admins sign in with their Sanoptis Microsoft account and can:
- Create plugins — adds a row to the admin DB and auto-enrols the creator as first member.
- Add/rotate/delete secrets — Fernet-encrypts the plaintext and stores the ciphertext in Postgres.
- Manage members — add/remove users (by UPN) from a plugin's membership set.
- Flip access policy — per-plugin, between
publicandrestricted. - Audit — every mutation and every successful secret read is logged with actor UPN, action, and target.
All operations run under the admin's own delegated token — no elevated service principal, no app-permission scopes. Non-admins (absent from ADMIN_UPNS) see a "Not authorized" page.
Deployed to Azure Container Apps via infra/azure/admin_app.tf. Container image built and pushed by .github/workflows/deploy-admin.yml on pushes to main that touch sanoptis_admin/**.
Local dev:
cd sanoptis_admin
SANOPTIS_ADMIN_CONFIG=$(pwd)/web.yaml \
DATABASE_URL=sqlite:///admin.db \
ENTRA_CLIENT_SECRET=... \
SESSION_SECRET=$(python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())") \
DATA_ENCRYPTION_KEY=$(python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())") \
ADMIN_UPNS=you@sanoptis.com \
pixi run -e admin reflex run
See sanoptis_admin/web.yaml.example for the config schema.
Adding a new secret to a plugin
Preferred: use the admin app — "Add/rotate secret" writes to Postgres under the admin's own identity and records the rotation in the audit log.
Break-glass: the gha service principal has Key Vault Secrets Officer on the shared KV (see infra/entra/gha_app.tf) which lets an operator rotate container-level secrets (DATA_ENCRYPTION_KEY etc.) — but plugin secrets live in Postgres, not KV, so the admin app is the only route for those.
Adding a new plugin
Preferred: use the admin app — "Create plugin" registers it and auto-enrols you as first member. "Add secret" then creates each secret and (for restricted plugins) "Add member" grants access.
In your plugin's code, unchanged from above:
get_secret("myplugin/api-key")
Repo layout
| Path | Purpose |
|---|---|
sanoptis_auth/ |
Python package: azure-identity chain + admin-broker client (consumed by plugins) |
sanoptis_auth/mcp_server/ |
stdio MCP server exposing start_sanoptis_login and whoami |
sanoptis_admin/ |
Reflex admin web app: plugins, secrets, memberships, audit log, /api/secrets/* broker |
tests/ |
pytest — tests/ for the SDK, sanoptis_admin/tests/ for the admin app |
infra/azure/ |
CI-managed Terraform: Key Vault, Container App, Postgres, ACR |
infra/entra/ |
Operator-managed Terraform: plugin public-client app, admin confidential app, GHA OIDC SP |
.github/workflows/ |
CI, release, terraform plan/apply, admin-app deploy |
Architecture decisions
- Postgres broker over direct KV. Plugin users never touch Key Vault. The admin app owns memberships + audit; KV is used only for container-level secrets (Fernet DEK, OAuth client secret, session key, admin UPN list, DB DSN).
- Public client (no client secret) for the plugin app. The plugin Entra app uses delegated access — tokens are minted on behalf of the signed-in user via the browser popup. Nothing to leak or rotate at the app level.
- Per-user MSAL cache. Token cache lives in
${CLAUDE_PLUGIN_DATA}/msal_cache.binwhen running inside a Claude Code plugin, otherwise~/.sanoptis-auth/msal_cache.bin. Shared across all plugins on the same user account — one sign-in, many plugins. - No device-code fallback. Headless environments use
az login --tenant <tenant>beforehand; AzureCliCredential then produces tokens silently. This keeps the chain predictable and never blocks on an invisible prompt. - Entra for sign-in only. No Graph group lookups, no per-secret KV RBAC, no ARM role assignments at plugin lifecycle time. Membership lives in the admin's own Postgres.
- Terraform over Bicep. Reviewable, multi-cloud-portable, standard Sanoptis-BI infra tool. Runs via OIDC federated identity in CI — no stored service principal secret.
Provisioning new infrastructure
See infra/README.md for the one-time Azure bootstrap procedure.
Source of truth
This repo is the only place credentials for Sanoptis Claude Code plugins should be managed. If you find a plugin reading secrets from a dotfile or environment variable, file an issue — it should be migrated to sanoptis_auth.get_secret.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sanoptis_auth-0.5.2.tar.gz.
File metadata
- Download URL: sanoptis_auth-0.5.2.tar.gz
- Upload date:
- Size: 155.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
319bde686d7ab031a714c1268c0d10eaf21116d883d7664071c2af777322eb77
|
|
| MD5 |
3d7c6ea8096191e190514430bb1b6d17
|
|
| BLAKE2b-256 |
4615dfb1f00a316a88605d5bd01a9d8960f98b47f14cb62155a17059b2bbdc15
|
Provenance
The following attestation bundles were made for sanoptis_auth-0.5.2.tar.gz:
Publisher:
publish.yml on Sanoptis-BI/sanoptis-auth
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sanoptis_auth-0.5.2.tar.gz -
Subject digest:
319bde686d7ab031a714c1268c0d10eaf21116d883d7664071c2af777322eb77 - Sigstore transparency entry: 1317389211
- Sigstore integration time:
-
Permalink:
Sanoptis-BI/sanoptis-auth@60b9377b09e43ff5ea1256a04bfc705a1a5e1246 -
Branch / Tag:
refs/tags/v0.5.2 - Owner: https://github.com/Sanoptis-BI
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@60b9377b09e43ff5ea1256a04bfc705a1a5e1246 -
Trigger Event:
push
-
Statement type:
File details
Details for the file sanoptis_auth-0.5.2-py3-none-any.whl.
File metadata
- Download URL: sanoptis_auth-0.5.2-py3-none-any.whl
- Upload date:
- Size: 24.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d5a528ff15be9606bf0bb22ff244f05146d198027b1ad477728df2b4e534b75
|
|
| MD5 |
bf0a1483b83a97cd1c271b5b0fc043b8
|
|
| BLAKE2b-256 |
9b37852b06ebdf81534f4e7190aeb9ab44787590af29c85bb6815ae40b440245
|
Provenance
The following attestation bundles were made for sanoptis_auth-0.5.2-py3-none-any.whl:
Publisher:
publish.yml on Sanoptis-BI/sanoptis-auth
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sanoptis_auth-0.5.2-py3-none-any.whl -
Subject digest:
1d5a528ff15be9606bf0bb22ff244f05146d198027b1ad477728df2b4e534b75 - Sigstore transparency entry: 1317389216
- Sigstore integration time:
-
Permalink:
Sanoptis-BI/sanoptis-auth@60b9377b09e43ff5ea1256a04bfc705a1a5e1246 -
Branch / Tag:
refs/tags/v0.5.2 - Owner: https://github.com/Sanoptis-BI
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@60b9377b09e43ff5ea1256a04bfc705a1a5e1246 -
Trigger Event:
push
-
Statement type: