Cómo servir archivos autenticados con PHP y Apache

Hoy comparto un problema que todo desarrollador de apps web se encontrará en algún momento de su vida, tarde o temprano, y que por tanto suele verse planteado de vez en cuando en los foros de ayuda.

Cómo servir archivos autenticados con PHP y Apache

El problema es el del título del post. Se presenta cuando tu aplicación web tiene algunos archivos sensibles que deben ser protegidos de las miradas no autorizadas. Tales archivos pueden ser csvs, imágenes, zip; en definitiva, cualquier extensión de archivo.

Tu app web maneja archivos que no pueden servirse alegremente a cualquiera que los solicite, como ocurre con los assets: imágenes, archivos css, js, html, etc, sino que solo podrán descargárselos aquellas personas que previamente se hayan autenticado en tu app.

Nosotros, en concreto, vamos a solucionar este problema con PHP corriendo con el framework Slim y con el servidor web Apache, pero también puedes aplicar esta solución a cualquier app que corra sobre otro framework porque el principio de funcionamiento es el mismo.

Organizar tu app adecuadamente

La primera medida de seguridad que hay que tomar consiste en estructurar tu aplicación Slim de forma adecuada, separándola en una parte pública y en otra parte privada. Esto es fundamental.

public_html/
    .htaccess
    index.php
    styles/
    images/
    scripts/
app/
    routes/
    vendor/
    lib/
    data/

El esquema anterior separa la app en public_html y en app. La carpeta public_html almacena el contenido públicamente accesible, aquel que puede ver todo el mundo. El contenido de app no es accesible por el navegador web, está protegido, y contiene los archivos de tu aplicación.

Organizar tu app adecuadamente

Desactivar el listado de directorios de Apache

Por cierto, si utilizas Apache, otra medida de seguridad adicional que puedes tomar es desactivar la navegación por directorios:

Options -Indexes

Esta directiva de configuración de Apache se puede poner tanto en archivos .htaccess como en archivos de configuración de host virtual.

Si bien es cierto que implementar el esquema anterior (separar public_html y app) ya soluciona muchos problemas de seguridad, puesto que si lo haces correctamente el servidor web solo podrá acceder a public_html (los assets), implementar esta medida adicional no cuesta nada. Es una buena práctica de seguridad.

Programar una ruta para servir los archivos protegidos

Ok, pongamos ahora un ejemplo concreto: una app que crea o exporta (llámalo como quieras) archivos CSV a partir de los datos contenidos en una tabla MySQL.

En este caso pondremos los archivos generados en la carpeta app/exports. ¡Recuerda que app es en una carpeta protegida de los ojos no autorizados! Y programaremos una ruta Slim (también llamada acción de controlador, en jerga MVC) que sirva el contenido de los archivos por vía programática.

$app->get(
    '/csv/export',
    function () use ($app) {
        $filename = Book::getInstance()->exportCSV();
        header('Content-Type: application/csv');
        header('Content-Disposition: attachment; filename=myfile.csv');
        header('Pragma: no-cache');
        readfile($filename);
    }
);

Como ves, este ejemplo se comunica con la capa modelo por medio de la clase Book.

El método exportCSV es el siguiente.

public function exportCSV()
{
	$filename = time() . '.csv';        
	$filepath = APP_PATH . 'exports/' . $filename;
	$sql = "SELECT * INTO OUTFILE '$filepath' "
		. "FIELDS TERMINATED BY ',' "
		. "ENCLOSED BY '\"' "
		. "LINES TERMINATED BY '\n' "
		. "FROM {$this->table}";
	DBConnect::getInstance()->query($sql);        
	return($filepath);
}

Proteger las llamadas a tus archivos

Con lo anterior ya programado, solo falta proteger esta llamada: http://myapp.com/csv/export

Esto lo conseguimos programando en el index.php de Slim un hook slim.before.dispatch como el siguiente:

$app->hook('slim.before.dispatch', function () use ($app, $auth) {
    $privateRoutes = array(
        // your private urls here
        // ...
        '/csv/export',
    );
    if (!$auth->isLoggedIn() && in_array($app->router()->getCurrentRoute()->getPattern(), $privateRoutes)) {
        print_r(json_encode(array(
            'status' => 'error',
            'message' => 'Authenticated call. Please, provide credentials and try again.', )));
        exit;
    }
});

Conclusión

En resumen para que Apache no sirva los archivos sensibles que maneja tu aplicación, es importante que separes la misma en una parte pública y en otra parte privada, fuera del alcance del navegador web.

Es interesante que tomes otras medidas de seguridad adicionales que no cuesta nada implementar, por ejemplo, desactivar el listado de directorios.

El truco pues consiste en esconder los archivos protegidos de los ojos no autorizados y servirlos vía programática por medio de una acción de controlador protegida.

En Slim, como en cualquier otro framework de desarrollo MVC, un buen sitio para inicializar la autenticación de las rutas es el archivo index.php, también llamado Front Controller. En concreto, nosotros implementamos la autenticación en el hook slim.before.dispatch.