设备授权
RFC 8628 CLI Smart TV IoT
设备授权插件实现了 OAuth 2.0 设备授权授予类型 (RFC 8628),它使具有有限输入能力的设备(如智能电视、CLI 应用程序、IoT 设备和游戏机)能够进行身份验证。
试用
您可以使用 Better Auth CLI 立即测试设备授权流程:
npx @better-auth/cli login这将通过以下方式演示完整的设备授权流程:
- 从 Better Auth 演示服务器请求设备代码
- 显示用户代码供您输入
- 在浏览器中打开验证页面
- 轮询授权完成
CLI 登录命令是一个演示功能,它连接到 Better Auth 演示服务器,以展示设备授权流程的实际运行。
安装
将插件添加到您的认证配置
将设备授权插件添加到服务器配置中。
import { betterAuth } from "better-auth";
import { deviceAuthorization } from "better-auth/plugins";
export const auth = betterAuth({
// ... other config
plugins: [
deviceAuthorization({
// Optional configuration
expiresIn: "30m", // Device code expiration time
interval: "5s", // Minimum polling interval
}),
],
});迁移数据库
运行迁移或生成架构,以向数据库添加必要的表。
npx @better-auth/cli migratenpx @better-auth/cli generate请参阅 Schema 部分以手动添加字段。
添加客户端插件
将设备授权插件添加到客户端。
import { createAuthClient } from "better-auth/client";
import { deviceAuthorizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [
deviceAuthorizationClient(),
],
});工作原理
设备流程遵循以下步骤:
- 设备请求代码:设备从授权服务器请求设备代码和用户代码
- 用户授权:用户访问验证 URL 并输入用户代码
- 设备轮询令牌:设备轮询服务器,直到用户完成授权
- 访问授予:授权完成后,设备接收访问令牌
基本用法
请求设备授权
要启动设备授权,请使用客户端 ID 调用 device.code:
const { data, error } = await authClient.device.code({ client_id, // required scope,});| Prop | Description | Type |
|---|---|---|
client_id | The OAuth client identifier | string; |
scope? | Space-separated list of requested scopes (optional) | string; |
示例用法:
const { data } = await authClient.device.code({
client_id: "your-client-id",
scope: "openid profile email",
});
if (data) {
console.log(`Please visit: ${data.verification_uri}`);
console.log(`And enter code: ${data.user_code}`);
}轮询令牌
显示用户代码后,轮询访问令牌:
const { data, error } = await authClient.device.token({ grant_type, // required device_code, // required client_id, // required});| Prop | Description | Type |
|---|---|---|
grant_type | Must be "urn:ietf:params:oauth:grant-type:device_code" | string; |
device_code | The device code from the initial request | string; |
client_id | The OAuth client identifier | string; |
示例轮询实现:
let pollingInterval = 5; // Start with 5 seconds
const pollForToken = async () => {
const { data, error } = await authClient.device.token({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code,
client_id: yourClientId,
fetchOptions: {
headers: {
"user-agent": `My CLI`,
},
},
});
if (data?.access_token) {
console.log("Authorization successful!");
} else if (error) {
switch (error.error) {
case "authorization_pending":
// Continue polling
break;
case "slow_down":
pollingInterval += 5;
break;
case "access_denied":
console.error("Access was denied by the user");
return;
case "expired_token":
console.error("The device code has expired. Please try again.");
return;
default:
console.error(`Error: ${error.error_description}`);
return;
}
setTimeout(pollForToken, pollingInterval * 1000);
}
};
pollForToken();用户授权流程
用户授权流程需要两个步骤:
- 代码验证:检查输入的用户代码是否有效
- 授权:用户必须经过身份验证才能批准/拒绝设备
用户必须先经过身份验证才能批准或拒绝设备授权请求。如果未经过身份验证,请将他们重定向到登录页面,并提供返回 URL。
创建一个用户可以输入代码的页面:
export default function DeviceAuthorizationPage() {
const [userCode, setUserCode] = useState("");
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
try {
// Format the code: remove dashes and convert to uppercase
const formattedCode = userCode.trim().replace(/-/g, "").toUpperCase();
// Check if the code is valid using GET /device endpoint
const response = await authClient.device({
query: { user_code: formattedCode },
});
if (response.data) {
// Redirect to approval page
window.location.href = `/device/approve?user_code=${formattedCode}`;
}
} catch (err) {
setError("Invalid or expired code");
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={userCode}
onChange={(e) => setUserCode(e.target.value)}
placeholder="Enter device code (e.g., ABCD-1234)"
maxLength={12}
/>
<button type="submit">Continue</button>
{error && <p>{error}</p>}
</form>
);
}批准或拒绝设备
用户必须经过身份验证才能批准或拒绝设备授权请求:
批准设备
const { data, error } = await authClient.device.approve({ userCode, // required});| Prop | Description | Type |
|---|---|---|
userCode | The user code to approve | string; |
拒绝设备
const { data, error } = await authClient.device.deny({ userCode, // required});| Prop | Description | Type |
|---|---|---|
userCode | The user code to deny | string; |
示例批准页面
export default function DeviceApprovalPage() {
const { user } = useAuth(); // Must be authenticated
const searchParams = useSearchParams();
const userCode = searchParams.get("userCode");
const [isProcessing, setIsProcessing] = useState(false);
const handleApprove = async () => {
setIsProcessing(true);
try {
await authClient.device.approve({
userCode: userCode,
});
// Show success message
alert("Device approved successfully!");
window.location.href = "/";
} catch (error) {
alert("Failed to approve device");
}
setIsProcessing(false);
};
const handleDeny = async () => {
setIsProcessing(true);
try {
await authClient.device.deny({
userCode: userCode,
});
alert("Device denied");
window.location.href = "/";
} catch (error) {
alert("Failed to deny device");
}
setIsProcessing(false);
};
if (!user) {
// Redirect to login if not authenticated
window.location.href = `/login?redirect=/device/approve?user_code=${userCode}`;
return null;
}
return (
<div>
<h2>Device Authorization Request</h2>
<p>A device is requesting access to your account.</p>
<p>Code: {userCode}</p>
<button onClick={handleApprove} disabled={isProcessing}>
Approve
</button>
<button onClick={handleDeny} disabled={isProcessing}>
Deny
</button>
</div>
);
}高级配置
客户端验证
您可以验证客户端 ID,以确保只有授权的应用程序才能使用设备流程:
deviceAuthorization({
validateClient: async (clientId) => {
// Check if client is authorized
const client = await db.oauth_clients.findOne({ id: clientId });
return client && client.allowDeviceFlow;
},
onDeviceAuthRequest: async (clientId, scope) => {
// Log device authorization requests
await logDeviceAuthRequest(clientId, scope);
},
})自定义代码生成
自定义设备代码和用户代码的生成方式:
deviceAuthorization({
generateDeviceCode: async () => {
// Custom device code generation
return crypto.randomBytes(32).toString("hex");
},
generateUserCode: async () => {
// Custom user code generation
// Default uses: ABCDEFGHJKLMNPQRSTUVWXYZ23456789
// (excludes 0, O, 1, I to avoid confusion)
const charset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let code = "";
for (let i = 0; i < 8; i++) {
code += charset[Math.floor(Math.random() * charset.length)];
}
return code;
},
})错误处理
设备流程定义了特定的错误代码:
| Error Code | Description |
|---|---|
authorization_pending | 用户尚未批准(继续轮询) |
slow_down | 轮询过于频繁(增加间隔) |
expired_token | 设备代码已过期 |
access_denied | 用户拒绝了授权 |
invalid_grant | 无效的设备代码或客户端 ID |
示例:CLI 应用程序
以下是基于实际演示的 CLI 应用程序的完整示例:
import { createAuthClient } from "better-auth/client";
import { deviceAuthorizationClient } from "better-auth/client/plugins";
import open from "open";
const authClient = createAuthClient({
baseURL: "http://localhost:3000",
plugins: [deviceAuthorizationClient()],
});
async function authenticateCLI() {
console.log("🔐 Better Auth Device Authorization Demo");
console.log("⏳ Requesting device authorization...");
try {
// Request device code
const { data, error } = await authClient.device.code({
client_id: "demo-cli",
scope: "openid profile email",
});
if (error || !data) {
console.error("❌ Error:", error?.error_description);
process.exit(1);
}
const {
device_code,
user_code,
verification_uri,
verification_uri_complete,
interval = 5,
} = data;
console.log("\n📱 Device Authorization in Progress");
console.log(`Please visit: ${verification_uri}`);
console.log(`Enter code: ${user_code}\n`);
// Open browser with the complete URL
const urlToOpen = verification_uri_complete || verification_uri;
if (urlToOpen) {
console.log("🌐 Opening browser...");
await open(urlToOpen);
}
console.log(`⏳ Waiting for authorization... (polling every ${interval}s)`);
// Poll for token
await pollForToken(device_code, interval);
} catch (err) {
console.error("❌ Error:", err.message);
process.exit(1);
}
}
async function pollForToken(deviceCode: string, interval: number) {
let pollingInterval = interval;
return new Promise<void>((resolve) => {
const poll = async () => {
try {
const { data, error } = await authClient.device.token({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code: deviceCode,
client_id: "demo-cli",
});
if (data?.access_token) {
console.log("\nAuthorization Successful!");
console.log("Access token received!");
// Get user session
const { data: session } = await authClient.getSession({
fetchOptions: {
headers: {
Authorization: `Bearer ${data.access_token}`,
},
},
});
console.log(`Hello, ${session?.user?.name || "User"}!`);
resolve();
process.exit(0);
} else if (error) {
switch (error.error) {
case "authorization_pending":
// Continue polling silently
break;
case "slow_down":
pollingInterval += 5;
console.log(`⚠️ Slowing down polling to ${pollingInterval}s`);
break;
case "access_denied":
console.error("❌ Access was denied by the user");
process.exit(1);
break;
case "expired_token":
console.error("❌ The device code has expired. Please try again.");
process.exit(1);
break;
default:
console.error("❌ Error:", error.error_description);
process.exit(1);
}
}
} catch (err) {
console.error("❌ Network error:", err.message);
process.exit(1);
}
// Schedule next poll
setTimeout(poll, pollingInterval * 1000);
};
// Start polling
setTimeout(poll, pollingInterval * 1000);
});
}
// Run the authentication flow
authenticateCLI().catch((err) => {
console.error("❌ Fatal error:", err);
process.exit(1);
});安全注意事项
- 速率限制:插件强制执行轮询间隔以防止滥用
- 代码过期:设备代码和用户代码在配置的时间后过期(默认:30 分钟)
- 客户端验证:在生产环境中始终验证客户端 ID 以防止未授权访问
- 仅 HTTPS:在生产环境中始终使用 HTTPS 进行设备授权
- 用户代码格式:用户代码使用有限的字符集(排除相似字符,如 0/O、1/I)以减少输入错误
- 需要身份验证:用户必须经过身份验证才能批准或拒绝设备请求
选项
服务器
expiresIn:设备代码的过期时间。默认:"30m" (30 分钟)。
interval:最小轮询间隔。默认:"5s" (5 秒)。
userCodeLength:用户代码的长度。默认:8。
deviceCodeLength:设备代码的长度。默认:40。
generateDeviceCode:生成设备代码的自定义函数。返回字符串或 Promise<string>。
generateUserCode:生成用户代码的自定义函数。返回字符串或 Promise<string>。
validateClient:验证客户端 ID 的函数。接收 clientId 并返回布尔值或 Promise<boolean>。
onDeviceAuthRequest:设备授权请求时调用的钩子。接收 clientId 和可选的 scope。
客户端
没有客户端特定的配置选项。该插件添加了以下方法:
- device():验证用户代码的有效性
- device.code():请求设备和用户代码
- device.token():轮询访问令牌
- device.approve():批准设备(需要身份验证)
- device.deny():拒绝设备(需要身份验证)
Schema
该插件需要一个新表来存储设备授权数据。
表名:deviceCode
| Field Name | Type | Key | Description |
|---|---|---|---|
| id | string | 设备授权请求的唯一标识符 | |
| deviceCode | string | - | 设备验证代码 |
| userCode | string | - | 用于验证的用户友好代码 |
| userId | string | 批准/拒绝该请求的用户 ID | |
| clientId | string | OAuth 客户端标识符 | |
| scope | string | 请求的 OAuth 作用域 | |
| status | string | - | 当前状态:pending、approved 或 denied |
| expiresAt | Date | - | 设备代码过期时间 |
| lastPolledAt | Date | 设备最后一次轮询状态的时间 | |
| pollingInterval | number | 轮询之间的最小秒数 | |
| createdAt | Date | - | 请求创建时间 |
| updatedAt | Date | - | 请求最后更新时间 |