-
Notifications
You must be signed in to change notification settings - Fork 0
OAuth and SSO Integration
何家欢 edited this page Jun 7, 2026
·
3 revisions
本页面向业务系统开发者,说明如何把 Passkey-Auth 接入自己的站点。
| 场景 | 推荐方式 |
|---|---|
| 外部站点、标准 OAuth 接入 | Authorization Code Flow |
| 同一组织内登录门户跳 Auth 再回跳 | Link Challenge Flow |
| 后端已经能拿 Auth session cookie | Server Session Verify API |
生产环境优先使用 Authorization Code Flow。Link Challenge Flow 适合你同时控制原网站和 Auth 服务的场景。
业务后端生成 state,保存到业务 session,然后跳转:
https://auth.xxxxx/oauth/authorize?response_type=code&client_id=your-client-id&redirect_uri=https%3A%2F%2Flogin.xxxxx%2Fcallback&state=random-state
参数:
| 参数 | 必填 | 说明 |
|---|---|---|
response_type |
是 | 固定 code
|
client_id |
是 | OAuth client id |
redirect_uri |
是 | 必须在白名单 |
state |
是 | 随机 CSRF token |
Python 示例:
import secrets
from urllib.parse import urlencode
state = secrets.token_urlsafe(24)
session["oauth_state"] = state
query = urlencode({
"response_type": "code",
"client_id": "your-client-id",
"redirect_uri": "https://login.xxxxx/callback",
"state": state,
})
return redirect(f"https://auth.xxxxx/oauth/authorize?{query}")业务站点不需要直接处理 WebAuthn。Auth WebUI 内部会调用:
POST /api/login/options
POST /api/login/verify
POST /oauth/authorize/complete
成功后回跳:
https://login.xxxxx/callback?code=...&state=...
业务后端必须先校验:
state = request.args.get("state", "")
if not state or state != session.pop("oauth_state", ""):
abort(400, "invalid_state")如果 state 不匹配,不要换 token。
POST https://auth.xxxxx/oauth/token
Content-Type: application/x-www-form-urlencoded
code=AUTH_CODE&
redirect_uri=https%3A%2F%2Flogin.xxxxx%2Fcallback&
client_id=your-client-id&
client_secret=your-client-secret成功响应:
{
"ok": true,
"access_token": "signed-token",
"token_type": "Bearer",
"expires_in": 3600,
"authenticated": true,
"user": {
"sub": "stable-user-handle",
"id": 1,
"username": "alice",
"createdAt": 1780000000
}
}业务系统建议把 sub 作为本地用户绑定主键。
GET https://auth.xxxxx/oauth/userinfo
Authorization: Bearer ACCESS_TOKEN响应:
{
"sub": "stable-user-handle",
"id": 1,
"username": "alice",
"createdAt": 1780000000
}Link Challenge Flow 类似“跳转到 Auth 验证,成功后原站拿到签名结果”。
https://login.xxxxx 用户输入用户名
https://auth.xxxxx/oauth/challenge/{challenge}
https://login.xxxxx/callback?challenge=...&challenge_result=...&state=...&status=success
| 字段 | 说明 |
|---|---|
challenge_id |
随机挑战 ID |
client_id |
发起方 client |
return_uri |
回跳地址 |
username |
原网站输入的用户名 |
state |
防 CSRF 随机值 |
expires_at |
过期时间 |
completed_at |
Auth 验证成功时间 |
consumed_at |
callback 消费时间 |
user_id |
完成验证的 Auth 用户 |
demo 存储在 SQLite 表 oauth_challenge_requests。
challenge=...
challenge_result=...
state=...
status=success
status=success 只用于展示。真正可信的是服务端校验后的 challenge_result。
顺序建议:
- 校验原网站 session 里的
state - 校验
challenge_result签名 - 校验 token 中的
challenge_id - 校验 token 绑定的
client_id/state/user_id/sub - 校验 challenge 已 completed、未 consumed、未过期
- 标记 challenge consumed
- 建立业务 session
当前 demo 的核心校验函数:
passkey_demo/app.py::_consume_challenge_result_token
适合内部系统或 gateway 校验 Auth session。
POST https://auth.xxxxx/api/server/session/verify
Authorization: Bearer SERVER_API_TOKEN
Content-Type: application/json显式传入 session cookie:
{
"sessionCookie": "session=..."
}已登录响应:
{
"ok": true,
"authenticated": true,
"user": {
"sub": "stable-user-handle",
"id": 1,
"username": "alice",
"createdAt": 1780000000
}
}未登录响应:
{
"ok": true,
"authenticated": false
}import requests
import secrets
from flask import Flask, abort, redirect, request, session
from urllib.parse import urlencode
app = Flask(__name__)
app.secret_key = "business-app-secret"
AUTH_BASE = "https://auth.xxxxx"
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
REDIRECT_URI = "https://login.xxxxx/callback"
@app.get("/login")
def login():
state = secrets.token_urlsafe(24)
session["oauth_state"] = state
params = urlencode({
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"state": state,
})
return redirect(f"{AUTH_BASE}/oauth/authorize?{params}")
@app.get("/callback")
def callback():
state = request.args.get("state", "")
if not state or state != session.pop("oauth_state", ""):
abort(400, "invalid_state")
response = requests.post(
f"{AUTH_BASE}/oauth/token",
data={
"code": request.args.get("code", ""),
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
timeout=10,
)
response.raise_for_status()
token = response.json()
user = token["user"]
session["user_sub"] = user["sub"]
session["username"] = user["username"]
return "logged in"