Laravel: Securizando APIs

Desde hace un tiempo he estado trabajando en una apliación web para facilitar la gestión de servidores, para ello he desarrollado varias APIs para poder ejecutar comandos en servidores. Uno de los puntos más criticos de esto es permitir o denegar el acceso a los usuarios para llamar a estas APIs.

En mi caso, estoy usando el sitema de auth de Laravel con la librería Adldap2-Laravel para manejar accesos desde Active Directory. Por determinadas razones nececsito que mis usuarios puedan hacer llamadas via un token de autorización. Este paso es muy sencillo, Laravel con su módulo de Auth nos proporciona este mecanismo si utilizas el driver de base de datos y no el driver LDAP ya que no comprueba si el token que usas pertenece a un usuario dado de baja en ldap o le ha expirado la cuenta. Para corregir este comportamiento debemos hacer lo siguente.

Debemos añadir al archivo de configuración de autenticación (config/auth.php), los nuevos guards y providers “custom” que vamos a generar:

    'guards' => [
        ...
        'api' => [
            'driver' => 'token',
            'provider' => 'api-users',
        ],
    ],
    'providers' => [
        .....
         'api-users' => [
             'driver' => 'custom',
             'model'  => App\User::class,
         ],
    ],

El siguiente paso es indicar en el archivo de rutas de APIs que el guard de authenticación que queremos usar es el de ‘api’ (routes/api.php):

Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user();
});

Para que el provider de usuarios sepa que clase debe usar debemos crear nuestro proveedor e indicarselo a laravel:

app/Providers/CustomUserProvider.php

<?php
namespace App\Providers;

use Adldap\Laravel\Facades\Adldap;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

class CustomUserProvider extends EloquentUserProvider

{
	/**
	 * Retrieve a user by the given credentials. *
	 * @param array $credentials *
	 @return \Illuminate\Contracts\Auth\Authenticatable|null
	 */
	public

	function retrieveByCredentials(array $credentials)
	{
		if (empty($credentials)) {
			return;
		}
                // First we will add each credential element to the query as a where clause.
                // Then we can execute the query and, if we found a user, return it in a 
                // Eloquent User "model" that will be utilized by the Guard instances.
                $query = $this->createModel()->newQuery();
		foreach($credentials as $key => $value) {
			if (!Str::contains($key, 'password')) {
				$query->where($key, $value);
			}
		}

		$user = $query->first();
		if (!$user) {
			return;
		}

		// Securing API CALLS with LDAP checks

		$adldap_user = Adldap::search()->users()->find($user->id);
		if ($adldap_user->isExpired() || $adldap_user->isDisabled()) {
			return;
		};
		return $user;
	}
}

Dentro de la clase anterior hemos sobreescrito la función retrieveByCredentials para que además de comprobar el token del usuario compruebe si éste ha expirado en LDAP o está deshabilitado.

El siguiente archivo que debemos modificar es el AuthServiceProvider para que nos cargue nuestro proveedor ‘custom’:

app/Providers/AuthUserProvider.php


<?php 

namespace App\Providers;

use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider

	{
	/** * The policy mappings for the application. * * @var array */
	protected $policies = ['App\Model' => 'App\Policies\ModelPolicy', ];
	/**
	 * Register any authentication / authorization services.
	 *
	 * @return void
	 */
	public

	function boot()
		{
		$this->app['auth']->provider('custom',
		function ($app, array $config)
			{
			$model = $app['config']['auth.providers.users.model'];
			return new CustomUserProvider($app['hash'], $model);
			});
		$this->registerPolicies();

		//

		}
	}

Adicionalmente, como la librería de LDAP no está preparada para el uso de tokens, es necesarío parchear para que la primera vez que hace login el usuario le genere un token:

app/Controllers/Auth/LoginController.php

A este controller le debemos sobreescribir la funcion authenticated del siguiente modo:

    protected function authenticated(Request $request, $user)
    {
        if (empty($user->api_token)) {
            $user->api_token = str_random(60);
            $user->save();
        }
    }

De esta forma, la primera vez que se genera un usuario al no tener un token asignado lo generaremos al vuelo.

Para comrpobar si ha ido se está generando el token podemos revisar la base de datos o imprimirlo en el blade del siguiente modo:

  @if(Auth::check())
        <a href="api/user?api_token={{ Auth::user()->api_token }}">Mi usuario JSON</a>
  @endif

Snippet Consultar Active directory / LDAP

Este post trata de como consultar de una manera sencilla el Active Directory/LDAP corporativo, en concreto se extraerá la fecha de caducidad de una cuenta conociendo el login.

Antes de nada, hay que tener en cuenta que LDAP almacena las fechas a partir de los segundos transcurridos desde el 1 de enero de 1601. Por ello vamos a introducir en el código la siguiente función que nos ayudará a tratar con este tipo de fechas:

/// <summary>
/// Esta función convierte el tiempo de LDAP (AD) en una variable DateTime
/// </summary>
/// <param name="accountExpires">LDAP time</param>
/// <returns>La representación datetime del LDAP</returns>
static DateTime longToDatetime(Int64 accountExpires) {
      DateTime expireDate = DateTime.MaxValue;
      if (!accountExpires.Equals(Int64.MaxValue))
           expireDate = DateTime.FromFileTime(accountExpires);

       return expireDate;
   }

A continuación viene el código que tiene algo más de chicha, el acceso a AD:

public class UserResult {
        public String Name;
        public DateTime ExpireDate;
    }
 static UserResult GetUser(String userId)
        {
            UserResult res;
            DirectoryEntry searchRoot = new DirectoryEntry(dominio, "user","password");
            DirectorySearcher search = new DirectorySearcher(searchRoot);
            search.Filter = "(&(objectClass=user)(samaccountname="+userId+"))";
            search.PropertiesToLoad.Add("accountExpires");
            search.PropertiesToLoad.Add("samaccountname");
            SearchResult result;
            SearchResultCollection resultCol = search.FindAll();
            if (resultCol == null) { return null; }
            result = resultCol[0];
            res = new UserResult();
            res.ExpireDate = longToDatetime((Int64)result.Properties["accountExpires"][0]);
            res.Name = (string)result.Properties["samaccountname"][0];
            return res;
            
        }

El codigo anterior se conecta al active directory especificado por la variable dominio y por un usuario que tenga privilegios en el AD.

El string de domintio tiene que tener la forma:

const string dominio= "LDAP://DC=organizacion,DC=com";