mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
docs(device-authorization): gate approval on owner-only request details
Approve stays disabled unless the lookup returned the client ID, which GET /device includes only for the session that claimed the code. The demo also discards stale lookups, reports invalid or expired codes, and both examples show a registered client name instead of the raw ID.
This commit is contained in:
@@ -9,6 +9,14 @@ 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",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -18,15 +26,31 @@ export default function Page() {
|
||||
const [isDenyPending, startDenyTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [request, setRequest] = useState<{
|
||||
client_id?: string;
|
||||
client_id: string;
|
||||
scope?: string;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setRequest(null);
|
||||
setError(null);
|
||||
if (!session || !userCode) return;
|
||||
authClient.device({ query: { user_code: userCode } }).then(({ data }) => {
|
||||
if (data) setRequest(data);
|
||||
});
|
||||
let active = true;
|
||||
void authClient
|
||||
.device({ query: { user_code: userCode } })
|
||||
.then(({ data, error }) => {
|
||||
if (!active) return;
|
||||
if (!data?.client_id) {
|
||||
setError(
|
||||
error?.message ||
|
||||
"This code is invalid, expired, or was claimed by another session.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
setRequest({ client_id: data.client_id, scope: data.scope });
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [session, userCode]);
|
||||
|
||||
const handleApprove = () => {
|
||||
@@ -86,7 +110,18 @@ export default function Page() {
|
||||
|
||||
<div className="rounded-lg bg-muted p-4">
|
||||
<p className="text-sm font-medium">Requesting client</p>
|
||||
<p className="font-mono">{request?.client_id ?? "Loading..."}</p>
|
||||
{request ? (
|
||||
<>
|
||||
<p>
|
||||
{KNOWN_CLIENTS[request.client_id] ?? "Unrecognized client"}
|
||||
</p>
|
||||
<p className="font-mono text-xs text-muted-foreground">
|
||||
ID sent by the device: {request.client_id}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p>Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-muted p-4">
|
||||
|
||||
@@ -462,6 +462,11 @@ Users must be authenticated to approve or deny device authorization requests:
|
||||
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",
|
||||
};
|
||||
|
||||
export default function DeviceApprovalPage() {
|
||||
const { user } = useAuth(); // Must be authenticated
|
||||
const searchParams = useSearchParams();
|
||||
@@ -474,10 +479,16 @@ export default function DeviceApprovalPage() {
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setRequest(null);
|
||||
if (!user || !userCode) return;
|
||||
let active = true;
|
||||
authClient.device({ query: { user_code: userCode } }).then(({ data }) => {
|
||||
setRequest(data);
|
||||
// Owner-only details are missing when another session claimed the code.
|
||||
if (active && data?.client_id) setRequest(data);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [user, userCode]);
|
||||
|
||||
const handleApprove = async () => {
|
||||
@@ -519,7 +530,11 @@ export default function DeviceApprovalPage() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Device Authorization Request</h2>
|
||||
<p>Client: {request?.client_id}</p>
|
||||
<p>
|
||||
Client: {request?.client_id
|
||||
? knownClients[request.client_id] ?? `Unrecognized client (${request.client_id})`
|
||||
: "Loading..."}
|
||||
</p>
|
||||
<p>Scopes: {request?.scope || "None"}</p>
|
||||
<p>
|
||||
Resources: {Array.isArray(request?.resource)
|
||||
|
||||
Reference in New Issue
Block a user