diff --git a/demo/nextjs/app/(auth)/device/approve/page.tsx b/demo/nextjs/app/(auth)/device/approve/page.tsx index 9f920fe7a4..24ab9459e8 100644 --- a/demo/nextjs/app/(auth)/device/approve/page.tsx +++ b/demo/nextjs/app/(auth)/device/approve/page.tsx @@ -8,14 +8,7 @@ import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { useSessionQuery } from "@/data/user/session-query"; import { authClient } from "@/lib/auth-client"; - -/** - * Client IDs this app registered, mapped to names users recognize. A device - * chooses its own `client_id`, so never present an unknown ID as a trusted name. - */ -const KNOWN_CLIENTS: Record = { - "demo-cli": "Better Auth demo CLI", -}; +import { deviceClients } from "@/lib/device-clients"; export default function Page() { const router = useRouter(); @@ -113,7 +106,8 @@ export default function Page() { {request ? ( <>

- {KNOWN_CLIENTS[request.client_id] ?? "Unrecognized client"} + {deviceClients.get(request.client_id) ?? + "Unrecognized client"}

ID sent by the device: {request.client_id} diff --git a/demo/nextjs/lib/auth.ts b/demo/nextjs/lib/auth.ts index 0bd3b1a81e..cd4cba20c9 100644 --- a/demo/nextjs/lib/auth.ts +++ b/demo/nextjs/lib/auth.ts @@ -28,6 +28,7 @@ import { import { MysqlDialect } from "kysely"; import { createPool } from "mysql2/promise"; import { Stripe } from "stripe"; +import { deviceClients } from "./device-clients.ts"; import { createSCIMDemoPlugin, isSCIMDemoEmployeePortalEnabled, @@ -397,6 +398,7 @@ const authOptions = { }), ...scimDemoPlugins, deviceAuthorization({ + validateClient: (clientId) => deviceClients.has(clientId), expiresIn: "3min", interval: "5s", }), diff --git a/demo/nextjs/lib/device-clients.ts b/demo/nextjs/lib/device-clients.ts new file mode 100644 index 0000000000..ece93e362f --- /dev/null +++ b/demo/nextjs/lib/device-clients.ts @@ -0,0 +1,7 @@ +/** + * Device clients this app registered, mapped to names users recognize. + * The server accepts only these IDs, so the approval page can trust the name. + */ +export const deviceClients = new Map([ + ["demo-cli", "Better Auth demo CLI"], +]); diff --git a/docs/content/docs/plugins/device-authorization.mdx b/docs/content/docs/plugins/device-authorization.mdx index aff5409af3..c47fcea4d2 100644 --- a/docs/content/docs/plugins/device-authorization.mdx +++ b/docs/content/docs/plugins/device-authorization.mdx @@ -457,15 +457,14 @@ Users must be authenticated to approve or deny device authorization requests: #### Example Approval Page -`GET /device` returns `client_id` and `scope` only to the signed-in user who claimed the code. `client_id` is the identifier the device sent, so show a name from your own client registry rather than the raw value. Keep Approve disabled until the request details load. +`GET /device` returns `client_id` and `scope` only to the signed-in user who claimed the code. `client_id` is the identifier the device sent, so show a name from your own client registry rather than the raw value, and accept only registered IDs in `validateClient`. Keep Approve disabled until the request details load. Avoid approval pages that show only the user code, or that enable Approve before the client and scopes appear. On such a page the user cannot tell a request from their own device apart from one started by someone else, for example a code sent to them in a message. The risk is higher when `validateClient` is not configured, because anyone can then start a request. ```tsx title="app/device/approve/page.tsx" // Client IDs you registered, mapped to names your users recognize. -const knownClients: Record = { - "acme-cli": "Acme CLI", -}; +// Accept only these IDs in `validateClient` so the name can be trusted. +const knownClients = new Map([["acme-cli", "Acme CLI"]]); export default function DeviceApprovalPage() { const { user } = useAuth(); // Must be authenticated @@ -532,7 +531,7 @@ export default function DeviceApprovalPage() {

Device Authorization Request

Client: {request?.client_id - ? knownClients[request.client_id] ?? `Unrecognized client (${request.client_id})` + ? knownClients.get(request.client_id) ?? `Unrecognized client (${request.client_id})` : "Loading..."}

Scopes: {request?.scope || "None"}