Skip to content
This page was translated from the Chinese original, which has changed since — it may be out of date. The Chinese version is authoritative. View the Chinese original →

OIDC / OAuth2 Integration Live ​

Lurus unified identity authentication is built on Casdoor and exposes standard OIDC / OAuth2 interfaces. Any application that supports standard OIDC can connect to Lurus SSO directly, with no changes to its core authentication logic.

1Discovery URL auto-discovery
9Standard endpoints
5Grant Type / Flow
S256Enforced PKCE method

Quick Start ​

Most OIDC SDKs support Discovery, so a single URL is enough to automatically obtain all endpoints, algorithms, and capabilities.

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

Point the SDK directly at this URL during initialization (instead of hardcoding endpoints), so the application needs no changes when server keys are rotated or endpoints change.

Minimum configuration

client_id + redirect_uri + Discovery URL — with these three in place you can start the Authorization Code flow.

Standard Endpoints ​

All endpoints use https://identity.lurus.cn as the Base URL.

EndpointPathHTTP MethodPurpose
Discovery/.well-known/openid-configurationGETService metadata; the SDK automatically reads all endpoints and algorithms
Authorization/oauth/v2/authorizeGET / POSTStart the authorization flow, redirect to the login page
Token/oauth/v2/tokenPOSTExchange for access_token / id_token / refresh_token
UserInfo/oidc/v1/userinfoGETRead the current user’s claims with the access token
JWKS/oauth/v2/keysGETFetch the JWK public key set to verify JWT signatures locally
Introspection/oauth/v2/introspectPOSTQuery token validity and metadata (server-side use)
Revocation/oauth/v2/revokePOSTRevoke an access / refresh token
End Session/oidc/v1/end_sessionGET / POSTLogout: terminate the Casdoor session
Device Authorization/oauth/v2/device_authorizationPOSTStarting endpoint for the Device Code Flow

Authorization Endpoint Parameters ​

ParameterRequiredDescription
client_id✓Obtained after creating an application in the console
redirect_uri✓Must exactly match the URI registered in the console
response_type✓Fixed as code for the Authorization Code flow
scope✓Must contain at least openid; multiple values are space-separated
stateRecommendedPrevents CSRF; returned unchanged in the callback
nonceRecommendedPrevents replay; written into the id_token
code_challengeRequired for PKCEBase64URL(SHA-256(code_verifier))
code_challenge_methodRequired for PKCEFixed as S256
promptOptionallogin forces re-login; consent forces the consent page to be shown
login_hintOptionalPre-fill the username to speed up login

Supported Grant Types / Flows ​

Choose the authorization flow based on the client type; SPA / Native / Web should prefer Authorization Code + PKCE.

FlowUse CaseRecommended
Authorization Code + PKCESPA, Native App, Web App✓ Preferred
Client CredentialsM2M, backend services✓ M2M scenarios
Device CodeCLI, TV, IoT, browserless devices✓ Special devices
Refresh TokenLong-lived sessions, silent renewalUse with the offline_access scope
JWT Bearer (Service User)Service accounts, exchange a signed JWT for a tokenService account scenarios

Authorization Code + PKCE in six steps:

  1. The client generates a code_verifier (random 43–128 characters).
  2. Compute code_challenge = Base64URL(SHA-256(verifier)).
  3. Redirect to /oauth/v2/authorize (with the challenge).
  4. The user logs in and authorizes; the callback carries the code.
  5. POST /oauth/v2/token (with code + code_verifier).
  6. Receive the 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 → obtain an access_token (no id_token, no user identity).

Refresh Token: include offline_access in the initial authorization scope → store the refresh_token securely → when the access_token expires, POST grant_type=refresh_token + refresh_token=<token> → obtain new tokens (the refresh_token may rotate).


Complete Authorization Code + PKCE Example ​

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 Reference ​

Standard OIDC scopes determine which claims are returned; Casdoor-specific scopes control audience, roles, and organization constraints.

Standard Scopes ​

ScopeDescriptionAffected Token
openidRequired, declares an OIDC request, returns the id_tokenid_token
profileGet name, given_name, family_name, preferred_username, localeid_token, userinfo
emailGet email, email_verifiedid_token, userinfo
phoneGet phone_number, phone_number_verifiedid_token, userinfo
addressGet the user’s address informationid_token, userinfo
offline_accessRequest a refresh_token (only valid in the Authorization Code flow)—

Casdoor-Specific Scopes ​

ScopeDescriptionAffected Token
urn:casdoor:iam:org:project:id:{projectid}:audAdd the specified project ID to the access token’s aud; server-side verification must matchaccess_token
urn:casdoor:iam:org:project:id:casdoor:audAdd Casdoor’s own project ID to aud (used to access the Casdoor API)access_token
urn:casdoor:iam:org:projects:rolesInclude the role list of all authorized projects in the tokenid_token, access_token, userinfo
urn:casdoor:iam:org:project:role:{rolekey}Request only a specific role claim, e.g. ...:role:adminid_token, access_token
urn:casdoor:iam:org:id:{orgid}Restrict the user to the given organization; enforce isolation for cross-org loginValidation only
urn:casdoor:iam:org:domain:primary:{domain}Restrict the user to the primary domain of their organization, e.g. ...:primary:lurus.cnValidation only
urn:casdoor:iam:user:metadataInclude the user’s custom metadata in the token (Base64 key-value pairs)id_token, access_token, userinfo
urn:casdoor:iam:user:resourceownerGet the ID, name, and primary domain of the user’s organizationid_token, access_token, userinfo
urn:casdoor:iam:org:idp:id:{idp_id}Jump directly to the specified IdP (WeCom, Feishu), skipping the IdP selection pageBehavior control

Common combination (Web App): openid profile email offline_access urn:casdoor:iam:org:projects:roles urn:casdoor:iam:org:project:id:{projectid}:aud


Claims Reference ​

The table below indicates which token each claim appears in, and which scope it depends on.

Standard Claims ​

ClaimDescriptionid_tokenaccess_tokenuserinfoRequired Scope
subUnique user ID (Casdoor internal ID)✓✓ (JWT)✓Always
issIssuer, fixed as https://identity.lurus.cn✓✓—Always
audAudience, the application’s client_id✓✓—Always
exp / iatExpiry / issued-at time (Unix)✓✓—Always
auth_timeThe user’s actual login time✓——Always
nonceAnti-replay random value✓——Always (if present)
amrAuthentication method, e.g. ["pwd"], ["mfa"]✓——Always
name / given_name / family_nameFull name / given name / family name✓*—✓profile
preferred_usernameLogin name (username@primarydomain)✓—✓profile
locale / updated_atLanguage preference (e.g. zh) / information update time✓*—✓profile
email / email_verifiedEmail / whether verified (Boolean)✓*—✓email

✓* = returned only when the response_type includes id_token or when explicitly requested.

Casdoor-Specific Claims ​

ClaimDescriptionid_tokenaccess_tokenuserinfo
urn:casdoor:iam:org:project:rolesThe user’s project roles, structured as { "roleName": { "orgId": "domain" } }✓✓ (JWT)✓
urn:casdoor:iam:org:domain:primaryThe primary domain of the user’s organization✓✓ (JWT)✓
urn:casdoor:iam:user:metadataThe user’s custom metadata, { "key": "base64value" }✓✓ (JWT)✓
urn:casdoor:iam:user:resourceowner:id / :name / :primary_domainThe ID / name / primary domain of the user’s organization✓✓ (JWT)✓

Role claim example:

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

Metadata claim example (the value is Base64; decode with atob() / base64.StdEncoding.DecodeString() before use):

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

Token Verification ​

After receiving a Bearer token, the server must not judge validity by format alone; it must:

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)
LanguageLibraryInstall
TS / Node.jsoidc-client-tsbun add oidc-client-ts
TS / Node.jsjose (lightweight verification)bun add jose
Gogo-oidcgo get github.com/coreos/go-oidc/v3
Pythonpython-josepip install python-jose
Elixir / Phoenixlurus-phoenix (platform internal library)See 2l-bs-admin

Go Verification Example ​

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 (opaque tokens) ​

When the access token is in an opaque format (not a JWT), use Introspection to verify it:

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 ​

For devices without browser input (CLI, TV, IoT). Lurus CLI products (Lumen, kova-cli) all use this flow.

Sequence:

  1. The device POSTs to /device_authorization.
  2. It receives a device_code + user_code + verification_uri.
  3. Display the user_code and URL to the user.
  4. The user opens verification_uri in a browser, enters the user_code, logs in, and authorizes.
  5. The device polls /token every interval seconds.
  6. After the user authorizes, the next poll returns the access / id token.

Step 1: Request a 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"

Response:

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: Show to the User ​

Display the verification_uri (https://identity.lurus.cn/device) + user_code, or scan the QR code for verification_uri_complete. Times out after 5 minutes.

Step 3: Poll the Token Endpoint ​

Poll every interval seconds until success or timeout. Error handling: authorization_pending → keep waiting; slow_down → slow down the rate; otherwise → fail and exit.

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

A TypeScript implementation is isomorphic: fetch POST /device_authorization to start the flow, then poll /token with setTimeout(interval*1000) — on authorization_pending continue, on slow_down increase the interval, and on resp.ok return the tokens.


FAQ ​

The most common integration errors and one-shot fixes.

audience error (aud claim mismatch)

Symptom: verification reports token audience mismatch / invalid audience. Cause: the access token’s aud contains only client_id by default. Fix: add urn:casdoor:iam:org:project:id:{projectid}:aud to the scope to explicitly write the project ID into aud.

roles claim is empty or missing

Cause: the user has no User Grant in that Project, or the role scope was not requested. Checks: ① In the console, go to Project → Authorizations and confirm a role Grant exists. ② Include urn:casdoor:iam:org:projects:roles in the scope. ③ Enable "Assert Roles on Authentication" in the Project settings.

no email in the id_token

Cause: the email scope is missing. Fix: add email to the scope (e.g. openid profile email).

Refresh token is invalid and re-login is required

Renewal returns invalid_grant. Possible causes: the initial authorization scope did not include offline_access; the refresh token expired; the user revoked the session in the console. Fix: restart the Authorization Code flow; for long-term renewal, ensure the scope includes offline_access and the application type is "Web" or "Native".

PKCE code_verifier mismatch

/token returns invalid_grant: code verifier mismatch. Cause: the verifier changed between the two steps, or the Base64URL encoding is inconsistent (includes = padding or used +/). Fix: use base64url (no padding, -_ instead of +/); in an SPA, use sessionStorage to keep the verifier across pages.


Next Steps

Powered by Lurus Technologies