cauthy-auth 0.10.0 → 0.12.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,39 @@ 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
+ "/sign-in/google-one-tap/begin",
1106
+ "/sign-in/google-one-tap",
1107
+ "/me/passkeys",
1108
+ "/me/passkeys/add/begin",
1109
+ "/me/passkeys/add/finish",
1110
+ "/me/passkeys/remove"
1111
+ ].includes(route) || route.startsWith("/sign-in/social/") || route.startsWith("/callback/") || route.startsWith("/me/identities/link/");
1064
1112
  if (!own) return null;
1065
1113
  if (tenant === "") {
1066
1114
  return problem(500, "not_configured", "cauthy-auth: tenant (CAUTHY_TENANT) is required for the application's own sign-in screens");
@@ -1073,15 +1121,21 @@ function createCauthy(given = {}) {
1073
1121
  return json(lookCache.look);
1074
1122
  }
1075
1123
  if (route === "/methods" && !post) {
1076
- const [providers, settings] = await Promise.all([ask(request, "GET", "/v1/auth/social/providers"), ask(request, "GET", "/v1/auth/settings")]);
1124
+ const [providers, settings, place] = await Promise.all([
1125
+ ask(request, "GET", "/v1/auth/social/providers"),
1126
+ ask(request, "GET", "/v1/auth/settings"),
1127
+ ask(request, "GET", `/v1/auth/webauthn/place?origin=${encodeURIComponent(originOf(request))}`)
1128
+ ]);
1077
1129
  const list = Array.isArray(providers.data) ? providers.data : [];
1130
+ const here = place.data?.here === true;
1078
1131
  return json({
1079
1132
  providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? "") })).filter((p) => p.id !== ""),
1080
1133
  signUp: settings.data?.allow_signup !== false,
1081
1134
  // A sign-in link needs a page of the application to open.
1082
1135
  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
1136
+ passkey: place.error ? true : here || place.data?.on_cauthy === true,
1137
+ passkeyHere: here,
1138
+ passkeyOnCauthy: place.error ? true : place.data?.on_cauthy === true
1085
1139
  });
1086
1140
  }
1087
1141
  if (route.startsWith("/sign-in/social/") && !post) {
@@ -1143,6 +1197,14 @@ function createCauthy(given = {}) {
1143
1197
  const list = Array.isArray(res.data) ? res.data : [];
1144
1198
  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
1199
  }
1200
+ if (route === "/me/passkeys" && !post) {
1201
+ const mine = await signedInAs(request);
1202
+ if ("response" in mine) return mine.response;
1203
+ const res = await ask(request, "GET", "/v1/auth/webauthn/credentials", void 0, void 0, mine.token);
1204
+ if (res.error) return problem(res.status, res.error.code, res.error.message, void 0, mine.headers);
1205
+ const list = Array.isArray(res.data) ? res.data : [];
1206
+ return json({ passkeys: list.map(passkeyOf) }, 200, mine.headers);
1207
+ }
1146
1208
  if ((route === "/me" || route === "/me/sessions") && !post) {
1147
1209
  const mine = await signedInAs(request);
1148
1210
  if ("response" in mine) return mine.response;
@@ -1274,6 +1336,33 @@ function createCauthy(given = {}) {
1274
1336
  if (res.error) return refused(res);
1275
1337
  return json({ status: "unlinked", provider }, 200, mine.headers);
1276
1338
  }
1339
+ if (route === "/me/passkeys/add/begin") {
1340
+ const res = await as("/v1/auth/webauthn/register/begin", { password });
1341
+ if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
1342
+ const step = { kind: "enrol", token: String(res.data?.session_id ?? ""), returnTo: afterSignIn };
1343
+ const h = new Headers(mine.headers);
1344
+ h.append("set-cookie", writeCookie(stepCookie, await seal(step, await key), attrs(request, PASSKEY_SECONDS)));
1345
+ return json({ status: "started", options: res.data?.options?.publicKey ?? res.data?.options ?? null }, 200, h);
1346
+ }
1347
+ if (route === "/me/passkeys/add/finish") {
1348
+ const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
1349
+ const step = sealed ? await unseal(sealed, await key) : null;
1350
+ 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);
1351
+ if (!isCredential(b.credential)) return problem(400, "bad_request", "Send the passkey the browser made.", void 0, mine.headers);
1352
+ const name = typeof b.name === "string" ? b.name.trim().slice(0, 60) : "";
1353
+ const res = await as("/v1/auth/webauthn/register/finish", { session_id: step.token, nickname: name, credential: b.credential });
1354
+ const h = new Headers(mine.headers);
1355
+ h.append("set-cookie", writeCookie(stepCookie, "", attrs(request, 0)));
1356
+ 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);
1357
+ return json({ status: "added", passkey: passkeyOf(res.data) }, 200, h);
1358
+ }
1359
+ if (route === "/me/passkeys/remove") {
1360
+ const id = typeof b.id === "string" ? b.id : "";
1361
+ if (!/^[0-9a-f-]{36}$/i.test(id)) return problem(400, "invalid_passkey", "Say which passkey to remove.", void 0, mine.headers);
1362
+ const res = await as(`/v1/auth/webauthn/credentials/${id}`, void 0, "DELETE");
1363
+ if (res.error) return refused(res);
1364
+ return json({ status: "removed", id }, 200, mine.headers);
1365
+ }
1277
1366
  if (route === "/me/sessions/end") {
1278
1367
  const res = await as("/v1/auth/me/sessions", void 0, "DELETE");
1279
1368
  if (res.error) return refused(res);
@@ -1300,6 +1389,40 @@ function createCauthy(given = {}) {
1300
1389
  if (res.error) return problem(res.status, res.error.code, res.error.message);
1301
1390
  return json({ status: "verified" });
1302
1391
  }
1392
+ if (route === "/sign-in/passkey/begin") {
1393
+ const res = await ask(request, "POST", "/v1/auth/webauthn/login/begin", {});
1394
+ if (res.error) return problem(res.status, res.error.code, res.error.message);
1395
+ const step = { kind: "passkey", token: String(res.data?.session_id ?? ""), returnTo: afterSignIn };
1396
+ return json(
1397
+ { status: "started", options: res.data?.options?.publicKey ?? res.data?.options ?? null },
1398
+ 200,
1399
+ new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, PASSKEY_SECONDS)) })
1400
+ );
1401
+ }
1402
+ if (route === "/sign-in/passkey/finish") {
1403
+ const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
1404
+ const step = sealed ? await unseal(sealed, await key) : null;
1405
+ if (step === null || step.kind !== "passkey") return problem(400, "step_expired", "Try the passkey again: it was asked for too long ago.");
1406
+ if (!isCredential(b.credential)) return problem(400, "bad_request", "Send the passkey the browser answered with.");
1407
+ const res = await ask(request, "POST", "/v1/auth/webauthn/login/finish", { session_id: step.token, credential: b.credential });
1408
+ const clear = writeCookie(stepCookie, "", attrs(request, 0));
1409
+ if (res.error) {
1410
+ const failed2 = ["assertion_invalid", "session_expired", "clone_detected", "not_found"].includes(res.error.code);
1411
+ 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 }));
1412
+ }
1413
+ return signedIn(request, res.data, [clear]);
1414
+ }
1415
+ if (route === "/sign-in/google-one-tap/begin") {
1416
+ const res = await ask(request, "POST", "/v1/auth/google/one-tap/begin", {});
1417
+ if (res.error) return problem(res.status, res.error.code, res.error.message);
1418
+ return json({ status: "started", clientId: String(res.data?.client_id ?? ""), nonce: String(res.data?.nonce ?? "") });
1419
+ }
1420
+ if (route === "/sign-in/google-one-tap") {
1421
+ if (typeof b.credential !== "string" || b.credential === "") return problem(400, "bad_request", "Send the credential Google gave the page.");
1422
+ const res = await ask(request, "POST", "/v1/auth/google/one-tap", { credential: b.credential });
1423
+ if (res.error) return problem(res.status, res.error.code, res.error.message);
1424
+ return signedIn(request, res.data);
1425
+ }
1303
1426
  if (route === "/sign-in/magic-link") {
1304
1427
  const bad = checkEmail(b.email);
1305
1428
  if (bad) return invalid(bad);