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/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) {
@@ -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}`, "x-cauthy-client-id": clientId } : { "x-cauthy-links": "hosted" },
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 = ["/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/");
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([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
+ ]);
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
- // Asked for on Cauthy's page: the person goes there for that step and comes back.
1079
- 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
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);