Muchas organizaciones que usan Google Workspace siguen autenticando a su personal en Drupal contra un servidor LDAP interno. Funciona, pero obliga a mantener una conexión de red entre el servidor web y el directorio, añade módulos con bastante configuración y no aporta nada que Google no haga ya: la cuenta corporativa, la contraseña y el segundo factor viven allí.
En un proyecto reciente para un centro de investigación sustituimos LDAP por login con Google usando el módulo OpenID Connect. En este artículo contamos cómo lo hicimos, qué conviene revisar antes de tocar código y qué detalles pueden romper el sitio si no se tienen en cuenta.
Requisitos previos
- Drupal 11 con Composer.
- Una organización en Google Workspace y acceso (propio o de su equipo de IT) a Google Cloud Console.
- Saber qué usuarios entran hoy por LDAP y cuáles con contraseña de Drupal.
1. Evaluar antes de instalar
Antes de escribir una línea, merece la pena responder a cuatro preguntas.
¿Cuánta gente usa LDAP de verdad? La tabla authmap guarda qué cuentas están vinculadas a un proveedor externo:
SELECT provider, COUNT(*) FROM authmap GROUP BY provider;
En nuestro caso, el módulo LDAP estaba en modo mixed y la mayoría del personal entraba con contraseña de Drupal. Eso simplifica mucho la migración, pero hay que confirmarlo en producción, no en una copia local.
¿LDAP asigna roles? Si hay perfiles de ldap_authorization que mapean grupos a roles, habrá que reproducir ese mapeo. Si los roles se asignan a mano, como era nuestro caso, no hay nada que migrar.
¿Hay código propio que dependa de LDAP? Busca campos y servicios del módulo en tus módulos custom. Nosotros teníamos un hook_user_insert() que leía el campo ldap_user_last_checked para saber si la cuenta venía de LDAP. Al desinstalar ldap_user ese campo desaparece y el hook fallaría en cualquier alta de usuario.
¿Qué versión del módulo vas a usar? Para Drupal 11 solo está disponible la rama 3.x, que a día de hoy sigue en alpha. Las versiones alpha no están cubiertas por la política de avisos de seguridad de drupal.org, y estamos hablando del login. El módulo tiene mucho uso y mantenimiento activo, pero es una decisión que el cliente debe conocer y aceptar.
2. Instalar el módulo
ddev composer require 'drupal/openid_connect:^3.0@alpha'
ddev drush pm:enable openid_connect
El módulo depende de externalauth, que es el que gestiona la relación entre la cuenta de Drupal y la identidad externa en authmap.
3. Crear el cliente OAuth en Google Cloud
En Google Cloud Console, dentro del proyecto de la organización:
- Configura la pantalla de consentimiento OAuth con el tipo de usuario Internal. Así solo pueden autenticarse cuentas del propio Workspace. Es la primera y la mejor barrera.
- Crea unas credenciales de tipo OAuth client ID para aplicación web.
- Registra un Authorized redirect URI por entorno, con el path
/openid-connect/google:
https://proyecto.ddev.site/openid-connect/google
https://pre.proyecto.com/openid-connect/google
https://www.proyecto.com/openid-connect/google
Si el proyecto de Google Cloud lo gestiona el equipo de IT del cliente, este es el paso que más tarda. Conviene pedirlo al principio.
4. Sacar las credenciales de la configuración
El cliente de Google es una entidad de configuración (openid_connect.client.google) que incluye el secreto. No queremos ese secreto en config/sync, así que lo exportamos con los valores vacíos y los sobrescribimos en settings.php desde variables de entorno:
// Google login credentials.
$config['openid_connect.client.google']['settings']['client_id'] = getenv('GOOGLE_OAUTH_CLIENT_ID');
$config['openid_connect.client.google']['settings']['client_secret'] = getenv('GOOGLE_OAUTH_CLIENT_SECRET');
Y las documentamos en .env.example para que cada entorno sepa qué tiene que definir:
# Google login (OAuth client of the Google Cloud project)
GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_CLIENT_SECRET=
5. Configurar OpenID Connect
La configuración se hace en /admin/config/people/openid-connect. Esta es la que exportamos a openid_connect.settings.yml:
always_save_userinfo: true
connect_existing_users: true
override_registration_settings: false
end_session_enabled: false
user_login_display: hidden
redirect_login: admin/content
role_mappings: { }
Las opciones que importan:
connect_existing_users: true: si ya existe una cuenta de Drupal con el mismo email, se vincula en lugar de crear una nueva. Sin esto, el personal acabaría con cuentas duplicadas. Revisa antes que los emails de las cuentas existentes estén bien escritos: un dominio con una errata no se emparejará.end_session_enabled: false: Google no ofrece un endpoint de cierre de sesión OIDC, así que lo desactivamos.user_login_display: hidden: el módulo no añade el botón al formulario de login. Lo colocamos nosotros (lo vemos en el paso 9).redirect_login: destino tras entrar con Google.
6. Restringir el acceso al dominio de la organización
La pantalla de consentimiento Internal ya impide que entren cuentas de fuera, pero no conviene que la seguridad del login dependa solo de una opción de Google Cloud que otra persona puede cambiar. Lo comprobamos también en Drupal con hook_openid_connect_pre_authorize(), que permite rechazar el login devolviendo FALSE:
const MYMODULE_GOOGLE_LOGIN_DOMAIN = 'example.org';
/**
* Implements hook_openid_connect_pre_authorize().
*
* Only verified accounts of the organization's Google Workspace can log in.
*/
function mymodule_openid_connect_pre_authorize($account, array $context) {
$userinfo = $context['userinfo'];
$email = mb_strtolower($userinfo['email'] ?? '');
$is_workspace_account = ($userinfo['hd'] ?? '') === MYMODULE_GOOGLE_LOGIN_DOMAIN;
$is_verified = ($userinfo['email_verified'] ?? FALSE) === TRUE;
$has_domain_email = str_ends_with($email, '@' . MYMODULE_GOOGLE_LOGIN_DOMAIN);
if ($is_workspace_account && $is_verified && $has_domain_email) {
return TRUE;
}
\Drupal::messenger()->addError(t('Only @domain accounts can log in with Google.', [
'@domain' => MYMODULE_GOOGLE_LOGIN_DOMAIN,
]));
return FALSE;
}
Por qué tres condiciones:
hd(hosted domain) es el claim que Google añade solo a las cuentas gestionadas por un Workspace. Es lo que Google recomienda comprobar para restringir el acceso a una organización, porque el email por sí solo no garantiza que la cuenta pertenezca a ella.email_verifiedevita cuentas con un email sin confirmar.- El sufijo con
@evita falsos positivos comox@fakeexample.orgox@example.org.evil.com, que un simplestr_contains()dejaría pasar.
7. No dar roles por defecto a las cuentas nuevas
Es habitual tener un hook_user_insert() que asigna un rol por defecto a quien se registra. En nuestro caso, el rol de candidato. Si no se excluye el login con Google, la primera vez que entre alguien del personal sin cuenta previa recibirá ese rol.
Detectamos el alta por la ruta de retorno de OpenID Connect:
/**
* Implements hook_user_insert().
*/
function mymodule_user_insert(UserInterface $account) {
// Staff accounts created on their first Google login get their roles from
// an administrator.
if (\Drupal::routeMatch()->getRouteName() === 'openid_connect.redirect_controller_redirect') {
return;
}
// Default role logic for self-registered users.
}
Este es también el momento de eliminar cualquier referencia a campos de LDAP como ldap_user_last_checked.
8. Redirigir según el rol
Si tu sitio redirige a cada usuario a una página distinta tras el login, revisa dónde está esa lógica. Lo habitual es añadir un #submit a user_login_form, y el login con Google no pasa por ese formulario, así que esa redirección no se ejecuta.
Para el personal con rol, el destino fijo de redirect_login es suficiente. El caso problemático es la persona que entra por primera vez: su cuenta se crea sin rol y la llevaríamos a una página a la que todavía no tiene acceso. La mandamos a su perfil con hook_openid_connect_post_authorize(), que se ejecuta antes de que el módulo resuelva la redirección:
/**
* Implements hook_openid_connect_post_authorize().
*
* Sends users without a role to their profile until an administrator assigns
* them one.
*/
function mymodule_openid_connect_post_authorize(UserInterface $account, array $context) {
if ($account->getRoles(TRUE) !== []) {
return;
}
\Drupal::request()->getSession()->set('openid_connect_destination', 'user');
}
9. Dejar claro quién debe usar cada acceso
En nuestro sitio conviven dos públicos: el personal del centro, que entra con Google, y los candidatos y evaluadores externos, que siguen usando email y contraseña. Un botón de Google suelto encima del formulario genera dudas, así que separamos la página de login en dos bloques:
- Candidatos y evaluadores externos: el formulario de siempre.
- Personal del centro: una etiqueta "No para candidatos", el texto "Entra con tu cuenta de Google @example.org" y el botón con el diseño oficial de Google.
Para hacerlo sin anidar formularios, envolvemos user_login_form con un #theme_wrappers propio y renderizamos el formulario de OpenID Connect al lado:
/**
* Implements hook_theme().
*/
function mymodule_theme($existing, $type, $theme, $path) {
return [
'mymodule_user_login' => [
'render element' => 'element',
],
];
}
/**
* Prepares variables for the log in page layout.
*/
function template_preprocess_mymodule_user_login(array &$variables) {
$variables['password_login'] = $variables['element']['#children'];
$variables['staff_login'] = \Drupal::formBuilder()->getForm(OpenIDConnectLoginForm::class);
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function mymodule_form_user_login_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// Only on the log in page, not in log in blocks.
if (\Drupal::routeMatch()->getRouteName() === 'user.login') {
$form['#theme_wrappers'] = ['form', 'mymodule_user_login'];
}
}
El orden de #theme_wrappers es lo que evita el anidamiento: primero se envuelve en <form> y después ese formulario completo se coloca dentro de nuestra plantilla, junto al de Google.
10. Retirar LDAP sin romper el despliegue
Con el login con Google validado, desinstalamos los módulos de LDAP (ldap, ldap_servers, ldap_user, ldap_authentication, ldap_authorization, ldap_query y authorization, si solo lo usaba LDAP), borramos sus ficheros de config/sync y las entradas de config_ignore.
Lo que no hicimos en el mismo despliegue fue el composer remove. En los entornos remotos, drush deploy desinstala los módulos al importar la configuración, y para desinstalar un módulo su código tiene que estar presente. Si se quita el paquete a la vez, la importación falla. Por eso lo hacemos en dos pasos:
- Primer despliegue: config sin LDAP. Los módulos se desinstalan.
- Segundo despliegue:
composer remove drupal/ldap drupal/authorization.
Las filas antiguas de authmap con proveedor ldap_user no molestan, pero pueden limpiarse con un hook_post_update_NAME().
Y antes de desinstalar nada en cualquier entorno: copia de seguridad de la base de datos.
Qué probar
| Caso | Resultado esperado |
|---|---|
| Cuenta del Workspace con cuenta previa en Drupal | Entra con su cuenta, sin duplicados |
| Cuenta del Workspace sin cuenta previa | Se crea sin rol y va a su perfil |
| Segundo login de esa misma cuenta | Entra con la misma cuenta |
| Cuenta de Gmail personal | Rechazada con mensaje |
| Email no verificado | Rechazado |
Dominio parecido (fakeexample.org, example.org.evil.com) | Rechazado |
| Login con contraseña de un candidato | Sin cambios |
Las comprobaciones de dominio se pueden probar sin pasar por Google llamando a los hooks con drush php:eval y distintos userinfo. El enlace con cuentas existentes, en cambio, solo se puede validar con un login real.
Conclusión
Sustituir LDAP por Google con OpenID Connect es un cambio acotado: en nuestro caso, entre uno y tres días de desarrollo y pruebas, más el tiempo que tarde IT en crear el cliente OAuth. Se gana en seguridad y mantenimiento, y el personal entra con la misma cuenta y el mismo segundo factor que usa para todo lo demás.
Lo que más trabajo da no es el módulo, sino lo que hay alrededor: el código que dependía de LDAP, la lógica de roles y redirecciones pensada para el formulario de login y dejar claro en la interfaz quién debe usar cada acceso.