Esta semana he tenido que implementar un sistema de autenticación de cuentas de usuario contra un Active Directory en Typescript. Active Directory (AD) es un servicio de directorios desarrollado por Microsoft basado en el protocolo LDAP.
Su funcionamiento utiliza la estructura genérica LDAP (Lightweight Directory Access Protocol), ya que este protocolo viene implementado de forma similar a una base de datos, la cual almacena en forma centralizada toda la información relativa a un dominio de autenticación dentro de una red organizativa. Este protocolo define una serie de convenciones de nomenclatura y una estructura en árbol que vamos a descubrir en este artículo.
La nomenclatura en LDAP
Las entradas se almacenan en una estructura de datos de árbol. La raíz del árbol se conoce como sufijo, y las ramas son contenedores. Esos contenedores pueden ser unidades organizativa. Las hojas del árbol son las entidades individuales y tienen una serie de atributos basado en su objectClass.
Puede verse un ejemplo de esta estructura en la siguiente imagen. El sufijo es dc = example. Debajo están las ramas: ou = devel y doc. Bajo la rama de los usuarios, hay distintas que se refieren entidades que se refieren a usuarios individuales.
La cadena para referirnos a uno de estos nodos, conocida como distinguished name (DN), siempre se lee de abajo hacia arriba en árbol, reuniendo los identificadores y separándolos por coma. Por ejemplo para referirnos al usuario Octocat:
cn=Octocat,ou=doc,dc=example,dc=com
Esquema en LDAP
El esquema especifica los atributos que tienen los nodos, que representan la información que se almacena sobre cada entidad. Como hemos comentado, un atributo que tienen todas las entidades es objectClass, que especifica qué tipo de entidad es. Este objectClass actúa como una interfaz que representa los atributos que podría contener la entidad. En cada objectClass, algunos atributos son obligatorios y otros son opcionales. Conociendo los objectClass que tiene una entidad podemos saber sobre qué atributos podemos buscar.
Verificando de credenciales en LDAP
Por fin llegamos a lo que nos interesa, ya hemos descubierto la teoría sobre la nomenclatura, estructura y el protocolo de comunicación, pero como autenticamos una cuenta de usuario contra un LDAP
La verificación de credenciales con LDAP suele ser un proceso de dos pasos. Primero, nuestra aplicación necesita acceder al servidor con un usuario con privilegios de lectura y búsqueda en los usuarios para obtener la información del usuario que se intenta autenticar. Para ello, realizamos una búsqueda para encontrar sus atributos y buscar el nombre de usuario contra el que se autenticaría contra el ldap. comunente como sAMAccountName. Luego, se debe intentar acceder de nuevo al servidor como el DN del usuario, utilizando la contraseña provista.
El cliente necesita vincularse (método bind
), que es el término que utiliza LDAP para la autenticación. Esto requiere un viaje de ida y vuelta. Tenemos que tener en cuenta que en nuestra aplicación necesitaremos la URL del servidor, el DN del lector y la contraseña del lector.
A continuación, se muestra la clase Typescript que se encarga de autenticar en un LDAP utilizando la librería ldapjs
(http://ldapjs.org/). En este snippet de código podemos ver cómo recogemos de config los valores que necesitamos para autenticar y en el objeto SearchOptions como hacer una consulta para encontrar un usuario por email.
Una vez que hemos encontrado el usuario, es importante cerrar el cliente para cerrar la conexión contra el servicio.
import AuthServiceI from '../../domain/auth/authService' import { User } from '../../domain/model' import ldap, { SearchOptions } from 'ldapjs' import assert from 'assert' import LDAPConfig from '../../config/interfaces/ldapConfig' export default class LDAPAuthService implements AuthServiceI { config: LDAPConfig constructor(ldapConfig: LDAPConfig) { this.config = ldapConfig } async login(email: string, password: string): Promise<User> { return new Promise((resolve, reject) => { const ldapConfig: LDAPConfig = this.config const client = ldap.createClient({ url: ldapConfig.hostname, reconnect: true, }) client.bind(ldapConfig.username, ldapConfig.password, err => { assert.ifError(err) const opts: SearchOptions = { filter: '(&(objectClass=user)(mail=' + email + '))', scope: 'sub', paged: true, sizeLimit: 200, } let object: any = null client.search(ldapConfig.search, opts, (err, res) => { if (err) { return reject(err) } res.on('searchEntry', entry => { object = entry.object }) res.on('error', err => { console.error('error: ' + err.message) client.destroy() reject(err) }) res.on('end', result => { if (!object) { client.destroy() return reject('Invalid user on ldap') } client.bind(object.dn, password, err => { if (err) { client.destroy() console.error('Invalid Login', err) return reject(err) } client.destroy() return resolve(new User(object.dn, object.sAMAccountName, object.mail)) }) }) }) }) }) } }
Referencias:
https://doc.opensuse.org/documentation/leap/security/html/book.security/cha-security-ldap.html