Skip to content
Esta página se tradujo del original en chino, que ha cambiado desde entonces, por lo que puede estar desactualizada. La versión china es la autorizada. Ver el original en chino →

Integración OIDC / OAuth2 En producción ​

La autenticación de identidad unificada de Lurus se basa en Casdoor y expone interfaces estándar OIDC / OAuth2. Cualquier aplicación que admita OIDC estándar puede integrarse directamente con Lurus SSO, sin necesidad de modificar su lógica de autenticación principal.

1Descubrimiento automático con Discovery URL
9Endpoints estándar
5Grant Type / Flow
S256Método PKCE obligatorio

Inicio rápido ​

La gran mayoría de los SDK de OIDC admiten Discovery: con una sola URL obtienen automáticamente todos los endpoints, algoritmos y capacidades.

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

Inicializa el SDK apuntando directamente a esta URL (en lugar de codificar los endpoints), de modo que la aplicación no necesite cambios cuando el servidor rota claves o modifica endpoints.

Configuración mínima

client_id + redirect_uri + Discovery URL — con estos tres elementos ya puedes iniciar el flujo de código de autorización.

Endpoints estándar ​

Todos los endpoints usan https://identity.lurus.cn como Base URL.

Nombre del endpointRutaMétodo HTTPUso
Discovery/.well-known/openid-configurationGETMetadatos del servicio; el SDK lee automáticamente todos los endpoints y algoritmos
Authorization/oauth/v2/authorizeGET / POSTInicia el flujo de autorización, redirige a la página de inicio de sesión
Token/oauth/v2/tokenPOSTIntercambia por access_token / id_token / refresh_token
UserInfo/oidc/v1/userinfoGETLee los claims del usuario actual usando el access token
JWKS/oauth/v2/keysGETObtiene el conjunto de claves públicas JWK para verificar JWT localmente
Introspection/oauth/v2/introspectPOSTConsulta la validez y metadatos de un token (uso del servidor)
Revocation/oauth/v2/revokePOSTRevoca un access / refresh token
End Session/oidc/v1/end_sessionGET / POSTCierre de sesión: termina la sesión de Casdoor
Device Authorization/oauth/v2/device_authorizationPOSTEndpoint inicial del Device Code Flow

Parámetros del endpoint Authorization ​

ParámetroObligatorioDescripción
client_id✓Se obtiene tras crear la aplicación en la consola
redirect_uri✓Debe coincidir exactamente con la URI registrada en la consola
response_type✓En el flujo de código de autorización es fijo code
scope✓Debe incluir al menos openid; varios valores separados por espacios
stateRecomendadoPreviene CSRF; se devuelve con el valor original en el callback
nonceRecomendadoPreviene reenvíos; se escribe en el id_token
code_challengeObligatorio con PKCEBase64URL(SHA-256(code_verifier))
code_challenge_methodObligatorio con PKCEFijo S256
promptOpcionallogin fuerza un nuevo inicio de sesión; consent fuerza mostrar la página de consentimiento
login_hintOpcionalRellena previamente el nombre de usuario para acelerar el inicio de sesión

Grant Type / Flow admitidos ​

Elige el flujo de autorización según el tipo de cliente; para SPA / Native / Web se prefiere Authorization Code + PKCE.

FlowCaso de uso¿Recomendado?
Authorization Code + PKCESPA, Native App, Web App✓ Preferido
Client CredentialsM2M, servicios backend✓ Escenarios M2M
Device CodeCLI, TV, IoT, dispositivos sin navegador✓ Dispositivos especiales
Refresh TokenSesiones de larga duración, renovación silenciosaJunto con el scope offline_access
JWT Bearer (Service User)Cuentas de servicio, intercambio de JWT firmado por tokenEscenarios de cuenta de servicio

Authorization Code + PKCE, seis pasos:

  1. El cliente genera un code_verifier (cadena aleatoria de 43-128 caracteres).
  2. Calcula code_challenge = Base64URL(SHA-256(verifier)).
  3. Redirige a /oauth/v2/authorize (con el challenge).
  4. El usuario inicia sesión y autoriza; el callback incluye el code.
  5. POST a /oauth/v2/token (con code + code_verifier).
  6. Se obtienen los tokens access / id / refresh.

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 → se obtiene un access_token (sin id_token, sin identidad de usuario).

Refresh Token: la primera autorización incluye el scope offline_access → almacena de forma segura el refresh_token → cuando el access_token expire, POST grant_type=refresh_token + refresh_token=<token> → se obtiene un nuevo token (el refresh_token puede rotarse).


Ejemplo completo de 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" }

Lista de Scopes ​

Los scopes estándar de OIDC determinan qué claims se devuelven; los scopes específicos de Casdoor controlan el audience, los roles y las restricciones de organización.

Scopes estándar ​

ScopeDescripciónToken afectado
openidObligatorio; declara una solicitud OIDC y devuelve id_tokenid_token
profileObtiene name, given_name, family_name, preferred_username, localeid_token, userinfo
emailObtiene email, email_verifiedid_token, userinfo
phoneObtiene phone_number, phone_number_verifiedid_token, userinfo
addressObtiene la información de dirección del usuarioid_token, userinfo
offline_accessSolicita un refresh_token (solo válido en el flujo Authorization Code)—

Scopes específicos de Casdoor ​

ScopeDescripciónToken afectado
urn:casdoor:iam:org:project:id:{projectid}:audAñade el project ID indicado al aud del access token; la verificación de firma del servidor debe coincidiraccess_token
urn:casdoor:iam:org:project:id:casdoor:audAñade el propio project ID de Casdoor al aud (para acceder a la API de Casdoor)access_token
urn:casdoor:iam:org:projects:rolesEl token incluye la lista de roles de todos los proyectos autorizadosid_token, access_token, userinfo
urn:casdoor:iam:org:project:role:{rolekey}Solicita solo el claim de un rol específico, p. ej. ...:role:adminid_token, access_token
urn:casdoor:iam:org:id:{orgid}Restringe a que el usuario pertenezca a esa organización; fuerza el aislamiento en inicios de sesión entre organizacionesPara validación
urn:casdoor:iam:org:domain:primary:{domain}Restringe al dominio principal de la organización del usuario, p. ej. ...:primary:lurus.cnPara validación
urn:casdoor:iam:user:metadataEl token incluye los metadatos personalizados del usuario (pares clave-valor Base64)id_token, access_token, userinfo
urn:casdoor:iam:user:resourceownerObtiene el ID, nombre y dominio principal de la organización a la que pertenece el usuarioid_token, access_token, userinfo
urn:casdoor:iam:org:idp:id:{idp_id}Salta directamente al IdP indicado (WeCom, Feishu) y omite la página de selección de IDPControl de comportamiento

Combinación habitual (Web App): openid profile email offline_access urn:casdoor:iam:org:projects:roles urn:casdoor:iam:org:project:id:{projectid}:aud


Lista de Claims ​

La siguiente tabla indica en qué token aparece cada claim y de qué scope depende.

Claims estándar ​

ClaimDescripciónid_tokenaccess_tokenuserinfoScope requerido
subID único del usuario (ID interno de Casdoor)✓✓ (JWT)✓Siempre
issIssuer, fijo https://identity.lurus.cn✓✓—Siempre
audAudience, el client_id de la aplicación✓✓—Siempre
exp / iatHora de expiración / emisión (Unix)✓✓—Siempre
auth_timeHora real del inicio de sesión del usuario✓——Siempre
nonceValor aleatorio anti-reenvío✓——Siempre (si existe)
amrMétodo de autenticación, p. ej. ["pwd"], ["mfa"]✓——Siempre
name / given_name / family_nameNombre completo / nombre / apellido✓*—✓profile
preferred_usernameNombre de inicio de sesión (username@primarydomain)✓—✓profile
locale / updated_atPreferencia de idioma (p. ej. zh) / hora de actualización de la información✓*—✓profile
email / email_verifiedCorreo / si está verificado (Boolean)✓*—✓email

✓* = solo se devuelve si el response_type incluye id_token o se solicita explícitamente.

Claims específicos de Casdoor ​

ClaimDescripciónid_tokenaccess_tokenuserinfo
urn:casdoor:iam:org:project:rolesRoles de proyecto del usuario, estructura { "roleName": { "orgId": "domain" } }✓✓ (JWT)✓
urn:casdoor:iam:org:domain:primaryDominio principal de la organización a la que pertenece el usuario✓✓ (JWT)✓
urn:casdoor:iam:user:metadataMetadatos personalizados del usuario, { "key": "base64value" }✓✓ (JWT)✓
urn:casdoor:iam:user:resourceowner:id / :name / :primary_domainID / nombre / dominio principal de la organización del usuario✓✓ (JWT)✓

Ejemplo de claim de roles:

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

Ejemplo de claim de metadata (el value está en Base64; para usarlo hay que decodificarlo con atob() / base64.StdEncoding.DecodeString()):

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

Verificación de tokens ​

Cuando el servidor recibe un Bearer token, no debe juzgar su validez solo por el formato; es obligatorio:

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)

Bibliotecas recomendadas ​

LenguajeBibliotecaInstalación
TS / Node.jsoidc-client-tsbun add oidc-client-ts
TS / Node.jsjose (verificación de firma ligera)bun add jose
Gogo-oidcgo get github.com/coreos/go-oidc/v3
Pythonpython-josepip install python-jose
Elixir / Phoenixlurus-phoenix (biblioteca interna de la plataforma)ver 2l-bs-admin

Ejemplo de verificación de firma en 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 (tokens opacos) ​

Cuando el access token tiene un formato opaco (no es un JWT), usa Introspection para verificarlo:

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 ​

Apto para dispositivos sin entrada de navegador (CLI, TV, IoT). Los productos CLI de Lurus (Lumen, kova-cli) usan todos este flujo.

Secuencia:

  1. El dispositivo hace POST a /device_authorization.
  2. Recibe device_code + user_code + verification_uri.
  3. Muestra el user_code y la URL al usuario.
  4. El usuario abre verification_uri en el navegador, introduce el user_code e inicia sesión para autorizar.
  5. El dispositivo consulta /token cada interval segundos.
  6. Una vez que el usuario autoriza, la siguiente consulta devuelve los tokens access / id.

Step 1: Solicitar el 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"

Respuesta:

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: Mostrar al usuario ​

Muestra el verification_uri (https://identity.lurus.cn/device) + user_code, o escanea el verification_uri_complete. Expira en 5 minutos.

Step 3: Consultar el endpoint Token ​

Consulta cada interval segundos hasta que tenga éxito o expire. Manejo de errores: authorization_pending → sigue esperando; slow_down → reduce la frecuencia; otros → sale con fallo.

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

La implementación en TypeScript es isomórfica: fetch POST a /device_authorization para iniciar el flujo, y luego consulta /token con setTimeout(interval*1000); ante authorization_pending continúa, ante slow_down aumenta el interval, y con resp.ok devuelve los tokens.


Preguntas frecuentes ​

Los errores de integración más comunes y su solución directa.

Error de audience (el claim aud no coincide)

Síntoma: la verificación de firma reporta token audience mismatch / invalid audience. Causa: el aud del access token contiene por defecto solo el client_id. Solución: añade al scope urn:casdoor:iam:org:project:id:{projectid}:aud para escribir explícitamente el project ID en el aud.

El claim roles está vacío o ausente

Causa: el usuario no tiene un User Grant en ese Project, o no se solicitó el scope de roles. Comprobación: ① en la consola, Project → Authorizations, confirma que hay un Grant de rol; ② el scope incluye urn:casdoor:iam:org:projects:roles; ③ en la configuración del Project, activa «Assert Roles on Authentication».

El id_token no contiene email

Causa: falta el scope email. Solución: añade email al scope (p. ej. openid profile email).

El refresh token deja de ser válido y obliga a iniciar sesión de nuevo

La renovación devuelve invalid_grant. Posibles causas: la primera autorización no incluyó el scope offline_access; el refresh token expiró; el usuario revocó la sesión en la consola. Solución: vuelve a iniciar el flujo Authorization Code; si necesitas renovación de larga duración, asegúrate de que el scope incluya offline_access y de que el tipo de aplicación sea «Web» o «Native».

El code_verifier de PKCE no coincide

/token devuelve invalid_grant: code verifier mismatch. Causa: el verifier cambió entre los dos pasos, o la codificación Base64URL es inconsistente (incluye padding = o usa +/). Solución: usa base64url (sin padding, con -_ en lugar de +/); en una SPA usa sessionStorage para conservar el verifier entre páginas.


Siguiente paso

Enlaces relacionados ​

Powered by Lurus Technologies