Microsoft SSO + admin-broker credential client for Sanoptis Claude Code plugins.
Project description
sanoptis-auth
Microsoft SSO + Azure Key Vault 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 more per-plugin dotfiles, chmod gymnastics, or hand-rolled credential bootstrap docs.
Install
pip install sanoptis-auth
What it does
- Fetches secrets from
kv-sanoptis-pluginsAzure Key Vault. - Authenticates the user via Microsoft device-code flow on first use; caches the refresh token (90 days) for subsequent calls.
- Surfaces the login prompt as a clickable link in Claude chat (via the bundled
start_sanoptis_loginMCP tool) — no terminal required. - Enforces RBAC at the Entra security-group level: a user with
sg-sanoptis-plugin-yokoy-userscan fetch yokoy secrets but not DATEV secrets, even from the same machine.
Use it in a plugin
Add to your plugin's requirements.txt:
sanoptis-auth>=0.1.2
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")
Register the MCP server alongside your own in .mcp.json so users can run start_sanoptis_login from chat:
{
"mcpServers": {
"sanoptis-auth": {
"command": "python",
"args": ["-m", "sanoptis_auth.mcp_server"],
"env": {
"PYTHONPATH": "${CLAUDE_PLUGIN_DATA}/site-packages",
"CLAUDE_PLUGIN_DATA": "${CLAUDE_PLUGIN_DATA}"
}
}
}
}
First-time login (end user)
- Install the plugin in Claude Code or Cowork.
- Use any tool that needs a secret. Claude will say something like "Sanoptis login required — run the start_sanoptis_login tool."
- Run that tool. Claude shows you a clickable Microsoft device-code URL and a short code.
- Click the link, sign in with your Sanoptis Microsoft account, paste the code.
- Done — the next 90 days of plugin usage are automatic.
Local dev override
For working offline or before your Entra group 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 group memberships. It replaces the old Terraform plugins-variable flow and the deprecated rotate-secrets.yml workflow.
Admins sign in with their Sanoptis Microsoft account and can:
- Create plugins — provisions
sg-sanoptis-plugin-<name>-usersin Entra. - Add/rotate/delete secrets — writes to
kv-sanoptis-pluginsand assignsKey Vault Secrets Userto the plugin group scoped to each specific secret. - Manage members — resolves email → Entra user and adds/removes the group membership.
- Audit — every mutation is logged to the app's SQLite store 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 web.yaml's admins: list) 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())") \
pixi run -e admin reflex run
See sanoptis_admin/web.yaml.example for the config schema.
Adding a new secret to the vault
Preferred: use the admin app — "Add/rotate secret" writes to Key Vault under the admin's own identity and records the rotation in the audit log.
Break-glass (CLI, for cases where the admin app is unavailable):
az login --tenant ea0bd7d3-b29f-47f4-aedc-da7b52a28ba0
az keyvault secret set --vault-name kv-sanoptis-plugins --name "myplugin--api-key" --value "..."
The gha service principal retains Key Vault Secrets Officer on the vault (see infra/entra/gha_app.tf) so an operator can rotate via az from a CI-OIDC-authenticated shell if needed.
Adding a new plugin
Preferred: use the admin app — "Create plugin" provisions the Entra group and registers the plugin in the admin DB. "Add secret" then creates each secret and grants the group per-secret read access.
The old Terraform plugins-variable flow is retired; infra/entra/groups.tf and role_assignments.tf no longer exist. Group and per-secret RBAC lifecycle is owned by the admin app at runtime.
In your plugin code (unchanged):
get_secret("myplugin--api-key")
Repo layout
| Path | Purpose |
|---|---|
sanoptis_auth/ |
Python package: MSAL + KV client + MCP server (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 |
tests/ |
pytest suite — tests/ for the library, tests/admin/ for the admin app |
infra/azure/ |
CI-managed Terraform: Key Vault, Container App, ACR, Storage (File share for admin SQLite) |
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
- Public client (no client secret). The Entra app uses delegated access — secrets are read as the signed-in user, not as the app. Nothing to leak or rotate at the app level.
- Per-plugin token cache. Each plugin's
${CLAUDE_PLUGIN_DATA}dir gets its own MSAL cache. The user signs in once per plugin; refresh tokens last 90 days. Trade-off: prevents one plugin from reading another's tokens. - Hand-rolled MSAL over
azure-identity.DefaultAzureCredential.DefaultAzureCredentialtries 7 different credential chains and surfaces opaque error messages. We usemsal.PublicClientApplicationdirectly with a single device-code path — failures are explicit. - Group-scoped RBAC at the secret level. A user with
sg-sanoptis-plugin-yokoy-userscannot read DATEV secrets even if they install the DATEV plugin. RBAC is enforced by Azure Key Vault, not by application code. - Terraform over Bicep. Reviewable, multi-cloud-portable, and the standard Sanoptis-BI infra tool. Runs via OIDC federated identity in CI — no service principal secret stored in GitHub Actions.
Provisioning new infrastructure
See infra/README.md for the one-time Azure bootstrap procedure (Andrej / Matthias action).
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.2.0.tar.gz.
File metadata
- Download URL: sanoptis_auth-0.2.0.tar.gz
- Upload date:
- Size: 141.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6d70650bbe1f06061233d6c19a22c55d1834b21e0970e1b09ed0719fe88bcc3f
|
|
| MD5 |
2b1765983b807019372eb97b56690ed5
|
|
| BLAKE2b-256 |
82690301aaeabaf18afdadd9324abc4e88b67ca9dd2a517c52e30a6463be61bf
|
Provenance
The following attestation bundles were made for sanoptis_auth-0.2.0.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.2.0.tar.gz -
Subject digest:
6d70650bbe1f06061233d6c19a22c55d1834b21e0970e1b09ed0719fe88bcc3f - Sigstore transparency entry: 1306896057
- Sigstore integration time:
-
Permalink:
Sanoptis-BI/sanoptis-auth@262fdd5f5dbefd32b48e9085dedc76659884d89f -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Sanoptis-BI
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@262fdd5f5dbefd32b48e9085dedc76659884d89f -
Trigger Event:
push
-
Statement type:
File details
Details for the file sanoptis_auth-0.2.0-py3-none-any.whl.
File metadata
- Download URL: sanoptis_auth-0.2.0-py3-none-any.whl
- Upload date:
- Size: 22.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
10f0220f1e4e5c5416aa00057c29a17be799e0c1a0b40537abfd2319e869d116
|
|
| MD5 |
20ab4100254552d59efac09022cca24b
|
|
| BLAKE2b-256 |
654db95da3d86361d00b379eb357bdaa64e97fdd83806040c6559bc1b7c3d690
|
Provenance
The following attestation bundles were made for sanoptis_auth-0.2.0-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.2.0-py3-none-any.whl -
Subject digest:
10f0220f1e4e5c5416aa00057c29a17be799e0c1a0b40537abfd2319e869d116 - Sigstore transparency entry: 1306896169
- Sigstore integration time:
-
Permalink:
Sanoptis-BI/sanoptis-auth@262fdd5f5dbefd32b48e9085dedc76659884d89f -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Sanoptis-BI
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@262fdd5f5dbefd32b48e9085dedc76659884d89f -
Trigger Event:
push
-
Statement type: