Flask

Flask

Tutorial Flask: Autenticación JWT con Flask-JWT-Extended

Flask: Aprende a configurar JWT con Flask-JWT-Extended para proteger rutas y manejar sesiones de usuario de forma segura y eficiente.

Aprende Flask y certifícate

Configuración inicial de JWT en Flask

Para implementar autenticación mediante JWT (JSON Web Tokens) en una aplicación Flask, es esencial configurar la extensión Flask-JWT-Extended. Esta herramienta facilita la gestión de tokens JWT, permitiendo proteger rutas y manejar sesiones de usuario de forma segura.

Instalación de Flask-JWT-Extended:

Primero, se debe instalar la extensión utilizando pip:

pip install Flask-JWT-Extended

Configuración básica de la aplicación:

Una vez instalada la extensión, se procede a configurar la aplicación Flask para habilitar JWT:

from flask import Flask
from flask_jwt_extended import JWTManager

app = Flask(__name__)

# Clave secreta para firmar los tokens JWT
app.config['JWT_SECRET_KEY'] = 'clave_secreta_segura'

# Inicialización del gestor de JWT
jwt = JWTManager(app)

La clave secreta es fundamental para la seguridad de los tokens. Es recomendable almacenarla de forma segura y no exponerla en el código fuente, utilizando variables de entorno o un gestor de configuraciones.

Opciones de configuración adicionales:

Flask-JWT-Extended ofrece múltiples opciones de configuración para adaptar el comportamiento de los tokens JWT:

  • JWT_ACCESS_TOKEN_EXPIRES: Define el tiempo de expiración del token de acceso.
  • JWT_REFRESH_TOKEN_EXPIRES: Establece la duración del token de refresco.
  • JWT_TOKEN_LOCATION: Especifica dónde buscar los tokens (por ejemplo, en los encabezados o en las cookies).
  • JWT_ALGORITHM: Determina el algoritmo de encriptación utilizado para firmar los tokens.

Ejemplo de configuración avanzada:

from datetime import timedelta

app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1)
app.config['JWT_REFRESH_TOKEN_EXPIRES'] = timedelta(days=30)
app.config['JWT_TOKEN_LOCATION'] = ['headers']
app.config['JWT_ALGORITHM'] = 'HS256'

El uso de timedelta facilita la definición de periodos de expiración, mejorando el control sobre la vigencia de los tokens.

Inicialización del gestor de JWT:

La instancia de JWTManager es esencial para el funcionamiento de la extensión. Permite manejar eventos y personalizar el comportamiento ante diferentes situaciones relacionadas con los tokens.

jwt = JWTManager(app)

Gestión de respuestas y errores:

Es posible personalizar las respuestas que se envían al cliente cuando ocurren ciertos eventos, como un token caducado o credenciales inválidas. Por ejemplo:

from flask import jsonify

@jwt.expired_token_loader
def expired_token_callback(jwt_header, jwt_payload):
    return jsonify({
        'mensaje': 'El token ha expirado',
        'error': 'token_expirado'
    }), 401

@jwt.invalid_token_loader
def invalid_token_callback(error):
    return jsonify({
        'mensaje': 'Token inválido',
        'error': 'token_invalido'
    }), 422

Personalizar estas respuestas mejora la experiencia de usuario y facilita el manejo de errores en el lado del cliente.

Consideraciones sobre la ubicación de los tokens:

Dependiendo de las necesidades de la aplicación, los tokens JWT pueden enviarse en los encabezados HTTP, en las cookies o en el cuerpo de la solicitud. La configuración por defecto utiliza los encabezados:

app.config['JWT_TOKEN_LOCATION'] = ['headers']

Si se prefiere utilizar cookies para almacenar los tokens:

app.config['JWT_TOKEN_LOCATION'] = ['cookies']
app.config['JWT_COOKIE_SECURE'] = True  # Requiere HTTPS
app.config['JWT_COOKIE_CSRF_PROTECT'] = True

Al utilizar cookies, es importante habilitar la protección CSRF para prevenir ataques y asegurar que las cookies sean seguras, especialmente en entornos de producción.

Protección de rutas con JWT:

Aunque se profundizará en secciones posteriores, es relevante mencionar que, tras la configuración inicial, ya es posible proteger rutas utilizando decoradores proporcionados por Flask-JWT-Extended.

Ejemplo básico:

from flask_jwt_extended import jwt_required

@app.route('/protegido', methods=['GET'])
@jwt_required()
def ruta_protegida():
    return jsonify(mensaje='Acceso autorizado a contenido protegido')

Este enfoque garantiza que solo usuarios con un token válido puedan acceder a determinadas partes de la aplicación.

Generación y verificación de tokens

Una vez configurado Flask-JWT-Extended en la aplicación, es fundamental implementar la generación y verificación de tokens JWT para manejar la autenticación de usuarios de manera segura.

Generación de tokens al iniciar sesión:

Al autenticar a un usuario, se debe generar un token de acceso que permita identificarlo en solicitudes posteriores. Utilizamos la función create_access_token proporcionada por Flask-JWT-Extended.

from flask import request, jsonify
from flask_jwt_extended import create_access_token
from werkzeug.security import check_password_hash

@app.route('/login', methods=['POST'])
def login():
    correo = request.json.get('correo')
    contraseña = request.json.get('contraseña')

    if not correo or not contraseña:
        return jsonify({'mensaje': 'Correo y contraseña son requeridos'}), 400

    usuario = Usuario.query.filter_by(correo=correo).first()

    if usuario and check_password_hash(usuario.contraseña, contraseña):
        token_acceso = create_access_token(identity=usuario.id)
        return jsonify({'token': token_acceso}), 200
    else:
        return jsonify({'mensaje': 'Credenciales inválidas'}), 401

En este ejemplo:

  • Se recibe el correo y la contraseña del usuario.
  • Se verifica que el usuario exista y que la contraseña sea correcta utilizando check_password_hash.
  • Se genera un token de acceso con create_access_token, pasando como identidad el id del usuario.
  • Se devuelve el token en la respuesta JSON.

Personalización del token con claims adicionales:

Es posible agregar información extra al token mediante claims personalizados:

token_acceso = create_access_token(
    identity=usuario.id,
    additional_claims={'rol': usuario.rol}
)

Así, el token incluirá un claim rol con el rol del usuario, útil para la gestión de permisos en la aplicación.

Verificación de tokens en rutas protegidas:

Para acceder a rutas que requieren autenticación, el cliente debe enviar el token en el encabezado Authorization. Al usar el decorador @jwt_required(), la extensión verifica automáticamente la validez del token antes de ejecutar la función.

from flask_jwt_extended import jwt_required, get_jwt_identity

@app.route('/perfil', methods=['GET'])
@jwt_required()
def perfil():
    usuario_id = get_jwt_identity()
    usuario = Usuario.query.get(usuario_id)
    return jsonify({
        'nombre': usuario.nombre,
        'correo': usuario.correo
    }), 200

En este caso:

  • @jwt_required() protege la ruta /perfil.
  • get_jwt_identity() obtiene la identidad del usuario del token, que es el id que se estableció al generarlo.
  • Se utiliza el id para recuperar los datos del usuario en la base de datos.

Manejo de tokens expirados o inválidos:

La extensión proporciona manejadores para casos en que el token haya expirado o sea inválido. Estas situaciones deben gestionarse para ofrecer mensajes claros al usuario.

@jwt.expired_token_loader
def token_expirado_callback(jwt_header, jwt_payload):
    return jsonify({'mensaje': 'El token ha expirado'}), 401

@jwt.invalid_token_loader
def token_invalido_callback(error):
    return jsonify({'mensaje': 'Token inválido'}), 422

Uso de tokens de refresco (Refresh Tokens):

Para mejorar la seguridad, los tokens de acceso suelen tener una vida corta. Los tokens de refresco permiten obtener nuevos tokens de acceso sin requerir que el usuario vuelva a iniciar sesión.

Generación de un token de refresco al iniciar sesión:

from flask_jwt_extended import create_refresh_token

token_acceso = create_access_token(identity=usuario.id)
token_refresco = create_refresh_token(identity=usuario.id)
return jsonify({
    'token': token_acceso,
    'refresh_token': token_refresco
}), 200

Endpoint para renovar el token de acceso usando el token de refresco:

@app.route('/refresh', methods=['POST'])
@jwt_required(refresh=True)
def refresh():
    usuario_id = get_jwt_identity()
    nuevo_token_acceso = create_access_token(identity=usuario_id)
    return jsonify({'token': nuevo_token_acceso}), 200

Aquí:

  • @jwt_required(refresh=True) indica que esta ruta requiere un token de refresco válido.
  • Se genera un nuevo token de acceso para el usuario.

Envío del token en las solicitudes:

El cliente debe enviar el token en el encabezado Authorization con el esquema Bearer:

Authorization: Bearer <token>

Ejemplo de solicitud con curl:

curl -H "Authorization: Bearer <token>" http://localhost:5000/perfil

Protección de múltiples rutas:

Se pueden proteger tantas rutas como sea necesario utilizando @jwt_required(), garantizando que solo usuarios autenticados puedan acceder a ellas.

Obtención de claims adicionales en rutas protegidas:

Si se añadieron claims personalizados al generar el token, se pueden obtener en las rutas protegidas:

from flask_jwt_extended import get_jwt

@app.route('/admin', methods=['GET'])
@jwt_required()
def admin_panel():
    claims = get_jwt()
    if claims['rol'] != 'admin':
        return jsonify({'mensaje': 'Acceso denegado'}), 403
    return jsonify({'mensaje': 'Bienvenido al panel de administración'}), 200

En este ejemplo:

  • Se verifica que el usuario tenga el rol de admin antes de permitir el acceso.
  • Si el rol no es adecuado, se devuelve un error 403 (Forbidden).

Protección de rutas con decoradores JWT

Una de las ventajas principales de Flask-JWT-Extended es la facilidad para proteger rutas utilizando decoradores. Estos decoradores permiten controlar el acceso a diferentes partes de la aplicación, asegurando que solo usuarios autenticados puedan acceder a recursos protegidos.

Uso básico de @jwt_required()

El decorador más común es @jwt_required(), que verifica la presencia de un token JWT válido antes de permitir el acceso a la ruta.

from flask_jwt_extended import jwt_required

@app.route('/datos-protegidos', methods=['GET'])
@jwt_required()
def obtener_datos_protegidos():
    return jsonify({'mensaje': 'Acceso concedido a datos protegidos'}), 200

En este ejemplo:

  • @jwt_required() protege la ruta /datos-protegidos.
  • Si el token está ausente o es inválido, se retorna un error automáticamente.
  • Si el token es válido, se ejecuta la función y se devuelve la respuesta.

Obteniendo la identidad del usuario:

Dentro de una ruta protegida, es común necesitar la identidad del usuario. Esto se obtiene con la función get_jwt_identity().

from flask_jwt_extended import get_jwt_identity

@app.route('/perfil', methods=['GET'])
@jwt_required()
def ver_perfil():
    usuario_id = get_jwt_identity()
    usuario = Usuario.query.get(usuario_id)
    return jsonify({
        'nombre': usuario.nombre,
        'correo': usuario.correo
    }), 200

Aquí:

  • get_jwt_identity() extrae la identidad establecida al generar el token.
  • Se utiliza el usuario_id para obtener los datos del usuario en la base de datos.

Protección opcional con @jwt_required(optional=True)

En algunas situaciones, es útil permitir el acceso tanto a usuarios autenticados como no autenticados, pero brindar información adicional si el usuario ha iniciado sesión.

@app.route('/informacion', methods=['GET'])
@jwt_required(optional=True)
def informacion():
    usuario_id = get_jwt_identity()
    if usuario_id:
        mensaje = f'Información personalizada para el usuario {usuario_id}'
    else:
        mensaje = 'Información general para visitantes'
    return jsonify({'mensaje': mensaje}), 200

En este caso:

  • Si el usuario proporciona un token válido, obtiene contenido personalizado.
  • Si no hay token o es inválido, recibe contenido general sin restricciones.

Requiriendo refresh tokens con @jwt_required(fresh=True)

Para operaciones sensibles, es posible exigir que el token sea fresco, es decir, generado recientemente o con autenticación adicional.

@app.route('/configuracion', methods=['PUT'])
@jwt_required(fresh=True)
def actualizar_configuracion():
    # Lógica para actualizar configuración
    return jsonify({'mensaje': 'Configuración actualizada'}), 200

Para obtener un token fresco, durante la autenticación inicial se puede indicar que el token generado sea fresco:

from flask_jwt_extended import create_access_token

token_acceso = create_access_token(identity=usuario.id, fresh=True)

Alternativamente, se puede obtener un token fresco proporcionando credenciales nuevamente:

@app.route('/reauth', methods=['POST'])
def reautenticar():
    correo = request.json.get('correo')
    contraseña = request.json.get('contraseña')

    usuario = Usuario.query.filter_by(correo=correo).first()
    if usuario and check_password_hash(usuario.contraseña, contraseña):
        nuevo_token = create_access_token(identity=usuario.id, fresh=True)
        return jsonify({'token': nuevo_token}), 200
    else:
        return jsonify({'mensaje': 'Credenciales inválidas'}), 401

Protección de rutas con tokens de refresco:

Las rutas destinadas a obtener un nuevo token de acceso deben protegerse con el decorador @jwt_required(refresh=True), que exige un token de refresco válido.

from flask_jwt_extended import jwt_required, get_jwt_identity, create_access_token

@app.route('/token/refresh', methods=['POST'])
@jwt_required(refresh=True)
def refrescar_token():
    usuario_id = get_jwt_identity()
    nuevo_token = create_access_token(identity=usuario_id)
    return jsonify({'token': nuevo_token}), 200

Aquí:

  • @jwt_required(refresh=True) indica que se espera un token de refresco.
  • Se genera y devuelve un nuevo token de acceso para el usuario.

Manejo de roles y permisos en rutas (sin detalles de claims):

Aunque la implementación de roles y claims se detallará en otra sección, es posible adelantar que los decoradores pueden trabajar en conjunto con información adicional en los tokens para controlar el acceso según permisos específicos.

Personalización de mensajes de error:

Cuando un usuario no está autorizado para acceder a una ruta, Flask-JWT-Extended devuelve errores predeterminados. Sin embargo, es posible personalizar estos mensajes para mejorar la claridad y usabilidad.

from flask_jwt_extended import JWTManager

jwt = JWTManager(app)

@jwt.unauthorized_loader
def sin_autorizacion(mensaje):
    return jsonify({'mensaje': 'Se requiere autenticación para acceder a este recurso'}), 401

@jwt.needs_fresh_token_loader
def token_no_fresco(jwt_header, jwt_payload):
    return jsonify({'mensaje': 'Se requiere un token fresco para esta operación'}), 401

Decoradores adicionales:

Flask-JWT-Extended ofrece otros decoradores para casos específicos:

  • @jwt_optional(): Versión anterior a @jwt_required(optional=True), se utiliza si se trabaja con versiones previas de la extensión.
  • @jwt_required(locations=['cookies']): Si los tokens se envían en ubicaciones distintas al encabezado, se debe especificar en el decorador.

Ejemplo completo de protección de rutas:

@app.route('/admin/panel', methods=['GET'])
@jwt_required()
def panel_admin():
    claims = get_jwt()
    if claims.get('rol') != 'admin':
        return jsonify({'mensaje': 'Acceso denegado: privilegios insuficientes'}), 403
    return jsonify({'mensaje': 'Bienvenido al panel de administración'}), 200

En este ejemplo:

  • Se utiliza get_jwt() para obtener los claims del token.
  • Se verifica que el usuario tenga el rol adecuado antes de permitir el acceso.

Consideraciones sobre la seguridad de las rutas:

  • Es importante proteger todas las rutas que manejan información sensible o permiten modificar datos.
  • Los decoradores de Flask-JWT-Extended simplifican este proceso, pero es responsabilidad del desarrollador asegurar que se apliquen correctamente.

Integración con otras extensiones:

Si la aplicación utiliza otras herramientas de autenticación o control de acceso, como Flask-Login, es necesario coordinar su uso con Flask-JWT-Extended para evitar conflictos y garantizar una experiencia de usuario consistente.

Al dominar el uso de los decoradores de Flask-JWT-Extended, se logra un control preciso sobre el acceso a los recursos de la aplicación, fortaleciendo la seguridad y confiabilidad del sistema.

Implementación de roles y claims en tokens

En aplicaciones que requieren diferentes niveles de acceso, es fundamental implementar un sistema de roles y claims en los tokens JWT. Esto permite asignar permisos específicos a los usuarios y controlar el acceso a recursos según su rol.

Los claims personalizados son datos adicionales que se incluyen en el token JWT al momento de su creación. Estos claims pueden ser usados para almacenar información relevante como el rol del usuario, permisos especiales o cualquier otro dato necesario para la autorización.

Para incluir claims personalizados, se utiliza el parámetro additional_claims al crear el token con create_access_token. Por ejemplo:

from flask_jwt_extended import create_access_token

@app.route('/login', methods=['POST'])
def login():
    # Proceso de autenticación del usuario
    # ...

    # Definir los claims personalizados
    claims = {'rol': usuario.rol}

    # Generar el token con los claims adicionales
    access_token = create_access_token(identity=usuario.id, additional_claims=claims)

    return jsonify({'access_token': access_token}), 200

En este ejemplo, se asume que el objeto usuario tiene un atributo rol que indica el rol asignado, como 'admin' o 'editor'. El claim 'rol' se añade al token para que pueda ser consultado en las rutas protegidas.

Para utilizar los claims en las rutas protegidas, se emplea la función get_jwt, que permite obtener todos los claims del token actual:

from flask_jwt_extended import jwt_required, get_jwt

@app.route('/admin', methods=['GET'])
@jwt_required()
def admin():
    claims = get_jwt()
    if claims['rol'] != 'admin':
        return jsonify({'mensaje': 'Acceso denegado: privilegios insuficientes'}), 403
    return jsonify({'mensaje': 'Bienvenido al panel de administración'}), 200

Aquí, se verifica que el claim 'rol' tenga el valor 'admin' antes de permitir el acceso al recurso. Si el usuario no tiene los permisos necesarios, se devuelve un mensaje de error con el código de estado HTTP 403.

Para simplificar la gestión de permisos, es posible crear decoradores personalizados que verifiquen los roles de los usuarios. Esto permite reutilizar código y mantener las rutas más organizadas:

from functools import wraps
from flask_jwt_extended import verify_jwt_in_request, get_jwt
from flask import jsonify

def role_required(rol):
    def wrapper(fn):
        @wraps(fn)
        def decorator(*args, **kwargs):
            verify_jwt_in_request()
            claims = get_jwt()
            if claims.get('rol') != rol:
                return jsonify({'mensaje': f'Acceso denegado: se requiere rol {rol}'}), 403
            return fn(*args, **kwargs)
        return decorator
    return wrapper

Con este decorador, se puede proteger una ruta especificando el rol requerido:

@app.route('/dashboard', methods=['GET'])
@role_required('admin')
def dashboard():
    return jsonify({'mensaje': 'Acceso autorizado al dashboard'}), 200

Este enfoque mejora la mantenibilidad del código y facilita la gestión de múltiples roles en la aplicación.

Si un usuario puede tener varios roles o permisos, se pueden almacenar en el token como una lista. Por ejemplo:

claims = {'roles': usuario.roles}
access_token = create_access_token(identity=usuario.id, additional_claims=claims)

En las rutas protegidas, se verifica si el usuario tiene alguno de los roles necesarios:

def roles_required(required_roles):
    def wrapper(fn):
        @wraps(fn)
        def decorator(*args, **kwargs):
            verify_jwt_in_request()
            claims = get_jwt()
            user_roles = claims.get('roles', [])
            if not any(role in user_roles for role in required_roles):
                return jsonify({'mensaje': f'Acceso denegado: se requiere uno de los roles {required_roles}'}), 403
            return fn(*args, **kwargs)
        return decorator
    return wrapper

Aplicando el decorador para múltiples roles:

@app.route('/reporte', methods=['GET'])
@roles_required(['admin', 'editor'])
def generar_reporte():
    return jsonify({'mensaje': 'Generando reporte...'}), 200

Este método permite que usuarios con el rol 'admin' o 'editor' puedan acceder a la ruta /reporte, proporcionando una mayor flexibilidad en el control de acceso.

Además de roles, se pueden implementar claims específicos para controlar permisos más granulares. Por ejemplo, si se desea permitir ciertas acciones basadas en permisos individuales:

claims = {'permisos': usuario.permisos}
access_token = create_access_token(identity=usuario.id, additional_claims=claims)

Luego, en las rutas, se verifica la presencia de un permiso particular:

def permission_required(permission):
    def wrapper(fn):
        @wraps(fn)
        def decorator(*args, **kwargs):
            verify_jwt_in_request()
            claims = get_jwt()
            permisos = claims.get('permisos', [])
            if permission not in permisos:
                return jsonify({'mensaje': f'Acceso denegado: se requiere el permiso {permission}'}), 403
            return fn(*args, **kwargs)
        return decorator
    return wrapper

Aplicando el decorador:

@app.route('/datos-sensibles', methods=['GET'])
@permission_required('ver_datos_sensibles')
def datos_sensibles():
    return jsonify({'mensaje': 'Accediendo a datos sensibles'}), 200

Es importante considerar que los roles y permisos pueden cambiar durante la sesión del usuario. Dado que los tokens JWT son sin estado (stateless), cualquier cambio en los roles o permisos no se reflejará hasta que se emita un nuevo token. Para mitigar este problema, se puede reducir el tiempo de expiración de los tokens o implementar una lista de revocación (blacklist).

Para invalidar tokens específicos antes de su expiración, se puede mantener un registro de tokens revocados. Al recibir una solicitud, se verifica si el token está en la lista de revocación.

Registrar el token al momento de su creación:

from flask_jwt_extended import get_jti

@app.route('/login', methods=['POST'])
def login():
    # Autenticación del usuario
    # ...

    # Generar el token y obtener su JTI (JWT ID)
    access_token = create_access_token(identity=usuario.id, additional_claims=claims)
    jti = get_jti(access_token)

    # Almacenar el JTI en una lista permitida (whitelist)
    agregar_a_lista_blanqueo(jti)

    return jsonify({'access_token': access_token}), 200

Al proteger una ruta, se verifica si el JTI está en la lista:

@jwt.token_in_blocklist_loader
def verificar_lista_revocación(jwt_header, jwt_payload):
    jti = jwt_payload['jti']
    return jti in lista_revocación

Con esta verificación, si un token ha sido revocado (por ejemplo, al cerrar sesión o cambiar permisos), se puede impedir su uso.

Además de claims personalizados, JWT define claims estándar como exp, iss y aud. Es recomendable utilizarlos para mejorar la seguridad y la integridad de los tokens. Por ejemplo, establecer el emisor (issuer) y la audiencia (audience) del token:

app.config['JWT_ENCODE_ISSUER'] = 'mi_aplicacion'
app.config['JWT_DECODE_AUDIENCE'] = 'mis_usuarios'

Al crear el token:

access_token = create_access_token(
    identity=usuario.id,
    additional_claims=claims,
    issuer='mi_aplicacion',
    audience='mis_usuarios'
)

Estos claims permiten que la aplicación verifique que el token fue emitido por ella misma y que está destinado a los usuarios correctos.

Implementar roles y claims en tokens JWT con Flask-JWT-Extended proporciona un control detallado sobre la autorización en la aplicación. Al aprovechar estas funcionalidades, se logra una gestión efectiva de permisos y se refuerza la seguridad de los recursos protegidos.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

Plan mensual

19.00 € /mes

Precio normal mensual: 19 €
47 % DE DESCUENTO

Plan anual

10.00 € /mes

Ahorras 108 € al año
Precio normal anual: 120 €
Aprende Flask online

Todas las lecciones de Flask

Accede a todas las lecciones de Flask y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Flask y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Instalar y configurar Flask-JWT-Extended en una aplicación Flask.
  • Definir una clave secreta segura para firmar tokens JWT.
  • Configurar opciones de expiración y ubicación de los tokens JWT.
  • Personalizar respuestas para eventos como token caducado o inválido.
  • Proteger rutas con decoradores para un acceso seguro a datos.