Authcode

Autorización

Si fueras a crear un sistema para tu blog, tendrías páginas públicas pero no quisieras que cualquiera pudiera editar o borrar posts en él.

Necesitas un mecanismo para que a ciertas páginas solo tengan acceso usuarios autenticados, quizas que también cumplan con algunas condiciones. De eso se trata esta guía.

Protegiendo tus vistas

Para lograr que a ciertas vistas solo tengan acceso los usuarios autenticados usas el decorador auth.protected(). Ejemplo:

@auth.protected()
def myview():
    return u'Solo puede verme un usuario autenticado'

Nota

Nota que el decorador está siendo llamado (tiene un par de paréntesis al final de la línea). Estos son necesarios, si los olvidas tendrás un error. [1]

Advertencia

¡Cuidado!
Si defines las rutas a tus vistas con decoradores —como lo hace Flask— ten mucho cuidado en poner el decorador de autenticación después del de la ruta o, de otro modo, tus vistas quedarán desprotegidas. Hazlo de esta forma:
  @app.route('/admin/')
  @auth.protected()
  def myview():
      ...

Roles

Una necesidad muy común es darle acceso al usuario solo si tiene un rol o permiso específico. Por lo mismo, Authcode tiene una forma directa de hacerlo: usando el argumento

role = nombredelrol

Para darle acceso a más de un rol, puedes usar

roles = [nombredelrol1, nombredelrol2,  ...]

(nota que, en este caso, el argumento se llama roles, en plural)

Ejemplo:

@auth.protected(role='admin')
def myview1():
    return u'Solo puede verme los usuarios con el rol “admin”'

@auth.protected(roles=['foo', 'bar'])
    def myview2():
        return u'Solo puede verme los usuario con el rol “foo”, el rol “bar” (o ambos)'

Pruebas genéricas

El decorador también puede tomar como argumento una o más funciones para “probar” al usuario. Las pruebas toman como argumentos al usuario autenticado y cualquier otro argumento que la vista haya recibido. Solo si todas devuelven un valor positivo (como True) se da acceso a la vista al usuario.

def test_can_delete(user, *args, **kwargs):
    return user.has_role('admin') or user.can_delete == True

@auth.protected(test_can_delete)
@app.route('/admin/')
def myview1():
    ...

Pruebas del usuario

Cada uno de los argumentos tipo llave=valor —siempre y cuando llave no sea tests, role, roles, csrf, url_sign_in o request— se toma como si fuera un método del modelo de usuario que se quiere usar como prueba.

Este método se llama pasándole el valor y cualquier otro argumento que la vista haya recibido, como argumentos.

class UserMixin(object):

    permissions =  db.Column(ARRAY(db.String))

    def has_perm(self, name, *args, **kwargs):
    """Return True if the user has this permission."""
        return name in self.permissions

# ...

@auth.protected(has_perm='can create posts')
@app.route('/posts/new/')
def new_post():
    ...

En este ejemplo se llama al método user.has_perm('can create posts') y solo se da acceso a la vista si este devuelve un valor positivo.

Si el objeto user no tiene un método con ese nombre, al intentar acceder a la vista, se lanza una excepción AttributeError.

Definir pruebas así te da toda la flexibilidad de las pruebas genéricas, manteniendo el decorador con una sintaxis simple.

Una cosa más...

El último truco del decorador @auth.protected es el poder activar/desactivar la protección contra ataques CSRF, como podrás leerlo en la siguiente sección.

Protección CSRF

Esta biblioteca incluye un mecanismo para protegerte de ataques CSRF (Cross Site Request Forgery). Este tipo de ataque ocurre cuando un sitio web malicioso contiene un enlace, un formulario o código JavaScript que busca realizar alguna acción en tu sitio web, aprovechando las credenciales de un usuario ya autenticado.

Funciona por que es el navegador del usuario quien hace la solicitud y, aunque esta se origina en un sitio diferente al atacado, todas las solicitudes a él incluyen la cookie que identifica al usuario.

Un ataque relacionado, llamado login CSRF —en que el sitio atacante engaña al navegador del usuario para que se autentique con las credenciales de alguien más— también esta cubierto.

La primera linea de defensa es asegurarte que ninguno de los GET en tus sitios tengan efectos secundarios. Las solicitudes por métodos POST, PUT, DELETE, etc. puedes entonces protegerlas siguiendo los pasos de abajo.

Como usarla

Authcode genera un código único para cada sesión de cada usuario que este debe usar al hacer cualquier actividad en el sitio. Nadie más puede ver ese código: el de otros usuarios es diferente. Exigiéndolo para cualquier acción que haga cambios, te aseguras que solo funcionen las páginas generadas por tu sitio y no los de otro sitio web malicioso.

  1. En todos los formularios enviado por POST, usa csrf_token() para incluir este código como un campo oculto. e.g.:
 <form action="" method="post">
   <input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
   ...
   <button type="submit">Guardar</button>
 </form>

Esto no debes hacerlo en los formularios que se envían a URLs externas, o estarás divulgando el codigo a ese otro sito, lo que puede ocasionar una vulnerabilidad.

  1. Si la vista correspondiente está decorada con @auth.protected y el formulario no es enviado por GET o HEAD, no tienes que hacer nada, pues el decorador ya está validando el código CSFR automáticamente.

Puedes forzar a que se haga la validación con otros métodos de envio, por ejemplo GET, pasándo el argumento csrf=True al decorador.

@auth.protected(csrf=True)
def myview():
    ...

Asi mismo, si lo necesias, puedes desactivar la revisión automática usando el parámetro csrf=False. Luego, el método csrf_token_is_valid() te servirá para hacer la validación manual cuando lo necesites.

@auth.protected(csrf=False)
def myview():
    ...
    if not auth.csrf_token_is_valid(request):
        raise Forbidden()
    ...

AJAX

Para usar la protección contra ataques CSRF en solicitudes AJAX, podrías pasar el código manualmente en cada solicitud que hagas, pero hay una mejor forma.

Authcode acepta recibir el código CSRF como valor de la cabecera HTTP “X-CSRFToken”. Esto es conveniente, por que las bibliotecas de JavaScript más populares permiten incluir automáticamente cabeceras personalizadas en todas las solicitudes AJAX.

El siguiente ejemplo usa la biblioteca jQuery para mostrar como funciona; Solo es necesario ejecutar la función ajaxSetup una vez, para que todas las solicitudes AJAX incluyan el código CSRF automáticamente.

En este caso, he insertado el código CSRF en una etiqueta <meta> en cada página:

<meta name="csrf-token" content="{{ csrf_token() }}">

y de ahí puede leerlo el código para poner la cabecera en las solicitudes AJAX, ademas de impedir que el código CSRF se envie a otros dominios, usando settings.crossDomain en jQuery 1.5.1 y más nuevos:

// Obtengo el código CSRF de mi etiqueta <meta>
window.CSRFToken = $('meta[name="csrf-token"]').attr('content');

function csrfSafeMethod(method) {
    // Estos métodos HTTP no necesitan protección CSRF
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
    beforeSend: function(xhr, settings) {
        if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("X-CSRFToken", window.CSRFToken);
        }
    }
});
Angular.js

Al hacer una llamada AJAX, Angular.js busca el código CSRF en una cookie llamada XSRF-TOKEN y la envía de vuelta usando la cabecera HTTP X-XSRF-TOKEN. Esto es ligeramente diferente a lo que espera por defecto Authcode, así que tienes que hacer unos ajustes.

Primero, crea la cookie con el código CSRF. Esto depende mucho de tu framework específico, pero este es un ejemplo en Flask para hacerlo automáticamente al cargar cada página

@app.after_request
def after_request(resp):
    user = g.get('user', None)
    if user is not None:
        token = auth.get_csrf_token()
        resp.set_cookie('XSRF-TOKEN', token.decode('ascii'));
    return resp

Y finalmente, cambia el nombre de la cabecera HTTP desde donde Authcode leerá el código

auth = authcode.Auth(..., csrf_header='X-XSRF-TOKEN')

Autorización denegada

Si un usuario no autenticado intenta acceder una de las vistas protegidas por @auth.protected(), es redirigido por a la página de login por defecto, definida en las opciones globales. Esto pagina puede cambiar para una vista específica usando el parámetro url_sign_in, que puede ser una URL fija o un invocable que devuelva la URL que quieres.

La URL que el usuario intentaba visitar queda guardada en su sesión y una vez que se autentica, se le redirige ahí.

Hay casos, sin embargo, que un usuario autenticado no tendrá permisos para acceder a una vista, si no tiene cierto rol o no pasa cierta prueba, o si se requería un código CSRF y este no se encuentra o es inválido. En esos caso, el decorador @auth.protected() lanza una excepción 403 Forbidden.

No suele haber una página por defecto para este error, o si la hay no es muy amigable, por lo que vas a querer usar tu propia vista. Los detalles de como hacerlo varían en cada framework, pero por ejemplo en Flask puedes agragarla de este modo:

@app.errorhandler(403)
def gone(error=None):
    return render_template('forbidden.html'), 403
[1]Técnicamente es un generador de decoradores: una función que al ejecutarse devuelve un decorador.