设备授权

RFC 8628 CLI Smart TV IoT

设备授权插件实现了 OAuth 2.0 设备授权许可(RFC 8628),使具有有限输入能力的设备(如智能电视、命令行应用、物联网设备和游戏主机)能够进行身份验证。

【The Device Authorization plugin implements the OAuth 2.0 Device Authorization Grant (RFC 8628), enabling authentication for devices with limited input capabilities such as smart TVs, CLI applications, IoT devices, and gaming consoles.】

试一试

【Try It Out】

你现在可以使用 Better Auth CLI 测试设备授权流程:

【You can test the device authorization flow right now using the Better Auth CLI:】

npx @better-auth/cli login

这将演示完整的设备授权流程,包括:

  1. 从 Better Auth 演示服务器请求设备代码
  2. 显示供你输入的用户代码
  3. 打开浏览器访问验证页面
  4. 轮询以完成授权

【This will demonstrate the complete device authorization flow by:

  1. Requesting a device code from the Better Auth demo server
  2. Displaying a user code for you to enter
  3. Opening your browser to the verification page
  4. Polling for authorization completion】

CLI 登录命令是一个演示功能,它连接到 Better Auth 演示服务器,以展示设备授权流程的实际操作。

安装

【Installation】

将插件添加到你的认证配置

将设备授权插件添加到你的服务器配置中。

auth.ts
import { betterAuth } from "better-auth";
import { deviceAuthorization } from "better-auth/plugins"; 

export const auth = betterAuth({
  // ... other config
  plugins: [
    deviceAuthorization({ 
      verificationUri: "/device", 
    }), 
  ],
});

迁移数据库

运行迁移或生成架构以将必要的表添加到数据库中。

npx @better-auth/cli migrate
npx @better-auth/cli generate

See the Schema section to add the fields manually.

添加客户端插件

将设备授权插件添加到你的客户端。

auth-client.ts
import { createAuthClient } from "better-auth/client";
import { deviceAuthorizationClient } from "better-auth/client/plugins"; 

export const authClient = createAuthClient({
  plugins: [
    deviceAuthorizationClient(), 
  ],
});

运作原理

【How It Works】

设备流程遵循以下步骤:

【The device flow follows these steps:】

  1. 设备请求代码:设备从授权服务器请求设备代码和用户代码
  2. 用户授权:用户访问验证网址并输入用户代码
  3. 设备轮询令牌:设备会轮询服务器,直到用户完成授权
  4. 访问已授权:一旦获得授权,设备将收到一个访问令牌

基本用法

【Basic Usage】

请求设备授权

【Requesting Device Authorization】

要启动设备授权,请使用客户端 ID 调用 device.code

【To initiate device authorization, call device.code with the client ID:】

POST
/device/code
const { data, error } = await authClient.device.code({    client_id, // required    scope,});
PropDescriptionType
client_id
The OAuth client identifier
string;
scope?
Space-separated list of requested scopes (optional)
string;

使用示例:

【Example usage:】

const { data } = await authClient.device.code({
  client_id: "your-client-id",
  scope: "openid profile email",
});

if (data) {
  console.log(`User code: ${data.user_code}`);
  console.log(`Verification URL: ${data.verification_uri}`);
  console.log(`Complete verification URL: ${data.verification_uri_complete}`);
}

正在轮询令牌

【Polling for Token】

显示用户代码后,轮询以获取访问令牌:

【After displaying the user code, poll for the access token:】

POST
/device/token
const { data, error } = await authClient.device.token({    grant_type, // required    device_code, // required    client_id, // required});
PropDescriptionType
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;

示例轮询实现:

【Example polling implementation:】

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();

用户授权流程

【User Authorization Flow】

用户授权流程需要两个步骤:

  1. 代码验证:检查输入的用户代码是否有效
  2. 授权:用户必须身份验证以批准或拒绝设备

【The user authorization flow requires two steps:

  1. Code Verification: Check if the entered user code is valid
  2. Authorization: User must be authenticated to approve/deny the device】

用户必须经过身份验证后才能批准或拒绝设备授权请求。如果未经过身份验证,请将其重定向到登录页面,并附带返回网址。

创建一个页面,让用户可以输入他们的代码:

【Create a page where users can enter their code:】

app/device/page.tsx
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>
  );
}

批准或拒绝设备

【Approving or Denying Device】

用户必须经过身份验证才能批准或拒绝设备授权请求:

【Users must be authenticated to approve or deny device authorization requests:】

批准设备

【Approve Device】

POST
/device/approve
const { data, error } = await authClient.device.approve({    userCode, // required});
PropDescriptionType
userCode
The user code to approve
string;

拒绝设备

【Deny Device】

POST
/device/deny
const { data, error } = await authClient.device.deny({    userCode, // required});
PropDescriptionType
userCode
The user code to deny
string;

示例批准页面

【Example Approval Page】

app/device/approve/page.tsx
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>
  );
}

高级配置

【Advanced Configuration】

客户端验证

【Client Validation】

你可以验证客户端 ID,以确保只有授权的应用可以使用设备流程:

【You can validate client IDs to ensure only authorized applications can use the device flow:】

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);
  },
})

自定义代码生成

【Custom Code Generation】

自定义设备和用户代码的生成方式:

【Customize how device and user codes are generated:】

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 Handling】

设备流程定义了特定的错误代码:

【The device flow defines specific error codes:】

错误代码描述
authorization_pending用户尚未批准(继续轮询)
slow_down轮询过于频繁(增加间隔)
expired_token设备代码已过期
access_denied用户拒绝授权
invalid_grant无效的设备代码或客户端 ID

示例:命令行应用

【Example: CLI Application】

这是一个基于实际演示的完整 CLI 应用示例:

【Here's a complete example for a CLI application based on the actual demo:】

要在 API 请求中使用访问令牌,请确保已将 Bearer 插件 添加到你的身份验证实例中。

cli-auth.ts
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 to verification page
    const urlToOpen = verification_uri_complete || verification_uri;
    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);
});

安全注意事项

【Security Considerations】

  1. 速率限制:该插件执行轮询间隔以防止滥用
  2. 代码过期:设备和用户代码在配置的时间后过期(默认:30 分钟)
  3. 客户端验证:在生产环境中始终验证客户端ID以防止未经授权的访问
  4. 仅限 HTTPS:在生产环境中始终使用 HTTPS 进行设备授权
  5. 用户代码格式:用户代码使用有限的字符集(排除易混淆的字符如0/O、1/I)以减少输入错误
  6. 需要身份验证:用户必须经过身份验证才能批准或拒绝设备请求

选项

【Options】

服务器

【Server】

verificationUri:用户可以输入设备代码的验证页面的 URL。将其与你的验证页面的路由匹配。在响应中以 verification_uri 返回。可以是绝对 URL(例如 https://example.com/device)或相对路径(例如 /device)。默认值:/device

expiresIn:设备代码的过期时间。默认值:30m(30 分钟)。

间隔:最小轮询间隔。默认值:5s(5 秒)。

userCodeLength:用户代码的长度。默认值:8

deviceCodeLength:设备代码的长度。默认值:40

generateDeviceCode:自定义函数,用于生成设备代码。返回一个字符串或 Promise<string>

generateUserCode:用于生成用户代码的自定义函数。返回一个字符串或 Promise<string>

validateClient:用于验证客户端 ID 的函数。接收一个 clientId 并返回布尔值或 Promise<boolean>

onDeviceAuthRequest:当请求设备授权时调用的钩子。接收 clientId 和可选的 scope。

客户端

【Client】

没有特定于客户端的配置选项。该插件添加了以下方法:

【No client-specific configuration options. The plugin adds the following methods:】

  • device():验证用户代码的有效性
  • device.code():请求设备和用户代码
  • device.token():轮询获取访问令牌
  • device.approve():批准设备(需要身份验证)
  • device.deny():拒绝设备(需要身份验证)

架构

【Schema】

该插件需要一个新表来存储设备授权数据。

【The plugin requires a new table to store device authorization data.】

表名:deviceCode

【Table Name: deviceCode

Field NameTypeKeyDescription
idstringUnique identifier for the device authorization request
deviceCodestring-The device verification code
userCodestring-The user-friendly code for verification
userIdstringThe ID of the user who approved/denied
clientIdstringThe OAuth client identifier
scopestringRequested OAuth scopes
statusstring-Current status: pending, approved, or denied
expiresAtDate-When the device code expires
lastPolledAtDateLast time the device polled for status
pollingIntervalnumberMinimum seconds between polls
createdAtDate-When the request was created
updatedAtDate-When the request was last updated