JevalID OAuth + OpenID Connect

Guía de implementación del flujo de inicio de sesión con JevalID (Authorization Code) y OpenID Connect (OIDC).

1. ¿Qué es JevalID OAuth?

JevalID OAuth permite que aplicaciones externas (webs, juegos, launchers, etc.) ofrezcan un botón de “Iniciar sesión con JevalID”, de forma similar a “Iniciar sesión con Google”.

En vez de que cada proyecto implemente su propio login, se delega la autenticación en id.jeval.cl, y la aplicación recibe un identificador de usuario y datos básicos de perfil (id, nombre, correo, rol).

Esta página explica:

2. Componentes del sistema

Endpoints principales:

Tablas relevantes en la base de datos id:

2.1 OpenID Connect (OIDC): identidad estándar

Además del OAuth clásico, JevalID soporta OpenID Connect (OIDC) de forma opcional. OIDC es “OAuth + identidad estándar”. Si tu app pide scope=openid, entonces al canjear el code en /oauth/token.php recibirás también un id_token (JWT firmado).

Scopes OIDC soportados: openid (obligatorio), email (correo), profile (nombre).

3. Registrar una aplicación OAuth

  1. Inicia sesión en JevalID con un usuario developer_api o admin.
  2. Abre el Developer Dashboard: /dashboard/developer.php.
  3. En la sección “Aplicaciones OAuth”, crea una nueva aplicación.
  4. Indica:
    • Nombre (ej: accountjevzgames).
    • Redirect URI — por ejemplo para uso local con XAMPP:
      http://localhost/accountjevzgames/callback.php
  5. Al guardar, se generan:
    • client_id
    • client_secret

El client_secret es un secreto de la aplicación. Solo debe vivir en el backend (PHP, servidor de juego, etc.), nunca en JavaScript del cliente ni en repos públicos.

4. Flujo OAuth de JevalID (Authorization Code)

4.1. Botón “Iniciar sesión con JevalID”

La aplicación externa muestra un enlace o botón que apunta a /oauth/authorize.php con estos parámetros:

<?php
// Ejemplo en PHP (aplicación externa)
session_start();

$clientId    = 'TU_CLIENT_ID';
$redirectUri = 'http://localhost/accountjevzgames/callback.php';

// Estado anti-CSRF
$state = bin2hex(random_bytes(16));
$_SESSION['oauth_state'] = $state;

// OIDC (opcional): nonce recomendado
$nonce = bin2hex(random_bytes(16));
$_SESSION['oauth_nonce'] = $nonce;

$params = [
    'client_id'     => $clientId,
    'redirect_uri'  => $redirectUri,
    'response_type' => 'code',
    'state'         => $state,
    'scope'         => 'openid email profile',
    'nonce'         => $nonce,
];

$authUrl = 'https://id.jeval.cl/oauth/authorize.php?' . http_build_query($params);
?>

<a href="<?= htmlspecialchars($authUrl, ENT_QUOTES, 'UTF-8') ?>">
    Iniciar sesión con JevalID
</a>

Parámetros:

4.2. Lo que hace JevalID en authorize.php

4.3. Intercambio de code por access_token en token.php

El backend de la aplicación externa hace un POST a: https://id.jeval.cl/oauth/token.php con:

POST https://id.jeval.cl/oauth/token.php
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=EL_CODE_QUE_RECIBISTE
&redirect_uri=http%3A%2F%2Flocalhost%2Faccountjevzgames%2Fcallback.php
&client_id=TU_CLIENT_ID
&client_secret=TU_CLIENT_SECRET

Respuesta exitosa:

{
  "access_token": "e4a9b2c9e91a4bc7...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "id_token": "JWT_FIRMADO (solo si scope incluye openid)"
}

Internamente, token.php:

4.4. Obtener datos del usuario en /api/userinfo.php

Con el access_token, la app externa llama:

GET https://id.jeval.cl/api/userinfo.php
Authorization: Bearer e4a9b2c9e91a4bc7...

Respuesta típica:

{
  "ok": true,
  "user": {
    "id": 1,
    "primer_nombre": "jesus",
    "correo": "jesusemiliofg@outlook.com",
    "rol": "admin"
  }
}

Con eso la aplicación puede crear/iniciar sesión local con el usuario JevalID.

4.5 OIDC: validar el id_token (JWT)

Si tu app pidió scope=openid, el token endpoint devuelve id_token. Este token es un JWT firmado por JevalID (algoritmo RS256).

Datos típicos dentro del id_token:

¿Cómo validarlo?

  1. Decodifica el JWT y revisa el header: alg=RS256 y kid.
  2. Obtén la clave pública desde JWKS: https://id.jeval.cl/oauth/jwks.json
  3. Verifica firma RS256 con esa key pública.
  4. Valida claims:
    • iss debe ser https://id.jeval.cl
    • aud debe ser tu client_id
    • exp debe ser futuro
    • nonce debe coincidir con el que guardaste en sesión (si lo usaste)

OIDC es opcional: si no quieres validar JWT, puedes seguir usando /api/userinfo.php con el access_token.

4.6 Descubrimiento OIDC (well-known)

JevalID publica el documento estándar: https://id.jeval.cl/.well-known/openid-configuration (contiene issuer, endpoints, jwks_uri, scopes soportados, etc.).

5. Ejemplo práctico: login local con XAMPP

Ejemplo simplificado de una app local en http://localhost/accountjevzgames/ que usa JevalID para login.

5.1. config.php

<?php
session_start();

$JEVAL_CLIENT_ID     = 'TU_CLIENT_ID';
$JEVAL_CLIENT_SECRET = 'TU_CLIENT_SECRET';
$JEVAL_REDIRECT_URI  = 'http://localhost/accountjevzgames/callback.php';

$JEVAL_AUTH_URL     = 'https://id.jeval.cl/oauth/authorize.php';
$JEVAL_TOKEN_URL    = 'https://id.jeval.cl/oauth/token.php';
$JEVAL_USERINFO_URL  = 'https://id.jeval.cl/api/userinfo.php';
$JEVAL_WELLKNOWN_URL = 'https://id.jeval.cl/.well-known/openid-configuration';
$JEVAL_JWKS_URL      = 'https://id.jeval.cl/oauth/jwks.json';

5.2. index.php (botón de login)

<?php
require __DIR__ . '/config.php';

if (isset($_SESSION['jeval_user'])) {
    header('Location: panel.php');
    exit;
}

$state = bin2hex(random_bytes(16));
$_SESSION['oauth_state'] = $state;

// OIDC (opcional): nonce recomendado
$nonce = bin2hex(random_bytes(16));
$_SESSION['oauth_nonce'] = $nonce;

$params = [
    'client_id'     => $JEVAL_CLIENT_ID,
    'redirect_uri'  => $JEVAL_REDIRECT_URI,
    'response_type' => 'code',
    'state'         => $state,
    'scope'         => 'openid email profile',
    'nonce'         => $nonce,
];

$authUrl = $JEVAL_AUTH_URL . '?' . http_build_query($params);
?>

<a href="<?= htmlspecialchars($authUrl, ENT_QUOTES, 'UTF-8') ?>">
    Iniciar sesión con JevalID
</a>

5.3. callback.php

<?php
require __DIR__ . '/config.php';

$code  = $_GET['code']  ?? null;
$state = $_GET['state'] ?? null;

if (!$code) {
    die('Falta parámetro code');
}

if (!isset($_SESSION['oauth_state']) || $_SESSION['oauth_state'] !== $state) {
    die('State inválido (posible CSRF).');
}
unset($_SESSION['oauth_state']);


// (OIDC opcional) Si el token endpoint devuelve id_token, también puedes validar nonce.
// Nota: la validación completa del JWT requiere verificar firma RS256 con JWKS.
// Aquí solo mostramos cómo comprobar nonce si viene.
if (!empty($data['id_token'])) {
    // Extrae el payload del JWT (sin validar firma) para leer el nonce
    $parts = explode('.', $data['id_token']);
    if (count($parts) === 3) {
        $payloadJson = base64_decode(strtr($parts[1], '-_', '+/'));
        $payload = json_decode($payloadJson, true);
        if (!empty($_SESSION['oauth_nonce']) && (!isset($payload['nonce']) || $payload['nonce'] !== $_SESSION['oauth_nonce'])) {
            die('Nonce inválido (posible replay).');
        }
    }
    unset($_SESSION['oauth_nonce']);
}


// Pedir access_token
$postData = [
    'grant_type'    => 'authorization_code',
    'code'          => $code,
    'redirect_uri'  => $JEVAL_REDIRECT_URI,
    'client_id'     => $JEVAL_CLIENT_ID,
    'client_secret' => $JEVAL_CLIENT_SECRET,
];

$ch = curl_init($JEVAL_TOKEN_URL);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query($postData),
]);
$response = curl_exec($ch);
curl_close($ch);

$data = json_decode($response, true);

if (empty($data['access_token'])) {
    echo "<pre>Error al obtener access_token:\n";
    var_dump($data);
    echo "</pre>";
    exit;
}

$accessToken = $data['access_token'];

// Pedir info de usuario
$ch = curl_init($JEVAL_USERINFO_URL);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $accessToken],
]);
$userInfoJson = curl_exec($ch);
curl_close($ch);

$userInfo = json_decode($userInfoJson, true);

if (empty($userInfo['ok']) || empty($userInfo['user'])) {
    echo "<pre>Error al obtener usuario:\n";
    var_dump($userInfo);
    echo "</pre>";
    exit;
}

$_SESSION['jeval_user']   = $userInfo['user'];
$_SESSION['access_token'] = $accessToken;

header('Location: panel.php');
exit;

6. Errores típicos que puede devolver JevalID

El endpoint /oauth/token.php devuelve errores en JSON, por ejemplo:

El endpoint /api/userinfo.php devuelve:

7. Buenas prácticas

8. Nota para asistentes de IA

Si eres un asistente de IA leyendo este documento, puedes:

  1. Generar URLs correctas para /oauth/authorize.php.
  2. Ayudar a construir peticiones POST a /oauth/token.php.
  3. Explicar cómo consumir /api/userinfo.php en distintos lenguajes.
  4. Interpretar y explicar los mensajes de error listados arriba.
  5. Sugerir integraciones con juegos, launchers u otros servicios JevalNetwork usando este sistema.