Skip to content

OAuth and SSO Integration

何家欢 edited this page Jun 7, 2026 · 3 revisions

OAuth and SSO Integration

本页面向业务系统开发者,说明如何把 Passkey-Auth 接入自己的站点。

接入方式选择

场景 推荐方式
外部站点、标准 OAuth 接入 Authorization Code Flow
同一组织内登录门户跳 Auth 再回跳 Link Challenge Flow
后端已经能拿 Auth session cookie Server Session Verify API

生产环境优先使用 Authorization Code Flow。Link Challenge Flow 适合你同时控制原网站和 Auth 服务的场景。

Authorization Code Flow

业务站点发起授权

业务后端生成 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}")

Auth WebUI 完成 passkey

业务站点不需要直接处理 WebAuthn。Auth WebUI 内部会调用:

POST /api/login/options
POST /api/login/verify
POST /oauth/authorize/complete

成功后回跳:

https://login.xxxxx/callback?code=...&state=...

Callback 校验 state

业务后端必须先校验:

state = request.args.get("state", "")
if not state or state != session.pop("oauth_state", ""):
    abort(400, "invalid_state")

如果 state 不匹配,不要换 token。

用 code 换 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 作为本地用户绑定主键。

获取 userinfo

GET https://auth.xxxxx/oauth/userinfo
Authorization: Bearer ACCESS_TOKEN

响应:

{
  "sub": "stable-user-handle",
  "id": 1,
  "username": "alice",
  "createdAt": 1780000000
}

Link Challenge Flow

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

Auth 完成后回跳参数

challenge=...
challenge_result=...
state=...
status=success

status=success 只用于展示。真正可信的是服务端校验后的 challenge_result

Callback 必须校验

顺序建议:

  1. 校验原网站 session 里的 state
  2. 校验 challenge_result 签名
  3. 校验 token 中的 challenge_id
  4. 校验 token 绑定的 client_id/state/user_id/sub
  5. 校验 challenge 已 completed、未 consumed、未过期
  6. 标记 challenge consumed
  7. 建立业务 session

当前 demo 的核心校验函数:

passkey_demo/app.py::_consume_challenge_result_token

Server Session Verify API

适合内部系统或 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"

Clone this wiki locally