@emulators/okta 0.4.0 → 0.5.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/README.md +98 -0
- package/dist/fonts/favicon.ico +0 -0
- package/dist/index.js +215 -73
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# @emulators/okta
|
|
2
|
+
|
|
3
|
+
Okta identity provider emulation with OAuth 2.0 / OIDC, user management, groups, apps, and authorization servers.
|
|
4
|
+
|
|
5
|
+
Part of [emulate](https://github.com/vercel-labs/emulate) — local drop-in replacement services for CI and no-network sandboxes.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @emulators/okta
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Endpoints
|
|
14
|
+
|
|
15
|
+
### OAuth / OIDC
|
|
16
|
+
|
|
17
|
+
Default org server and custom authorization server paths (`/oauth2/:authServerId/...`):
|
|
18
|
+
|
|
19
|
+
- `GET /.well-known/openid-configuration` — OIDC discovery (default)
|
|
20
|
+
- `GET /oauth2/:authServerId/.well-known/openid-configuration` — per-server discovery
|
|
21
|
+
- `GET /oauth2/v1/keys` — JSON Web Key Set (JWKS)
|
|
22
|
+
- `GET /oauth2/v1/authorize` — authorization endpoint
|
|
23
|
+
- `POST /oauth2/v1/token` — token endpoint
|
|
24
|
+
- `GET /oauth2/v1/userinfo` — user info
|
|
25
|
+
- `POST /oauth2/v1/revoke` — token revocation
|
|
26
|
+
- `POST /oauth2/v1/introspect` — token introspection
|
|
27
|
+
- `GET /oauth2/v1/logout` — end session
|
|
28
|
+
|
|
29
|
+
### Users
|
|
30
|
+
- `GET /api/v1/users` — list users
|
|
31
|
+
- `POST /api/v1/users` — create user
|
|
32
|
+
- `GET /api/v1/users/me` — current user (from token)
|
|
33
|
+
- `GET /api/v1/users/:userId` — get user
|
|
34
|
+
- `PUT /api/v1/users/:userId` — replace user
|
|
35
|
+
- `POST /api/v1/users/:userId` — partial update
|
|
36
|
+
- `DELETE /api/v1/users/:userId` — delete user
|
|
37
|
+
- `GET /api/v1/users/:userId/groups` — list user groups
|
|
38
|
+
- `POST /api/v1/users/:userId/lifecycle/activate` — activate
|
|
39
|
+
- `POST /api/v1/users/:userId/lifecycle/deactivate` — deactivate
|
|
40
|
+
- `POST /api/v1/users/:userId/lifecycle/suspend` — suspend
|
|
41
|
+
- `POST /api/v1/users/:userId/lifecycle/unsuspend` — unsuspend
|
|
42
|
+
- `POST /api/v1/users/:userId/lifecycle/reactivate` — reactivate
|
|
43
|
+
|
|
44
|
+
### Groups
|
|
45
|
+
- `GET /api/v1/groups` — list groups
|
|
46
|
+
- `POST /api/v1/groups` — create group
|
|
47
|
+
- `GET /api/v1/groups/:groupId` — get group
|
|
48
|
+
- `PUT /api/v1/groups/:groupId` — update group
|
|
49
|
+
- `DELETE /api/v1/groups/:groupId` — delete group
|
|
50
|
+
- `GET /api/v1/groups/:groupId/users` — list group members
|
|
51
|
+
- `PUT /api/v1/groups/:groupId/users/:userId` — add user to group
|
|
52
|
+
- `DELETE /api/v1/groups/:groupId/users/:userId` — remove user from group
|
|
53
|
+
|
|
54
|
+
### Apps
|
|
55
|
+
- `GET /api/v1/apps` — list apps
|
|
56
|
+
- `POST /api/v1/apps` — create app
|
|
57
|
+
- `GET /api/v1/apps/:appId` — get app
|
|
58
|
+
- `PUT /api/v1/apps/:appId` — update app
|
|
59
|
+
- `DELETE /api/v1/apps/:appId` — delete app
|
|
60
|
+
- `GET /api/v1/apps/:appId/users` — list assigned users
|
|
61
|
+
- `PUT /api/v1/apps/:appId/users/:userId` — assign user
|
|
62
|
+
- `DELETE /api/v1/apps/:appId/users/:userId` — unassign user
|
|
63
|
+
- `POST /api/v1/apps/:appId/lifecycle/activate` — activate app
|
|
64
|
+
- `POST /api/v1/apps/:appId/lifecycle/deactivate` — deactivate app
|
|
65
|
+
|
|
66
|
+
### Authorization Servers
|
|
67
|
+
- `GET /api/v1/authorizationServers` — list
|
|
68
|
+
- `POST /api/v1/authorizationServers` — create
|
|
69
|
+
- `GET /api/v1/authorizationServers/:authServerId` — get
|
|
70
|
+
- `PUT /api/v1/authorizationServers/:authServerId` — update
|
|
71
|
+
- `DELETE /api/v1/authorizationServers/:authServerId` — delete
|
|
72
|
+
- `POST /api/v1/authorizationServers/:authServerId/lifecycle/activate` — activate
|
|
73
|
+
- `POST /api/v1/authorizationServers/:authServerId/lifecycle/deactivate` — deactivate
|
|
74
|
+
|
|
75
|
+
## Seed Configuration
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
okta:
|
|
79
|
+
users:
|
|
80
|
+
- login: testuser@example.com
|
|
81
|
+
email: testuser@example.com
|
|
82
|
+
firstName: Test
|
|
83
|
+
lastName: User
|
|
84
|
+
groups:
|
|
85
|
+
- name: Everyone
|
|
86
|
+
description: All users
|
|
87
|
+
apps:
|
|
88
|
+
- name: My App
|
|
89
|
+
label: My App
|
|
90
|
+
authorization_servers:
|
|
91
|
+
- name: default
|
|
92
|
+
audiences: ["api://default"]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Links
|
|
96
|
+
|
|
97
|
+
- [Full documentation](https://emulate.dev)
|
|
98
|
+
- [GitHub](https://github.com/vercel-labs/emulate)
|
|
Binary file
|
package/dist/index.js
CHANGED
|
@@ -126,6 +126,7 @@ var FONTS = {
|
|
|
126
126
|
"geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
|
|
127
127
|
"GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
|
|
128
128
|
};
|
|
129
|
+
var FAVICON = readFileSync(join(__dirname, "fonts", "favicon.ico"));
|
|
129
130
|
function parsePagination(c) {
|
|
130
131
|
const page = Math.max(1, parseInt(c.req.query("page") ?? "1", 10) || 1);
|
|
131
132
|
const per_page = Math.min(100, Math.max(1, parseInt(c.req.query("per_page") ?? "30", 10) || 30));
|
|
@@ -303,6 +304,132 @@ body{
|
|
|
303
304
|
.app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
|
|
304
305
|
.app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
|
|
305
306
|
.empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
|
|
307
|
+
|
|
308
|
+
.inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
|
|
309
|
+
.inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
|
|
310
|
+
.inspector-tabs a{
|
|
311
|
+
padding:7px 16px;border-radius:6px;text-decoration:none;
|
|
312
|
+
font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
|
|
313
|
+
transition:color .15s,border-color .15s;
|
|
314
|
+
}
|
|
315
|
+
.inspector-tabs a:hover{color:#33ff00;}
|
|
316
|
+
.inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
|
|
317
|
+
.inspector-section{margin-bottom:24px;}
|
|
318
|
+
.inspector-section h2{
|
|
319
|
+
font-family:'Geist Pixel',monospace;
|
|
320
|
+
font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
|
|
321
|
+
}
|
|
322
|
+
.inspector-section h3{
|
|
323
|
+
font-family:'Geist Pixel',monospace;
|
|
324
|
+
font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
|
|
325
|
+
}
|
|
326
|
+
.inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
|
|
327
|
+
.inspector-table th,.inspector-table td{
|
|
328
|
+
text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
|
|
329
|
+
font-size:.8125rem;
|
|
330
|
+
}
|
|
331
|
+
.inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
|
|
332
|
+
.inspector-table td{color:#33ff00;}
|
|
333
|
+
.inspector-table tbody tr{transition:background .1s;}
|
|
334
|
+
.inspector-table tbody tr:hover{background:#0a3300;}
|
|
335
|
+
.inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
|
|
336
|
+
|
|
337
|
+
.checkout-layout{
|
|
338
|
+
display:flex;min-height:calc(100vh - 42px);
|
|
339
|
+
}
|
|
340
|
+
.checkout-summary{
|
|
341
|
+
flex:1;background:#020;padding:48px 40px 48px 10%;
|
|
342
|
+
display:flex;flex-direction:column;justify-content:center;
|
|
343
|
+
border-right:1px solid #0a3300;
|
|
344
|
+
}
|
|
345
|
+
.checkout-form-side{
|
|
346
|
+
flex:1;background:#000;padding:48px 10% 48px 40px;
|
|
347
|
+
display:flex;flex-direction:column;justify-content:center;
|
|
348
|
+
}
|
|
349
|
+
.checkout-merchant{
|
|
350
|
+
display:flex;align-items:center;gap:10px;margin-bottom:6px;
|
|
351
|
+
}
|
|
352
|
+
.checkout-merchant-name{
|
|
353
|
+
font-family:'Geist Pixel',monospace;
|
|
354
|
+
font-size:.9375rem;font-weight:600;color:#33ff00;
|
|
355
|
+
}
|
|
356
|
+
.checkout-test-badge{
|
|
357
|
+
font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
|
|
358
|
+
background:#0a3300;color:#1a8c00;padding:2px 8px;border-radius:4px;
|
|
359
|
+
}
|
|
360
|
+
.checkout-total{
|
|
361
|
+
font-family:'Geist Pixel',monospace;
|
|
362
|
+
font-size:2rem;font-weight:700;color:#33ff00;margin:8px 0 28px;
|
|
363
|
+
}
|
|
364
|
+
.checkout-line-item{
|
|
365
|
+
display:flex;align-items:center;gap:14px;padding:14px 0;
|
|
366
|
+
border-bottom:1px solid #0a3300;
|
|
367
|
+
}
|
|
368
|
+
.checkout-line-item:first-child{border-top:1px solid #0a3300;}
|
|
369
|
+
.checkout-item-icon{
|
|
370
|
+
width:42px;height:42px;border-radius:6px;background:#0a3300;
|
|
371
|
+
display:flex;align-items:center;justify-content:center;flex-shrink:0;
|
|
372
|
+
font-family:'Geist Pixel',monospace;font-size:.875rem;font-weight:700;color:#116600;
|
|
373
|
+
}
|
|
374
|
+
.checkout-item-details{flex:1;min-width:0;}
|
|
375
|
+
.checkout-item-name{font-size:.875rem;font-weight:600;color:#33ff00;}
|
|
376
|
+
.checkout-item-qty{font-size:.75rem;color:#1a8c00;margin-top:2px;}
|
|
377
|
+
.checkout-item-price{
|
|
378
|
+
font-size:.875rem;font-weight:600;color:#33ff00;text-align:right;white-space:nowrap;
|
|
379
|
+
}
|
|
380
|
+
.checkout-item-unit{font-size:.6875rem;color:#1a8c00;text-align:right;margin-top:2px;}
|
|
381
|
+
.checkout-totals{margin-top:20px;}
|
|
382
|
+
.checkout-totals-row{
|
|
383
|
+
display:flex;justify-content:space-between;padding:6px 0;
|
|
384
|
+
font-size:.8125rem;color:#1a8c00;
|
|
385
|
+
}
|
|
386
|
+
.checkout-totals-row.total{
|
|
387
|
+
border-top:1px solid #0a3300;margin-top:8px;padding-top:14px;
|
|
388
|
+
font-size:.9375rem;font-weight:600;color:#33ff00;
|
|
389
|
+
}
|
|
390
|
+
.checkout-form-section{margin-bottom:24px;}
|
|
391
|
+
.checkout-form-label{
|
|
392
|
+
font-size:.8125rem;font-weight:600;color:#33ff00;margin-bottom:8px;display:block;
|
|
393
|
+
}
|
|
394
|
+
.checkout-input{
|
|
395
|
+
width:100%;padding:10px 12px;border:1px solid #0a3300;border-radius:6px;
|
|
396
|
+
background:#020;color:#33ff00;font:inherit;font-size:.875rem;
|
|
397
|
+
transition:border-color .15s;outline:none;
|
|
398
|
+
}
|
|
399
|
+
.checkout-input:focus{border-color:#33ff00;}
|
|
400
|
+
.checkout-input::placeholder{color:#116600;}
|
|
401
|
+
.checkout-card-box{
|
|
402
|
+
border:1px solid #0a3300;border-radius:6px;padding:14px;
|
|
403
|
+
background:#020;
|
|
404
|
+
}
|
|
405
|
+
.checkout-card-row{
|
|
406
|
+
display:flex;gap:12px;margin-top:10px;
|
|
407
|
+
}
|
|
408
|
+
.checkout-card-row .checkout-input{flex:1;}
|
|
409
|
+
.checkout-sim-note{
|
|
410
|
+
font-size:.6875rem;color:#1a8c00;margin-top:10px;text-align:center;
|
|
411
|
+
font-style:italic;
|
|
412
|
+
}
|
|
413
|
+
.checkout-pay-btn{
|
|
414
|
+
width:100%;padding:14px;border:none;border-radius:8px;
|
|
415
|
+
background:#33ff00;color:#000;font:inherit;font-size:.9375rem;font-weight:700;
|
|
416
|
+
cursor:pointer;transition:background .15s;
|
|
417
|
+
font-family:'Geist Pixel',monospace;
|
|
418
|
+
}
|
|
419
|
+
.checkout-pay-btn:hover{background:#44ff22;}
|
|
420
|
+
.checkout-cancel{
|
|
421
|
+
text-align:center;margin-top:14px;
|
|
422
|
+
}
|
|
423
|
+
.checkout-cancel a{
|
|
424
|
+
color:#1a8c00;text-decoration:none;font-size:.8125rem;
|
|
425
|
+
transition:color .15s;
|
|
426
|
+
}
|
|
427
|
+
.checkout-cancel a:hover{color:#33ff00;}
|
|
428
|
+
@media(max-width:768px){
|
|
429
|
+
.checkout-layout{flex-direction:column;}
|
|
430
|
+
.checkout-summary{padding:32px 20px;border-right:none;border-bottom:1px solid #0a3300;}
|
|
431
|
+
.checkout-form-side{padding:32px 20px;}
|
|
432
|
+
}
|
|
306
433
|
`;
|
|
307
434
|
var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
|
|
308
435
|
function emuBar(service) {
|
|
@@ -322,6 +449,7 @@ function head(title) {
|
|
|
322
449
|
<head>
|
|
323
450
|
<meta charset="utf-8"/>
|
|
324
451
|
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
452
|
+
<link rel="icon" href="/_emulate/favicon.ico"/>
|
|
325
453
|
<title>${escapeHtml(title)} | emulate</title>
|
|
326
454
|
<style>${CSS}</style>
|
|
327
455
|
</head>`;
|
|
@@ -353,6 +481,25 @@ ${emuBar(service)}
|
|
|
353
481
|
${POWERED_BY}
|
|
354
482
|
</body></html>`;
|
|
355
483
|
}
|
|
484
|
+
function renderFormPostPage(action, fields, service) {
|
|
485
|
+
const hiddens = Object.entries(fields).filter(([, v]) => v != null).map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}"/>`).join("\n");
|
|
486
|
+
return `${head("Redirecting")}
|
|
487
|
+
<body onload="document.forms[0].submit()">
|
|
488
|
+
${emuBar(service)}
|
|
489
|
+
<div class="content">
|
|
490
|
+
<div class="content-inner" style="text-align:center">
|
|
491
|
+
<div class="card-subtitle">Redirecting…</div>
|
|
492
|
+
<form method="POST" action="${escapeAttr(action)}">
|
|
493
|
+
${hiddens}
|
|
494
|
+
<noscript><button type="submit" class="user-btn" style="margin-top:12px;justify-content:center">
|
|
495
|
+
<span class="user-login">Continue</span>
|
|
496
|
+
</button></noscript>
|
|
497
|
+
</form>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
${POWERED_BY}
|
|
501
|
+
</body></html>`;
|
|
502
|
+
}
|
|
356
503
|
function renderUserButton(opts) {
|
|
357
504
|
const hiddens = Object.entries(opts.hiddenFields).map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}"/>`).join("");
|
|
358
505
|
const nameLine = opts.name ? `<div class="user-meta">${escapeHtml(opts.name)}</div>` : "";
|
|
@@ -539,7 +686,10 @@ function getOktaStore(store) {
|
|
|
539
686
|
apps: store.collection("okta.apps", ["okta_id", "name"]),
|
|
540
687
|
oauthClients: store.collection("okta.oauth_clients", ["client_id", "auth_server_id"]),
|
|
541
688
|
authorizationServers: store.collection("okta.auth_servers", ["server_id"]),
|
|
542
|
-
groupMemberships: store.collection("okta.group_memberships", [
|
|
689
|
+
groupMemberships: store.collection("okta.group_memberships", [
|
|
690
|
+
"group_okta_id",
|
|
691
|
+
"user_okta_id"
|
|
692
|
+
]),
|
|
543
693
|
appAssignments: store.collection("okta.app_assignments", ["app_okta_id", "user_okta_id"])
|
|
544
694
|
};
|
|
545
695
|
}
|
|
@@ -553,9 +703,7 @@ function appRoutes({ app, store, baseUrl, tokenMap }) {
|
|
|
553
703
|
const q = (c.req.query("q") ?? "").toLowerCase();
|
|
554
704
|
let apps = oktaStore.apps.all();
|
|
555
705
|
if (q) {
|
|
556
|
-
apps = apps.filter(
|
|
557
|
-
(entry) => `${entry.name} ${entry.label}`.toLowerCase().includes(q)
|
|
558
|
-
);
|
|
706
|
+
apps = apps.filter((entry) => `${entry.name} ${entry.label}`.toLowerCase().includes(q));
|
|
559
707
|
}
|
|
560
708
|
const { page, per_page } = parsePagination(c);
|
|
561
709
|
const total = apps.length;
|
|
@@ -783,9 +931,7 @@ function groupRoutes({ app, store, baseUrl, tokenMap }) {
|
|
|
783
931
|
const q = (c.req.query("q") ?? "").toLowerCase();
|
|
784
932
|
let groups = oktaStore.groups.all();
|
|
785
933
|
if (q) {
|
|
786
|
-
groups = groups.filter(
|
|
787
|
-
(group) => `${group.name} ${group.description ?? ""}`.toLowerCase().includes(q)
|
|
788
|
-
);
|
|
934
|
+
groups = groups.filter((group) => `${group.name} ${group.description ?? ""}`.toLowerCase().includes(q));
|
|
789
935
|
}
|
|
790
936
|
const { page, per_page } = parsePagination(c);
|
|
791
937
|
const total = groups.length;
|
|
@@ -1091,10 +1237,10 @@ async function createIdToken(oktaStore, user, clientId, nonce, issuer, scope) {
|
|
|
1091
1237
|
return new SignJWT(claims).setProtectedHeader({ alg: "RS256", kid: KID, typ: "JWT" }).setIssuer(issuer).setAudience(clientId).setIssuedAt(now).setExpirationTime("1h").sign(privateKey);
|
|
1092
1238
|
}
|
|
1093
1239
|
function unauthorizedOAuthError() {
|
|
1094
|
-
return new Response(
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
);
|
|
1240
|
+
return new Response(JSON.stringify({ error: "invalid_token", error_description: "The access token is invalid." }), {
|
|
1241
|
+
status: 401,
|
|
1242
|
+
headers: { "Content-Type": "application/json" }
|
|
1243
|
+
});
|
|
1098
1244
|
}
|
|
1099
1245
|
function oauthRoutes({ app, store, baseUrl, tokenMap }) {
|
|
1100
1246
|
const oktaStore = getOktaStore(store);
|
|
@@ -1163,7 +1309,11 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
|
|
|
1163
1309
|
}
|
|
1164
1310
|
if (!matchesRedirectUri(redirectUri, client.redirect_uris)) {
|
|
1165
1311
|
return c.html(
|
|
1166
|
-
renderErrorPage(
|
|
1312
|
+
renderErrorPage(
|
|
1313
|
+
"Redirect URI mismatch",
|
|
1314
|
+
"The redirect_uri is not registered for this application.",
|
|
1315
|
+
SERVICE_LABEL
|
|
1316
|
+
),
|
|
1167
1317
|
400
|
|
1168
1318
|
);
|
|
1169
1319
|
}
|
|
@@ -1171,25 +1321,27 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
|
|
|
1171
1321
|
}
|
|
1172
1322
|
const users = oktaStore.users.all();
|
|
1173
1323
|
const callbackPath = `${buildOAuthBasePath(authServerId)}/authorize/callback`;
|
|
1174
|
-
const buttons = users.map(
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1324
|
+
const buttons = users.map(
|
|
1325
|
+
(user) => renderUserButton({
|
|
1326
|
+
letter: (user.login[0] ?? "?").toUpperCase(),
|
|
1327
|
+
login: user.login,
|
|
1328
|
+
name: userDisplayName(user),
|
|
1329
|
+
email: user.email,
|
|
1330
|
+
formAction: callbackPath,
|
|
1331
|
+
hiddenFields: {
|
|
1332
|
+
user_ref: user.okta_id,
|
|
1333
|
+
redirect_uri: redirectUri,
|
|
1334
|
+
scope,
|
|
1335
|
+
state,
|
|
1336
|
+
nonce,
|
|
1337
|
+
client_id: clientId,
|
|
1338
|
+
response_mode: responseMode,
|
|
1339
|
+
code_challenge: codeChallenge,
|
|
1340
|
+
code_challenge_method: codeChallengeMethod,
|
|
1341
|
+
auth_server_id: authServerId
|
|
1342
|
+
}
|
|
1343
|
+
})
|
|
1344
|
+
).join("\n");
|
|
1193
1345
|
const subtitle = clientName ? `Sign in to <strong>${escapeHtml(clientName)}</strong> with your Okta account.` : "Choose a seeded user to continue.";
|
|
1194
1346
|
return c.html(
|
|
1195
1347
|
renderCardPage(
|
|
@@ -1223,10 +1375,7 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
|
|
|
1223
1375
|
}
|
|
1224
1376
|
const user = findUserByRef(oktaStore, userRef);
|
|
1225
1377
|
if (!user) {
|
|
1226
|
-
return c.html(
|
|
1227
|
-
renderErrorPage("Unknown user", "The selected user is not available.", SERVICE_LABEL),
|
|
1228
|
-
400
|
|
1229
|
-
);
|
|
1378
|
+
return c.html(renderErrorPage("Unknown user", "The selected user is not available.", SERVICE_LABEL), 400);
|
|
1230
1379
|
}
|
|
1231
1380
|
const configuredClients = getClientsForServer(oktaStore.oauthClients.all(), authServerId);
|
|
1232
1381
|
if (configuredClients.length > 0) {
|
|
@@ -1239,7 +1388,11 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
|
|
|
1239
1388
|
}
|
|
1240
1389
|
if (!matchesRedirectUri(redirectUri, client.redirect_uris)) {
|
|
1241
1390
|
return c.html(
|
|
1242
|
-
renderErrorPage(
|
|
1391
|
+
renderErrorPage(
|
|
1392
|
+
"Redirect URI mismatch",
|
|
1393
|
+
"The redirect_uri is not registered for this application.",
|
|
1394
|
+
SERVICE_LABEL
|
|
1395
|
+
),
|
|
1243
1396
|
400
|
|
1244
1397
|
);
|
|
1245
1398
|
}
|
|
@@ -1258,17 +1411,7 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
|
|
|
1258
1411
|
});
|
|
1259
1412
|
debug("okta.oauth", `[callback] code=${code.slice(0, 8)}... user=${user.login} server=${authServerId}`);
|
|
1260
1413
|
if (responseMode === "form_post") {
|
|
1261
|
-
|
|
1262
|
-
<html>
|
|
1263
|
-
<head><title>Submit</title></head>
|
|
1264
|
-
<body onload="document.forms[0].submit()">
|
|
1265
|
-
<form method="POST" action="${escapeAttr(redirectUri)}">
|
|
1266
|
-
<input type="hidden" name="code" value="${escapeAttr(code)}" />
|
|
1267
|
-
<input type="hidden" name="state" value="${escapeAttr(state)}" />
|
|
1268
|
-
</form>
|
|
1269
|
-
</body>
|
|
1270
|
-
</html>`;
|
|
1271
|
-
return c.html(html);
|
|
1414
|
+
return c.html(renderFormPostPage(redirectUri, { code, state }, SERVICE_LABEL));
|
|
1272
1415
|
}
|
|
1273
1416
|
const url = new URL(redirectUri);
|
|
1274
1417
|
url.searchParams.set("code", code);
|
|
@@ -1276,7 +1419,10 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
|
|
|
1276
1419
|
return c.redirect(url.toString(), 302);
|
|
1277
1420
|
};
|
|
1278
1421
|
app.post("/oauth2/v1/authorize/callback", (c) => handleAuthorizeCallback(c, ORG_AUTH_SERVER_ID));
|
|
1279
|
-
app.post(
|
|
1422
|
+
app.post(
|
|
1423
|
+
"/oauth2/:authServerId/v1/authorize/callback",
|
|
1424
|
+
(c) => handleAuthorizeCallback(c, c.req.param("authServerId"))
|
|
1425
|
+
);
|
|
1280
1426
|
const handleToken = async (c, authServerId) => {
|
|
1281
1427
|
const server = resolveServer(authServerId, baseUrl, oktaStore);
|
|
1282
1428
|
if (!server) return oktaError(c, 404, "E0000007", `Not found: authorization server '${authServerId}'`);
|
|
@@ -1306,7 +1452,10 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
|
|
|
1306
1452
|
return c.json({ error: "invalid_grant", error_description: "redirect_uri does not match." }, 400);
|
|
1307
1453
|
}
|
|
1308
1454
|
if (validatedClient && validatedClient.client_id !== pending.clientId) {
|
|
1309
|
-
return c.json(
|
|
1455
|
+
return c.json(
|
|
1456
|
+
{ error: "invalid_grant", error_description: "Authorization code was not issued to this client." },
|
|
1457
|
+
400
|
|
1458
|
+
);
|
|
1310
1459
|
}
|
|
1311
1460
|
if (pending.codeChallenge !== null) {
|
|
1312
1461
|
if (!codeVerifier) {
|
|
@@ -1356,14 +1505,7 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
|
|
|
1356
1505
|
id: user.id,
|
|
1357
1506
|
scopes: parseScope(scope)
|
|
1358
1507
|
});
|
|
1359
|
-
const idToken = await createIdToken(
|
|
1360
|
-
oktaStore,
|
|
1361
|
-
user,
|
|
1362
|
-
audienceClient,
|
|
1363
|
-
pending.nonce,
|
|
1364
|
-
server.issuer,
|
|
1365
|
-
scope
|
|
1366
|
-
);
|
|
1508
|
+
const idToken = await createIdToken(oktaStore, user, audienceClient, pending.nonce, server.issuer, scope);
|
|
1367
1509
|
return c.json({
|
|
1368
1510
|
token_type: "Bearer",
|
|
1369
1511
|
expires_in: 3600,
|
|
@@ -1382,7 +1524,10 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
|
|
|
1382
1524
|
return c.json({ error: "invalid_grant", error_description: "Authorization server mismatch." }, 400);
|
|
1383
1525
|
}
|
|
1384
1526
|
if (validatedClient && validatedClient.client_id !== existing.clientId) {
|
|
1385
|
-
return c.json(
|
|
1527
|
+
return c.json(
|
|
1528
|
+
{ error: "invalid_grant", error_description: "Refresh token was not issued to this client." },
|
|
1529
|
+
400
|
|
1530
|
+
);
|
|
1386
1531
|
}
|
|
1387
1532
|
const user = oktaStore.users.findOneBy("okta_id", existing.userOktaId);
|
|
1388
1533
|
if (!user) return c.json({ error: "invalid_grant", error_description: "Unknown user." }, 400);
|
|
@@ -1552,9 +1697,7 @@ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
|
|
|
1552
1697
|
if (!postLogoutRedirectUri) return c.text("Logged out");
|
|
1553
1698
|
const scopedClients = getClientsForServer(oktaStore.oauthClients.all(), authServerId);
|
|
1554
1699
|
if (scopedClients.length > 0) {
|
|
1555
|
-
const isAllowed = scopedClients.some(
|
|
1556
|
-
(client) => matchesRedirectUri(postLogoutRedirectUri, client.redirect_uris)
|
|
1557
|
-
);
|
|
1700
|
+
const isAllowed = scopedClients.some((client) => matchesRedirectUri(postLogoutRedirectUri, client.redirect_uris));
|
|
1558
1701
|
if (!isAllowed) return c.text("Invalid post_logout_redirect_uri", 400);
|
|
1559
1702
|
}
|
|
1560
1703
|
return c.redirect(postLogoutRedirectUri, 302);
|
|
@@ -1678,14 +1821,16 @@ function userRoutes({ app, store, baseUrl, tokenMap }) {
|
|
|
1678
1821
|
if (!user) return oktaError(c, 404, "E0000007", "Not found: user");
|
|
1679
1822
|
const memberships = oktaStore.groupMemberships.findBy("user_okta_id", user.okta_id);
|
|
1680
1823
|
const groups = memberships.map((membership) => oktaStore.groups.findOneBy("okta_id", membership.group_okta_id)).filter((group) => Boolean(group));
|
|
1681
|
-
return c.json(
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1824
|
+
return c.json(
|
|
1825
|
+
groups.map((group) => ({
|
|
1826
|
+
id: group.okta_id,
|
|
1827
|
+
profile: {
|
|
1828
|
+
name: group.name,
|
|
1829
|
+
description: group.description
|
|
1830
|
+
},
|
|
1831
|
+
type: group.type
|
|
1832
|
+
}))
|
|
1833
|
+
);
|
|
1689
1834
|
});
|
|
1690
1835
|
app.post("/api/v1/users/:userId/lifecycle/activate", (c) => {
|
|
1691
1836
|
const auth = requireManagementAuth(c, tokenMap);
|
|
@@ -1833,10 +1978,7 @@ function seedDefaults(store, _baseUrl) {
|
|
|
1833
1978
|
client_id: "okta-test-app",
|
|
1834
1979
|
client_secret: "",
|
|
1835
1980
|
name: "Sample Public PKCE Client",
|
|
1836
|
-
redirect_uris: [
|
|
1837
|
-
"http://localhost:3000/official-sdk/callback",
|
|
1838
|
-
"http://localhost:3000/official-sdk"
|
|
1839
|
-
],
|
|
1981
|
+
redirect_uris: ["http://localhost:3000/official-sdk/callback", "http://localhost:3000/official-sdk"],
|
|
1840
1982
|
response_types: ["code"],
|
|
1841
1983
|
grant_types: ["authorization_code", "refresh_token"],
|
|
1842
1984
|
token_endpoint_auth_method: "none",
|