Implementar la autenticación de dos factores (2FA) en una aplicación web utilizando Python y Flask. Este tutorial cubrirá los siguientes pasos:
Configuración del entorno. Creación de una aplicación Flask básica. Implementación del registro y el inicio de sesión de usuarios. Integración de la autenticación de dos factores usando un autenticador de tiempo basado en tokens (TOTP). 1. Configuración del entorno
Primero, necesitas instalar algunas dependencias. Asegúrate de tener Python y pip instalados en tu sistema. Luego, instala Flask y otras bibliotecas necesarias.
pip install Flask Flask-SQLAlchemy Flask-WTF pyotp qrcode[pil]
2. Creación de una aplicación Flask básica
Crea una estructura de directorios básica para tu aplicación Flask:
your_project/
app.py
templates/
base.html
login.html
register.html
two_factor.html
static/
app.py
from flask import Flask, render_template, redirect, url_for, request, flash, session
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Length, EqualTo
import pyotp
import qrcode
import qrcode.image.svg
from io import BytesIO
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(150), nullable=False, unique=True)
password = db.Column(db.String(150), nullable=False)
otp_secret = db.Column(db.String(16), nullable=False)
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=2, max=150)])
password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Login')
class TwoFactorForm(FlaskForm):
token = StringField('Token', validators=[DataRequired()])
submit = SubmitField('Verify')
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
otp_secret = pyotp.random_base32()
new_user = User(username=form.username.data, password=form.password.data, otp_secret=otp_secret)
db.session.add(new_user)
db.session.commit()
flash('Account created! Please log in.', 'success')
return redirect(url_for('login'))
return render_template('register.html', form=form)
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and user.password == form.password.data:
session['username'] = user.username
session['otp_secret'] = user.otp_secret
return redirect(url_for('two_factor'))
else:
flash('Login Unsuccessful. Please check username and password', 'danger')
return render_template('login.html', form=form)
@app.route('/two_factor', methods=['GET', 'POST'])
def two_factor():
form = TwoFactorForm()
otp_secret = session.get('otp_secret')
if not otp_secret:
return redirect(url_for('login'))
if form.validate_on_submit():
user = User.query.filter_by(username=session.get('username')).first()
totp = pyotp.TOTP(user.otp_secret)
if totp.verify(form.token.data):
session['authenticated'] = True
flash('Logged in successfully!', 'success')
return redirect(url_for('dashboard'))
else:
flash('Invalid token. Please try again.', 'danger')
return render_template('two_factor.html', form=form)
@app.route('/dashboard')
def dashboard():
if not session.get('authenticated'):
return redirect(url_for('login'))
return f"Welcome to your dashboard, {session['username']}!"
@app.route('/qr_code')
def qr_code():
otp_secret = session.get('otp_secret')
if not otp_secret:
return redirect(url_for('login'))
totp = pyotp.TOTP(otp_secret)
img = qrcode.make(totp.provisioning_uri(session.get('username'), issuer_name="YourApp"))
buffer = BytesIO()
img.save(buffer, format='PNG')
buffer.seek(0)
return buffer.getvalue(), 200, {'Content-Type': 'image/png'}
if __name__ == '__main__':
db.create_all()
app.run(debug=True)
templates/base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2FA Tutorial</title>
</head>
<body>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</body>
</html>
templates/register.html
{% extends "base.html" %}
{% block content %}
<form method="POST" action="">
{{ form.hidden_tag() }}
<div>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span><br>
{% endfor %}
</div>
<div>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span><br>
{% endfor %}
</div>
<div>
{{ form.confirm_password.label }}<br>
{{ form.confirm_password(size=32) }}<br>
{% for error in form.confirm_password.errors %}
<span style="color: red;">[{{ error }}]</span><br>
{% endfor %}
</div>
<div>
{{ form.submit() }}
</div>
</form>
{% endblock %}
templates/login.html
{% extends "base.html" %}
{% block content %}
<form method="POST" action="">
{{ form.hidden_tag() }}
<div>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span><br>
{% endfor %}
</div>
<div>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span><br>
{% endfor %}
</div>
<div>
{{ form.submit() }}
</div>
</form>
{% endblock %}
templates/two_factor.html
{% extends "base.html" %}
{% block content %}
<img src="{{ url_for('qr_code') }}" alt="QR Code"><br>
Scan the QR code above with your authenticator app and enter the token below.<br>
<form method="POST" action="">
{{ form.hidden_tag() }}
<div>
{{ form.token.label }}<br>
{{ form.token(size=32) }}<br>
{% for error in form.token.errors %}
<span style="color: red;">[{{ error }}]</span><br>
{% endfor %}
</div>
<div>
{{ form.submit() }}
</div>
</form>
{% endblock %}
3. Implementación del registro y el inicio de sesión de usuarios
El código anterior ya incluye la lógica básica para el registro y el inicio de sesión de usuarios.
4. Integración de la autenticación de dos factores usando un autenticador de tiempo basado en tokens (TOTP)
La parte crucial de este tutorial es la integración de la autenticación de dos factores usando TOTP. En el código anterior, pyotp se utiliza para generar y verificar tokens de tiempo basados en secretos únicos para cada usuario.
Ejecución de la aplicación
Para ejecutar la aplicación, asegúrate de estar en el directorio del proyecto y ejecuta:
python app.py
Abre tu navegador y visita http://127.0.0.1:5000/register para registrar un nuevo usuario y seguir el flujo de trabajo de la autenticación de dos factores.