Saltar al contenido principal

Autenticación CAS (auth_casattras)

Configuración de administración del inicio de sesión contra el CAS del Ministerio (cas.colombiaaprende.edu.co). El plugin autentica al usuario contra el CAS y puebla su perfil con los atributos que el servidor libera en la respuesta de validación del ticket.

Ruta: Administración del sitio → Extensiones → Autenticación → CAS-Attras (o buscar "casattras" en el buscador de administración).

Por qué casattras y no cas

En Moodle 5.0 el plugin auth_cas fue removido del core (MDL-78778) y trasladado a moodlehq/moodle-auth_cas. Este proyecto corre 5.1, así que no existe public/auth/cas/. La vía instalada es auth_casattras, que obtiene los atributos desde la propia respuesta del CAS (phpCAS::getAttributes()) en lugar de consultarlos por LDAP.

Los dos plugins no pueden convivir habilitados: comparten la librería phpCAS y entran en conflicto. El propio auth_casattras se autodeshabilita si detecta auth_cas activo.

Ajustes del servidor

AjusteClaveValorNotas
Nombre del servidorhostnamecas.colombiaaprende.edu.coMismo host en todos los ambientes; lo que cambia entre ellos es el service
Ruta basebaseuricasphpCAS normaliza cas, /cas y /cas/ al mismo valor
Puertoport443
Versión del protocolocasversionCAS_VERSION_3_0La 3.0 es la que libera atributos de forma estándar, que es lo que el plugin necesita
Autenticación múltiplemultiauth0Con 1 el formulario local se mantiene y el CAS aparece como enlace adicional. Con 0, toda visita a /login/index.php se redirige al CAS y no queda salida por navegador si el CAS falla. Ver "Acceso de emergencia sin CAS"
Validar certificadocertificatecheck1
Ruta del certificadocertificatepath/etc/ssl/certs/ca-certificates.crtEl plugin solo distingue entre "un archivo CA concreto" y "no validar nada"; apuntar al bundle del sistema equivale a usar la cadena de CA pública. El certificado del CAS es de una CA pública, así que no hace falta un CA cert propio
Proxy CASproxycas0
Logout centralizadologoutcas1prelogout_hook() llama a phpCAS::logoutWithRedirectService(), que redirige al slug / del wwwroot (o a logoutreturnurl si está configurado) tras cerrar la sesión del CAS. Es necesario con multiauth = 0: si la sesión del CAS sobrevive al logout de Moodle, el usuario que vuelva a /login/index.php es re-autenticado al instante y percibe que no puede salir. Cierra la sesión en todos los servicios que compartan el CAS, incluido el Drupal
URL de retorno tras logoutlogoutreturnurlvacío (usa wwwroot)A dónde vuelve el usuario después del logout centralizado. El hook añade una barra final si falta: el CAS solo redirige cuando el service termina en / o en una ruta, no cuando es un host a secas
Nombre del métodoauth_nameCASEs la etiqueta del botón en la página de login
Logo del métodoauth_logovacíoSin logo no se renderiza icono. Subir una imagen aquí es la única forma de que aparezca

El service que se envía al CAS no se configura: se deriva de $CFG->wwwroot. Debe ser https, sin barra final, y detrás de nginx-proxy-manager requiere $CFG->sslproxy = true o el service sale en http:// y el CAS rechaza el ticket.

Mapeo de atributos

Campo de MoodleAtributo CASNotas
usernameNo se mapea: sale de phpCAS::getUser(), que devuelve el uid
firstnamegivenname
lastnamesn
emailmail2El correo viene en mail2, no en mail. Mapear mail deja a todos los usuarios sin correo
idnumberuidEquivalente del "Número de documento"
profile_field_tipousuariotipousuarioCampo de perfil personalizado; ver abajo

Los cinco llevan field_updatelocal_* = onlogin y field_lock_* = locked. Los dos son necesarios, no opcionales: update_user_record_by_id() descarta el campo si cualquiera de los dos está vacío, y solo actualiza cuando field_updatelocal es exactamente onlogin. La combinación produce el comportamiento "aplicar en cada login y sobrescribir": el CAS es la fuente de verdad y el usuario no puede editar esos campos en Moodle.

El casing importa

get_userinfo() hace $casattras[$field], una búsqueda de clave exacta. Si el CAS devuelve givenName, mapear givenname no trae nada. El síntoma es característico: usuario creado, sesión iniciada y campos vacíos.

Los nombres en minúsculas de la tabla están confirmados contra el CAS del Ministerio. Si en otro entorno hay dudas, poner el sitio en modo desarrollador y leer el log: el plugin registra el atributo esperado y los que sí llegaron.

Cuentas preexistentes: el requisito que no es evidente

Un usuario que ya existe en Moodle no entra por CAS aunque el CAS lo autentique correctamente, salvo que su campo auth sea casattras.

authenticate_user_login() restringe la autenticación al plugin del propio usuario. Con auth = manual, el plugin CAS nunca llega a consultarse: el hook de login deja una contraseña ficticia y manual la compara contra el hash local.

El síntoma engaña, porque todo el recorrido externo funciona:

  1. El usuario pulsa el botón CAS y es redirigido.
  2. Se autentica correctamente en el CAS.
  3. Vuelve a Moodle con un ticket válido.
  4. Moodle le muestra la página de login con "Nombre de usuario o contraseña incorrectos".

Cómo distinguirlo de un problema real del CAS: en el log del contenedor aparece Failed Login: <uid> en una petición cuyo referer es el propio CAS. Si el referer es el CAS, la autenticación externa funcionó y el rechazo es de Moodle.

La solución es migrar la cuenta — cambiar el campo auth de una cuenta que ya existe en este mismo ambiente, nunca traer ni copiar la cuenta desde otro ambiente:

UPDATE mdl_user SET auth = 'casattras' WHERE username = '<uid>';

Capturar antes los valores actuales de auth, firstname, lastname, email e idnumber: con field_updatelocal_* = onlogin el CAS los sobrescribe en el primer login. El hash de contraseña local se conserva en la fila, así que volver a manual restaura el login local.

Esto condiciona toda la integración

Si el sitio se pobló con usuarios importados cuyos usernames son los mismos identificadores que emite el CAS, entonces la creación automática de cuentas casi no se ejercita y el camino que importa es el de actualización — el que habilita field_updatelocal_* = onlogin. Con el valor por defecto oncreate, un usuario que ya existe no recibiría ningún dato del CAS.

Antes de una migración masiva, dos comprobaciones: excluir las cuentas que no existen en el CAS (administración, cuentas de servicio, usuarios de prueba), y revisar si las cuentas a migrar tienen una contraseña local utilizable. Una columna password que no empieza por $ no es un hash en formato crypt, y validate_internal_user_password() valida con password_verify(): esas cuentas no pueden autenticarse localmente hoy, así que migrarlas no les quita acceso — se lo da.

Esta migración es siempre intra-ambiente: cambia cuentas que ya existen en ese Moodle. No es una vía para llevar cuentas de Local o Pruebas hacia Certificación o Producción — ver qué NO incluye la entrega.

Campo de perfil tipousuario

El atributo tipousuario se guarda en un campo de perfil personalizado y no dispara ninguna asignación automática de rol. Esto es deliberado.

En Drupal los roles son globales, y por eso su módulo puede mapear atributo → rol directamente. Moodle no funciona así: student, teacher y editingteacher son asignaciones contextuales, solo asignables en contexto de curso o de módulo. Lo que determina qué ve un usuario es en qué cursos está matriculado, no una etiqueta en su cuenta; el equivalente funcional de "darle acceso de estudiante" es matricularlo. Esa separación entre autenticación (quién eres) y matriculación (a qué entras) es el modelo estándar de Moodle: el CAS resuelve la identidad y la matriculación sigue por los medios actuales del sitio (manual, self, cohort).

Ruta: Administración del sitio → Usuarios → Campos de perfil de usuario. Tipo texto, shortname tipousuario. Debe existir antes de configurar field_map_profile_field_tipousuario: el plugin lee la lista de campos personalizados desde la base de datos, y un mapeo hacia un campo inexistente se guarda pero no se usa.

El campo funciona además como instrumento de descubrimiento. Tras varios logins reales entrega el dato necesario para diseñar la asignación de roles:

SELECT d.data AS tipousuario, COUNT(*) AS usuarios
FROM mdl_user_info_data d
JOIN mdl_user_info_field f ON f.id = d.fieldid
WHERE f.shortname = 'tipousuario'
GROUP BY d.data ORDER BY usuarios DESC;

Por cada valor hay que definir con el cliente a qué cursos debe entrar y con qué rol. El camino estándar es una cohorte por valor y matriculación por cohorte (enrol_cohort); el core de Moodle no tiene cohortes dinámicas, así que la pertenencia automática requiere un plugin comunitario de cohortes por reglas.

Habilitar y deshabilitar

La lista de plugins de autenticación habilitados es el ajuste auth, una lista completa separada por comas, no un añadido:

docker exec colombiaaprende_web php /var/www/admin/cli/cfg.php --name=auth --set=email,casattras
docker exec colombiaaprende_web php /var/www/admin/cli/purge_caches.php

Para que el auto-registro de usuarios nuevos ocurra, authpreventaccountcreation debe estar en 0.

Deshabilitar (--set=email) es el rollback completo del inicio de sesión por CAS. Conviene mantener al menos una cuenta administradora con auth manual y contraseña conocida, y tener su sesión abierta mientras se toca cualquier ajuste de autenticación.

Acceso de emergencia sin CAS (solo Pruebas)

Con multiauth = 0 el formulario local de Moodle deja de ser alcanzable desde el navegador. auth_casattras no implementa un bypass web: el auth_cas del core histórico aceptaba /login/index.php?authCAS=NOCAS, pero casattras no tiene equivalente — los únicos optional_param de loginpage_hook() son username, ticket y authCASattras.

Por eso el ambiente de Pruebas lleva un parche propio que restaura ese bypass:

https://campusvirtual.metaversodenegocios.com/login/index.php?nocas=1

Ese enlace omite el redirect y devuelve el formulario local de usuario y contraseña. Sirve para poder seguir trabajando en el desarrollo cuando el CAS está caído o no responde; sin él, la única vía de acceso en esa situación es SSH al servidor.

Solo aplica a Pruebas

Este bypass no existe ni debe existir en Certificación ni en Producción. Es una herramienta del periodo de desarrollo y pruebas, no parte de la entrega.

  • No está en el árbol de código fuente: vive únicamente en el servidor de Pruebas. Por eso cualquier despliegue de auth/casattras desde el repositorio lo revierte en silencio, y con multiauth = 0 eso deja SSH como única vía de acceso si el CAS falla. Si hay que redesplegar ese plugin en Pruebas, reaplicar el parche después.
  • En Certificación y Producción el acceso de rescate se resuelve por los medios del MEN, no por una URL pública. Un parámetro que devuelve el formulario local en un sitio donde el CAS es el único método de identidad es una puerta que no corresponde abrir fuera de un ambiente de desarrollo.

Aun con el bypass, solo entran las cuentas que tengan una contraseña local utilizable: una columna password que no empieza por $ no es un hash en formato crypt y password_verify() la rechaza. Ver "Cuentas preexistentes".

El procedimiento completo —parche, despliegue y verificación— está en el runbook pruebas/CAS_FORZAR_LOGIN_PRUEBAS.md del repositorio.

Verificación

  1. /login/index.php responde 302 hacia https://cas.colombiaaprende.edu.co/cas/login?service=<wwwroot urlencoded>..., sin mostrar el formulario local. El botón de login del tema lleva al mismo sitio: get_login_url() resuelve a /login/index.php, así que botón y slug son un único destino. Si el service sale con otro esquema, host o puerto, revisar wwwroot y sslproxy.

  2. El Location incluye &gateway=true, que añade phpCAS::checkAuthentication() antes de forceAuthentication(). La cadena completa son tres saltos: Moodle → CAS con gateway → rebote a Moodle sin ticket → CAS sin gateway → formulario del CAS.

    No verificar con curl -L sin cookie jar

    phpCAS guarda el resultado del chequeo de gateway en $_SESSION. Sin persistir la sesión, el rebote se repite indefinidamente y aparenta un bucle de redirección que no existe en un navegador. Usar curl -c cookies.txt -b cookies.txt -L.

  3. Tras autenticarse, el usuario vuelve a Moodle con sesión iniciada y con firstname, lastname, email e idnumber poblados:

    SELECT id, username, auth, firstname, lastname, email, idnumber
    FROM mdl_user WHERE auth = 'casattras';
  4. El usuario no tendrá matrículas: es el comportamiento esperado, no un fallo. Ver la sección del campo tipousuario.

  5. Con logoutcas = 1, cerrar sesión (/login/logout.php, con sesskey) responde con un 302 en cadena que termina en el slug / del wwwroot (o en logoutreturnurl si está configurado), con el usuario deslogeado tanto de Moodle como del CAS. Probar sin cookie jar previo para no arrastrar una sesión de CAS ya vigente:

    curl -c cookies.txt -b cookies.txt -sS -o /dev/null -D - -L \
    "$WWWROOT/login/logout.php?sesskey=<sesskey de la sesión activa>" \
    | grep -iE "^HTTP/|^location:"

Problemas comunes

  • "Usuario o contraseña incorrectos" después de autenticarse en el CAS: la cuenta ya existe con otro auth. Ver "Cuentas preexistentes". Es el fallo más probable en un sitio con usuarios importados.
  • Usuario creado pero campos vacíos: casing del atributo. Poner el sitio en modo desarrollador (debug = 32767) dejando debugdisplay = 0, repetir el login y leer docker logs colombiaaprende_web: el plugin registra el atributo esperado y los nombres de los que sí llegaron. Corregir el field_map_* correspondiente; no requiere redespliegue. Devolver debug a 0 al terminar.
  • Login correcto y ningún atributo: la política de liberación de atributos en CAS se define por servicio, así que el login puede funcionar y phpCAS::getAttributes() volver vacío. Es una solicitud al administrador del CAS, no un problema de Moodle.
  • Error de SSL al validar el ticket: confirmar que el contenedor alcanza el CAS (curl a https://cas.colombiaaprende.edu.co/cas/login debe dar 200). Si el certificado no valida contra la cadena pública, copiar el CA cert del cliente al árbol montado, fuera del docroot, y apuntar certificatepath ahí.
  • Un campo muestra Array: el CAS devolvió un atributo multivaluado. phpCAS entrega un array y Moodle lo castea a string al guardarlo. Se resuelve decidiendo con el cliente qué valor tomar.
  • Icono roto junto al botón de CAS: auth_logo vacío en una versión del plugin sin el guard correspondiente. Subir un logo o aplicar el guard.

Paridad con producción

Pruebas replica el comportamiento de producción (campus.colombiaaprende.edu.co) y del Drupal, donde el enlace al CAS en el formulario estándar está desactivado: multiauth = 0, logoutcas = 1, sin acceso de invitado (guestloginbutton = 0), sin auto-registro (registerauth vacío) y con forgottenpasswordurl apuntando a la recuperación de Colombia Aprende, porque la contraseña vive en el CAS y el flujo interno de Moodle no aplica.

El hostname ya coincide en todos los ambientes; lo único que cambia es el service, que se deriva de wwwroot, así que el CAS debe tener registrados todos los dominios.

Dos diferencias deliberadas frente a Certificación y Producción, ninguna forma parte de la entrega:

  • El bypass ?nocas=1, exclusivo de Pruebas. Ver "Acceso de emergencia sin CAS".
  • El parche de prelogout_hook() que usa phpCAS::logoutWithRedirectService() en vez de phpCAS::logoutWithURL(). En Certificación y Producción el logout centralizado ya redirige correctamente al slug / del sitio: esos ambientes no comparten el despliegue de código de este proyecto para auth/casattras —el plugin queda fuera de los cinco componentes propios que cubre el RFC de entrega (ver Integración y despliegue)—, así que el fix no debe transferirse ahí. Aplicarlo sería redundante en el mejor caso y, si esos ambientes usan una versión distinta del plugin o del logoutreturnurl, un cambio de comportamiento no solicitado por el cliente en un ambiente donde el CAS ya funciona como se requiere.
No propagar el fix de logout fuera de Pruebas

El commit que corrige prelogout_hook() vive en el repositorio (a diferencia del bypass ?nocas=1, que nunca se versiona), así que viajaría por una sincronización de código normal si alguien copiara auth/casattras completo hacia Certificación o Producción. El procedimiento estándar de despliegue no lo hace —ver la tabla de componentes en Integración y despliegue—, pero cualquier transferencia manual de ese directorio fuera de Pruebas debe excluir explícitamente este cambio.

Antes del cambio hay que definir qué pasa con las cuentas existentes: los usuarios creados con auth email o manual no entran por CAS salvo que se migren, y esa migración se acuerda con el cliente, no se ejecuta por defecto.

Historial de cambios

Ver CHANGELOG.md en la raíz del sitio de documentación.