@crvouga/mockingbird-service-oauth 0.1.0 → 0.2.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 +12 -0
- package/README.md +1 -1
- package/dist/{chunk-X6FYBOEA.js → chunk-PCHI2SDJ.js} +68 -10
- package/dist/{chunk-X6FYBOEA.js.map → chunk-PCHI2SDJ.js.map} +2 -2
- package/dist/{chunk-WYBO4XJW.js → chunk-QQXXWRJ3.js} +2 -2
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +2 -2
- package/package.json +1 -1
- /package/dist/{chunk-WYBO4XJW.js.map → chunk-QQXXWRJ3.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog — @crvouga/mockingbird-service-oauth
|
|
2
2
|
|
|
3
|
+
## 0.2.0 (2026-09-24)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- open every live example in a modal window scoped to the example ([c27de26](https://github.com/crvouga/mockingbird/commit/c27de26b677ccb257c3d352090ce905e39f4e470))
|
|
8
|
+
|
|
9
|
+
## 0.1.1 (2026-09-23)
|
|
10
|
+
|
|
11
|
+
### Fixes and improvements
|
|
12
|
+
|
|
13
|
+
- let browser clients finish sign-in and redeem codes cross-origin ([c9fe735](https://github.com/crvouga/mockingbird/commit/c9fe735b5c5417ba3c1f6a3bfe7086a61a60639e))
|
|
14
|
+
|
|
3
15
|
## 0.1.0 (2026-09-22)
|
|
4
16
|
|
|
5
17
|
Initial release.
|
package/README.md
CHANGED
|
@@ -211,7 +211,7 @@ This is a ready-to-use local/test identity provider, **not a production authenti
|
|
|
211
211
|
|
|
212
212
|
Vendor-hosted Google Identity Services/One Tap, native Apple AuthenticationServices, passkeys, MFA, CAPTCHA, password recovery, email delivery/relay forwarding, app-transfer migration, vendor risk engines, tokeninfo/introspection, logout, GitHub Apps installation/device flows, and Microsoft Graph/tenant administration are not implemented. Other configurable OIDC providers can use the generic profile, but their proprietary scopes and claims are not emulated. Scopes are limited to each profile plus explicitly configured additional scopes. Microsoft uses the configured mock issuer, not real Entra tenant routing. GitHub is the OAuth app login surface, not the full REST API.
|
|
213
213
|
|
|
214
|
-
No implicit flow, dynamic client registration, wildcard redirect matching,
|
|
214
|
+
No implicit flow, dynamic client registration, wildcard redirect matching, persistent signing-key import, or distributed-session coordination is provided. Private-use redirect schemes work only when their complete URI is explicitly registered; executable/local schemes such as `javascript:`, `data:` and `file:` are rejected. Sign-in and consent pages extend CSP `form-action` with the transaction's redirect origin (or custom scheme) so browsers follow the redirect back to the client. Discovery, JWKS, token, userinfo and revoke answer `OPTIONS` preflights and reflect the request `Origin`, so browser (SPA + PKCE) clients can redeem codes directly; the interaction pages send no CORS headers. Snapshot restore is for the same runtime/instance; signing keys are not serialized. The default backing store is in-memory and state disappears when the process exits. A secure browser context and Web Crypto, Fetch and standard Web APIs are required; Node 22+, Bun and modern browsers provide them.
|
|
215
215
|
|
|
216
216
|
References: [Google OpenID Connect](https://developers.google.com/identity/openid-connect/openid-connect), [Apple authorization request](https://developer.apple.com/documentation/signinwithapplerestapi/request-an-authorization-to-the-sign-in-with-apple-server.), and [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html).
|
|
217
217
|
|
|
@@ -1384,7 +1384,7 @@ var restoreNamespace = (sqlite, namespace, snapshot) => {
|
|
|
1384
1384
|
};
|
|
1385
1385
|
|
|
1386
1386
|
// ../core/dist/version.js
|
|
1387
|
-
var PACKAGE_VERSION = true ? "0.
|
|
1387
|
+
var PACKAGE_VERSION = true ? "0.2.0" : UNRELEASED_VERSION;
|
|
1388
1388
|
|
|
1389
1389
|
// ../core/dist/webhooks.js
|
|
1390
1390
|
var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
@@ -2328,7 +2328,7 @@ function page(title, body, status = 200, action = "'self'", nonce = crypto.rando
|
|
|
2328
2328
|
}
|
|
2329
2329
|
);
|
|
2330
2330
|
}
|
|
2331
|
-
function loginPage(transaction, client, accounts, base, signup = false, error = "") {
|
|
2331
|
+
function loginPage(transaction, client, accounts, base, action, signup = false, error = "") {
|
|
2332
2332
|
const hidden = `<input type="hidden" name="transaction" value="${escapeHtml(transaction)}">`;
|
|
2333
2333
|
const endpoint = escapeHtml(`${base}/interaction`);
|
|
2334
2334
|
const intro = `<span class="eyebrow">Sign in</span><h1 id="title">${signup ? "Create your account" : "Choose an account"}</h1><p>${signup ? "Create an account to continue to" : "Choose an account to continue to"}<br><span class="app">${escapeHtml(client)}</span></p>${error ? `<div class="error" role="alert">${escapeHtml(error)}</div>` : ""}`;
|
|
@@ -2336,10 +2336,11 @@ function loginPage(transaction, client, accounts, base, signup = false, error =
|
|
|
2336
2336
|
return page(
|
|
2337
2337
|
signup ? "Create account" : "Choose an account",
|
|
2338
2338
|
`${intro}${body}<div class="divider"></div><form method="post" action="${endpoint}">${hidden}<button class="secondary" name="action" value="deny">Cancel sign-in</button></form>`,
|
|
2339
|
-
error ? 400 : 200
|
|
2339
|
+
error ? 400 : 200,
|
|
2340
|
+
action
|
|
2340
2341
|
);
|
|
2341
2342
|
}
|
|
2342
|
-
function consentPage(transaction, client, account, scopes2, base, privacy) {
|
|
2343
|
+
function consentPage(transaction, client, account, scopes2, base, action, privacy) {
|
|
2343
2344
|
const labels = {
|
|
2344
2345
|
openid: "Confirm your identity",
|
|
2345
2346
|
email: "View your email address",
|
|
@@ -2354,7 +2355,9 @@ function consentPage(transaction, client, account, scopes2, base, privacy) {
|
|
|
2354
2355
|
(s) => `<li><span class="check" aria-hidden="true">\u2713</span>${escapeHtml(labels[s] ?? s)}</li>`
|
|
2355
2356
|
).join(
|
|
2356
2357
|
""
|
|
2357
|
-
)}</ul><p>You can cancel now without sharing anything.</p><form method="post" action="${escapeHtml(base)}/interaction"><input type="hidden" name="transaction" value="${escapeHtml(transaction)}">${privacyFields}<button class="primary" name="action" value="allow">Allow & continue</button><button class="secondary" name="action" value="deny">Cancel</button></form
|
|
2358
|
+
)}</ul><p>You can cancel now without sharing anything.</p><form method="post" action="${escapeHtml(base)}/interaction"><input type="hidden" name="transaction" value="${escapeHtml(transaction)}">${privacyFields}<button class="primary" name="action" value="allow">Allow & continue</button><button class="secondary" name="action" value="deny">Cancel</button></form>`,
|
|
2359
|
+
200,
|
|
2360
|
+
action
|
|
2358
2361
|
);
|
|
2359
2362
|
}
|
|
2360
2363
|
|
|
@@ -2571,6 +2574,11 @@ var fail = (error, description, status = 400) => json4({ error, error_descriptio
|
|
|
2571
2574
|
var scopes = (value) => new Set(value.split(/\s+/).filter(Boolean));
|
|
2572
2575
|
var validEmail = (s) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s) && s.length <= 254;
|
|
2573
2576
|
var WEB_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
2577
|
+
var formTarget = (redirectUri) => {
|
|
2578
|
+
const u = new URL(redirectUri);
|
|
2579
|
+
return WEB_SCHEMES.has(u.protocol) ? u.origin : u.protocol;
|
|
2580
|
+
};
|
|
2581
|
+
var formAction = (redirectUri) => `'self' ${formTarget(redirectUri)}`;
|
|
2574
2582
|
var FORBIDDEN_REDIRECT_SCHEMES = /* @__PURE__ */ new Set(["blob:", "data:", "file:", "javascript:", "vbscript:"]);
|
|
2575
2583
|
var safeRedirect = (s) => {
|
|
2576
2584
|
try {
|
|
@@ -2842,6 +2850,38 @@ var OAuthAPI = class {
|
|
|
2842
2850
|
const issuer = this.issuer(request);
|
|
2843
2851
|
const mount = new URL(issuer).pathname.replace(/\/$/, "");
|
|
2844
2852
|
const path = mount && url.pathname.startsWith(`${mount}/`) ? url.pathname.slice(mount.length) : url.pathname;
|
|
2853
|
+
const paths = this.paths();
|
|
2854
|
+
const cors = [
|
|
2855
|
+
"/.well-known/openid-configuration",
|
|
2856
|
+
paths.jwks,
|
|
2857
|
+
"/jwks",
|
|
2858
|
+
paths.token,
|
|
2859
|
+
"/token",
|
|
2860
|
+
paths.userinfo,
|
|
2861
|
+
"/userinfo",
|
|
2862
|
+
"/oauth2/v3/userinfo",
|
|
2863
|
+
"/user/emails",
|
|
2864
|
+
paths.revoke,
|
|
2865
|
+
"/revoke"
|
|
2866
|
+
].includes(path);
|
|
2867
|
+
if (!cors) return this.route(request, url, issuer, path);
|
|
2868
|
+
const origin = request.headers.get("origin");
|
|
2869
|
+
const response = request.method === "OPTIONS" ? new Response(null, {
|
|
2870
|
+
status: 204,
|
|
2871
|
+
headers: {
|
|
2872
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
2873
|
+
"access-control-allow-headers": request.headers.get("access-control-request-headers") ?? "authorization, content-type",
|
|
2874
|
+
"access-control-max-age": "600"
|
|
2875
|
+
}
|
|
2876
|
+
}) : await this.route(request, url, issuer, path);
|
|
2877
|
+
if (origin) {
|
|
2878
|
+
response.headers.set("access-control-allow-origin", origin);
|
|
2879
|
+
response.headers.set("access-control-expose-headers", "www-authenticate");
|
|
2880
|
+
response.headers.append("vary", "Origin");
|
|
2881
|
+
}
|
|
2882
|
+
return response;
|
|
2883
|
+
}
|
|
2884
|
+
async route(request, url, issuer, path) {
|
|
2845
2885
|
const paths = this.paths();
|
|
2846
2886
|
if (request.method === "GET" && path === "/.well-known/openid-configuration" && this.provider !== "github")
|
|
2847
2887
|
return json4({
|
|
@@ -3107,7 +3147,7 @@ var OAuthAPI = class {
|
|
|
3107
3147
|
accounts.sort(
|
|
3108
3148
|
(a, b) => Number(b.email === hint || b.id === hint) - Number(a.email === hint || a.id === hint)
|
|
3109
3149
|
);
|
|
3110
|
-
return loginPage(id, client.name, accounts, issuer);
|
|
3150
|
+
return loginPage(id, client.name, accounts, issuer, formAction(auth.redirectUri));
|
|
3111
3151
|
}
|
|
3112
3152
|
consent(id, client, account, auth, issuer) {
|
|
3113
3153
|
const existing = this.identities.get(this.identityKey(client, account.id));
|
|
@@ -3119,6 +3159,7 @@ var OAuthAPI = class {
|
|
|
3119
3159
|
account,
|
|
3120
3160
|
allowed,
|
|
3121
3161
|
issuer,
|
|
3162
|
+
formAction(auth.redirectUri),
|
|
3122
3163
|
this.provider === "apple" && scopes(auth.scope).has("email") && !existing ? {
|
|
3123
3164
|
hideEmail: account.privateEmail ?? (this.behavior.config.apple?.emailMode === "hide" || (this.behavior.config.apple?.emailMode ?? "choose") === "choose" && auth.decisions.hideEmail),
|
|
3124
3165
|
choice: (this.behavior.config.apple?.emailMode ?? "choose") === "choose"
|
|
@@ -3141,7 +3182,14 @@ var OAuthAPI = class {
|
|
|
3141
3182
|
);
|
|
3142
3183
|
const accounts = () => this.accounts.list({ order: "oldest", where: (a) => !a.disabled }).map((a) => a.value);
|
|
3143
3184
|
if (request.method === "GET")
|
|
3144
|
-
return loginPage(
|
|
3185
|
+
return loginPage(
|
|
3186
|
+
id,
|
|
3187
|
+
client.name,
|
|
3188
|
+
accounts(),
|
|
3189
|
+
issuer,
|
|
3190
|
+
formAction(auth.redirectUri),
|
|
3191
|
+
p.get("screen") === "signup"
|
|
3192
|
+
);
|
|
3145
3193
|
const action = p.get("action");
|
|
3146
3194
|
if (action === "deny") {
|
|
3147
3195
|
this.transactions.delete(id);
|
|
@@ -3158,6 +3206,7 @@ var OAuthAPI = class {
|
|
|
3158
3206
|
client.name,
|
|
3159
3207
|
[],
|
|
3160
3208
|
issuer,
|
|
3209
|
+
formAction(auth.redirectUri),
|
|
3161
3210
|
true,
|
|
3162
3211
|
"Enter a full name and a valid email address."
|
|
3163
3212
|
);
|
|
@@ -3167,6 +3216,7 @@ var OAuthAPI = class {
|
|
|
3167
3216
|
client.name,
|
|
3168
3217
|
[],
|
|
3169
3218
|
issuer,
|
|
3219
|
+
formAction(auth.redirectUri),
|
|
3170
3220
|
true,
|
|
3171
3221
|
"This email already has an account. Go back to choose it."
|
|
3172
3222
|
);
|
|
@@ -3179,7 +3229,15 @@ var OAuthAPI = class {
|
|
|
3179
3229
|
if (action === "select") {
|
|
3180
3230
|
const account = this.accounts.get(p.get("account") ?? "");
|
|
3181
3231
|
if (!account || account.disabled)
|
|
3182
|
-
return loginPage(
|
|
3232
|
+
return loginPage(
|
|
3233
|
+
id,
|
|
3234
|
+
client.name,
|
|
3235
|
+
accounts(),
|
|
3236
|
+
issuer,
|
|
3237
|
+
formAction(auth.redirectUri),
|
|
3238
|
+
false,
|
|
3239
|
+
"Choose an available account."
|
|
3240
|
+
);
|
|
3183
3241
|
auth.accountId = account.id;
|
|
3184
3242
|
auth.authTime = this.now();
|
|
3185
3243
|
this.transactions.update(id, auth);
|
|
@@ -3283,7 +3341,7 @@ var OAuthAPI = class {
|
|
|
3283
3341
|
""
|
|
3284
3342
|
)}<button class="primary">Continue</button></form><script nonce="${nonce}">document.getElementById('callback').submit()</script>`,
|
|
3285
3343
|
200,
|
|
3286
|
-
|
|
3344
|
+
formTarget(auth.redirectUri),
|
|
3287
3345
|
nonce
|
|
3288
3346
|
);
|
|
3289
3347
|
return result;
|
|
@@ -3507,4 +3565,4 @@ export {
|
|
|
3507
3565
|
createMultiRuntime,
|
|
3508
3566
|
OAuthAPI
|
|
3509
3567
|
};
|
|
3510
|
-
//# sourceMappingURL=chunk-
|
|
3568
|
+
//# sourceMappingURL=chunk-PCHI2SDJ.js.map
|