---
title: "授权码与 PKCE"
description: "通过浏览器完成用户登录和授权，再以一次性授权码换取令牌。"
url: "https://auth.kai.com/docs/authorization-code"
language: "zh-CN"
version: "Better Auth 1.7.1"
updated: "2026-09-22"
---

# 授权码与 PKCE

通过浏览器完成用户登录和授权，再以一次性授权码换取令牌。

## 发现服务

读取 `https://auth.kai.com/.well-known/openid-configuration`，要求返回的 `issuer` 与配置的 `https://auth.kai.com` 完全一致。使用文档返回的 authorization、token、JWKS 和 UserInfo URL，不猜测 `/api` 路径。

## 创建授权事务

每次登录生成高熵随机 `state`、`nonce` 和 PKCE `code_verifier`。`code_challenge` 是 verifier 的 SHA-256 摘要经 base64url 编码，`code_challenge_method` 固定 `S256`。将事务保存到当前浏览器或服务端会话，设置短有效期，消费后立即删除。

```text
GET /api/auth/oauth2/authorize
  ?client_id=YOUR_CLIENT_ID
  &response_type=code
  &redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback
  &scope=openid%20profile%20email
  &state=RANDOM_STATE
  &nonce=RANDOM_NONCE
  &code_challenge=BASE64URL_SHA256_VERIFIER
  &code_challenge_method=S256
```

这是纯登录示例，故不携带 `resource`。若要访问资源服务器，在用户授权阶段发送已获准的准确 resource identifier，并在兑换时保持该授权目标，不能在拿到 code 后扩展资源或 scope。

## 要求重新验证

确实需要用户重新证明身份时使用标准 `prompt=login`，或用 `max_age` 限定最近认证时间。Auth 对已有登录会话提供独立的重新验证与取消流程；完成密码、OTP、Passkey 或 Lark 所需的全部验证后，原地更新会话证据和 `auth_time`，保留 SID 与已有应用授权，不额外创建会话。普通登录不应无条件强制重新验证。

`prompt=none` 保持静默语义，无法满足条件时返回协议错误，不打开交互页面。账号安全设置改变导致的令牌撤销与普通重新验证不同，见[令牌校验与 claims](/docs/claims)。

## 高风险操作的额外验证

应用需要在高风险操作前再次确认当前用户时，创建新的授权码事务，在原有 `openid`、`state`、`nonce`、PKCE 参数之外添加：

```text
acr_values=urn:kai:acr:step-up
```

这个值请求 KAI 的额外验证策略，不指定验证方式，也不代表某个统一的 MFA 等级。KAI 按账号已配置的安全方式选择流程：只要配置了高等级安全因子就不能退回密码；账号没有配置这类因子时才允许密码。设备暂不可用、验证失败或被限流不会解除该限制。没有可用验证方式时，用户需要先解决账号访问问题。

已登录用户在独立页面完成验证，过程保留当前浏览器会话。普通登录本身不能替代这次额外验证。验证凭据绑定本次应用、用户、浏览器会话和完整授权事务，只能签发一次授权码；其他应用或另一笔 `state` / `nonce` 事务不能借用。可同时使用 `prompt=login` 或 `max_age`，整个恢复与授权流程保留其绑定，不要求用户反复登录。

回调仍使用应用登记的 `redirect_uri`，仍须完成标准授权码兑换和 ID token 校验。随后额外检查：

- `acr` 必须等于 `urn:kai:acr:step-up`；
- `kai_step_up.status` 为 `verified` 时表示已完成高等级额外验证，为 `password_only` 时只表示完成密码验证；由业务方决定密码结果是否足够；
- `auth_time` 是这次证明的秒级时间，业务方应检查自己的新鲜度窗口，并确认 `iss` / `sub` 与当前业务账号一致。

应用不会获得具体因子、设备或 `amr` 明细。用户主动取消时返回标准 OAuth 错误；关闭页面、断网或超时可能没有回调，应用应按未完成处理。这些情况都不会签发表示成功的 ID token。`prompt=none` 在缺少本次已完成证明时返回 `interaction_required`，未登录时返回 `login_required`。

刷新令牌不会执行新验证，也不会更新这次结果的 `auth_time`。需要更新证明时必须重新发起上述授权事务。此结果只证明身份验证完成，不等于用户批准了转账、修改资料等具体业务操作；业务操作及其参数仍由应用自行确认和授权。

完整请求示例、三种结果、字段类型、失败处理和 CLI 边界见[额外身份验证](/docs/step-up)。

## 接收回调

只在本应用登记的回调接收响应。拒绝缺失、重复、不匹配或过期的 `state`；检查授权响应中的 `iss` 与预期 issuer 一致。处理 `error`（例如用户拒绝）时也验证事务。不要把 code、令牌或完整回调 URL 发送到日志或分析服务。

验证成功后立即消费事务，用与授权请求完全相同的 `redirect_uri` 和原 `code_verifier` 兑换 code。授权码只可使用一次，失败后重新发起登录。

## 兑换令牌

向 Discovery 的 `token_endpoint` POST `application/x-www-form-urlencoded`：

```text
grant_type=authorization_code
client_id=YOUR_CLIENT_ID
code=AUTHORIZATION_CODE
redirect_uri=https://app.example.com/oauth/callback
code_verifier=ORIGINAL_VERIFIER
```

public 客户端不发送 secret；confidential 客户端另外使用 HTTP Basic 认证。不要在前端复制 confidential secret，也不要为了方便测试改变现有应用的客户端类型。

响应包含 `access_token`、`token_type`、`expires_in`；有效 scope 含 `openid` 时提供 ID token，允许 refresh grant 且获准 `offline_access` 时提供 refresh token。检查实际返回的 `scope`，资源策略可能缩小权限。

## 验证身份

用 JWKS 验证 ID token 签名，检查 `iss`、`aud`（本应用的 client ID）、`exp`、`nonce`，必要时按 OIDC 规范检查 `azp` 和 `at_hash`。仅解码 JWT 不等于验证。使用经过验证的 `iss` 与 `sub` 识别账号，不以邮箱作为永久主键。

向 UserInfo 发送 `Authorization: Bearer ACCESS_TOKEN`。其 `sub` 必须与 ID token 一致。`profile`、`email` 控制可读的用户信息，不保证 ID token 内包含所有资料字段。

## 刷新与退出

refresh token 只在用户已授权 `offline_access` 时使用；客户端需要支持刷新轮换，成功后使用新返回的 refresh token。不要并行刷新同一授权链，也不要把原始 refresh token 显示在测试报告中。

退出应用和退出 Auth 会话是不同动作。跨应用退出、令牌撤销及回跳请按[退出与授权撤销](/docs/logout)实现。
