cauthy-auth 0.18.0 → 0.20.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
@@ -994,6 +994,7 @@ function createCauthy(given = {}) {
994
994
  const passkeyOf = (d) => ({
995
995
  id: String(d?.id ?? ""),
996
996
  name: String(d?.nickname ?? ""),
997
+ kind: ["security_key", "synced", "device"].includes(d?.kind) ? d.kind : "unknown",
997
998
  createdAt: String(d?.created_at ?? ""),
998
999
  lastUsedAt: d?.last_used_at ? String(d.last_used_at) : null
999
1000
  });
@@ -1133,7 +1134,7 @@ function createCauthy(given = {}) {
1133
1134
  "/me/passkeys/remove",
1134
1135
  "/me/agents",
1135
1136
  "/me/agents/disconnect"
1136
- ].includes(route) || route.startsWith("/sign-in/social/") || route.startsWith("/callback/") || route.startsWith("/me/identities/link/");
1137
+ ].includes(route) || route.startsWith("/sign-in/social/") || route.startsWith("/callback/") || route.startsWith("/me/identities/link/") || route === "/sign-in/sso" || route === "/sign-in/sso/discover" || route === "/sso/callback";
1137
1138
  if (!own) return null;
1138
1139
  if (tenant === "") {
1139
1140
  return problem(500, "not_configured", "cauthy-auth: tenant (CAUTHY_TENANT) is required for the application's own sign-in screens");
@@ -1146,10 +1147,11 @@ function createCauthy(given = {}) {
1146
1147
  return json(lookCache.look);
1147
1148
  }
1148
1149
  if (route === "/methods" && !post) {
1149
- const [providers, settings, place] = await Promise.all([
1150
+ const [providers, settings, place, sso] = await Promise.all([
1150
1151
  ask(request, "GET", "/v1/auth/social/providers"),
1151
1152
  ask(request, "GET", "/v1/auth/settings"),
1152
- ask(request, "GET", `/v1/auth/webauthn/place?origin=${encodeURIComponent(originOf(request))}`)
1153
+ ask(request, "GET", `/v1/auth/webauthn/place?origin=${encodeURIComponent(originOf(request))}`),
1154
+ ask(request, "GET", "/v1/auth/sso/connections")
1153
1155
  ]);
1154
1156
  const list = Array.isArray(providers.data) ? providers.data : [];
1155
1157
  const here = place.data?.here === true;
@@ -1161,9 +1163,46 @@ function createCauthy(given = {}) {
1161
1163
  passkey: place.error ? true : here || place.data?.on_cauthy === true,
1162
1164
  passkeyHere: here,
1163
1165
  passkeyOnCauthy: place.error ? true : place.data?.on_cauthy === true,
1166
+ // A company's sign-in, found by the person's work address.
1167
+ sso: Array.isArray(sso.data) && sso.data.some((c) => c?.discoverable === true),
1164
1168
  ...settings.data?.captcha_enabled === true && settings.data?.captcha_site_key ? { captchaSiteKey: String(settings.data.captcha_site_key) } : {}
1165
1169
  });
1166
1170
  }
1171
+ if (route === "/sign-in/sso" && !post) {
1172
+ const returnTo = localPath(url.searchParams.get("return_to"), afterSignIn);
1173
+ const failed2 = (code) => redirect(`${returnTo}${returnTo.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(code)}`);
1174
+ const email = (url.searchParams.get("email") ?? "").trim();
1175
+ const connection = (url.searchParams.get("connection") ?? "").trim();
1176
+ if (email === "" && connection === "") return failed2("sso_not_found");
1177
+ const verifier = random(48);
1178
+ const challenge = b64url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
1179
+ const callback2 = `${basePath}/sso/callback`;
1180
+ const started = await ask(request, "POST", "/v1/auth/sso/start", {
1181
+ ...connection ? { connection } : { email },
1182
+ redirect_uri: `${originOf(request)}${callback2}`,
1183
+ code_challenge: challenge
1184
+ }, callback2);
1185
+ if (started.error || typeof started.data?.redirect_url !== "string") return failed2(started.error?.code ?? "sso_unavailable");
1186
+ const step = { kind: "sso", token: verifier, returnTo };
1187
+ return redirect(started.data.redirect_url, [writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS))]);
1188
+ }
1189
+ if (route === "/sso/callback" && !post) {
1190
+ const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
1191
+ const step = sealed ? await unseal(sealed, await key) : null;
1192
+ const clear = writeCookie(stepCookie, "", attrs(request, 0));
1193
+ const back = step?.returnTo ?? afterSignIn;
1194
+ const failedTo = (code2) => redirect(`${back}${back.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(code2)}`, [clear]);
1195
+ if (step === null || step.kind !== "sso") return failedTo("state_mismatch");
1196
+ const refused = url.searchParams.get("error");
1197
+ if (refused) return failedTo(refused);
1198
+ const code = url.searchParams.get("code");
1199
+ if (!code) return failedTo("sso_failed");
1200
+ const done = await ask(request, "POST", "/v1/auth/sso/token", { code, redirect_uri: `${originOf(request)}${basePath}/sso/callback`, code_verifier: step.token });
1201
+ if (done.error) return failedTo(done.error.code);
1202
+ const s = await stored(done.data);
1203
+ if (s === null) return failedTo("unavailable");
1204
+ return redirect(back, [...await write(request, s, readCookies(request.headers.get("cookie"))), clear]);
1205
+ }
1167
1206
  if (route.startsWith("/sign-in/social/") && !post) {
1168
1207
  const provider = providerIn(route.slice("/sign-in/social/".length));
1169
1208
  if (!PROVIDER_ID.test(provider)) return new Response("Not found", { status: 404 });
@@ -1273,6 +1312,14 @@ function createCauthy(given = {}) {
1273
1312
  }
1274
1313
  const b = await body(request);
1275
1314
  if (b === null) return problem(400, "bad_request", "Send the form as JSON.");
1315
+ if (route === "/sign-in/sso/discover") {
1316
+ const bad = checkEmail(b.email);
1317
+ if (bad) return invalid(bad);
1318
+ const res = await ask(request, "POST", "/v1/auth/sso/discover", { email: String(b.email).trim() });
1319
+ if (res.error) return problem(res.status, res.error.code, res.error.message);
1320
+ const c = res.data?.connection;
1321
+ return json(c ? { status: "found", connection: { id: String(c.slug ?? ""), name: String(c.display_name ?? ""), enforced: c.enforced === true } } : { status: "none" });
1322
+ }
1276
1323
  if (route === "/sign-in/email") {
1277
1324
  const bad = firstProblem(checkEmail(b.email), checkPassword(b.password));
1278
1325
  if (bad) return invalid(bad);
@@ -1409,7 +1456,8 @@ function createCauthy(given = {}) {
1409
1456
  return json({ status: "unlinked", provider }, 200, mine.headers);
1410
1457
  }
1411
1458
  if (route === "/me/passkeys/add/begin") {
1412
- const res = await as("/v1/auth/webauthn/register/begin", { password });
1459
+ const kind = b.kind === "device" || b.kind === "security_key" ? b.kind : "";
1460
+ const res = await as("/v1/auth/webauthn/register/begin", { password, ...kind ? { kind } : {} });
1413
1461
  if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
1414
1462
  const step = { kind: "enrol", token: String(res.data?.session_id ?? ""), returnTo: afterSignIn };
1415
1463
  const h = new Headers(mine.headers);