docs(device-authorization): validate demo clients and use Map lookups

The demo accepts only registered device client IDs, so the approval page
can show their registered name. Both examples look names up in a Map, so
a client ID such as __proto__ falls back to the unrecognized label.
This commit is contained in:
Gustavo Valverde
2026-09-28 14:13:31 +01:00
parent bb0c8c6a9b
commit 3cad3e18c4
4 changed files with 16 additions and 14 deletions
@@ -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<string, string> = {
"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 ? (
<>
<p>
{KNOWN_CLIENTS[request.client_id] ?? "Unrecognized client"}
{deviceClients.get(request.client_id) ??
"Unrecognized client"}
</p>
<p className="font-mono text-xs text-muted-foreground">
ID sent by the device: {request.client_id}
+2
View File
@@ -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",
}),
+7
View File
@@ -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<string, string>([
["demo-cli", "Better Auth demo CLI"],
]);
@@ -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<string, string> = {
"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() {
<h2>Device Authorization Request</h2>
<p>
Client: {request?.client_id
? knownClients[request.client_id] ?? `Unrecognized client (${request.client_id})`
? knownClients.get(request.client_id) ?? `Unrecognized client (${request.client_id})`
: "Loading..."}
</p>
<p>Scopes: {request?.scope || "None"}</p>