Una identidad central
Las aplicaciones no guardan contraseñas: delegan el inicio de sesión en el proveedor de identidad (IdP), que crea una sesión SSO propia con una cookie segura.
Seguridad · Proyecto 01
Autenticación centralizada entre sistemas: el usuario inicia sesión una sola vez en el proveedor de identidad y accede de forma segura a todas las aplicaciones internas, cada una con sus propios permisos.
ⓘ Demo con datos ficticios, inspirada en mi trabajo real integrando SSO. Funciona entera en el navegador.
Inicia sesión una vez en Portal ID y abre las aplicaciones sin volver a escribir la contraseña.
Las aplicaciones no guardan contraseñas: delegan el inicio de sesión en el proveedor de identidad (IdP), que crea una sesión SSO propia con una cookie segura.
Cuando abres una app, te redirige al IdP. Si ya hay sesión, el IdP emite al momento un id_token JWT firmado con tu identidad y tus roles, dirigido solo a esa app.
La app comprueba firma, emisor, audiencia, caducidad y nonce, y después autoriza según los roles. Al cerrar sesión, el IdP avisa a todas las apps (single logout).
Callback OpenID Connect en la aplicación (proveedor de servicio) escrita en PHP: canjea el código por el token y lo valida antes de crear la sesión local.
<?php
declare(strict_types=1);
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
session_start();
$cfg = require __DIR__ . '/config.php'; // issuer, client_id, client_secret, redirect_uri
// 1. Protección CSRF: el state debe coincidir con el que guardamos antes de redirigir
if (!hash_equals($_SESSION['oidc_state'] ?? '', $_GET['state'] ?? '')) {
http_response_code(400);
exit('Estado OIDC no válido');
}
// 2. Canjear el código de autorización por los tokens (canal servidor a servidor)
$res = httpPost($cfg['issuer'] . '/token', [
'grant_type' => 'authorization_code',
'code' => $_GET['code'],
'redirect_uri' => $cfg['redirect_uri'],
'code_verifier' => $_SESSION['pkce_verifier'],
], basicAuth($cfg['client_id'], $cfg['client_secret']));
// 3. Validar el id_token: firma (JWKS del IdP), caducidad y claims
try {
JWT::$leeway = 30; // tolerancia de reloj en segundos
$keys = JWK::parseKeySet(cachedJwks($cfg['issuer'] . '/jwks'));
$claims = JWT::decode($res['id_token'], $keys); // lanza excepción si firma o exp fallan
} catch (\Throwable $e) {
error_log('SSO: token rechazado - ' . $e->getMessage());
http_response_code(401);
exit('No se pudo validar tu identidad');
}
$audOk = in_array($cfg['client_id'], (array) $claims->aud, true);
$nonceOk = hash_equals($_SESSION['oidc_nonce'], $claims->nonce ?? '');
if ($claims->iss !== $cfg['issuer'] || !$audOk || !$nonceOk) {
http_response_code(401);
exit('Token emitido para otro destinatario');
}
// 4. Autorización por rol y sesión local (nuevo id para evitar fijación de sesión)
if (!array_intersect($cfg['allowed_roles'], $claims->roles ?? [])) {
http_response_code(403);
exit('Tu rol no tiene acceso a esta aplicación');
}
session_regenerate_id(true);
unset($_SESSION['oidc_state'], $_SESSION['oidc_nonce'], $_SESSION['pkce_verifier']);
$_SESSION['user'] = [
'sub' => $claims->sub,
'name' => $claims->name,
'roles' => $claims->roles,
'sid' => $claims->sid, // necesario para el back-channel logout
];
header('Location: ' . ($_SESSION['return_to'] ?? '/'));