ArcTower-OAuth2接入文档
OAuth 2.0 / OpenID Connect 认证流程文档展路径
OAuth 2.0 / OpenID Connect 认证流程文档#
1. 概述#
AxT 的 SSO 基于 OAuth 2.0 授权码模式(Authorization Code Flow) + OpenID Connect (OIDC) 实现。第三方客户端通过此流程获取 accessToken、refreshToken 以及(可选)idToken,用于访问受保护的 API 资源和获取用户身份信息。
角色说明#
- 客户端(Client): 在 ArcTower 创建 OAuth App 的开发者/应用
- 用户(Resource Owner): 登录并授权的最终用户
- 授权服务器(Authorization Server): ArcTower SSO Provider,部署于
auth.axtrk.com - 资源服务器(Resource Server): ArcTower 用户 API,部署于
api.axtrk.com
部署域划分#
| 域名 | 职责 |
|---|---|
auth.axtrk.com | SSO Issuer —— OIDC Discovery / JWKS / 授权 / Token / UserInfo / 撤销 / 内省 |
api.axtrk.com | 业务 API(含 OAuth App CRUD、第一方用户 API) |
account.axtrk.com | 前端授权页面(承载登录 UI) |
Token 有效期与签名#
| Token | 有效期 | 签名算法 | 说明 |
|---|---|---|---|
| code | 5 分钟 | —— | 临时授权码,一次性使用 |
| accessToken | 10 分钟 | RS256 | 访问令牌,用于请求 API 资源 |
| refreshToken | 14 天 | RS256 | 刷新令牌,原子轮换 + 重放检测 |
| idToken | 10 分钟 | RS256 | OIDC 身份令牌,请求 scope 含 openid 时签发 |
所有 Token 均使用 RS256 非对称签名,按 token 类型隔离密钥。资源服务器可通过 JWKS 端点获取公钥独立验证,无需联系授权服务器。
支持的安全特性#
- ✅ PKCE (RFC 7636) —— S256 / plain 两种方法
- ✅ state 参数 —— 服务端存储并强制校验(防 CSRF)
- ✅ Nonce(OIDC) —— 绑定到 idToken,防重放
- ✅ Refresh Token Rotation —— 每次刷新签发新 RT,旧 RT 失效,检测到重放自动撤销整条链
- ✅ 单点退出(SLO) —— 第一方账户登出/改密时级联撤销所有 OAuth 会话
- ✅ Token 内省(RFC 7662) / Token 撤销(RFC 7009)
2. OIDC Discovery#
所有协议端点和公钥信息可通过标准 Discovery 端点动态发现,客户端应首选使用 Discovery 而非硬编码 URL。
2.1 获取 OIDC 配置#
请求:
GET https://auth.axtrk.com/.well-known/openid-configuration返回示例:
{
"issuer": "https://auth.axtrk.com",
"authorization_endpoint": "https://auth.axtrk.com/api/v1/oauth/authorize/{clientId}",
"token_endpoint": "https://auth.axtrk.com/api/v1/oauth/access",
"userinfo_endpoint": "https://auth.axtrk.com/api/v1/o/me",
"jwks_uri": "https://auth.axtrk.com/.well-known/jwks.json",
"revocation_endpoint": "https://auth.axtrk.com/api/v1/oauth/revoke-token",
"introspection_endpoint": "https://auth.axtrk.com/api/v1/oauth/introspect",
"end_session_endpoint": "https://auth.axtrk.com/api/v1/oauth/logout",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"subject_types_supported": ["pairwise"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["openid", "profile", "email", "phone"],
"token_endpoint_auth_methods_supported": ["client_secret_post"],
"code_challenge_methods_supported": ["S256", "plain"],
"claims_supported": ["sub", "iss", "aud", "exp", "iat", "nonce", "auth_time",
"preferred_username", "picture", "email", "email_verified",
"phone_number", "phone_number_verified"]
}2.2 获取公钥集 (JWKS)#
资源服务器用公钥离线验证 Token 签名。
请求:
GET https://auth.axtrk.com/.well-known/jwks.json返回示例:
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "oauth-v1",
"n": "...(modulus)...",
"e": "AQAB"
},
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "id-v1",
"n": "...(modulus)...",
"e": "AQAB"
}
]
}JWT Header 中的
kid字段决定使用哪把公钥验证;oauth-v1用于 accessToken,id-v1用于 idToken。
3. 标准授权码流程(含 PKCE)#
+-------------------+
| 客户端 |
+-------------------+
|
| 1. 生成 code_verifier + code_challenge = BASE64URL(SHA256(verifier))
| 将用户跳转至 AxT 授权页
| GET https://account.axtrk.com/oauth/authorize
| ?clientId={clientId}
| &redirect_uri={uri}
| &state={state}
| &nonce={nonce} (OIDC 可选)
| &scope=openid+profile+email (含 openid 时触发 OIDC)
| &code_challenge={challenge}
| &code_challenge_method=S256
v
+----------------------+
| 用户登录 + 授权 |
+----------------------+
|
| 2. 授权通过后重定向
| {redirect_uri}?code={code}&state={state}
v
+-------------------+
| 客户端 |
+-------------------+
|
| 3. 校验 state == 原值 (否则终止)
|
| 4. 使用 code + code_verifier 换取 Token
| POST https://auth.axtrk.com/api/v1/oauth/access
| Body: {clientId, clientSecret, code, redirectUri, state, codeVerifier}
v
+-------------------+
| 授权服务器 |
| (auth.axtrk.com) |
+-------------------+
|
| 5. 校验 code / state / PKCE / clientSecret
| 颁发 accessToken + refreshToken (+ idToken 若 scope 含 openid)
v
+-------------------+
| 客户端 |
+-------------------+
|
| 6. 访问受保护 API
| GET https://auth.axtrk.com/api/v1/o/me
| Header: Authorization: Bearer {accessToken}
v
+-------------------+
| 授权服务器 |
+-------------------+
|
| 7. 返回 UserInfo
v
...
|
| 8. accessToken 过期后使用 refreshToken 轮换
| POST https://auth.axtrk.com/api/v1/oauth/refresh
| Body: {clientId, clientSecret, refreshToken}
| → 返回全新的 accessToken + refreshToken(旧 RT 立即失效)
v
...4. 第三方快捷登录(tpo_provider)#
未登录用户可附加 tpo_provider 参数,让用户通过 Google/GitHub/QQ 一键登录或自动注册为 FastUser,无需手动跳转登录页。
客户端
│ 1. 跳转授权页(附 tpo_provider=github)
│ GET https://account.axtrk.com/oauth/authorize?...&tpo_provider=github
▼
前端检测未登录 + tpo_provider
│ 2. 自动跳转至 GitHub OAuth
▼
GitHub 登录授权
│ 3. 回调 /auth/tpo/callback/{platform}?code=...
▼
AxT 回调处理
│ 4. POST /api/v1/tpo-auth/login
│ 查找已有用户 or 创建 FastUser
│ 返回第一方 accessToken
▼
前端恢复 OAuth 上下文
│ 5. 自动跳回 /oauth/authorize 完成授权
▼
后续与标准流程一致5. 详细接口#
5.1 授权端点#
请求: GET https://account.axtrk.com/oauth/authorize
前端授权页本身不是后端接口,它会调用后端 GET https://auth.axtrk.com/api/v1/oauth/authorize/{clientId} 生成授权码并重定向。
Query 参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| clientId | string | 是 | 客户端 ID(路径参数) |
| redirect_uri | string | 否 | 回调 URL,未传则使用配置中第一个 |
| state | string | 强烈建议 | 防 CSRF 的随机串,服务端会存储并在换 token 时校验 |
| nonce | string | OIDC 建议 | 防 idToken 重放,会写入 idToken 的 nonce claim |
| scope | string | OIDC 必填 | 空格分隔,含 openid 时触发 OIDC 并签发 idToken。可选:profile / email / phone |
| code_challenge | string | 公共客户端必填 | PKCE 挑战值 = BASE64URL(SHA256(code_verifier)) |
| code_challenge_method | string | 配合 challenge | S256(推荐)或 plain |
| tpo_provider | string | 否 | google / github / qq —— 未登录时自动走第三方登录 |
PKCE 生成示例(JavaScript):
// 1. 生成 code_verifier: 43-128 字符的随机串
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
// 2. 生成 code_challenge
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
const challenge = base64url(new Uint8Array(digest));
// 存储 verifier,跳转授权页并传 challenge跳转回调: 授权成功后浏览器被重定向至:
{redirect_uri}?code={code}&state={state}| 参数 | 说明 |
|---|---|
| code | 临时授权码,5 分钟有效,一次性 |
| state | 原始传入值,客户端必须校验 |
5.2 Token 端点#
用 code 换 accessToken(+ idToken)+ refreshToken。
请求:
POST https://auth.axtrk.com/api/v1/oauth/access
Content-Type: application/jsonBody:
{
"clientId": "your_client_id",
"clientSecret": "your_client_secret",
"code": "abc123",
"redirectUri": "https://your-app.com/callback",
"state": "random123",
"codeVerifier": "your_code_verifier"
}| 字段 | 必填 | 说明 |
|---|---|---|
| clientId | 是 | —— |
| clientSecret | 是 | —— |
| code | 是 | 步骤 2 获得的授权码 |
| redirectUri | 条件 | 若授权时传了必须一致 |
| state | 条件 | 若授权时传了必须一致(服务端校验) |
| codeVerifier | 条件 | 若授权时传了 code_challenge 必须提供 |
返回示例(含 OIDC):
{
"code": 200,
"data": {
"accessToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9hdXRoLXYxIn0...",
"refreshToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9hdXRoX3Jmcy12MSJ9...",
"idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImlkLXYxIn0...",
"tokenType": "Bearer",
"expiresIn": 600,
"refreshExpiresIn": 1209600,
"scope": "profile"
},
"message": "Success"
}| 字段 | 说明 |
|---|---|
| accessToken | RS256 JWT,用于访问 API |
| refreshToken | RS256 JWT,用于刷新(原子轮换) |
| idToken | 仅当请求 scope 含 openid 时返回 |
| tokenType | 固定 Bearer |
| expiresIn | accessToken 秒数(10m) |
| refreshExpiresIn | refreshToken 秒数(14d) |
| scope | 实际授权的权限列表(空格分隔) |
5.3 刷新 Token#
请求:
POST https://auth.axtrk.com/api/v1/oauth/refresh
Content-Type: application/jsonBody:
{
"clientId": "your_client_id",
"clientSecret": "your_client_secret",
"refreshToken": "old_refresh_token"
}返回: 结构同 5.2,但不含 idToken。
重要机制:
- 每次刷新都会签发全新的
accessToken + refreshToken,旧 RT 立即失效 - 若检测到同一个 RT 被重复使用(重放攻击),服务器会撤销当前整条令牌链,要求用户重新登录
- 客户端必须在本地立即用新 RT 替换旧 RT
5.4 UserInfo 端点#
请求:
GET https://auth.axtrk.com/api/v1/o/me
Authorization: Bearer {accessToken}返回示例:
{
"code": 200,
"data": {
"user": {
"openId": "axt_15fd483903e9198109206c92866c9f33",
"username": "Darf",
"country": "CN",
"active": true,
"avatarUrl": "https://q2.qlogo.cn/headimg_dl?dst_uin=1680839&spec=640",
"registrationTime": "2025-07-17T16:18:19.179092Z",
"twoFactorAuth": false
},
"role": {
"prefix": "AT",
"role": "USER",
"expireTime": null
}
},
"message": "Success"
}
openId是用户 UUID + 应用oidHashKey的 SHA256 哈希,同一用户对不同应用的 openId 不同(pairwise)。
5.5 Token 撤销(RFC 7009)#
用于客户端主动注销单个 accessToken 或 refreshToken。
请求:
POST https://auth.axtrk.com/api/v1/oauth/revoke-token
Content-Type: application/jsonBody:
{
"clientId": "your_client_id",
"clientSecret": "your_client_secret",
"token": "token_to_revoke"
}传入 accessToken 或 refreshToken 均可。按 RFC 7009,即使 token 已失效也会返回 200。
5.6 Token 内省(RFC 7662)#
资源服务器/客户端可查询 Token 是否仍然有效及其 claims。
请求:
POST https://auth.axtrk.com/api/v1/oauth/introspect
Content-Type: application/jsonBody:
{
"clientId": "your_client_id",
"clientSecret": "your_client_secret",
"token": "token_to_inspect"
}返回(活跃):
{
"code": 200,
"data": {
"active": true,
"sub": "axt_15fd483903e9198109206c92866c9f33",
"client_id": "your_client_id",
"token_type": "Bearer",
"exp": 1745678901,
"iat": 1745635701,
"scope": ["profile"]
}
}返回(失效):
{
"code": 200,
"data": { "active": false }
}5.7 结束会话(OIDC End Session)#
通知授权服务器用户已从客户端登出,清除对应 Token。
请求:
POST https://auth.axtrk.com/api/v1/oauth/logout
Content-Type: application/jsonBody:
{
"clientId": "your_client_id",
"clientSecret": "your_client_secret",
"token": "access_or_refresh_token"
}按 clientTier 分流:
| Tier | 行为 |
|---|---|
STANDARD | 仅撤销本次传入的 token(RFC 7009 / OIDC 标准行为) |
ADVANCED / INTERNAL | 撤销本次 token + 反向单点退出:清空该用户在 SSO 端的所有第一方 access token,并级联撤销该用户在所有 OAuth 客户端的会话(等同于用户主动 POST /api/v1/auth/logout) |
接口签名对调用方完全透明,是否级联取决于该 OAuth App 注册时的 tier。普通客户端无法通过此端点触发反向单点退出。
6. idToken 结构(OIDC)#
请求 scope 含 openid 时 /access 返回的 idToken 为标准 JWT,包含:
| Claim | 说明 |
|---|---|
| iss | https://auth.axtrk.com |
| sub | 用户 openId(pairwise,按 client 隔离) |
| aud | clientId |
| exp / iat | 有效期 / 签发时间(epoch 秒) |
| nonce | 授权请求传入的值(防重放) |
| auth_time | 用户实际认证时间(epoch 秒) |
| preferred_username | profile scope 时返回 |
| picture | profile scope 时返回(头像 URL) |
| email / email_verified | email scope 且用户绑定了邮箱时返回 |
| phone_number / phone_number_verified | phone scope 且用户绑定了手机时返回 |
客户端应使用 JWKS 中 id-v1 公钥验证 idToken 签名,并校验 iss、aud、exp、nonce。
7. 单点退出(SLO)#
7.1 SSO → 客户端(向下级联)#
以下场景会触发用户在所有 OAuth 客户端的会话失效(后端自动级联):
- 用户在第一方
POST /api/v1/auth/logout登出 - 用户在第一方
POST /api/v1/user/password/change修改密码 - 用户在授权列表中
POST /api/v1/oauth/revoke/{clientId}撤销单个应用(仅该应用)
客户端收到 API 401 后应当视为会话已失效,引导用户重新授权。
7.2 客户端 → SSO(反向级联,仅 ADVANCED / INTERNAL)#
受信任 tier 的客户端可通过 POST /api/v1/oauth/logout(5.7)发起反向单点退出,效果等同于用户在第一方主动登出:
- 撤销该用户在 SSO 端的所有第一方 access token
- 级联撤销该用户在所有 OAuth 客户端的会话
STANDARD 客户端调用同一端点只会撤销本次 token,不会触发反向级联。
INTERNAL 客户端因获得的是第一方 access/refresh token,也可以直接调 POST /api/v1/auth/logout,效果一致。
8. 第三方快捷登录集成#
8.1 支持的平台#
| tpo_provider | 平台 |
|---|---|
| github | GitHub |
| QQ 互联 |
8.2 集成示例#
<!-- 使用 GitHub 快捷登录 -->
<a href="https://account.axtrk.com/oauth/authorize
?clientId=YOUR_CLIENT_ID
&redirect_uri=https://your-app.com/callback
&state=random123
&scope=openid+profile+email
&code_challenge=YOUR_CHALLENGE
&code_challenge_method=S256
&tpo_provider=github">
使用 GitHub 登录
</a>8.3 FastUser 说明#
通过 tpo_provider 自动注册的用户为 FastUser:
- 仅持有第三方平台信息,无邮箱/手机/密码
- 可在 AxT 面板绑定邮箱或手机号升级为完整用户
- 部分敏感功能对 FastUser 不可用
9. 错误处理#
| HTTP | 常见场景 |
|---|---|
| 400 | 参数缺失、code 无效、state/PKCE 校验失败、redirect_uri 不匹配 |
| 401 | clientSecret 错误、Token 失效、refreshToken 重放 |
| 403 | 用户账户被封禁或未激活 |
| 429 | 触发限流(登录失败过多 / 请求频次超限) |
10. 安全建议#
- 必须 为每次授权生成新的
state和nonce,并在回调校验 - 强烈建议 使用 PKCE(S256),即使是 confidential 客户端
- 必须 使用 HTTPS 承载所有端点
- 必须 在服务端存储
clientSecret,不要在前端暴露 - 建议 资源服务器通过 JWKS 离线验证签名,避免每次请求回调授权服务器
- 检测到
refreshToken重放时立即引导用户重新登录