@hilbras/keystone 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +423 -0
  2. package/README.md +85 -1
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +7 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +17 -11
  8. package/dist/index.js.map +1 -1
  9. package/dist/plugins/auth.d.ts +2 -0
  10. package/dist/plugins/auth.d.ts.map +1 -1
  11. package/dist/plugins/auth.js +21 -7
  12. package/dist/plugins/auth.js.map +1 -1
  13. package/dist/plugins/machinePrincipal.d.ts +28 -0
  14. package/dist/plugins/machinePrincipal.d.ts.map +1 -0
  15. package/dist/plugins/machinePrincipal.js +45 -0
  16. package/dist/plugins/machinePrincipal.js.map +1 -0
  17. package/dist/plugins/rateLimit.d.ts +10 -7
  18. package/dist/plugins/rateLimit.d.ts.map +1 -1
  19. package/dist/plugins/rateLimit.js +75 -36
  20. package/dist/plugins/rateLimit.js.map +1 -1
  21. package/dist/routes/admin/organizations.d.ts.map +1 -1
  22. package/dist/routes/admin/organizations.js +2 -0
  23. package/dist/routes/admin/organizations.js.map +1 -1
  24. package/dist/routes/admin/platform.d.ts.map +1 -1
  25. package/dist/routes/admin/platform.js +14 -1
  26. package/dist/routes/admin/platform.js.map +1 -1
  27. package/dist/routes/apiKeys.d.ts.map +1 -1
  28. package/dist/routes/apiKeys.js +37 -5
  29. package/dist/routes/apiKeys.js.map +1 -1
  30. package/dist/routes/auth.d.ts.map +1 -1
  31. package/dist/routes/auth.js +104 -3
  32. package/dist/routes/auth.js.map +1 -1
  33. package/dist/routes/emailVerification.d.ts.map +1 -1
  34. package/dist/routes/emailVerification.js +2 -0
  35. package/dist/routes/emailVerification.js.map +1 -1
  36. package/dist/routes/federation.d.ts.map +1 -1
  37. package/dist/routes/federation.js +8 -2
  38. package/dist/routes/federation.js.map +1 -1
  39. package/dist/routes/magicLinks.d.ts.map +1 -1
  40. package/dist/routes/magicLinks.js +2 -0
  41. package/dist/routes/magicLinks.js.map +1 -1
  42. package/dist/routes/oauth2.d.ts.map +1 -1
  43. package/dist/routes/oauth2.js +24 -4
  44. package/dist/routes/oauth2.js.map +1 -1
  45. package/dist/routes/password.d.ts.map +1 -1
  46. package/dist/routes/password.js +2 -0
  47. package/dist/routes/password.js.map +1 -1
  48. package/dist/routes/profile.js +2 -2
  49. package/dist/routes/profile.js.map +1 -1
  50. package/dist/routes/scim.d.ts.map +1 -1
  51. package/dist/routes/scim.js +2 -0
  52. package/dist/routes/scim.js.map +1 -1
  53. package/dist/routes/serviceAccounts.d.ts.map +1 -1
  54. package/dist/routes/serviceAccounts.js +17 -1
  55. package/dist/routes/serviceAccounts.js.map +1 -1
  56. package/dist/routes/sessions.js +3 -3
  57. package/dist/routes/sessions.js.map +1 -1
  58. package/dist/routes/smsOtp.d.ts.map +1 -1
  59. package/dist/routes/smsOtp.js +10 -0
  60. package/dist/routes/smsOtp.js.map +1 -1
  61. package/dist/routes/totp.d.ts.map +1 -1
  62. package/dist/routes/totp.js +38 -6
  63. package/dist/routes/totp.js.map +1 -1
  64. package/dist/routes/webauthn.d.ts.map +1 -1
  65. package/dist/routes/webauthn.js +8 -2
  66. package/dist/routes/webauthn.js.map +1 -1
  67. package/dist/services/configuration/profiles.d.ts +26 -0
  68. package/dist/services/configuration/profiles.d.ts.map +1 -1
  69. package/dist/services/configuration/profiles.js +80 -1
  70. package/dist/services/configuration/profiles.js.map +1 -1
  71. package/dist/services/events/subscribers/auditLog.d.ts.map +1 -1
  72. package/dist/services/events/subscribers/auditLog.js +38 -3
  73. package/dist/services/events/subscribers/auditLog.js.map +1 -1
  74. package/dist/services/events/types.d.ts +3 -1
  75. package/dist/services/events/types.d.ts.map +1 -1
  76. package/dist/services/events/validate.d.ts +1 -0
  77. package/dist/services/events/validate.d.ts.map +1 -1
  78. package/dist/services/events/validate.js +5 -1
  79. package/dist/services/events/validate.js.map +1 -1
  80. package/dist/services/localRateLimit.d.ts +44 -0
  81. package/dist/services/localRateLimit.d.ts.map +1 -0
  82. package/dist/services/localRateLimit.js +86 -0
  83. package/dist/services/localRateLimit.js.map +1 -0
  84. package/dist/services/refreshTokenState.d.ts +5 -0
  85. package/dist/services/refreshTokenState.d.ts.map +1 -0
  86. package/dist/services/refreshTokenState.js +30 -0
  87. package/dist/services/refreshTokenState.js.map +1 -0
  88. package/dist/services/scopes.d.ts +113 -0
  89. package/dist/services/scopes.d.ts.map +1 -0
  90. package/dist/services/scopes.js +138 -0
  91. package/dist/services/scopes.js.map +1 -0
  92. package/dist/services/setup/token.d.ts +12 -0
  93. package/dist/services/setup/token.d.ts.map +1 -1
  94. package/dist/services/setup/token.js +27 -3
  95. package/dist/services/setup/token.js.map +1 -1
  96. package/dist/services/tokens.d.ts +1 -0
  97. package/dist/services/tokens.d.ts.map +1 -1
  98. package/dist/services/tokens.js +1 -1
  99. package/dist/services/tokens.js.map +1 -1
  100. package/dist/services/trustedProxies.d.ts +24 -0
  101. package/dist/services/trustedProxies.d.ts.map +1 -1
  102. package/dist/services/trustedProxies.js +19 -0
  103. package/dist/services/trustedProxies.js.map +1 -1
  104. package/dist/services/webhooks.d.ts +16 -0
  105. package/dist/services/webhooks.d.ts.map +1 -1
  106. package/dist/services/webhooks.js +48 -3
  107. package/dist/services/webhooks.js.map +1 -1
  108. package/dist/setup-server.js +47 -3
  109. package/dist/setup-server.js.map +1 -1
  110. package/docs/API-REVIEW.md +121 -0
  111. package/docs/API.md +457 -0
  112. package/docs/ARCHITECTURE.md +142 -0
  113. package/docs/CONTRIBUTING.md +61 -0
  114. package/docs/DEPLOYMENT.md +257 -0
  115. package/docs/INTEGRATION.md +336 -0
  116. package/docs/LOGIN_FORM_INTEGRATION.md +306 -0
  117. package/docs/MIGRATION-1.7.md +70 -0
  118. package/docs/MIGRATION-1.8.md +183 -0
  119. package/docs/MIGRATION-1.9.md +200 -0
  120. package/docs/MIGRATION-2.0.md +203 -0
  121. package/docs/MIGRATION-2.4.md +185 -0
  122. package/docs/PERFORMANCE.md +155 -0
  123. package/docs/RBAC.md +100 -0
  124. package/docs/RE-AUDIT.md +72 -0
  125. package/docs/README.md +54 -0
  126. package/docs/RELEASE-1.7.md +53 -0
  127. package/docs/ROADMAP.md +41 -0
  128. package/docs/SECURITY.md +143 -0
  129. package/docs/adrs/001-identity-connectors-as-adapters.md +27 -0
  130. package/docs/adrs/002-versioned-event-bus.md +30 -0
  131. package/docs/adrs/003-bullmq-for-background-work.md +20 -0
  132. package/docs/adrs/004-argon2id-password-hashing.md +19 -0
  133. package/docs/plans/KEYSTONE_IMPROVEMENT_PLAN.md +334 -0
  134. package/docs/plans/UI_SIMPLIFICATION_IMPROVEMENT_PLAN.md +271 -0
  135. package/docs/security/audit.md +82 -0
  136. package/docs/security/configuration.md +60 -0
  137. package/docs/security/enterprise-sso.md +193 -0
  138. package/docs/security/mtls.md +132 -0
  139. package/docs/security/proxy-security.md +128 -0
  140. package/docs/security/rate-limiting.md +79 -0
  141. package/docs/security/registry-exceptions.md +34 -0
  142. package/docs/security/registry.json +649 -0
  143. package/docs/security/registry.md +657 -0
  144. package/docs/security/scopes.md +45 -0
  145. package/docs/security/supply-chain.md +49 -0
  146. package/docs/security/trust-boundaries.md +111 -0
  147. package/package.json +15 -6
@@ -0,0 +1,257 @@
1
+ # Deploying Keystone Online
2
+
3
+ This guide explains how to run Hilbras Keystone on a public server so external projects can connect to it over the internet.
4
+
5
+ ---
6
+
7
+ ## Architecture
8
+
9
+ ```
10
+ Internet
11
+ │
12
+ ├──► https://app.yourdomain.com (your React/Vue/Angular app)
13
+ │
14
+ └──► https://auth.yourdomain.com (Keystone identity server)
15
+ │
16
+ ├──► PostgreSQL
17
+ ├──► Redis
18
+ └──► Google / GitHub / etc.
19
+ ```
20
+
21
+ Your frontend and Keystone run as two separate services. The frontend loads the Keystone SDK from the Keystone domain and talks to it via HTTPS.
22
+
23
+ ---
24
+
25
+ ## What you need
26
+
27
+ 1. A server or VPS (e.g. AWS, DigitalOcean, Hetzner, Fly.io).
28
+ 2. A domain name.
29
+ 3. Two DNS records pointing to your server:
30
+ - `auth.yourdomain.com` → your server IP
31
+ - `app.yourdomain.com` → your server IP (or another host for your frontend)
32
+ 4. Docker and Docker Compose installed on the server.
33
+ 5. SSL certificate (use Caddy, Nginx + Let's Encrypt, or Cloudflare).
34
+
35
+ ---
36
+
37
+ ## Step 1 — Prepare environment variables
38
+
39
+ You can configure almost everything through the Keystone setup wizard in the browser. You only need a minimal `.env` to start the wizard the first time.
40
+
41
+ Create `.env` on the server with just enough to launch the setup UI:
42
+
43
+ ```env
44
+ NODE_ENV=production
45
+ PORT=4001
46
+ HOST=0.0.0.0
47
+ KEYSTONE_SETUP_MODE=true
48
+ ```
49
+
50
+ Then run:
51
+
52
+ ```bash
53
+ docker compose up -d
54
+ ```
55
+
56
+ Open `https://auth.yourdomain.com/setup` and fill in:
57
+
58
+ - Database URL
59
+ - Redis URL
60
+ - Public Keystone URL (`https://auth.yourdomain.com`)
61
+ - Client app URL (`https://app.yourdomain.com`)
62
+ - Allowed CORS origins (`https://app.yourdomain.com`)
63
+ - Cookie domain (`.yourdomain.com`)
64
+ - Secure cookies (enabled for HTTPS)
65
+ - Auto-generated internal API key and encryption key
66
+ - Google OAuth credentials
67
+ - Email/SMS providers
68
+
69
+ The wizard writes `.env` for you. No file editing needed after the first launch.
70
+
71
+ If you prefer to pre-configure everything in `.env` instead, use:
72
+
73
+ ```env
74
+ NODE_ENV=production
75
+ PORT=4001
76
+ HOST=0.0.0.0
77
+
78
+ DATABASE_URL=postgresql://hilbras:strong-password@postgres:5432/hilbras
79
+ REDIS_URL=redis://redis:6379
80
+
81
+ AUTH_API_PUBLIC_URL=https://auth.yourdomain.com
82
+ CLIENT_APP_URL=https://app.yourdomain.com
83
+ ALLOWED_ORIGINS=https://app.yourdomain.com
84
+
85
+ COOKIE_DOMAIN=.yourdomain.com
86
+ COOKIE_SECURE=true
87
+
88
+ KEYSTONE_INTERNAL_API_KEY=change-me-to-a-long-random-string
89
+ KEYSTONE_ENCRYPTION_KEY=...
90
+
91
+ GOOGLE_CLIENT_ID=...
92
+ GOOGLE_CLIENT_SECRET=...
93
+
94
+ EMAIL_PROVIDER=smtp
95
+ EMAIL_FROM=noreply@yourdomain.com
96
+ SMTP_HOST=smtp.yourprovider.com
97
+ SMTP_PORT=587
98
+ SMTP_USER=...
99
+ SMTP_PASS=...
100
+ SMTP_SECURE=true
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Step 2 — Update Google OAuth redirect URI
106
+
107
+ In Google Cloud Console, add:
108
+
109
+ ```
110
+ https://auth.yourdomain.com/auth/callback/google
111
+ ```
112
+
113
+ Remove or keep the localhost URI only for local development.
114
+
115
+ ---
116
+
117
+ ## Step 3 — Deploy with Docker Compose
118
+
119
+ Copy the project to the server, then run:
120
+
121
+ ```bash
122
+ docker compose up -d
123
+ ```
124
+
125
+ This starts PostgreSQL, Redis, and Keystone.
126
+
127
+ For the first run, enable setup mode so the wizard creates the owner account:
128
+
129
+ ```bash
130
+ KEYSTONE_SETUP_MODE=true docker compose up -d
131
+ ```
132
+
133
+ Then open the setup UI (port 5173 if exposed, or use the backend setup endpoints) and complete setup.
134
+
135
+ ---
136
+
137
+ ## Step 4 — Put Keystone behind HTTPS
138
+
139
+ ### Option A: Caddy (simplest)
140
+
141
+ Create `Caddyfile`:
142
+
143
+ ```
144
+ auth.yourdomain.com {
145
+ reverse_proxy localhost:4001
146
+ }
147
+
148
+ app.yourdomain.com {
149
+ root * /var/www/app
150
+ file_server
151
+ }
152
+ ```
153
+
154
+ Run Caddy:
155
+
156
+ ```bash
157
+ caddy run
158
+ ```
159
+
160
+ Caddy automatically obtains and renews Let's Encrypt certificates.
161
+
162
+ ### Option B: Nginx + Let's Encrypt
163
+
164
+ Use `certbot` to obtain certificates and proxy to Keystone:
165
+
166
+ ```nginx
167
+ server {
168
+ listen 443 ssl;
169
+ server_name auth.yourdomain.com;
170
+
171
+ ssl_certificate /etc/letsencrypt/live/auth.yourdomain.com/fullchain.pem;
172
+ ssl_certificate_key /etc/letsencrypt/live/auth.yourdomain.com/privkey.pem;
173
+
174
+ location / {
175
+ proxy_pass http://localhost:4001;
176
+ proxy_http_version 1.1;
177
+ proxy_set_header Host $host;
178
+ proxy_set_header X-Real-IP $remote_addr;
179
+ # $remote_addr, NOT $proxy_add_x_forwarded_for. The append form lets a client
180
+ # prepend a forged address, which then becomes the value Keystone believes.
181
+ proxy_set_header X-Forwarded-For $remote_addr;
182
+ proxy_set_header X-Forwarded-Proto $scheme;
183
+
184
+ # Drop inbound identity headers. Keystone ignores them from untrusted peers
185
+ # anyway, but stripping them here keeps a misconfiguration from being
186
+ # exploitable and keeps the proxy honest.
187
+ proxy_set_header X-Forwarded-Client-Cert "";
188
+ proxy_set_header X-Client-Cert-Fingerprint "";
189
+ proxy_set_header X-Service-Account-Id "";
190
+ }
191
+ }
192
+ ```
193
+
194
+ **Set the trusted proxy list.** Since 2.0.0, Keystone does not believe
195
+ forwarded headers unless the request arrived from a configured proxy, and it
196
+ keys rate limits on the peer address otherwise — which, behind a reverse proxy,
197
+ means every client shares one budget:
198
+
199
+ ```bash
200
+ KEYSTONE_TRUSTED_PROXIES="127.0.0.1,::1"
201
+ ```
202
+
203
+ Use the address the proxy connects from. Keep the list as narrow as possible.
204
+ Full requirements, including why each header matters:
205
+ [security/proxy-security.md](security/proxy-security.md).
206
+
207
+ ### Option C: Cloudflare
208
+
209
+ Point your DNS to Cloudflare, enable the orange cloud, and set SSL/TLS to **Full (strict)**. No extra certificate setup needed.
210
+
211
+ ---
212
+
213
+ ## Step 5 — Update your project's script tag
214
+
215
+ In your frontend `index.html`, change the SDK URL to production:
216
+
217
+ ```html
218
+ <script
219
+ src="https://auth.yourdomain.com/sdk/keystone-dropin.js"
220
+ data-keystone-url="https://auth.yourdomain.com"
221
+ data-keystone-after-login="https://app.yourdomain.com/dashboard"
222
+ ></script>
223
+ ```
224
+
225
+ And connect the project:
226
+
227
+ ```html
228
+ <script>
229
+ Keystone.connect("my-project", "https://app.yourdomain.com/callback")
230
+ .then((c) => console.log("Connected:", c.clientId));
231
+ </script>
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Important production checklist
237
+
238
+ - [ ] HTTPS everywhere
239
+ - [ ] `COOKIE_SECURE=true`
240
+ - [ ] `COOKIE_DOMAIN=.yourdomain.com` (with leading dot)
241
+ - [ ] `ALLOWED_ORIGINS` set to your frontend domain only
242
+ - [ ] Strong `KEYSTONE_INTERNAL_API_KEY` and `KEYSTONE_ENCRYPTION_KEY`
243
+ - [ ] Google OAuth redirect URI is the HTTPS production URL
244
+ - [ ] Database and Redis are not exposed to the internet
245
+ - [ ] Regular backups of PostgreSQL data
246
+ - [ ] Server firewall allows only ports 80, 443, and SSH
247
+
248
+ ---
249
+
250
+ ## Scaling
251
+
252
+ For high traffic:
253
+
254
+ - Use a managed PostgreSQL (AWS RDS, Supabase, etc.).
255
+ - Use managed Redis (Redis Cloud, AWS ElastiCache).
256
+ - Run multiple Keystone containers behind a load balancer.
257
+ - Ensure `KEYSTONE_ENCRYPTION_KEY` and signing keys are shared between instances.
@@ -0,0 +1,336 @@
1
+ # Integrating Keystone with Your Projects
2
+
3
+ Keystone exposes **standard OIDC/OAuth2** endpoints, a **REST API**, and **machine credentials**, so any application — web, mobile, CLI, or microservice — can use it as its identity provider.
4
+
5
+ ---
6
+
7
+ ## 1. Web / SPA: OIDC Authorization Code + PKCE
8
+
9
+ The recommended flow for React, Vue, Angular, Svelte, Next.js (client-side), and mobile apps.
10
+
11
+ ### Discovery
12
+
13
+ ```bash
14
+ curl http://localhost:4001/.well-known/openid-configuration
15
+ ```
16
+
17
+ ### Example: React helper
18
+
19
+ ```ts
20
+ // auth.ts
21
+ const ISSUER = "http://localhost:4001";
22
+ const CLIENT_ID = "your-app-client-id"; // from /v1/admin/organizations/:id/applications
23
+
24
+ export function generateCodeVerifier() {
25
+ const array = new Uint8Array(32);
26
+ crypto.getRandomValues(array);
27
+ return btoa(String.fromCharCode(...array))
28
+ .replace(/\+/g, "-")
29
+ .replace(/\//g, "_")
30
+ .replace(/=+$/, "");
31
+ }
32
+
33
+ async function sha256(plain: string) {
34
+ const encoder = new TextEncoder();
35
+ const data = encoder.encode(plain);
36
+ const hash = await crypto.subtle.digest("SHA-256", data);
37
+ return btoa(String.fromCharCode(...new Uint8Array(hash)))
38
+ .replace(/\+/g, "-")
39
+ .replace(/\//g, "_")
40
+ .replace(/=+$/, "");
41
+ }
42
+
43
+ export async function startLogin() {
44
+ const verifier = generateCodeVerifier();
45
+ const challenge = await sha256(verifier);
46
+ localStorage.setItem("keystone-code-verifier", verifier);
47
+
48
+ const params = new URLSearchParams({
49
+ response_type: "code",
50
+ client_id: CLIENT_ID,
51
+ redirect_uri: "http://localhost:5173/callback",
52
+ scope: "openid profile email",
53
+ state: crypto.randomUUID(),
54
+ code_challenge: challenge,
55
+ code_challenge_method: "S256",
56
+ });
57
+
58
+ window.location.href = `${ISSUER}/oauth2/authorize?${params.toString()}`;
59
+ }
60
+
61
+ export async function exchangeCode(code: string) {
62
+ const verifier = localStorage.getItem("keystone-code-verifier");
63
+ if (!verifier) throw new Error("Missing PKCE verifier");
64
+
65
+ const res = await fetch(`${ISSUER}/oauth2/token`, {
66
+ method: "POST",
67
+ headers: { "Content-Type": "application/json" },
68
+ body: JSON.stringify({
69
+ grant_type: "authorization_code",
70
+ code,
71
+ redirect_uri: "http://localhost:5173/callback",
72
+ client_id: CLIENT_ID,
73
+ code_verifier: verifier,
74
+ }),
75
+ });
76
+
77
+ const data = await res.json();
78
+ localStorage.setItem("keystone-access-token", data.access_token);
79
+ localStorage.setItem("keystone-refresh-token", data.refresh_token);
80
+ return data;
81
+ }
82
+ ```
83
+
84
+ ### Callback page
85
+
86
+ ```tsx
87
+ // pages/callback.tsx
88
+ import { useEffect } from "react";
89
+ import { useRouter, useSearchParams } from "next/navigation";
90
+ import { exchangeCode } from "./auth";
91
+
92
+ export default function Callback() {
93
+ const router = useRouter();
94
+ const params = useSearchParams();
95
+
96
+ useEffect(() => {
97
+ const code = params.get("code");
98
+ if (!code) return;
99
+ exchangeCode(code).then(() => router.replace("/dashboard"));
100
+ }, [params, router]);
101
+
102
+ return <p>Signing in…</p>;
103
+ }
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 2. Server-side web app (Next.js App Router / SSR)
109
+
110
+ Use Keystone as an external OAuth provider with NextAuth.js, Auth.js, or a custom server-side OAuth flow.
111
+
112
+ ### NextAuth.js provider example
113
+
114
+ ```ts
115
+ // auth.ts (NextAuth v5 / Auth.js)
116
+ import NextAuth from "next-auth";
117
+ import { OAuthConfig } from "next-auth/providers";
118
+
119
+ const keystoneProvider: OAuthConfig<any> = {
120
+ id: "keystone",
121
+ name: "Keystone",
122
+ type: "oauth",
123
+ issuer: "http://localhost:4001",
124
+ authorization: { params: { scope: "openid profile email" } },
125
+ checks: ["pkce", "state"],
126
+ clientId: process.env.KEYSTONE_CLIENT_ID!,
127
+ clientSecret: process.env.KEYSTONE_CLIENT_SECRET!,
128
+ profile(profile) {
129
+ return {
130
+ id: profile.sub,
131
+ email: profile.email,
132
+ name: profile.name,
133
+ image: profile.picture,
134
+ };
135
+ },
136
+ };
137
+
138
+ export const { handlers, auth, signIn, signOut } = NextAuth({
139
+ providers: [keystoneProvider],
140
+ });
141
+ ```
142
+
143
+ > Store `clientSecret` server-side only.
144
+
145
+ ---
146
+
147
+ ## 3. Backend / microservice: API keys
148
+
149
+ For service-to-service or machine-to-machine calls, create an API key in the dashboard or via the API, then send it as a bearer token.
150
+
151
+ ### Create an API key
152
+
153
+ ```bash
154
+ curl -X POST http://localhost:4001/auth/api-keys \
155
+ -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
156
+ -H "Content-Type: application/json" \
157
+ -d '{"name":"payment-service","scopes":["read:users","write:orders"]}'
158
+ ```
159
+
160
+ Response includes `key` once. Save it securely.
161
+
162
+ ### Use the API key
163
+
164
+ ```bash
165
+ curl http://localhost:4001/auth/me \
166
+ -H "Authorization: Bearer $API_KEY"
167
+ ```
168
+
169
+ ### Check authorization
170
+
171
+ ```bash
172
+ curl -X POST http://localhost:4001/v1/authz/check \
173
+ -H "Authorization: Bearer $API_KEY" \
174
+ -H "Content-Type: application/json" \
175
+ -d '{
176
+ "organizationId": "00000000-0000-0000-0000-000000000000",
177
+ "resource": "order",
178
+ "action": "create"
179
+ }'
180
+ ```
181
+
182
+ Authorization checks are organization-scoped. Platform roles (`owner`/`user`) and organization roles (`owner`/`admin`/`member`) are separate; never infer one from the other. User-management responses are redacted and do not include password hashes or TOTP secrets.
183
+
184
+ ---
185
+
186
+ ## 4. Python / FastAPI backend
187
+
188
+ ```python
189
+ import httpx
190
+ from jose import jwt, jwk
191
+ from jose.utils import base64url_decode
192
+ import requests
193
+
194
+ KEYSTONE_URL = "http://localhost:4001"
195
+ JWKS = requests.get(f"{KEYSTONE_URL}/.well-known/jwks.json").json()
196
+
197
+ def get_signing_key(token: str):
198
+ header = jwt.get_unverified_header(token)
199
+ for key in JWKS["keys"]:
200
+ if key["kid"] == header["kid"]:
201
+ return key
202
+ raise ValueError("Signing key not found")
203
+
204
+ def verify_token(token: str):
205
+ key = get_signing_key(token)
206
+ return jwt.decode(token, key, algorithms=["RS256"], issuer=KEYSTONE_URL)
207
+
208
+ async def fetch_user(token: str):
209
+ async with httpx.AsyncClient() as client:
210
+ r = await client.get(
211
+ f"{KEYSTONE_URL}/oauth2/userinfo",
212
+ headers={"Authorization": f"Bearer {token}"}
213
+ )
214
+ r.raise_for_status()
215
+ return r.json()
216
+ ```
217
+
218
+ ---
219
+
220
+ ## 5. Mobile apps
221
+
222
+ Mobile apps should use the same **Authorization Code + PKCE** flow as SPAs. Use a system browser (ASWebAuthenticationSession on iOS, Custom Tabs on Android) to open:
223
+
224
+ ```
225
+ http://localhost:4001/oauth2/authorize?response_type=code&client_id=...&redirect_uri=myapp://callback&scope=openid%20profile%20email&code_challenge=...&code_challenge_method=S256&state=...
226
+ ```
227
+
228
+ Register `myapp://callback` as the redirect scheme in your mobile app.
229
+
230
+ ---
231
+
232
+ ## 6. CLI / scripts
233
+
234
+ For CLI tools, use `/auth/token-login` with a user's email/password (if you accept password input) or issue a service account / API key.
235
+
236
+ ```bash
237
+ export TOKEN=$(curl -s -X POST http://localhost:4001/auth/token-login \
238
+ -H "Content-Type: application/json" \
239
+ -d '{"email":"owner@example.com","password":"..."}' | jq -r .accessToken)
240
+
241
+ curl -H "Authorization: Bearer $TOKEN" http://localhost:4001/auth/me
242
+ ```
243
+
244
+ If the account has MFA enabled, the password step returns `401` with
245
+ `code: "MFA_REQUIRED"` and no token. Exchange the returned challenge for one:
246
+
247
+ ```bash
248
+ CHALLENGE=$(curl -s -X POST http://localhost:4001/auth/token-login \
249
+ -H "Content-Type: application/json" \
250
+ -d '{"email":"owner@example.com","password":"..."}' | jq -r .challenge)
251
+
252
+ read -r -p "Authenticator code: " CODE
253
+
254
+ export TOKEN=$(curl -s -X POST http://localhost:4001/auth/mfa/verify \
255
+ -H "Content-Type: application/json" \
256
+ -d "$(jq -nc --arg c "$CHALLENGE" --arg k "$CODE" '{challenge:$c, code:$k, factor:"totp"}')" \
257
+ | jq -r .accessToken)
258
+ ```
259
+
260
+ The challenge is single-use and short-lived, so request a fresh one per attempt.
261
+ A TOTP time-step is also accepted only once: repeating the same code is rejected
262
+ even with a new challenge.
263
+
264
+ ---
265
+
266
+ ## 7. Federation: let users sign in through external IdPs
267
+
268
+ Keystone acts as the broker. Your app only ever talks to Keystone.
269
+
270
+ ```
271
+ User → Google / Azure / Okta → Keystone → Keystone access token
272
+ ```
273
+
274
+ From your app's perspective, the flow is identical to section 1 — redirect to Keystone's OAuth2 authorize endpoint. Keystone handles the external IdP dance and returns your app a Keystone token.
275
+
276
+ Enable providers in the admin dashboard or by setting environment variables (`GOOGLE_CLIENT_ID`, `GITHUB_CLIENT_ID`, etc.).
277
+
278
+ ---
279
+
280
+ ## 8. Register your application in Keystone
281
+
282
+ Before any OAuth2 flow, register your app under an organization:
283
+
284
+ ```bash
285
+ curl -X POST http://localhost:4001/v1/admin/organizations/:orgId/applications \
286
+ -H "Authorization: Bearer $OWNER_TOKEN" \
287
+ -H "Content-Type: application/json" \
288
+ -d '{
289
+ "name": "My SaaS",
290
+ "redirectUris": ["http://localhost:5173/callback"],
291
+ "allowedOrigins": ["http://localhost:5173"]
292
+ }'
293
+ ```
294
+
295
+ Response:
296
+
297
+ ```json
298
+ {
299
+ "id": "...",
300
+ "clientId": "my-saas-...",
301
+ "clientSecret": "..." // store securely
302
+ }
303
+ ```
304
+
305
+ ---
306
+
307
+ ## 9. Token verification summary
308
+
309
+ | Token type | Verify at | Use case |
310
+ |------------|-----------|----------|
311
+ | JWT access token | `/.well-known/jwks.json` | User sessions, SPAs, mobile |
312
+ | API key | `/auth/me` or locally against hash | Machine-to-machine |
313
+ | Refresh token | `/auth/refresh` | Silent re-authentication |
314
+ | ID token | JWKS + issuer + audience | OIDC clients |
315
+
316
+ ---
317
+
318
+ ## 10. Security checklist
319
+
320
+ - Always use **PKCE** for public clients (SPAs, mobile, desktop).
321
+ - Store `clientSecret` server-side only.
322
+ - Verify the JWT issuer (`iss`) and audience (`aud`) if you set them.
323
+ - Use short-lived access tokens and rotate refresh tokens.
324
+ - Scope API keys to the minimum permissions required.
325
+ - Call `/v1/authz/check` for sensitive actions even if the user is authenticated.
326
+
327
+ ---
328
+
329
+ ## Quick start for a new project
330
+
331
+ 1. Run Keystone locally: `./start.sh`
332
+ 2. Complete setup and create an owner account.
333
+ 3. Create an organization and application via the admin dashboard.
334
+ 4. Copy the `clientId` and `clientSecret`.
335
+ 5. Use the OIDC endpoints above in your app.
336
+ 6. Validate tokens with `/.well-known/jwks.json`.