@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.
- package/CHANGELOG.md +423 -0
- package/README.md +85 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -11
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth.d.ts +2 -0
- package/dist/plugins/auth.d.ts.map +1 -1
- package/dist/plugins/auth.js +21 -7
- package/dist/plugins/auth.js.map +1 -1
- package/dist/plugins/machinePrincipal.d.ts +28 -0
- package/dist/plugins/machinePrincipal.d.ts.map +1 -0
- package/dist/plugins/machinePrincipal.js +45 -0
- package/dist/plugins/machinePrincipal.js.map +1 -0
- package/dist/plugins/rateLimit.d.ts +10 -7
- package/dist/plugins/rateLimit.d.ts.map +1 -1
- package/dist/plugins/rateLimit.js +75 -36
- package/dist/plugins/rateLimit.js.map +1 -1
- package/dist/routes/admin/organizations.d.ts.map +1 -1
- package/dist/routes/admin/organizations.js +2 -0
- package/dist/routes/admin/organizations.js.map +1 -1
- package/dist/routes/admin/platform.d.ts.map +1 -1
- package/dist/routes/admin/platform.js +14 -1
- package/dist/routes/admin/platform.js.map +1 -1
- package/dist/routes/apiKeys.d.ts.map +1 -1
- package/dist/routes/apiKeys.js +37 -5
- package/dist/routes/apiKeys.js.map +1 -1
- package/dist/routes/auth.d.ts.map +1 -1
- package/dist/routes/auth.js +104 -3
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/emailVerification.d.ts.map +1 -1
- package/dist/routes/emailVerification.js +2 -0
- package/dist/routes/emailVerification.js.map +1 -1
- package/dist/routes/federation.d.ts.map +1 -1
- package/dist/routes/federation.js +8 -2
- package/dist/routes/federation.js.map +1 -1
- package/dist/routes/magicLinks.d.ts.map +1 -1
- package/dist/routes/magicLinks.js +2 -0
- package/dist/routes/magicLinks.js.map +1 -1
- package/dist/routes/oauth2.d.ts.map +1 -1
- package/dist/routes/oauth2.js +24 -4
- package/dist/routes/oauth2.js.map +1 -1
- package/dist/routes/password.d.ts.map +1 -1
- package/dist/routes/password.js +2 -0
- package/dist/routes/password.js.map +1 -1
- package/dist/routes/profile.js +2 -2
- package/dist/routes/profile.js.map +1 -1
- package/dist/routes/scim.d.ts.map +1 -1
- package/dist/routes/scim.js +2 -0
- package/dist/routes/scim.js.map +1 -1
- package/dist/routes/serviceAccounts.d.ts.map +1 -1
- package/dist/routes/serviceAccounts.js +17 -1
- package/dist/routes/serviceAccounts.js.map +1 -1
- package/dist/routes/sessions.js +3 -3
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/smsOtp.d.ts.map +1 -1
- package/dist/routes/smsOtp.js +10 -0
- package/dist/routes/smsOtp.js.map +1 -1
- package/dist/routes/totp.d.ts.map +1 -1
- package/dist/routes/totp.js +38 -6
- package/dist/routes/totp.js.map +1 -1
- package/dist/routes/webauthn.d.ts.map +1 -1
- package/dist/routes/webauthn.js +8 -2
- package/dist/routes/webauthn.js.map +1 -1
- package/dist/services/configuration/profiles.d.ts +26 -0
- package/dist/services/configuration/profiles.d.ts.map +1 -1
- package/dist/services/configuration/profiles.js +80 -1
- package/dist/services/configuration/profiles.js.map +1 -1
- package/dist/services/events/subscribers/auditLog.d.ts.map +1 -1
- package/dist/services/events/subscribers/auditLog.js +38 -3
- package/dist/services/events/subscribers/auditLog.js.map +1 -1
- package/dist/services/events/types.d.ts +3 -1
- package/dist/services/events/types.d.ts.map +1 -1
- package/dist/services/events/validate.d.ts +1 -0
- package/dist/services/events/validate.d.ts.map +1 -1
- package/dist/services/events/validate.js +5 -1
- package/dist/services/events/validate.js.map +1 -1
- package/dist/services/localRateLimit.d.ts +44 -0
- package/dist/services/localRateLimit.d.ts.map +1 -0
- package/dist/services/localRateLimit.js +86 -0
- package/dist/services/localRateLimit.js.map +1 -0
- package/dist/services/refreshTokenState.d.ts +5 -0
- package/dist/services/refreshTokenState.d.ts.map +1 -0
- package/dist/services/refreshTokenState.js +30 -0
- package/dist/services/refreshTokenState.js.map +1 -0
- package/dist/services/scopes.d.ts +113 -0
- package/dist/services/scopes.d.ts.map +1 -0
- package/dist/services/scopes.js +138 -0
- package/dist/services/scopes.js.map +1 -0
- package/dist/services/setup/token.d.ts +12 -0
- package/dist/services/setup/token.d.ts.map +1 -1
- package/dist/services/setup/token.js +27 -3
- package/dist/services/setup/token.js.map +1 -1
- package/dist/services/tokens.d.ts +1 -0
- package/dist/services/tokens.d.ts.map +1 -1
- package/dist/services/tokens.js +1 -1
- package/dist/services/tokens.js.map +1 -1
- package/dist/services/trustedProxies.d.ts +24 -0
- package/dist/services/trustedProxies.d.ts.map +1 -1
- package/dist/services/trustedProxies.js +19 -0
- package/dist/services/trustedProxies.js.map +1 -1
- package/dist/services/webhooks.d.ts +16 -0
- package/dist/services/webhooks.d.ts.map +1 -1
- package/dist/services/webhooks.js +48 -3
- package/dist/services/webhooks.js.map +1 -1
- package/dist/setup-server.js +47 -3
- package/dist/setup-server.js.map +1 -1
- package/docs/API-REVIEW.md +121 -0
- package/docs/API.md +457 -0
- package/docs/ARCHITECTURE.md +142 -0
- package/docs/CONTRIBUTING.md +61 -0
- package/docs/DEPLOYMENT.md +257 -0
- package/docs/INTEGRATION.md +336 -0
- package/docs/LOGIN_FORM_INTEGRATION.md +306 -0
- package/docs/MIGRATION-1.7.md +70 -0
- package/docs/MIGRATION-1.8.md +183 -0
- package/docs/MIGRATION-1.9.md +200 -0
- package/docs/MIGRATION-2.0.md +203 -0
- package/docs/MIGRATION-2.4.md +185 -0
- package/docs/PERFORMANCE.md +155 -0
- package/docs/RBAC.md +100 -0
- package/docs/RE-AUDIT.md +72 -0
- package/docs/README.md +54 -0
- package/docs/RELEASE-1.7.md +53 -0
- package/docs/ROADMAP.md +41 -0
- package/docs/SECURITY.md +143 -0
- package/docs/adrs/001-identity-connectors-as-adapters.md +27 -0
- package/docs/adrs/002-versioned-event-bus.md +30 -0
- package/docs/adrs/003-bullmq-for-background-work.md +20 -0
- package/docs/adrs/004-argon2id-password-hashing.md +19 -0
- package/docs/plans/KEYSTONE_IMPROVEMENT_PLAN.md +334 -0
- package/docs/plans/UI_SIMPLIFICATION_IMPROVEMENT_PLAN.md +271 -0
- package/docs/security/audit.md +82 -0
- package/docs/security/configuration.md +60 -0
- package/docs/security/enterprise-sso.md +193 -0
- package/docs/security/mtls.md +132 -0
- package/docs/security/proxy-security.md +128 -0
- package/docs/security/rate-limiting.md +79 -0
- package/docs/security/registry-exceptions.md +34 -0
- package/docs/security/registry.json +649 -0
- package/docs/security/registry.md +657 -0
- package/docs/security/scopes.md +45 -0
- package/docs/security/supply-chain.md +49 -0
- package/docs/security/trust-boundaries.md +111 -0
- 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`.
|