DeskID
DeskID is a high-performance, product-agnostic authentication and identity microservice module. Consumer services validate JWTs via JWKS and assign product grants via admin API or deployment env.
Features
- Email/password register & login with email verification
- Password reset via Mail service
- Google + GitHub OAuth
- RS256 access tokens + refresh tokens (rotation + revocation)
- Public JWKS (
/.well-known/jwks.json) with deterministic key management - Orgs / memberships (
org_id/workspace_idclaims) - Product grants (
aud+roles.<product>) — assigned per product, not baked into Auth - Rate limiting on login, register, and password-reset endpoints
- Immutable audit log for all auth and admin events
- User data export and deletion (GDPR/CCPA)
- Request-ID middleware (
x-request-idon every response) - Authenticated token introspection (
/introspectrequires API key)
Quick start
cd DeskID
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
cp .env.example .env
# Edit .env — set AUTH_DATABASE_URL (PostgreSQL) and RSA keys
deskid
# → http://127.0.0.1:8090
Generate an RSA key pair for local dev:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem
openssl rsa -in private.pem -pubout -out public.pem
# Then set AUTH_JWT_PRIVATE_KEY_FILE=./private.pem and AUTH_JWT_PUBLIC_KEY_FILE=./public.pem
OpenAPI: http://127.0.0.1:8090/docs
Documentation & Guides
- Downstream Product & Service Onboarding Guide: End-to-end integration walkthrough for connecting downstream services, registering custom roles, stateless JWKS validation, and reconciliation feed polling.
- User Onboarding & Identity Lifecycle Guide: Complete guide covering platform admin bootstrapping, open vs closed registration modes, email verification, organization tenancy, and account management.
Integrate from another product
- Send users to Auth login API or OAuth start URLs
- Receive
access_token(Bearer JWT) - Validate locally with JWKS (
iss,aud,exp) — no call to Auth per request - Read claims:
sub,email,org_id,workspace_id,roles - Ensure users have a product grant for your audience (
POST /v1/admin/grantsorAUTH_DEFAULT_AUDIENCESin that environment)
curl http://127.0.0.1:8090/.well-known/jwks.json
Example claims (after granting audience myproduct):
{
"sub": "user_uuid",
"email": "dev@example.com",
"org_id": "…",
"workspace_id": "…",
"aud": ["myproduct"],
"roles": { "myproduct": "operator" },
"iss": "https://auth.deskid.local"
}
Enterprise IAM & Multi-Service Architecture
DeskID provides a centralized identity and access management control plane for all downstream integrated microservices across multi-tenant deployments.
Authentication & Token Issuance Flow
sequenceDiagram
autonumber
actor User as Client / User
participant DeskID as DeskID (Auth Service)
participant DB as DeskID Database
participant Service as Downstream Service
Note over User,DeskID: 1. Login & Initial Token Issuance
User->>DeskID: POST /v1/auth/login { email, password }
DeskID->>DB: Verify credentials, email status, lockout
DeskID->>DB: Fetch Org Membership & Grants (Global + Primary Org)
DeskID-->>User: Return Access Token (JWT) + Refresh Token
Note over User,Service: 2. Stateless API Request
User->>Service: GET /api/v1/resource (Authorization: Bearer <JWT>)
Service->>DeskID: GET /.well-known/jwks.json (Cached locally)
Service->>Service: Validate signature, exp, iss, and aud
Service->>Service: Authorize user based on roles[service_id] & org_id
Service-->>User: 200 OK (Protected Resource)
Note over User,DeskID: 3. Explicit Organization Switch
User->>DeskID: POST /v1/auth/switch-org { org_id: "org_tenant_2" }
DeskID->>DB: Validate active membership in org_tenant_2
DeskID->>DB: Resolve tenant-specific grants for org_tenant_2
DeskID-->>User: Return new scoped JWT (org_id: org_tenant_2, scoped roles)
Service-Wide vs Organization-Wide Grants
Grants are managed centrally and scoped at two distinct levels:
graph TD
subgraph DeskID["DeskID IAM Control Plane"]
User["User Identity"]
end
subgraph Grants["Grant Scoping"]
Global["Service-Wide Grant (org_id = null)<br>Global Admin / SRE / Auditor"]
OrgScoped["Organization-Wide Grant (org_id = 'org_tenant_1')<br>Tenant Member / Analyst / Operator"]
end
subgraph Services["Downstream Integrated Services"]
S1["Service A<br>(e.g. Analytics Engine)"]
S2["Service B<br>(e.g. Compute Cluster)"]
S3["Service C<br>(e.g. Storage Catalog)"]
end
User --> Global
User --> OrgScoped
Global -->|Always Active across all orgs| S1
Global -->|Always Active across all orgs| S2
OrgScoped -->|Active ONLY in Tenant 1| S1
OrgScoped -->|Active ONLY in Tenant 1| S3
-
Service-Wide (Global Platform) Grants (
org_id = null):- Applied to an integrated service across the entire platform regardless of which organization context the user is currently operating in.
- Typically used for platform operators, infrastructure administrators, and global security auditors.
- Always included in
rolesandaudacross all organization contexts.
-
Organization-Wide (Tenant-Scoped) Grants (
org_id = "<org_id>"):- Applied to an integrated service strictly within the boundary of that specific Organization.
- Allows fine-grained role separation (e.g. an admin on
service-ain Tenant 1, but a viewer onservice-ain Tenant 2). - Dynamically evaluated and bound to the JWT when the user activates or switches organizations via
POST /v1/auth/switch-org.
Audience Sets (aud) & Grant Resolution Rules
Every downstream service registered with DeskID is represented by a unique Audience Identifier (e.g. service-a, service-b, service-c).
When a token is requested or switched:
-
Audience Inclusion (
aud):- The
audclaim is automatically constructed from the set of all services where the user has an active grant in the current context: $$\text{aud} = [\text{"deskid"}, \text{service}_1, \text{service}_2, \dots]$$ - Downstream services must reject tokens that do not list their specific audience in
aud.
- The
-
Role Precedence & Overrides:
- Base global grants (
org_id = null) are loaded first. - Organization-specific grants (
org_id = active_org_id) take precedence and override global defaults for matching services. - Services for which the user has no grant are excluded from
rolesandaud.
- Base global grants (
Example Token Payload:
{
"sub": "usr_948a28f1",
"email": "user@company.internal",
"org_id": "org_tenant_1",
"workspace_id": "ws_production",
"aud": ["deskid", "service-a", "service-b", "service-c"],
"roles": {
"service-a": "admin",
"service-b": "operator",
"service-c": "viewer"
},
"token_version": 1,
"iss": "https://auth.company.internal",
"iat": 1725634800,
"exp": 1725635700
}
Downstream Service Validation Checklist
When integrating any backend service with DeskID:
- Fetch & Cache Public Keys: Retrieve the JWKS from
/.well-known/jwks.json. Refresh automatically on unknownkidor after cache TTL. - Stateless Verification: Verify the token signature with the public key matching
header.kid, ensuring:iss == AUTH_ISSUERaudcontains your service's audience identifier (e.g."service-a")exp > current_timestamp
- RBAC Enforcement: Check
claims["roles"]["service-a"]to enforce authorization (e.g.admin,operator,viewer). - Tenant Isolation: Filter data partitions, catalogs, or workspaces matching
claims["org_id"].
Zero-Downtime Key Rotation (JWKS Key Ring)
DeskID supports continuous zero-downtime RSA key rotation:
- The active signing key is configured via
AUTH_JWT_PRIVATE_KEY/AUTH_JWT_PUBLIC_KEY. - Previous/historical public verification keys can be supplied via
AUTH_JWT_PREVIOUS_PUBLIC_KEYS(JSON array/map) orAUTH_JWT_PREVIOUS_PUBLIC_KEYS_FILE. /.well-known/jwks.jsonadvertises all active and historical keys simultaneously.- When validating tokens, DeskID inspects the unverified
kidheader and matches against the full key ring.
Downstream Reconciliation Event Feed
Downstream services can maintain synchronized local caches of user status, organization lifecycle, and grant assignments by polling the chronological reconciliation feed:
GET /v1/admin/reconciliation/events?since_id=<event_id>&limit=50
Returns:
{
"events": [
{
"id": "evt_01",
"occurred_at": "2026-09-06T12:00:00Z",
"action": "admin.set_grant",
"resource_type": "product_grant",
"resource_id": "usr_123",
"previous_hash": "GENESIS",
"integrity_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"details": "{\"audience\": \"service-a\", \"role\": \"admin\", \"org_id\": \"org_tenant_1\"}"
}
],
"next_cursor": "evt_01",
"has_more": false
}
Cryptographic Audit Log & Offline Verification
All security events are cryptographically chained using SHA-256 (previous_hash $\rightarrow$ integrity_hash).
To verify chain integrity offline or from the database:
# Verify from live database:
AUTH_DATABASE_URL="postgresql+psycopg://..." python3 scripts/verify_audit_chain.py
# Verify from exported JSON:
python3 scripts/verify_audit_chain.py --file audit_export.json
OAuth setup
Create Google / GitHub OAuth apps. Redirect URIs:
http://localhost:8090/v1/oauth/google/callbackhttp://localhost:8090/v1/oauth/github/callback
AUTH_GOOGLE_CLIENT_ID=…
AUTH_GOOGLE_CLIENT_SECRET=…
AUTH_GITHUB_CLIENT_ID=…
AUTH_GITHUB_CLIENT_SECRET=…
AUTH_SPA_CALLBACK_URL=http://localhost:5173/auth/callback
Docker
docker compose up -d
Postgres + Auth on port 8090. Mount RSA PEMs as ./private.pem and ./public.pem (or set AUTH_JWT_*_KEY_FILE on the host). Set AUTH_ISSUER and AUTH_BOOTSTRAP_TOKEN.
Admin console
Platform admins can operate users, grants, and the audit log in the browser:
http://127.0.0.1:8090/admin/console
Protocol shape
Auth is a first-party JSON + RS256 JWT API, not an OpenID Connect authorization server. Products validate JWKS locally. Standard OIDC clients (discovery, authorization-code, PKCE) are a non-goal until explicitly scheduled.
Rate limits are per process. Run a single replica or put a shared limiter in front until a Redis/Cache backend is added.
API map
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /health |
none | Ready check (HTTP 503 when DB or keys fail) |
| GET | /admin/console |
browser | Platform-admin UI |
| POST | /v1/auth/register |
none | Email/password signup |
| POST | /v1/auth/login |
none | Password login |
| POST | /v1/auth/refresh |
none | Rotate refresh token |
| POST | /v1/auth/logout |
none | Revoke refresh token |
| GET | /v1/auth/me |
Bearer JWT | Current user profile |
| PATCH | /v1/auth/me |
Bearer JWT | Update profile (email change re-verifies) |
| POST | /v1/auth/me/change-password |
Bearer JWT | Change password; revokes other sessions |
| GET | /v1/auth/me/sessions |
Bearer JWT | List refresh sessions |
| DELETE | /v1/auth/me/sessions |
Bearer JWT | Revoke all sessions |
| DELETE | /v1/auth/me/sessions/{id} |
Bearer JWT | Revoke one session |
| POST | /v1/auth/verify-email |
none | Verify email with token |
| POST | /v1/auth/forgot-password |
none | Request password reset email |
| POST | /v1/auth/reset-password |
none | Reset password with token |
| GET | /v1/oauth/google/start |
none | Initiate Google OAuth |
| GET | /v1/oauth/github/start |
none | Initiate GitHub OAuth |
| GET | /.well-known/jwks.json |
none | Public JWKS |
| POST | /introspect |
API key | Token introspection |
| GET | /metrics |
none | In-process auth counters |
| GET/POST | /v1/orgs |
Bearer JWT | List/create orgs |
| DELETE | /v1/orgs/{id} |
Bearer JWT | Delete org (owner) |
| GET | /v1/orgs/{id}/members |
Bearer JWT | List members |
| POST | /v1/orgs/{id}/members |
Bearer JWT | Add/update member |
| DELETE | /v1/orgs/{id}/members/{user_id} |
Bearer JWT | Remove member |
| GET | /v1/me/export |
Bearer JWT | Export own data (GDPR) |
| POST | /v1/me/delete |
Bearer JWT | Delete own account (GDPR) |
| GET | /v1/admin/users |
Bearer JWT + admin | List/search users (q, limit, offset) |
| PATCH | /v1/admin/users/{id}/active |
Bearer JWT + admin | Suspend or activate |
| POST | /v1/admin/grants |
Bearer JWT + admin | Assign product grant |
| GET | /v1/admin/audit |
Bearer JWT + admin | Query audit log |
TypeScript SDK
import { DeskID } from './client'
const auth = new DeskID({ baseUrl: 'http://127.0.0.1:8090' })
// Auth flows
await auth.register({ email, password })
await auth.login({ email, password })
await auth.refresh(refreshToken)
await auth.logout(refreshToken)
await auth.me(accessToken)
// Email / password management
await auth.verifyEmail({ token })
await auth.forgotPassword({ email })
await auth.resetPassword({ token, password })
// Self-service (GDPR)
await auth.exportMyData(accessToken)
await auth.deleteMyAccount(accessToken)
// Admin
await auth.listUsers(accessToken)
await auth.setGrant(accessToken, { user_id, audience, role })
await auth.queryAuditLog(accessToken, { action: 'user.login', limit: 25 })
Running tests
pytest tests/ -v
Metadata
Release files for deskid 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| deskid-1.1.0.tar.gz | 50.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| deskid-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 90.3 kB
Release files / deskid-1.1.0.tar.gz
| Download URL | deskid-1.1.0.tar.gz |
|---|---|
| Size | 50.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2da2a601f65f955af903c9707a77b6f749d39e1fc31ad164ef8da180d714a5fb
|
|
BLAKE2b-256 checksum How to use checksums |
fd16c8170553264e0023d30bd8916c28c83ef3a067eee300cafd4cdd9454191a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / deskid-1.1.0-py3-none-any.whl
| Download URL | deskid-1.1.0-py3-none-any.whl |
|---|---|
| Size | 39.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
56545f0d50d8582035574691cf46150a7f4203fcbb78a9bd03d27dc8483587b0
|
|
BLAKE2b-256 checksum How to use checksums |
4b98ba0ac1804be3e570bc5d9ff52638bde47385bbcb04c54340d3b647ac32f5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|