cauthy-auth 0.9.0 → 0.11.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/dist/client.d.mts +61 -5
- package/dist/client.d.ts +61 -5
- package/dist/client.js +118 -12
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +117 -12
- package/dist/client.mjs.map +1 -1
- package/dist/index.js +121 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +121 -6
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +68 -5
- package/dist/react.d.ts +68 -5
- package/dist/react.js +203 -18
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +202 -18
- package/dist/react.mjs.map +1 -1
- package/dist/svelte.d.mts +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/svelte.js +116 -12
- package/dist/svelte.js.map +1 -1
- package/dist/svelte.mjs +116 -12
- package/dist/svelte.mjs.map +1 -1
- package/dist/vue.d.mts +1 -1
- package/dist/vue.d.ts +1 -1
- package/dist/vue.js +116 -12
- package/dist/vue.js.map +1 -1
- package/dist/vue.mjs +116 -12
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -921,7 +921,11 @@ function createCauthy(given = {}) {
|
|
|
921
921
|
if (prompt === "login" || prompt === "none") q.set("prompt", prompt);
|
|
922
922
|
const locale = url.searchParams.get("locale") ?? url.searchParams.get("ui_locales");
|
|
923
923
|
if (locale && /^[a-zA-Z-]{2,10}$/.test(locale)) q.set("ui_locales", locale);
|
|
924
|
-
|
|
924
|
+
let passkey = "";
|
|
925
|
+
if (url.searchParams.get("method") === "passkey") {
|
|
926
|
+
q.set("method", "passkey");
|
|
927
|
+
passkey = "#passkey";
|
|
928
|
+
}
|
|
925
929
|
return redirect(`${issuer}/v1/oauth/authorize?${q}${passkey}`, [
|
|
926
930
|
writeCookie(flowCookie, await seal(flow, await key), attrs(request, FLOW_SECONDS))
|
|
927
931
|
]);
|
|
@@ -946,6 +950,10 @@ function createCauthy(given = {}) {
|
|
|
946
950
|
const error = url.searchParams.get("error");
|
|
947
951
|
if (error !== null) {
|
|
948
952
|
if (error === "login_required") return redirect(flow.returnTo, [writeCookie(flowCookie, "", attrs(request, 0))]);
|
|
953
|
+
if (error === "access_denied") {
|
|
954
|
+
const back = pages.signIn ?? flow.returnTo;
|
|
955
|
+
return redirect(`${back}${back.includes("?") ? "&" : "?"}auth_error=cancelled`, [writeCookie(flowCookie, "", attrs(request, 0))]);
|
|
956
|
+
}
|
|
949
957
|
return failed(request, error.slice(0, 60));
|
|
950
958
|
}
|
|
951
959
|
const code = url.searchParams.get("code");
|
|
@@ -976,6 +984,14 @@ function createCauthy(given = {}) {
|
|
|
976
984
|
return redirect(`${issuer}/v1/oauth/logout?${q}`, clearParts(cookieName, attrs(request, 0), had));
|
|
977
985
|
}
|
|
978
986
|
const STEP_SECONDS = 600;
|
|
987
|
+
const PASSKEY_SECONDS = 300;
|
|
988
|
+
const isCredential = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && typeof v.id === "string" && typeof v.response === "object";
|
|
989
|
+
const passkeyOf = (d) => ({
|
|
990
|
+
id: String(d?.id ?? ""),
|
|
991
|
+
name: String(d?.nickname ?? ""),
|
|
992
|
+
createdAt: String(d?.created_at ?? ""),
|
|
993
|
+
lastUsedAt: d?.last_used_at ? String(d.last_used_at) : null
|
|
994
|
+
});
|
|
979
995
|
const problem = (status2, code, message, field, headers) => json({ error: { code, message, ...field ? { field } : {} } }, status2, headers);
|
|
980
996
|
const invalid = (p) => problem(400, p.code, p.message, p.field);
|
|
981
997
|
function ownRequest(request) {
|
|
@@ -1013,10 +1029,15 @@ function createCauthy(given = {}) {
|
|
|
1013
1029
|
accept: "application/json",
|
|
1014
1030
|
// A link Cauthy mails opens the application's page for it when it
|
|
1015
1031
|
// has one; otherwise Cauthy's own. Either way in the reader's language.
|
|
1016
|
-
...page ? { "x-cauthy-link-to": `${originOf(request)}${page}
|
|
1032
|
+
...page ? { "x-cauthy-link-to": `${originOf(request)}${page}` } : { "x-cauthy-links": "hosted" },
|
|
1033
|
+
// Whose request it is: what Cauthy mails goes in this application's
|
|
1034
|
+
// name, under its logo.
|
|
1035
|
+
...clientId ? { "x-cauthy-client-id": clientId } : {},
|
|
1017
1036
|
...await passedOn(request),
|
|
1018
1037
|
...bearer2 ? { authorization: `Bearer ${bearer2}` } : {},
|
|
1019
1038
|
...request.headers.get("accept-language") ? { "accept-language": request.headers.get("accept-language") } : {},
|
|
1039
|
+
// The language the application says its person reads, over the browser's.
|
|
1040
|
+
...request.headers.get("x-cauthy-locale") ? { "x-cauthy-locale": request.headers.get("x-cauthy-locale") } : {},
|
|
1020
1041
|
...request.headers.get("user-agent") ? { "user-agent": request.headers.get("user-agent") } : {},
|
|
1021
1042
|
...payload === void 0 ? {} : { "content-type": "application/json" }
|
|
1022
1043
|
},
|
|
@@ -1055,7 +1076,37 @@ function createCauthy(given = {}) {
|
|
|
1055
1076
|
let lookCache = null;
|
|
1056
1077
|
async function ownRoute(request, url, route) {
|
|
1057
1078
|
const post = request.method === "POST";
|
|
1058
|
-
const own = [
|
|
1079
|
+
const own = [
|
|
1080
|
+
"/sign-in/email",
|
|
1081
|
+
"/sign-in/mfa",
|
|
1082
|
+
"/sign-up/email",
|
|
1083
|
+
"/forgot-password",
|
|
1084
|
+
"/reset-password",
|
|
1085
|
+
"/verify-email",
|
|
1086
|
+
"/sign-in/magic-link",
|
|
1087
|
+
"/sign-in/magic-link/verify",
|
|
1088
|
+
"/sign-in/email-code",
|
|
1089
|
+
"/me",
|
|
1090
|
+
"/me/password",
|
|
1091
|
+
"/me/mfa/setup",
|
|
1092
|
+
"/me/mfa/verify",
|
|
1093
|
+
"/me/mfa/disable",
|
|
1094
|
+
"/me/mfa/recovery-codes",
|
|
1095
|
+
"/me/sessions",
|
|
1096
|
+
"/me/sessions/end",
|
|
1097
|
+
"/me/email",
|
|
1098
|
+
"/me/identities",
|
|
1099
|
+
"/me/identities/unlink",
|
|
1100
|
+
"/sign-out",
|
|
1101
|
+
"/methods",
|
|
1102
|
+
"/look",
|
|
1103
|
+
"/sign-in/passkey/begin",
|
|
1104
|
+
"/sign-in/passkey/finish",
|
|
1105
|
+
"/me/passkeys",
|
|
1106
|
+
"/me/passkeys/add/begin",
|
|
1107
|
+
"/me/passkeys/add/finish",
|
|
1108
|
+
"/me/passkeys/remove"
|
|
1109
|
+
].includes(route) || route.startsWith("/sign-in/social/") || route.startsWith("/callback/") || route.startsWith("/me/identities/link/");
|
|
1059
1110
|
if (!own) return null;
|
|
1060
1111
|
if (tenant === "") {
|
|
1061
1112
|
return problem(500, "not_configured", "cauthy-auth: tenant (CAUTHY_TENANT) is required for the application's own sign-in screens");
|
|
@@ -1068,15 +1119,21 @@ function createCauthy(given = {}) {
|
|
|
1068
1119
|
return json(lookCache.look);
|
|
1069
1120
|
}
|
|
1070
1121
|
if (route === "/methods" && !post) {
|
|
1071
|
-
const [providers, settings] = await Promise.all([
|
|
1122
|
+
const [providers, settings, place] = await Promise.all([
|
|
1123
|
+
ask(request, "GET", "/v1/auth/social/providers"),
|
|
1124
|
+
ask(request, "GET", "/v1/auth/settings"),
|
|
1125
|
+
ask(request, "GET", `/v1/auth/webauthn/place?origin=${encodeURIComponent(originOf(request))}`)
|
|
1126
|
+
]);
|
|
1072
1127
|
const list = Array.isArray(providers.data) ? providers.data : [];
|
|
1128
|
+
const here = place.data?.here === true;
|
|
1073
1129
|
return json({
|
|
1074
1130
|
providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? "") })).filter((p) => p.id !== ""),
|
|
1075
1131
|
signUp: settings.data?.allow_signup !== false,
|
|
1076
1132
|
// A sign-in link needs a page of the application to open.
|
|
1077
1133
|
magicLink: settings.data?.allow_magic_link === true && pages.signIn !== void 0,
|
|
1078
|
-
|
|
1079
|
-
|
|
1134
|
+
passkey: place.error ? true : here || place.data?.on_cauthy === true,
|
|
1135
|
+
passkeyHere: here,
|
|
1136
|
+
passkeyOnCauthy: place.error ? true : place.data?.on_cauthy === true
|
|
1080
1137
|
});
|
|
1081
1138
|
}
|
|
1082
1139
|
if (route.startsWith("/sign-in/social/") && !post) {
|
|
@@ -1138,6 +1195,14 @@ function createCauthy(given = {}) {
|
|
|
1138
1195
|
const list = Array.isArray(res.data) ? res.data : [];
|
|
1139
1196
|
return json({ identities: list.map((x) => ({ provider: String(x.provider ?? ""), email: String(x.email ?? ""), name: String(x.name ?? ""), linkedAt: String(x.linked_at ?? "") })) }, 200, mine.headers);
|
|
1140
1197
|
}
|
|
1198
|
+
if (route === "/me/passkeys" && !post) {
|
|
1199
|
+
const mine = await signedInAs(request);
|
|
1200
|
+
if ("response" in mine) return mine.response;
|
|
1201
|
+
const res = await ask(request, "GET", "/v1/auth/webauthn/credentials", void 0, void 0, mine.token);
|
|
1202
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message, void 0, mine.headers);
|
|
1203
|
+
const list = Array.isArray(res.data) ? res.data : [];
|
|
1204
|
+
return json({ passkeys: list.map(passkeyOf) }, 200, mine.headers);
|
|
1205
|
+
}
|
|
1141
1206
|
if ((route === "/me" || route === "/me/sessions") && !post) {
|
|
1142
1207
|
const mine = await signedInAs(request);
|
|
1143
1208
|
if ("response" in mine) return mine.response;
|
|
@@ -1269,6 +1334,33 @@ function createCauthy(given = {}) {
|
|
|
1269
1334
|
if (res.error) return refused(res);
|
|
1270
1335
|
return json({ status: "unlinked", provider }, 200, mine.headers);
|
|
1271
1336
|
}
|
|
1337
|
+
if (route === "/me/passkeys/add/begin") {
|
|
1338
|
+
const res = await as("/v1/auth/webauthn/register/begin", { password });
|
|
1339
|
+
if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
|
|
1340
|
+
const step = { kind: "enrol", token: String(res.data?.session_id ?? ""), returnTo: afterSignIn };
|
|
1341
|
+
const h = new Headers(mine.headers);
|
|
1342
|
+
h.append("set-cookie", writeCookie(stepCookie, await seal(step, await key), attrs(request, PASSKEY_SECONDS)));
|
|
1343
|
+
return json({ status: "started", options: res.data?.options?.publicKey ?? res.data?.options ?? null }, 200, h);
|
|
1344
|
+
}
|
|
1345
|
+
if (route === "/me/passkeys/add/finish") {
|
|
1346
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
1347
|
+
const step = sealed ? await unseal(sealed, await key) : null;
|
|
1348
|
+
if (step === null || step.kind !== "enrol") return problem(400, "step_expired", "Start adding the passkey again: it was asked for too long ago.", void 0, mine.headers);
|
|
1349
|
+
if (!isCredential(b.credential)) return problem(400, "bad_request", "Send the passkey the browser made.", void 0, mine.headers);
|
|
1350
|
+
const name = typeof b.name === "string" ? b.name.trim().slice(0, 60) : "";
|
|
1351
|
+
const res = await as("/v1/auth/webauthn/register/finish", { session_id: step.token, nickname: name, credential: b.credential });
|
|
1352
|
+
const h = new Headers(mine.headers);
|
|
1353
|
+
h.append("set-cookie", writeCookie(stepCookie, "", attrs(request, 0)));
|
|
1354
|
+
if (res.error) return problem(res.status, res.error.code === "attestation_invalid" ? "passkey_failed" : res.error.code, res.error.code === "attestation_invalid" ? "The passkey could not be added. Try again." : res.error.message, void 0, h);
|
|
1355
|
+
return json({ status: "added", passkey: passkeyOf(res.data) }, 200, h);
|
|
1356
|
+
}
|
|
1357
|
+
if (route === "/me/passkeys/remove") {
|
|
1358
|
+
const id = typeof b.id === "string" ? b.id : "";
|
|
1359
|
+
if (!/^[0-9a-f-]{36}$/i.test(id)) return problem(400, "invalid_passkey", "Say which passkey to remove.", void 0, mine.headers);
|
|
1360
|
+
const res = await as(`/v1/auth/webauthn/credentials/${id}`, void 0, "DELETE");
|
|
1361
|
+
if (res.error) return refused(res);
|
|
1362
|
+
return json({ status: "removed", id }, 200, mine.headers);
|
|
1363
|
+
}
|
|
1272
1364
|
if (route === "/me/sessions/end") {
|
|
1273
1365
|
const res = await as("/v1/auth/me/sessions", void 0, "DELETE");
|
|
1274
1366
|
if (res.error) return refused(res);
|
|
@@ -1295,6 +1387,29 @@ function createCauthy(given = {}) {
|
|
|
1295
1387
|
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
1296
1388
|
return json({ status: "verified" });
|
|
1297
1389
|
}
|
|
1390
|
+
if (route === "/sign-in/passkey/begin") {
|
|
1391
|
+
const res = await ask(request, "POST", "/v1/auth/webauthn/login/begin", {});
|
|
1392
|
+
if (res.error) return problem(res.status, res.error.code, res.error.message);
|
|
1393
|
+
const step = { kind: "passkey", token: String(res.data?.session_id ?? ""), returnTo: afterSignIn };
|
|
1394
|
+
return json(
|
|
1395
|
+
{ status: "started", options: res.data?.options?.publicKey ?? res.data?.options ?? null },
|
|
1396
|
+
200,
|
|
1397
|
+
new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, PASSKEY_SECONDS)) })
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
if (route === "/sign-in/passkey/finish") {
|
|
1401
|
+
const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
|
|
1402
|
+
const step = sealed ? await unseal(sealed, await key) : null;
|
|
1403
|
+
if (step === null || step.kind !== "passkey") return problem(400, "step_expired", "Try the passkey again: it was asked for too long ago.");
|
|
1404
|
+
if (!isCredential(b.credential)) return problem(400, "bad_request", "Send the passkey the browser answered with.");
|
|
1405
|
+
const res = await ask(request, "POST", "/v1/auth/webauthn/login/finish", { session_id: step.token, credential: b.credential });
|
|
1406
|
+
const clear = writeCookie(stepCookie, "", attrs(request, 0));
|
|
1407
|
+
if (res.error) {
|
|
1408
|
+
const failed2 = ["assertion_invalid", "session_expired", "clone_detected", "not_found"].includes(res.error.code);
|
|
1409
|
+
return problem(res.status, failed2 ? "passkey_failed" : res.error.code, failed2 ? "The passkey sign-in could not be completed. Try again, or sign in another way." : res.error.message, void 0, new Headers({ "set-cookie": clear }));
|
|
1410
|
+
}
|
|
1411
|
+
return signedIn(request, res.data, [clear]);
|
|
1412
|
+
}
|
|
1298
1413
|
if (route === "/sign-in/magic-link") {
|
|
1299
1414
|
const bad = checkEmail(b.email);
|
|
1300
1415
|
if (bad) return invalid(bad);
|