cauthy-auth 0.10.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/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
- const passkey = url.searchParams.get("method") === "passkey" ? "#passkey" : "";
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) {
@@ -1060,7 +1076,37 @@ function createCauthy(given = {}) {
1060
1076
  let lookCache = null;
1061
1077
  async function ownRoute(request, url, route) {
1062
1078
  const post = request.method === "POST";
1063
- const own = ["/sign-in/email", "/sign-in/mfa", "/sign-up/email", "/forgot-password", "/reset-password", "/verify-email", "/sign-in/magic-link", "/sign-in/magic-link/verify", "/sign-in/email-code", "/me", "/me/password", "/me/mfa/setup", "/me/mfa/verify", "/me/mfa/disable", "/me/mfa/recovery-codes", "/me/sessions", "/me/sessions/end", "/me/email", "/me/identities", "/me/identities/unlink", "/sign-out", "/methods", "/look"].includes(route) || route.startsWith("/sign-in/social/") || route.startsWith("/callback/") || route.startsWith("/me/identities/link/");
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/");
1064
1110
  if (!own) return null;
1065
1111
  if (tenant === "") {
1066
1112
  return problem(500, "not_configured", "cauthy-auth: tenant (CAUTHY_TENANT) is required for the application's own sign-in screens");
@@ -1073,15 +1119,21 @@ function createCauthy(given = {}) {
1073
1119
  return json(lookCache.look);
1074
1120
  }
1075
1121
  if (route === "/methods" && !post) {
1076
- const [providers, settings] = await Promise.all([ask(request, "GET", "/v1/auth/social/providers"), ask(request, "GET", "/v1/auth/settings")]);
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
+ ]);
1077
1127
  const list = Array.isArray(providers.data) ? providers.data : [];
1128
+ const here = place.data?.here === true;
1078
1129
  return json({
1079
1130
  providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? "") })).filter((p) => p.id !== ""),
1080
1131
  signUp: settings.data?.allow_signup !== false,
1081
1132
  // A sign-in link needs a page of the application to open.
1082
1133
  magicLink: settings.data?.allow_magic_link === true && pages.signIn !== void 0,
1083
- // Asked for on Cauthy's page: the person goes there for that step and comes back.
1084
- passkey: true
1134
+ passkey: place.error ? true : here || place.data?.on_cauthy === true,
1135
+ passkeyHere: here,
1136
+ passkeyOnCauthy: place.error ? true : place.data?.on_cauthy === true
1085
1137
  });
1086
1138
  }
1087
1139
  if (route.startsWith("/sign-in/social/") && !post) {
@@ -1143,6 +1195,14 @@ function createCauthy(given = {}) {
1143
1195
  const list = Array.isArray(res.data) ? res.data : [];
1144
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);
1145
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
+ }
1146
1206
  if ((route === "/me" || route === "/me/sessions") && !post) {
1147
1207
  const mine = await signedInAs(request);
1148
1208
  if ("response" in mine) return mine.response;
@@ -1274,6 +1334,33 @@ function createCauthy(given = {}) {
1274
1334
  if (res.error) return refused(res);
1275
1335
  return json({ status: "unlinked", provider }, 200, mine.headers);
1276
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
+ }
1277
1364
  if (route === "/me/sessions/end") {
1278
1365
  const res = await as("/v1/auth/me/sessions", void 0, "DELETE");
1279
1366
  if (res.error) return refused(res);
@@ -1300,6 +1387,29 @@ function createCauthy(given = {}) {
1300
1387
  if (res.error) return problem(res.status, res.error.code, res.error.message);
1301
1388
  return json({ status: "verified" });
1302
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
+ }
1303
1413
  if (route === "/sign-in/magic-link") {
1304
1414
  const bad = checkEmail(b.email);
1305
1415
  if (bad) return invalid(bad);