diff --git a/playgrounds/better-auth-hono/package-lock.json b/playgrounds/better-auth-hono/package-lock.json index 292cd85a..da9ac149 100644 --- a/playgrounds/better-auth-hono/package-lock.json +++ b/playgrounds/better-auth-hono/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@hono/node-server": "^2.0.4", - "better-auth": "^1.2.0", + "better-auth": "1.6.14", "better-sqlite3": "^12.0.0", "dotenv": "^17.4.2", "hono": "^4.7.0" diff --git a/playgrounds/better-auth-hono/package.json b/playgrounds/better-auth-hono/package.json index c87c0c0d..9decb29b 100644 --- a/playgrounds/better-auth-hono/package.json +++ b/playgrounds/better-auth-hono/package.json @@ -5,11 +5,12 @@ "type": "module", "scripts": { "dev": "tsx watch --env-file .env src/index.ts", - "start": "tsx --env-file .env src/index.ts" + "start": "tsx --env-file .env src/index.ts", + "dev:taskpane": "tsx watch src/taskpane-server.ts" }, "dependencies": { "@hono/node-server": "^2.0.4", - "better-auth": "^1.2.0", + "better-auth": "1.6.14", "better-sqlite3": "^12.0.0", "dotenv": "^17.4.2", "hono": "^4.7.0" diff --git a/playgrounds/better-auth-hono/public/device.html b/playgrounds/better-auth-hono/public/device.html new file mode 100644 index 00000000..46ffbd23 --- /dev/null +++ b/playgrounds/better-auth-hono/public/device.html @@ -0,0 +1,153 @@ + + + + + + Device Authorization — Writing Tools + + + +

Device Authorization

+

A device (e.g. the Word add-in) is requesting access to your account.

+ +
Loading…
+
+ + + + diff --git a/playgrounds/better-auth-hono/public/index.html b/playgrounds/better-auth-hono/public/index.html index fcda6a11..0ee23a1b 100644 --- a/playgrounds/better-auth-hono/public/index.html +++ b/playgrounds/better-auth-hono/public/index.html @@ -71,8 +71,8 @@

Hono + Better Auth POC

} async function signOut() { - await fetch(`${BASE}/api/auth/sign-out`, { method: "POST", credentials: "include" }); - window.location.reload(); + const res = await fetch(`${BASE}/api/auth/sign-out`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: "{}" }); + if (res.ok) window.location.reload(); } async function callProtected(mode) { diff --git a/playgrounds/better-auth-hono/src/auth.ts b/playgrounds/better-auth-hono/src/auth.ts index 6abade93..28a6e564 100644 --- a/playgrounds/better-auth-hono/src/auth.ts +++ b/playgrounds/better-auth-hono/src/auth.ts @@ -1,17 +1,36 @@ import { betterAuth } from "better-auth"; -import { bearer } from "better-auth/plugins"; +import { bearer, deviceAuthorization } from "better-auth/plugins"; import Database from "better-sqlite3"; import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// Device-flow client identifier. This is the app's own ID for the device +// authorization grant — it is NOT the Google OAuth client ID. +export const DEVICE_CLIENT_ID = "writing-tools-word-poc"; + export const auth = betterAuth({ // Run `npx @better-auth/cli migrate` after adding or changing plugins. database: new Database(path.join(__dirname, "../db/auth.db")), baseURL: process.env.BETTER_AUTH_URL ?? "http://localhost:3001", secret: process.env.BETTER_AUTH_SECRET, - plugins: [bearer()], + plugins: [ + bearer(), + deviceAuthorization({ + // Browser-facing approval page the user is sent to. + verificationUri: "/device.html", + // Device codes expire after 10 minutes if never approved. + expiresIn: "10m", + // Task pane must not poll faster than every 5 seconds. + interval: "5s", + // Only issue device codes for this app's known client ID. + validateClient: (clientId) => clientId === DEVICE_CLIENT_ID, + // Required by this plugin version's runtime options schema even though + // the TS type marks it optional. Empty = use the default deviceCode table. + schema: {}, + }), + ], socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID ?? "", diff --git a/playgrounds/better-auth-hono/src/index.ts b/playgrounds/better-auth-hono/src/index.ts index 4166f2db..c5cc420c 100644 --- a/playgrounds/better-auth-hono/src/index.ts +++ b/playgrounds/better-auth-hono/src/index.ts @@ -10,11 +10,14 @@ import chat from "./routes/chat.js"; const app = new Hono(); const PORT = 3001; -// CORS — allow the static test page and future frontend origins. +// CORS — allow the backend's own static pages (3001) AND the separate +// task-pane simulator origin (3002). The 3002 origin reproduces the Word +// task-pane / browser split: it has NO Better Auth cookie and must rely +// entirely on the bearer token returned by the device flow. app.use( "*", cors({ - origin: [`http://localhost:${PORT}`], + origin: ["http://localhost:3001", "http://localhost:3002"], allowHeaders: ["Content-Type", "Authorization"], allowMethods: ["GET", "POST", "OPTIONS"], credentials: true, diff --git a/playgrounds/better-auth-hono/src/taskpane-server.ts b/playgrounds/better-auth-hono/src/taskpane-server.ts new file mode 100644 index 00000000..20392cba --- /dev/null +++ b/playgrounds/better-auth-hono/src/taskpane-server.ts @@ -0,0 +1,17 @@ +import { Hono } from "hono"; +import { serve } from "@hono/node-server"; +import { serveStatic } from "@hono/node-server/serve-static"; + +// Standalone static server for the task-pane simulator. +// Runs on a DIFFERENT origin (3002) than the backend (3001) so that +// cookies set during browser login never reach this origin. The simulator +// must succeed using only the bearer token from the device flow. +const app = new Hono(); +const PORT = 3002; + +app.use("/*", serveStatic({ root: "./taskpane" })); + +serve({ fetch: app.fetch, port: PORT }, () => { + console.log(`Task-pane simulator at http://localhost:${PORT}`); + console.log(`(backend expected at http://localhost:3001)`); +}); diff --git a/playgrounds/better-auth-hono/taskpane/index.html b/playgrounds/better-auth-hono/taskpane/index.html new file mode 100644 index 00000000..1d42d664 --- /dev/null +++ b/playgrounds/better-auth-hono/taskpane/index.html @@ -0,0 +1,177 @@ + + + + + + Task-Pane Simulator (Device Flow) + + + +

Task-Pane Simulator

+

+ Origin http://localhost:3002 — no Better Auth cookie here. + Backend is http://localhost:3001. Success depends only on the + bearer token from the device flow. +

+ +
+ + + +
+ +
+
+ + + +