覆盖每种 API 认证方式的测试端点

Basic、Digest、Bearer 令牌、API 密钥、HMAC 签名、JWT、OAuth 1.0a 和带 PKCE 的 OAuth 2.0,附凭据。

示例最后一次针对 API 运行于

想在不搭建身份提供方的情况下测试 API 客户端的认证,可以用 https://api.testingapis.com/api/auth登录后查看 上的公共测试端点。每种方式一个端点:HTTP Basic、Digest、固定的 Bearer 令牌、放在请求头、查询参数或 Cookie 中的 API 密钥,以及 HMAC 请求签名。JWT 登录提供刷新令牌和用于反向测试的过期令牌,OAuth 2.0 服务器支持 PKCE 和客户端凭据, OAuth 1.0a 服务器会校验签名请求。凭据是固定且公开的,列在下面,也可以通过 GET /api/auth登录后查看 查看。凭据错误会返回规范的 401,有效令牌但缺少相应角色会返回 403。

端点与凭据

方式端点凭据
HTTP BasicGET /api/auth/basic登录后查看apibee / password123
HTTP Digest(MD5,qop=auth)GET /api/auth/digest登录后查看apibee / password123
Bearer 令牌GET /api/auth/bearer登录后查看Authorization: Bearer apibee-token-123
API 密钥(请求头)GET /api/auth/api-key/header登录后查看X-API-Key: apibee-key-123
API 密钥(查询参数)GET /api/auth/api-key/query登录后查看?api_key=apibee-key-123
API 密钥(Cookie)GET /api/auth/api-key/cookie登录后查看Cookie: api_key=apibee-key-123
HMAC-SHA256 签名POST /api/auth/hmac登录后查看密钥 apibee-hmac-secret
角色与 scopeGET /api/auth/roles/admin登录后查看admin-token, user-token, readonly-token
JWTPOST /api/auth/jwt/login登录后查看admin / admin123、user / user123
OAuth 2.0/api/auth/oauth/*登录后查看apibee-client / apibee-secret,公共客户端 apibee-public
OAuth 1.0a/api/auth/oauth1/*登录后查看Consumer apibee-consumer-key / apibee-consumer-secret,令牌 apibee-access-token / apibee-token-secret

Basic 与 Digest

密码缺失或错误时返回 401,并带有 WWW-Authenticate 响应头,正是它让浏览器和 HTTP 库提示输入凭据或带上凭据重试:

Terminal
$ curl -i -u apibee:password123 https://api.testingapis.com/api/auth/basic
HTTP/1.1 200 OK

{"authenticated":true,"scheme":"basic","user":"apibee"}

$ curl -i https://api.testingapis.com/api/auth/basic
HTTP/1.1 401 Unauthorized
www-authenticate: Basic realm="apibee", charset="UTF-8"

{"status":401,"error":"Unauthorized","message":"Missing or malformed Basic Authorization header."}
这个示例使用的是面向已登录成员的端点。登录创建免费账户

Digest 需要两次往返。第一个 401 带有一个 nonce,客户端用密码的哈希而不是密码本身来回应。curl 的 --digest 会完成这两步:

Terminal
$ curl -i --digest -u apibee:password123 https://api.testingapis.com/api/auth/digest
HTTP/1.1 401 Unauthorized
www-authenticate: Digest realm="apibee", qop="auth", algorithm=MD5, nonce="MTc5...", opaque="069d..."

HTTP/1.1 200 OK

{"authenticated":true,"scheme":"digest","user":"apibee","qop":"auth","nc":"00000001"}
这个示例使用的是面向已登录成员的端点。登录创建免费账户

要测试需要应对“隐藏受保护资源”的服务器的客户端,/api/auth/basic-hidden/{user}/{pass}登录后查看 在凭据不匹配时返回 404 而不是 401。

Bearer 令牌与 API 密钥

三个 API 密钥端点在不同位置接受同一个密钥,所以你可以检查客户端是否把它放在了 API 期望的位置。错误的 Bearer 令牌会返回 WWW-Authenticate: Bearer error="invalid_token"。

GET/api/auth/api-key/header
这个请求使用的是面向已登录成员的端点。登录创建免费账户

401 与 403

401 表示服务器不知道你是谁:没有凭据,或者凭据不对。403 表示它很清楚你是谁,但你没有权限。客户端应该区别对待两者。收到 401 后可以重新登录或刷新令牌;收到 403 后,重新登录也无济于事。角色端点会分别返回这两种情况:

Terminal
$ curl -i https://api.testingapis.com/api/auth/roles/admin
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="apibee"

$ curl -i -H "Authorization: Bearer user-token" https://api.testingapis.com/api/auth/roles/admin
HTTP/1.1 403 Forbidden

{"status":403,"error":"Forbidden","message":"This endpoint requires the 'admin' role.","roles":["user"],"scopes":["read","write"]}

$ curl -H "Authorization: Bearer admin-token" https://api.testingapis.com/api/auth/roles/admin
{"authorized":true,"user":"Alice Admin","roles":["admin","user"],"scopes":["read","write","delete"]}
这个示例使用的是面向已登录成员的端点。登录创建免费账户

DELETE /api/auth/roles/resource登录后查看 检查的是 scope 而不是角色。readonly-token 只有 read,所以会收到一个 403,并指出缺少的 delete scope。

HMAC 请求签名

支付和 webhook 类 API 常常对请求签名,而不是直接发送密钥。这里的签名是对 timestamp + "." + raw body 计算的 HMAC-SHA256,以小写十六进制表示,放在 X-Signature 中,时间戳放在 X-Timestamp 中。要对你实际发送的字节签名。签名之后重新序列化 JSON,是最常见的签名失效原因。

JavaScript
import { createHmac } from "node:crypto";

const secret = "apibee-hmac-secret";
const body = JSON.stringify({ orderId: 1001, amount: 49.99 });
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = createHmac("sha256", secret)
  .update(`${timestamp}.${body}`)
  .digest("hex");

const res = await fetch("https://api.testingapis.com/api/auth/hmac", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Timestamp": timestamp,
    "X-Signature": signature,
  },
  body,
});
console.log(res.status, await res.json());
这个示例使用的是面向已登录成员的端点。登录创建免费账户
输出
200 { authenticated: true, scheme: 'hmac-sha256', timestamp: 1791341858, bodyLength: 31 }

# The same signature sent with a different body:
401 {
  status: 401,
  error: 'Unauthorized',
  message: 'Signature mismatch.',
  hint: 'X-Signature = lowercase hex HMAC-SHA256(secret, X-Timestamp + "." + raw request body). Optional \'sha256=\' prefix.',
  bodyLength: 30
}

想手动核对签名,可以用哈希生成器在浏览器中计算 HMAC-SHA256。

JWT 登录、过期与刷新

/api/auth/jwt/login登录后查看 返回一个有效期 15 分钟的签名访问令牌和一个刷新令牌。刷新令牌会轮换:每次刷新都返回一个新的,旧的随即失效。/api/auth/jwt/expired登录后查看 提供一个签名正确但已经过期的令牌,让你不用等 15 分钟就能测试过期路径。

JavaScript
const BASE = "https://api.testingapis.com/api/auth/jwt";

const login = await fetch(`${BASE}/login`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ username: "user", password: "user123" }),
});
const tokens = await login.json();
// { accessToken, tokenType: "Bearer", expiresIn: 900, refreshToken, user }

const auth = { Authorization: `Bearer ${tokens.accessToken}` };
console.log((await fetch(`${BASE}/me`, { headers: auth })).status);    // 200
console.log((await fetch(`${BASE}/admin`, { headers: auth })).status); // 403: role is "user"

// A correctly signed token that expired an hour ago, for the negative test.
const { accessToken: expired } = await (await fetch(`${BASE}/expired`)).json();
const old = await fetch(`${BASE}/me`, { headers: { Authorization: `Bearer ${expired}` } });
console.log(old.status, old.headers.get("www-authenticate"));
// 401 Bearer error="invalid_token", error_description="The token expired at '...'"

// Refresh tokens rotate: use the new one next time.
const refreshed = await fetch(`${BASE}/refresh`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ refreshToken: tokens.refreshToken }),
});
console.log(refreshed.status); // 200
这个示例使用的是面向已登录成员的端点。登录创建免费账户

把其中任意一个令牌粘贴到 JWT 解码器中,即可查看它的声明和过期时间。

带 PKCE 的 OAuth 2.0

OAuth 服务器支持带 PKCE 的授权码模式、客户端凭据、密码和刷新令牌模式,以及令牌自省和撤销。授权会以用户 apibee 的身份自动批准,所以整个流程可以在一个脚本中跑完。localhost 或 127.0.0.1(任意端口)上的重定向 URI 以及 Postman 的回调地址都会收到真实的 302 重定向。

带 PKCE 的授权码模式

JavaScript
import { createHash, randomBytes } from "node:crypto";

const BASE = "https://api.testingapis.com/api/auth/oauth";
const redirectUri = "http://localhost:3000/callback";

// 1. A random code verifier and its S256 challenge.
const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");

// 2. Send the user to /authorize. This server approves at once and redirects
//    to the redirect URI with ?code=...&state=...
const authorize = new URL(`${BASE}/authorize`);
authorize.search = new URLSearchParams({
  response_type: "code",
  client_id: "apibee-public",
  redirect_uri: redirectUri,
  scope: "read profile",
  state: "xyz123",
  code_challenge: challenge,
  code_challenge_method: "S256",
});
const redirect = await fetch(authorize, { redirect: "manual" });
const code = new URL(redirect.headers.get("location")).searchParams.get("code");

// 3. Exchange the code and the verifier for tokens.
const tokenRes = await fetch(`${BASE}/token`, {
  method: "POST",
  body: new URLSearchParams({
    grant_type: "authorization_code",
    client_id: "apibee-public",
    code,
    redirect_uri: redirectUri,
    code_verifier: verifier,
  }),
});
const tokens = await tokenRes.json();

// 4. Call a protected endpoint.
const me = await fetch(`${BASE}/userinfo`, {
  headers: { Authorization: `Bearer ${tokens.access_token}` },
});
console.log(me.status, await me.json());
这个示例使用的是面向已登录成员的端点。登录创建免费账户
输出
200 {
  sub: 'apibee',
  name: 'TestingAPIs Tester',
  preferred_username: 'apibee',
  email: null,
  email_verified: null
}

PKCE 客户端应处理的两种失败:

Text
# Sending the same code a second time
400 { error: 'invalid_grant', error_description: 'Authorization code is invalid, expired or already used.' }

# Sending a code_verifier that doesn't match the challenge
400 { error: 'invalid_grant', error_description: 'PKCE verification failed: code_verifier does not match code_challenge.' }

客户端凭据

用于服务之间的调用,不涉及任何用户:

Terminal
$ curl -X POST https://api.testingapis.com/api/auth/oauth/token \
  -d "grant_type=client_credentials&client_id=apibee-client&client_secret=apibee-secret&scope=read"

{"access_token":"oat_3fc4...","token_type":"Bearer","expires_in":3600,"scope":"read"}
这个示例使用的是面向已登录成员的端点。登录创建免费账户

OAuth 1.0a 签名请求

OAuth 1.0a 不是单独发送令牌,而是用两个密钥(consumer 密钥和令牌密钥)为每个请求签名。测试服务器接受 HMAC-SHA1、 HMAC-SHA256、HMAC-SHA512 和 PLAINTEXT 签名。可以使用永不过期的固定访问令牌 apibee-access-token / apibee-token-secret,也可以走 three-legged 流程获取自己的令牌。授权会以用户 apibee 的身份自动批准。

  1. POST /api/auth/oauth1/request_token登录后查看,只用 consumer 的 key 和密钥签名,oauth_callback 设为一个 URL 或 oob。
  2. GET /api/auth/oauth1/authorize?oauth_token=…登录后查看 会带着 oauth_verifier 重定向到 localhost 或 Postman 的回调地址;对于 oob,则以 JSON 返回。
  3. POST /api/auth/oauth1/access_token登录后查看,用 request token 及其密钥签名,并带上 oauth_verifier。访问令牌有效期为一小时。
The protected resource, with the static access token:
1. New request: GET https://api.testingapis.com/api/auth/oauth1
2. Authorization → Type: OAuth 1.0
     Signature Method:  HMAC-SHA1
     Consumer Key:      apibee-consumer-key
     Consumer Secret:   apibee-consumer-secret
     Access Token:      apibee-access-token
     Token Secret:      apibee-token-secret
     Add authorization data to: Request Headers
3. Send. The response has "authenticated": true.

The three-legged flow, with the same Authorization settings:
1. POST https://api.testingapis.com/api/auth/oauth1/request_token
   Leave Access Token and Token Secret empty.
   Advanced → Callback URL: oob
2. Open GET https://api.testingapis.com/api/auth/oauth1/authorize?oauth_token=<oauth_token from step 1>
   and copy oauth_verifier from the JSON.
3. POST https://api.testingapis.com/api/auth/oauth1/access_token
     Access Token:  oauth_token from step 1
     Token Secret:  oauth_token_secret from step 1
     Advanced → Verifier: oauth_verifier from step 2
4. Use the oauth_token and oauth_token_secret it returns as
   Access Token and Token Secret for GET /api/auth/oauth1.

cURL 标签页中的 PLAINTEXT 不需要签名库:签名就是签名密钥本身,即 consumer_secret&token_secret,其中 & 以 %26 发送。先用它确认凭据无误,再切换到 HMAC-SHA1 测试你的签名代码。

当签名不匹配时,401 会包含服务器构造的签名基础字符串,方便你和客户端生成的进行对比。值得测试的失败情况:

Text
# Signed with the wrong token secret
401 signature_invalid: Signature mismatch.
details.signatureBaseString: GET&https%3A%2F%2Fapi.testingapis.com%2Fapi%2Fauth%2Foauth1&oauth_consumer_key%3D...

# The same oauth_nonce sent twice
401 nonce_used: This oauth_nonce was already used with this timestamp. Send a new nonce.

# The same request token exchanged twice
401 token_used: This request token was already exchanged for an access token.

OAuth 1.0a 签名规则说明了参数应放在哪里、时间戳的有效窗口以及每个 oauth_problem 代码。

值得编写的反向测试

  • 没有凭据:应返回 401 和 WWW-Authenticate 响应头。
  • 密码错误、密钥错误、令牌被篡改:返回 401,绝不能是 500。
  • 用户有效但缺少角色或 scope:返回 403,客户端不应让用户退出登录。
  • JWT 过期:客户端刷新一次,然后重试原来的请求。
  • 刷新令牌被使用两次:第二次使用失败。
  • OAuth 授权码被使用两次,或 verifier 错误:invalid_grant。
  • 针对另一个请求体计算的 HMAC 签名:401。
  • OAuth 1.0a 请求重复使用 nonce 或签名错误:401 nonce_used 或 signature_invalid。

这些测试背后的概念见认证与授权课程(英文)。