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.
- Drop-in UI Components & Next.js SDK (
@deskid/react,@deskid/vue,@deskid/nextjs) for 5-minute integration - Edge Auth Middleware for Next.js App Router and Pages Router zero-boilerplate route protection
- Modern Vue 3 Auth & Admin Portal (
frontend/) with live password strength checklist and human-friendly error messages - Passwordless Authentication via Email Magic Links and One-Time Passcodes (OTP)
- 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) and dynamic tenant switching - 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)
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:8090Generate 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.pemOpenAPI: http://127.0.0.1:8090/docs
- Client SDKs & Drop-in UI Guide (React, Vue, Next.js): 5-minute integration guide with
<SignInButton />,<DeskIdModal />,<UserButton />, and Next.js Edge middleware. - Adoption Strategy & Task Roadmap: High-multiplier strategic roadmap (OIDC discovery, team invites, drop-in components, SAML SSO).
- 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.
- 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.jsonExample 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"
}DeskID provides a centralized identity and access management control plane for all downstream integrated microservices across multi-tenant deployments.
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)
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.
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 (
{
"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
}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"].
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 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=50Returns:
{
"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
}All security events are cryptographically chained using SHA-256 (previous_hash 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.jsonCreate 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/callbackdocker compose up -dPostgres + 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.
Platform admins can operate users, grants, and the audit log in the browser:
http://127.0.0.1:8090/admin/console
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.
| 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 |
DeskID provides official first-party packages in packages/:
@deskid/nextjs: Next.js 13/14/15 Edge middleware, server helpers (getDeskIdSession()), and App Router components.@deskid/react: React 18 & 19 components (<DeskIdProvider>,<SignInButton>,<DeskIdModal>,<UserButton>) anduseDeskId()hook.@deskid/vue: Vue 3 Composition API plugin (createDeskId) and drop-in SFC components.@deskid/core: Universal client (DeskIdClient), safe JWT claims decoding, auto token refresh, and pluggable storage adapters.
import { deskIdAuthMiddleware } from '@deskid/nextjs/middleware'
export default deskIdAuthMiddleware({
publicRoutes: ['/', '/about', '/pricing', '/login'],
loginUrl: '/login'
})
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}import { DeskIdProvider, SignInButton, UserButton, useDeskId } from '@deskid/react'
export function App() {
return (
<DeskIdProvider baseUrl="https://auth.example.com" storage="cookie">
<Navbar />
</DeskIdProvider>
)
}
function Navbar() {
const { isAuthenticated } = useDeskId()
return (
<nav>
{isAuthenticated ? <UserButton showTenantSwitch={true} /> : <SignInButton mode="modal">Sign In</SignInButton>}
</nav>
)
}For full documentation and Vue 3 / Headless usage, see the SDK Monorepo Guide.
An interactive React demonstration app is included in examples/react-demo showcasing live auth state, modal login, token decoding, and protected API simulation:
cd examples/react-demo
pnpm run dev
# Visit http://localhost:3000pytest tests/ -v