Skip to content

OIDC / OAuth2 集成 已上线 ​

Lurus 统一身份认证基于 Casdoor,对外暴露标准 OIDC / OAuth2 接口。应用支持标准 OIDC 即可直接接入 Lurus SSO,无需修改核心认证逻辑。

1Discovery URL 自动发现
9标准端点
5Grant Type / Flow
S256PKCE 强制方式

快速开始 ​

绝大多数 OIDC SDK 支持 Discovery,只需一个 URL 即可自动获取所有端点、算法和能力。

Discovery URL: https://identity.lurus.cn/.well-known/openid-configuration

初始化 SDK 直接指向该 URL(而非硬编码端点),服务端密钥轮换或端点变更时应用无需修改。

最低配置

client_id + redirect_uri + Discovery URL — 三项齐备即可发起授权码流程。

标准端点 ​

所有端点以 https://identity.lurus.cn 为 Base URL。

端点名称路径HTTP 方法用途
Discovery/.well-known/openid-configurationGET服务元数据,SDK 自动读取所有端点与算法
Authorization/oauth/v2/authorizeGET / POST开启授权流,重定向到登录页
Token/oauth/v2/tokenPOST换取 access_token / id_token / refresh_token
UserInfo/oidc/v1/userinfoGET用 access token 读取当前用户 claims
JWKS/oauth/v2/keysGET获取 JWK 公钥集合,本地验签 JWT
Introspection/oauth/v2/introspectPOST查询 token 有效性与元信息(服务端用)
Revocation/oauth/v2/revokePOST撤销 access / refresh token
End Session/oidc/v1/end_sessionGET / POST登出:终止 Casdoor 会话
Device Authorization/oauth/v2/device_authorizationPOSTDevice Code Flow 起始端点

Authorization 端点参数 ​

参数必填说明
client_id✓控制台创建应用后获取
redirect_uri✓必须与控制台注册的 URI 完全一致
response_type✓授权码流程固定 code
scope✓至少含 openid,多个空格分隔
state推荐防 CSRF,回调时原值返回
nonce推荐防重放,写入 id_token
code_challengePKCE 必填Base64URL(SHA-256(code_verifier))
code_challenge_methodPKCE 必填固定 S256
prompt可选login 强制重登;consent 强制展示同意页
login_hint可选预填用户名,加速登录

支持的 Grant Type / Flow ​

按客户端类型选择授权流;SPA / Native / Web 首选 Authorization Code + PKCE。

Flow适用场景是否推荐
Authorization Code + PKCESPA、Native App、Web App✓ 首选
Client CredentialsM2M、后端服务✓ M2M 场景
Device CodeCLI、TV、IoT、无浏览器设备✓ 特殊设备
Refresh Token长期会话,静默续期配合 offline_access scope
JWT Bearer(Service User)服务账号,签名 JWT 换 token服务账号场景

Authorization Code + PKCE 六步:

  1. 客户端生成 code_verifier(随机 43-128 字符)。
  2. 算 code_challenge = Base64URL(SHA-256(verifier))。
  3. 重定向到 /oauth/v2/authorize(带 challenge)。
  4. 用户登录授权,回调带 code。
  5. POST /oauth/v2/token(带 code + code_verifier)。
  6. 获得 access / id / refresh token。

Client Credentials:POST /oauth/v2/token with grant_type=client_credentials + client_id + client_secret + scope=openid urn:casdoor:iam:org:project:id:{projectid}:aud → 获得 access_token(无 id_token,无用户身份)。

Refresh Token:初次授权 scope 含 offline_access → 安全存储 refresh_token → access_token 过期时 POST grant_type=refresh_token + refresh_token=<token> → 获新 token(refresh_token 可能轮换)。


Authorization Code + PKCE 完整示例 ​

TypeScript ​

typescript
import { createHash, randomBytes } from "crypto";

// Step 1: 生成 PKCE pair
function generatePKCE() {
  const verifier = randomBytes(32).toString("base64url");
  const challenge = createHash("sha256").update(verifier).digest("base64url");
  return { verifier, challenge };
}

// Step 2: 构造 Authorization URL(持久化 verifier + state 到 sessionStorage)
function buildAuthorizeURL(clientId: string, redirectUri: string): string {
  const { verifier, challenge } = generatePKCE();
  sessionStorage.setItem("pkce_verifier", verifier);
  const state = randomBytes(16).toString("base64url");
  sessionStorage.setItem("oauth_state", state);
  const params = new URLSearchParams({
    response_type: "code", client_id: clientId, redirect_uri: redirectUri,
    scope: "openid profile email offline_access", state,
    nonce: randomBytes(16).toString("base64url"),
    code_challenge: challenge, code_challenge_method: "S256",
  });
  return `https://identity.lurus.cn/oauth/v2/authorize?${params}`;
}

// Step 3: 回调处理 — 验证 state,提取 code
function handleCallback(callbackURL: string) {
  const url = new URL(callbackURL);
  const code = url.searchParams.get("code")!;
  const state = url.searchParams.get("state")!;
  if (state !== sessionStorage.getItem("oauth_state")) {
    throw new Error("State mismatch — possible CSRF attack");
  }
  return code;
}

// Step 4: 用 code 换 tokens
async function exchangeCode(code: string, clientId: string, redirectUri: string) {
  const verifier = sessionStorage.getItem("pkce_verifier")!;
  const resp = await fetch("https://identity.lurus.cn/oauth/v2/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code", code, redirect_uri: redirectUri,
      client_id: clientId, code_verifier: verifier,
    }),
  });
  if (!resp.ok) throw new Error(`Token exchange failed: ${await resp.text()}`);
  // 获得 { access_token, id_token, refresh_token, expires_in }
  return await resp.json();
}

curl ​

bash
# code_challenge = Base64URL(SHA-256(code_verifier))
CODE_VERIFIER="dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | sha256sum | cut -d' ' -f1 | xxd -r -p | base64 | tr '+/' '-_' | tr -d '=')

# 引导用户访问登录 URL
echo "https://identity.lurus.cn/oauth/v2/authorize?response_type=code\
&client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.example.com/callback\
&scope=openid%20profile%20email%20offline_access&state=random_state_value\
&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256"

# 用回调中的 code 换 tokens
curl -s -X POST https://identity.lurus.cn/oauth/v2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTHORIZATION_CODE_FROM_CALLBACK" \
  -d "redirect_uri=https://yourapp.example.com/callback" \
  -d "client_id=YOUR_CLIENT_ID" -d "code_verifier=${CODE_VERIFIER}" | jq .
# 响应:{ "access_token", "token_type":"Bearer", "expires_in":43199, "id_token", "refresh_token" }

Scopes 清单 ​

标准 OIDC scopes 决定返回哪些 claim;Casdoor 特有 scopes 控制 audience、角色与组织约束。

标准 Scopes ​

Scope说明影响的 Token
openid必填,声明 OIDC 请求,返回 id_tokenid_token
profile获取 name、given_name、family_name、preferred_username、localeid_token, userinfo
email获取 email、email_verifiedid_token, userinfo
phone获取 phone_number、phone_number_verifiedid_token, userinfo
address获取用户地址信息id_token, userinfo
offline_access请求 refresh_token(仅 Authorization Code 流有效)—

Casdoor 特有 Scopes ​

Scope说明影响的 Token
urn:casdoor:iam:org:project:id:{projectid}:aud将指定 project ID 加入 access token 的 aud;服务端验签必须匹配access_token
urn:casdoor:iam:org:project:id:casdoor:aud将 Casdoor 自身 project ID 加入 aud(访问 Casdoor API 用)access_token
urn:casdoor:iam:org:projects:rolestoken 中包含所有已授权项目的角色列表id_token, access_token, userinfo
urn:casdoor:iam:org:project:role:{rolekey}仅请求特定角色 claim,如 ...:role:adminid_token, access_token
urn:casdoor:iam:org:id:{orgid}限定用户必须属于该组织;跨组织登录强制隔离校验用
urn:casdoor:iam:org:domain:primary:{domain}限定用户所在组织主域名,如 ...:primary:lurus.cn校验用
urn:casdoor:iam:user:metadatatoken 中包含用户自定义 metadata(Base64 键值对)id_token, access_token, userinfo
urn:casdoor:iam:user:resourceowner获取用户所属组织的 ID、名称和主域名id_token, access_token, userinfo
urn:casdoor:iam:org:idp:id:{idp_id}直接跳转到指定 IdP(企业微信、飞书),跳过 IDP 选择页行为控制

常用组合(Web App):openid profile email offline_access urn:casdoor:iam:org:projects:roles urn:casdoor:iam:org:project:id:{projectid}:aud


Claims 清单 ​

下表标注每个 claim 出现在哪种 token,以及它依赖哪个 scope。

标准 Claims ​

Claim说明id_tokenaccess_tokenuserinfo依赖 Scope
sub用户唯一 ID(Casdoor 内部 ID)✓✓ (JWT)✓始终
issIssuer,固定 https://identity.lurus.cn✓✓—始终
audAudience,应用 client_id✓✓—始终
exp / iat过期 / 签发时间(Unix)✓✓—始终
auth_time用户实际登录时间✓——始终
nonce防重放随机值✓——始终(如有)
amr认证方式,如 ["pwd"]、["mfa"]✓——始终
name / given_name / family_name全名 / 名 / 姓✓*—✓profile
preferred_username登录名(username@primarydomain)✓—✓profile
locale / updated_at语言偏好(如 zh)/ 信息更新时间✓*—✓profile
email / email_verified邮箱 / 是否已验证(Boolean)✓*—✓email

✓* = 仅在 response_type 含 id_token 或显式请求时返回。

Casdoor 特有 Claims ​

Claim说明id_tokenaccess_tokenuserinfo
urn:casdoor:iam:org:project:roles用户项目角色,结构 { "roleName": { "orgId": "domain" } }✓✓ (JWT)✓
urn:casdoor:iam:org:domain:primary用户所属组织主域名✓✓ (JWT)✓
urn:casdoor:iam:user:metadata用户自定义 metadata,{ "key": "base64value" }✓✓ (JWT)✓
urn:casdoor:iam:user:resourceowner:id / :name / :primary_domain用户所属组织 ID / 名称 / 主域名✓✓ (JWT)✓

角色 claim 示例:

json
{ "urn:casdoor:iam:org:project:roles": { "admin": { "178204173316174381": "lurus.cn" }, "viewer": { "178204173316174381": "lurus.cn" } } }

Metadata claim 示例(value 为 Base64,使用时需 atob() / base64.StdEncoding.DecodeString() 解码):

json
{ "urn:casdoor:iam:user:metadata": { "department": "ZW5naW5lZXJpbmc=", "employee_id": "VTEwMDEy" } }

Token 校验 ​

服务端收到 Bearer token 后不要仅凭格式判断有效性,必须:

1. 从 JWKS 拉公钥(建议缓存 TTL 1小时): GET https://identity.lurus.cn/oauth/v2/keys
2. 用匹配 kid 的公钥验证 JWT 签名
3. 校验标准 claims:iss == "https://identity.lurus.cn";aud 含本应用 client_id 或 project_id;
   exp > now();nbf <= now()(如有)
4. 按需校验业务 claims(角色、组织 ID)

推荐库 ​

语言库安装
TS / Node.jsoidc-client-tsbun add oidc-client-ts
TS / Node.jsjose(轻量验签)bun add jose
Gogo-oidcgo get github.com/coreos/go-oidc/v3
Pythonpython-josepip install python-jose
Elixir / Phoenixlurus-phoenix(平台内部库)见 2l-bs-admin

Go 验签示例 ​

go
package auth

import (
    "context"
    "fmt"
    "github.com/coreos/go-oidc/v3/oidc"
)

var provider *oidc.Provider

func Init(ctx context.Context) error {
    var err error
    // SDK 自动从 Discovery URL 加载配置和 JWKS
    provider, err = oidc.NewProvider(ctx, "https://identity.lurus.cn")
    return err
}

func VerifyAccessToken(ctx context.Context, rawToken, clientID string) (*oidc.IDToken, error) {
    verifier := provider.Verifier(&oidc.Config{ClientID: clientID})
    token, err := verifier.Verify(ctx, rawToken)
    if err != nil {
        return nil, fmt.Errorf("token verification failed: %w", err)
    }
    return token, nil
}

Introspection(不透明 token) ​

access token 为不透明格式(非 JWT)时用 Introspection 验证:

bash
curl -X POST https://identity.lurus.cn/oauth/v2/introspect \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -d "token=ACCESS_TOKEN_TO_CHECK"
# 响应:{ "active": true, "sub": "...", "exp": 1234567890, ... } 或 { "active": false }

Device Authorization Flow ​

适用无浏览器输入设备(CLI、TV、IoT)。Lurus CLI 产品(Lumen、kova-cli)均用此流程。

时序:

  1. 设备 POST /device_authorization。
  2. 收到 device_code + user_code + verification_uri。
  3. 展示 user_code 和 URL 给用户。
  4. 用户在浏览器打开 verification_uri、输 user_code、登录授权。
  5. 设备每 interval 秒轮询 /token。
  6. 用户授权后下一次轮询返回 access / id token。

Step 1: 请求 Device Code ​

bash
curl -s -X POST https://identity.lurus.cn/oauth/v2/device_authorization \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=YOUR_CLIENT_ID" -d "scope=openid profile email"

响应:

json
{
  "device_code": "Ag_EE...zo9OA",
  "user_code": "GQWC-FWFK",
  "verification_uri": "https://identity.lurus.cn/device",
  "verification_uri_complete": "https://identity.lurus.cn/device?user_code=GQWC-FWFK",
  "expires_in": 300,
  "interval": 5
}

Step 2: 展示给用户 ​

展示 verification_uri(https://identity.lurus.cn/device)+ user_code,或扫码 verification_uri_complete。超时 5 分钟。

Step 3: 轮询 Token 端点 ​

每 interval 秒轮询,直到成功或超时。错误处理:authorization_pending → 继续等;slow_down → 放慢频率;其他 → 失败退出。

bash
while true; do
  RESPONSE=$(curl -s -X POST https://identity.lurus.cn/oauth/v2/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
    -d "device_code=Ag_EE...zo9OA" -d "client_id=YOUR_CLIENT_ID")
  ERROR=$(echo "$RESPONSE" | jq -r '.error // empty')
  if [ -z "$ERROR" ]; then echo "授权成功!"; echo "$RESPONSE" | jq .; break
  elif [ "$ERROR" = "authorization_pending" ]; then sleep 5
  elif [ "$ERROR" = "slow_down" ]; then sleep 10
  else echo "授权失败:$ERROR"; break; fi
done

TypeScript 实现同构:fetch POST /device_authorization 起 flow,再以 setTimeout(interval*1000) 轮询 /token,命中 authorization_pending 继续、slow_down 加大 interval、resp.ok 返回 tokens。


常见问题 ​

最常见的接入报错与一步到位的修法。

audience 错误(aud claim 不匹配)

现象:验签报 token audience mismatch / invalid audience。原因:access token 的 aud 默认只含 client_id。解决:scope 加 urn:casdoor:iam:org:project:id:{projectid}:aud,将 project ID 显式写入 aud。

roles claim 为空或缺失

原因:用户在该 Project 无 User Grant,或未请求角色 scope。检查:① 控制台 Project → Authorizations 确认有角色 Grant ② scope 含 urn:casdoor:iam:org:projects:roles ③ Project 设置开启「Assert Roles on Authentication」。

id_token 中没有 email

原因:scope 遗漏 email。解决:scope 加 email(如 openid profile email)。

Refresh token 失效需重新登录

续期返回 invalid_grant。可能:初次授权 scope 未含 offline_access;refresh token 过期;用户在控制台撤销了会话。解决:重新发起 Authorization Code 流程;需长期续期则确保 scope 含 offline_access 且应用类型为「Web」或「Native」。

PKCE code_verifier 不匹配

/token 返回 invalid_grant: code verifier mismatch。原因:verifier 在两步间变化,或 Base64URL 编码不一致(含 = padding 或用了 +/)。解决:用 base64url(无 padding,-_ 替代 +/);SPA 中用 sessionStorage 跨页面保持 verifier。


下一步

相关链接 ​

Powered by Lurus Technologies