
Teilen Sie:
Benjamin Aronov ist ein Entwickler-Befürworter bei Vonage. Er ist ein bewährter Community Builder mit einem Hintergrund in Ruby on Rails. Benjamin genießt die Strände von Tel Aviv, das er sein Zuhause nennt. Von Tel Aviv aus kann er einige der besten Startup-Gründer der Welt treffen und von ihnen lernen. Außerhalb der Tech-Branche reist Benjamin gerne um die Welt auf der Suche nach dem perfekten Pain au Chocolat.
Zwei-Faktor-Authentifizierung mit Django und Vonage einrichten
Bei den meisten Beispielen für die Zwei-Faktor-Authentifizierung wird der zweite Faktor bei der Anmeldung abgefragt. Dies schützt zwar vor gestohlenen Passwörtern, bietet jedoch weniger Schutz vor einem anderen häufigen Problem: einem Angreifer, der bereits über ein gültiges Sitzungs-Cookie verfügt.
Sobald sich ein Benutzer angemeldet hat, wird ein Django- -Sitzung kann tagelang oder wochenlang gültig bleiben. Wenn jemand dieses Cookie in die Hände bekommt, muss er den Anmeldeprozess nicht erneut durchlaufen. Aus Sicht der Anwendung ist er bereits authentifiziert.
Django ist ein hochentwickeltes Python-Web-Framework, das eine schnelle Entwicklung sowie ein übersichtliches, pragmatisches Design fördert. Es verfügt über integrierte Funktionen für die Authentifizierung, die Sitzungsverwaltung, ein ORM und ein Formularvalidierungssystem. Dieses Tutorial behandelt alle vier dieser Aspekte.
Bei einigen Applications ist es daher sinnvoll, einen zweiten Authentifizierungsfaktor nicht bei der Anmeldung, sondern unmittelbar vor einer sensiblen Aktion einzuführen. Gute Beispiele für sensible Aktionen, vor denen Sie möglicherweise einen zusätzlichen Authentifizierungsfaktor einführen möchten, sind: die Übertragung eines Tickets, eine Geldüberweisung, die Rotation eines API-Schlüssels oder die Änderung eines Auszahlungskontos.
Dieses Muster wird üblicherweise als „Step-up-Authentifizierung“. Der Benutzer meldet sich wie gewohnt an und kann die Bereiche der Anwendung mit geringem Risiko ohne Unterbrechung nutzen. Wenn er versucht, eine sensiblere Aktion durchzuführen, fordert die Anwendung einen zusätzlichen Verifizierungsschritt an.
In diesem Tutorial werden wir diesen „Step-up“-Authentifizierungsablauf in Django mithilfe der Vonage Verify API. Eine erfolgreiche Verifizierung wird fünf Minuten lang in der aktuellen Sitzung gespeichert und nach einer geschützten Aktion verbraucht. Außerdem werden wir die wichtigsten Fälle von API-Fehlern behandeln und den gesamten Ablauf testen, ohne echte Nachrichten zu versenden.
tl;dr: Lesen Sie die Schnellstartanleitung im GitHub-Repository der Vonage-Community.
The Setlist code entry page showing a masked destination number and a wide monospaced input containing the digits 9 1 4 8 9 7.
Hintergrund
Erweiterte Authentifizierung vs. 2FA bei der Anmeldung
Anmeldung mit 2FA schützt den Zugriff auf den Account selbst. Jede neue Sitzung erfordert den zweiten Faktor, unabhängig davon, ob der Benutzer eine Abrechnungskonsole öffnet oder eine Hilfeseite aufruft. Dies ist sinnvoll, wenn der Großteil der Applications vertrauliche Daten enthält. Typische Beispiele hierfür sind Bankanwendungen und Verwaltungssysteme.
Erweiterte Authentifizierung verfolgt einen anderen Ansatz. Bei normalen Aktivitäten wird die bestehende authentifizierte Sitzung genutzt, während bestimmte risikoreiche Aktionen eine aktuelle Zwei-Faktor-Prüfung erfordern. Dies kann nützlich sein, wenn nur ein kleiner Teil der Anwendung einen stärkeren Schutz benötigt. Außerdem wird der zusätzliche Aufwand auf die Aktion beschränkt, die ihn rechtfertigt.
Viele Systeme nutzen beide Ansätze. Die 2FA bei der Anmeldung kann den Zugriff auf den Account schützen, während die Step-up-Authentifizierung vor besonders sensiblen Vorgängen eine zusätzliche Überprüfung vorsieht. In diesem Tutorial konzentrieren wir uns auf den zweiten Teil.
Was Sie entwickeln werden: Eine Django-App für den Ticketumtausch
Die Beispielanwendung heißt „Setlist“. Sie verwaltet Konzertkarten. Ein Benutzer meldet sich an, ruft seine Karten auf und kann eine Karte an einen anderen Benutzer übertragen. Die Übertragung der Karte ist in diesem Beispiel der heikle Vorgang, da dabei etwas mit Wiederverkaufswert aus dem Account entfernt wird und vom Absender nicht rückgängig gemacht werden kann.
Hier ist der gesamte Ablauf aus Sicht des Benutzers:
Ada meldet sich wie gewohnt an und sieht sich ihre Tickets an – eine zusätzliche Überprüfung ist nicht erforderlich.
Ada klickt Übertragen auf einem Ticket. Da es sich hierbei um eine geschützte Aktion handelt, leitet die Anwendung sie zum Verifizierungsablauf weiter.
Wenn für Ada bereits eine bestätigte Telefonnummer hinterlegt ist, sendet Vonage einen Code an diese Nummer. Ist dies nicht der Fall, gibt sie zunächst eine ein.
Ada gibt den sechsstelligen Code ein. Vonage bestätigt, dass er korrekt ist.
Die Anwendung protokolliert die Verifizierung, wechselt den Sitzungsschlüssel und schließt die Übertragung ab – das Ticket erscheint nun in Graces Account.
Wenn Ada unmittelbar danach versucht, ein weiteres Ticket zu übertragen, muss sie sich erneut Verify. Ein einzelner Code kann nicht für mehrere Übertragungen verwendet werden.
Das Anzeigen von Tickets erfolgt über die normale authentifizierte Sitzung. Für die Übertragung eines Tickets ist ein Bestätigungscode erforderlich. Die VerificationRequiredMixin sorgt für die Einhaltung dieser Regel: Ohne eine kürzlich erfolgte Verifizierung der Sitzung wird jeder Versuch, die Übertragungsansicht aufzurufen, direkt zum Verifizierungsablauf weitergeleitet.
Der wiederverwendbare Teil befindet sich in einer Django-App namens stepup. Sie enthält keine ticketspezifische Logik, sodass derselbe Code später auch auf andere geschützte Aktionen angewendet werden kann.
The Setlist my-tickets page listing four concert tickets, each showing venue, date, seat, order reference, and a face value between $62 and $145, with a Transfer button.
Einrichtung
Voraussetzungen
Das brauchen Sie:
Python 3.12 oder neuer. Das Beispiel wurde mit Django 5.2.17 LTS und Version 4.8 des Vonage Python SDK erstellt.
Ein Vonage API-Konto.
Ein Handy, das SMS empfangen kann. Während der Testphase Ihres Accounts können Nachrichten nur an Numbers gesendet werden, die Sie als Testnummern hinzugefügt haben. Fügen Sie Ihr Handy unter „Ihre Numbers“ hinzu, bevor du den Ablauf testest.
Einrichten und Ausführen der Django-App
Klonen Sie das Repository und erstellen Sie eine virtuelle Umgebung:
git clone https://github.com/Vonage-Community/blog-django-2fa-vonage-verify.git
cd blog-django-2fa-vonage-verify
python -m venv .venv
source .venv/bin/activateInstallieren Sie die Abhängigkeiten:
pip install -r requirements.txt
Kopieren Sie die Beispiel-Umgebungsdatei:
cp .env.example .envGeben Sie anschließend Ihre Vonage-Anmeldedaten ein:
VONAGE_API_KEY=your_api_key
VONAGE_API_SECRET=your_api_secret
VONAGE_BRAND_NAME=Setlist
STEP_UP_TTL_SECONDS=300
Öffnen Sie Ihre API-Einstellungen-Seite um auf Ihren Vonage API-Schlüssel und Ihr API-Geheimnis zuzugreifen, die beide wie im Screenshot unten dargestellt angezeigt werden. Der API-Schlüssel befindet sich oben auf der Seite, und um auf Ihr API-Geheimnis zuzugreifen, lesen Sie bitte den Unterabschnitt "Account secret".
Hinweis: Falls Sie sich nicht an Ihr zuvor erstelltes API-Geheimnis erinnern können, klicken Sie auf "+ Neues Geheimnis erstellen" und speichern Sie es sicher.

Erstellen Sie die Datenbank und laden Sie die Beispieldaten:
python manage.py migrate
python manage.py seed_demo
Django starten:
python manage.py runserver
Öffnen http://127.0.0.1:8000/ und melden Sie sich an unter ada mit dem Passwort setlist-demo. Es gibt außerdem einen zweiten Account, grace, das wir als Empfänger bei der Übertragung von Tickets verwenden werden.
Das war's! Die App ist einsatzbereit!
Was die Vonage Verify API für Sie übernimmt
Bevor wir uns mit dem Django-Code befassen, lohnt es sich, die Aufgaben der Anwendung von denen von Vonage Verify zu trennen.
Wenn wir Verifizierungscodes selbst implementieren würden, müssten wir diese generieren, speichern, deren Gültigkeit begrenzen, Wiederholungslimits durchsetzen, übermittelte Werte sicher abgleichen und entscheiden, was nach zu vielen falschen Versuchen geschieht. Verify übernimmt all diese Schritte. Die Anwendung leitet eine Verifizierung ein, indem sie eine Telefonnummer an die API sendet und eine Anfrage-ID zurückerhält. Später übermittelt sie diese Anfrage-ID zusammen mit dem vom Nutzer eingegebenen Code – Verify entscheidet, ob der Code gültig ist, und der gesamte Lebenszyklus der Anfrage wird auf Seiten von Vonage verwaltet.
Daher benötigt dieses Projekt weder eine Codetabelle noch eine Ablaufspalte noch eine Bereinigungsaufgabe.
Verify kann auch den Kanal-Fallback verarbeiten. Der Workflow besteht aus einer geordneten Liste von Zustellmethoden. Wenn der erste Kanal nicht innerhalb von channel_timeout, kann Vonage zum nächsten Kanal übergehen. Für das Beispiel reicht ein einziger SMS-Kanal aus. Wenn Sie einen Fallback wünschen, können Sie VoiceChannel(to=number) danach einfügen, sodass dieselbe Verifizierung per Telefonanruf fortgesetzt werden kann, falls die SMS nicht ankommt.
Den Ablauf der mehrstufigen Authentifizierung erstellen
Die verifizierte Telefonnummer speichern
Das Standard-Benutzermodell von Django verfügt über kein Feld für die Telefonnummer. Bei einem neuen Projekt könnten Sie beschließen, ein solches Feld zu einem benutzerdefinierten Benutzermodell hinzuzufügen. In einer bestehenden Anwendung kann das Ersetzen des Benutzermodells jedoch eine weitaus umfangreichere Änderung darstellen als die Authentifizierungsfunktion selbst.
In diesem Beispiel ist die Telefonnummer in einem Eins-zu-Eins-Modell innerhalb der stepup App:
# stepup/models.py
from django.conf import settings
from django.core.validators import RegexValidator
from django.db import models
# Vonage wants numbers in E.164 without the leading "+": country code followed by
# the subscriber number, digits only. Validating here means a typo fails in the form
# rather than as an HTTP 422 from the API.
e164 = RegexValidator(
regex=r"^[1-9]\d{6,14}$",
message="Enter your number in international format, digits only, e.g. 447700900000.",
)
class VerifiedPhone(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="verified_phone",
)
number = models.CharField(max_length=15, validators=[e164])
confirmed_at = models.DateTimeField(
null=True,
blank=True,
help_text="Set the first time a code sent to this number is accepted.",
)Verwenden Sie settings.AUTH_USER_MODEL dieses hier anstelle eines direkten Imports django.contrib.auth.models.User . Ein direkter User Import funktioniert, solange das Projekt das Standard-Benutzermodell von Django beibehält. Durch die Verwendung dieser Einstellung funktioniert diese App auch dann weiter, wenn das Projekt stattdessen ein benutzerdefiniertes Modell verwendet.
Die Unterscheidung zwischen number und confirmed_at ist ebenfalls nützlich. Eine in ein Formular eingegebene Nummer ist noch nicht unbedingt eine verifizierte Nummer. In dieser Anwendung bleibt eine unbestätigte Nummer während der laufenden Verifizierung in der Sitzung erhalten. Die Datenbank wird erst aktualisiert, nachdem der Nutzer den an diese Nummer gesendeten Code erfolgreich übermittelt hat.
Die Verifizierung in der Django-Session speichern
Der Step-up-Zustand gehört in die Sitzung. Eine erste Implementierung könnte dies nach einer erfolgreichen Überprüfung setzen:
request.session["verified"] = TrueDas Problem ist, dass der Wert für die gesamte Dauer der Sitzung gültig bliebe, sofern er nicht explizit entfernt würde. Django-Sitzungen dauern in der Regel viel länger, als die Überprüfung dauern sollte. Die Standarddauer SESSION_COOKIE_AGE beträgt zwei Wochen.
Bei der mehrstufigen Authentifizierung möchten wir nicht nur wissen, ob eine Überprüfung stattgefunden hat, sondern auch, wann sie stattgefunden hat. Das Beispiel speichert einen Zeitstempel:
# stepup/session.py
import time
from django.conf import settings
VERIFIED_AT_KEY = "stepup_verified_at"
REQUEST_ID_KEY = "stepup_request_id"
NUMBER_KEY = "stepup_number"
LAST_REQUEST_ID_KEY = "stepup_last_request_id"
def mark_verified(session, request_id=""):
"""Record that this session just passed a verification."""
session[VERIFIED_AT_KEY] = time.time()
session.pop(REQUEST_ID_KEY, None)
session.pop(NUMBER_KEY, None)
if request_id:
session[LAST_REQUEST_ID_KEY] = request_id
def is_verified(session):
"""True if this session verified recently enough to still count."""
verified_at = session.get(VERIFIED_AT_KEY)
if not verified_at:
return False
return (time.time() - verified_at) < settings.STEP_UP_TTL_SECONDS
Die Standard-TTL (Time-To-Live) im Beispiel beträgt fünf Minuten. Das gibt dem Nutzer genügend Zeit, die Nachricht zu empfangen, den Code einzugeben und die geschützte Aktion abzuschließen, ohne dass die Verifizierung zu einer langfristig gültigen Berechtigung wird.
Die Beibehaltung dieses Status innerhalb der Sitzung ist auch dann wichtig, wenn derselbe Benutzer auf mehr als einem Gerät angemeldet ist. Wenn sich der Benutzer auf seinem Laptop authentifiziert, sollte die Sitzung auf seinem Smartphone nicht automatisch als Verified gelten. Der auf die Sitzung beschränkte Status ermöglicht uns dieses Verhalten ohne zusätzliche Geräteverfolgung.
Überprüfen Sie die next Weiterleitungsparameter
Der Verifizierungsablauf muss den Benutzer zurück auf die Seite leiten, die er ursprünglich angefordert hat. Eine gängige Methode zur Umsetzung ist die Verwendung eines next Parameter. Wichtig ist dabei, dass next eine vom Benutzer kontrollierte Eingabe ist.
Das ist unsicher:
return HttpResponseRedirect(request.POST["next"])
Ein Angreifer könnte eine URL wie die folgende erstellen:
https://setlist.example/verify/?next=https://evil.example/login
Der Nutzer würde die echte Anwendung aufrufen, den dortigen Authentifizierungsablauf durchlaufen und anschließend auf eine vom Angreifer kontrollierte Website weitergeleitet werden.
Django bietet url_has_allowed_host_and_scheme() für diesen Fall:
from django.utils.http import url_has_allowed_host_and_scheme
def safe_next(request, fallback="/"):
candidate = request.POST.get("next") or request.GET.get("next")
if candidate and url_has_allowed_host_and_scheme(
url=candidate,
allowed_hosts={request.get_host()},
require_https=request.is_secure(),
):
return candidate
return fallback
Diese Funktion sorgt dafür, dass Weiterleitungen auf dem aktuellen Host erfolgen, und erfasst zudem Formulare sowie protokollrelative URLs wie //evil.example. Es handelt sich um dieselbe Art von Überprüfung, die der Anmeldeablauf von Django für seinen eigenen next Parameter benötigt.
Django-Views mit einem Verifizierungs-Mixin schützen
Als Nächstes benötigen wir eine Schutzfunktion, die an sensible Ansichten angehängt werden kann.
# stepup/mixins.py
from urllib.parse import urlencode
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect
from django.urls import reverse
from . import session
class VerificationRequiredMixin(LoginRequiredMixin):
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated and not session.is_verified(request.session):
query = urlencode({"next": request.get_full_path()})
return redirect(f"{reverse('stepup:start')}?{query}")
return super().dispatch(request, *args, **kwargs)Das Mixin erweitert LoginRequiredMixin anstatt es zu ersetzen. In Django sind Mixins eine Möglichkeit, wiederverwendbare Funktionen in klassenbasierte Views einzubinden – jedes Mixin fügt einen spezifischen Logikteil hinzu, und mehrere Mixins können in einem einzigen View kombiniert werden. Dadurch werden hier zwei Fälle voneinander getrennt: Ein anonymer Nutzer muss sich anmelden, während ein authentifizierter Nutzer, der in letzter Zeit keine verstärkte Verifizierung durchlaufen hat, den Verifizierungsablauf abschließen muss. Django weiß bereits, wie der erste Fall zu behandeln ist, daher fügt dieses Mixin nur den zweiten hinzu.
Das Schützen einer Ansicht ist dann ganz einfach:
# tickets/views.py
class TicketTransferView(VerificationRequiredMixin, View):
...
Ansichten, die keine erweiterte Authentifizierung erfordern, verwenden weiterhin LoginRequiredMixin. In Setlist TicketListView und TicketDetailView bleiben weiterhin normale, authentifizierte Ansichten. Der Nutzer kann seine Tickets einsehen, ohne ein Telefon zur Hand zu haben.
Die Vonage Verify API in Django integrieren
Den Vonage-Python-Client konfigurieren
Der gesamte Vonage-spezifische Code ist in einem Modul zusammengefasst. Die Django-Views rufen eine kleine Schnittstelle auf, anstatt sich mit der Funktionsweise von SDK-Anfragen, Fehlern oder Antwortobjekten auseinanderzusetzen.
# stepup/client.py
import logging
from django.conf import settings
from pydantic import ValidationError
from vonage import Auth, Vonage
from vonage_http_client.errors import HttpRequestError
from vonage_verify import SmsChannel, VerifyRequest
logger = logging.getLogger(__name__)
_client = None
def get_client():
global _client
if _client is None:
_client = Vonage(
Auth(
api_key=settings.VONAGE_API_KEY,
api_secret=settings.VONAGE_API_SECRET,
)
)
return _client
Verify v2 unterstützt die JWT- und die Basic-Authentifizierung. Durch die Übergabe eines API-Schlüssels und eines Geheimschlüssels wird die Basic-Authentifizierung ausgewählt, wodurch sich dieses Beispiel auf zwei Umgebungsvariablen beschränkt. Für den Produktiveinsatz ist die Kombination aus Anwendungs-ID und privatem Schlüssel die bessere Standardeinstellung; diese ist zudem erforderlich, wenn Sie asynchrone Status-Callbacks nutzen möchten.
Überprüfung starten
Der erste API-Aufruf startet die Überprüfung:
def start_verification(number):
try:
# The SDK models are Pydantic, so a badly shaped number fails here rather
# than costing a round trip. It raises ValidationError, not HttpRequestError.
request = VerifyRequest(
brand=settings.VONAGE_BRAND_NAME,
code_length=6,
workflow=[SmsChannel(to=number)],
)
except ValidationError as err:
raise VerificationError(
"That number does not look right. Check the country code and try again.",
restart=True,
) from err
try:
response = get_client().verify.start_verification(request)
except HttpRequestError as err:
raise VerificationError(_start_message(err), restart=True) from err
return response.request_id
Es gibt zwei try Blöcke, da die Funktion zwei verschiedene Fehlerquellen aufweist. Darauf bin ich gestoßen, als ich den Fall einer ungültigen Nummer getestet habe. Die Anfrageobjekte des Vonage SDK v4 sind Pydantic-Modelle, also etwa so:
SmsChannel(to="1")kann ausgelöst werden pydantic.ValidationError während das Request-Objekt erstellt wird. Zu diesem Zeitpunkt ist noch keine HTTP-Anfrage erfolgt, sodass HttpRequestError wird es niemals erkennen. Die Trennung von Modellvalidierung und HTTP-Fehlern in separate Blöcke macht diesen Unterschied deutlich und verhindert, dass fehlerhafte Eingaben zu einer unerwarteten 500-Antwort führen.
Code überprüfen
Das Einreichen des Codes ist weniger aufwendig:
def check_code(request_id, code):
"""Submit a code for `request_id`. Returns None on success, raises otherwise."""
try:
get_client().verify.check_code(request_id=request_id, code=code)
except HttpRequestError as err:
message, restart = _check_message(err)
raise VerificationError(message, restart=restart) from errDas SDK löst bei fehlgeschlagenen Prüfungen eine Ausnahme aus, sodass die Anwendung kein separates Erfolgskennzeichen überprüfen muss.
Behandlung von Fehlern der Vonage Verify API
Die wichtigste Frage für die Benutzeroberfläche ist nicht nur, welcher HTTP-Fehler aufgetreten ist. Wir müssen entscheiden, ob der Benutzer mit der aktuellen Verifizierungsanfrage weiterarbeiten kann oder ob diese Anfrage abgeschlossen ist und er von vorne beginnen muss. Der Helper gibt sowohl eine Meldung als auch ein Neustart-Flag zurück:
def _check_message(err):
status = _status(err)
logger.warning("Verify check failed with HTTP %s: %s", status, err)
if status == 400:
return "That code is not right. Check the message and try again.", False
if status == 404:
return "That code has expired. Request a new one.", True
if status == 410:
# Verify enforces the attempt limit for us and retires the request.
return "Too many incorrect attempts. Request a new code.", True
if status == 429:
return "Too many attempts right now. Try again in a moment.", False
return "We could not check that code. Request a new one.", TrueEin False Wert bedeutet, dass die bestehende Verifizierungsanfrage weiterhin gültig ist. Der Benutzer bleibt auf dem Code-Formular und kann es erneut versuchen. Ein True Wert bedeutet, dass die Anwendung ihren ausstehenden Status aufheben und den Benutzer zum Anfang des Verifizierungsablaufs zurückführen sollte.
Der Fall des HTTP-Statuscodes 410 ist besonders nützlich, da „Verify“ die Begrenzung der Fehlversuche für uns durchsetzt. Wir müssen in Django keinen separaten Zähler führen.
Das Starten einer Verifizierung hat eine eigene Fehlerzuordnung. Zum Beispiel:
if status in (401, 403):
# A credentials problem is ours, not the user's. Do not leak the detail.
return "We could not send a code just now. Please try again later."
Ein 401- oder 403-Fehler weist auf ein Problem mit den Vonage-Anmeldedaten oder -Berechtigungen der Applications hin – siehe die Vonage-API-Fehlerreferenz für eine vollständige Liste der Fehlercodes. Die vollständigen Details sollten in den Serverprotokollen und nicht in der dem Benutzer angezeigten Meldung enthalten sein.
Ein 409 ist ein weiterer Fall, der behandelt werden sollte. Er kann auftreten, wenn für dieselbe Nummer bereits eine Überprüfung läuft, beispielsweise wenn jemand das Startformular zweimal absendet.
Erstellen der Validierungsansichten in Django
Der Browser-Ablauf umfasst zwei Schritte:
Eine Verifizierung für eine Telefonnummer starten.
Geben Sie den Code ein, den Sie auf diesem Telefon erhalten haben.
Die Ansicht „Telefonnummer“
Die erste Ansicht verarbeitet die Telefonnummer und erstellt die Verify-Anfrage.
# stepup/views.py
class StartVerificationView(LoginRequiredMixin, View):
template_name = "stepup/start.html"
def post(self, request):
phone = _confirmed_phone(request.user)
form = None
if phone:
number = phone.number
else:
form = PhoneNumberForm(request.POST)
if not form.is_valid():
return render(request, self.template_name, self._context(request, phone, form))
number = form.cleaned_data["number"]
try:
request_id = client.start_verification(number)
except client.VerificationError as err:
messages.error(request, err.message)
return render(request, self.template_name, self._context(request, phone, form))
session.start_pending(request.session, request_id, number)
return redirect(_with_next(reverse("stepup:check"), session.safe_next(request)))Im ersten Zweig gibt es eine wichtige Sicherheitsbeschränkung. Sobald ein Benutzer eine bestätigte Telefonnummer hat, erlaubt diese Ansicht ihm nicht, eine andere anzugeben. Andernfalls könnte sich ein Angreifer, der das Passwort des Nutzers kennt, anmelden, seine eigene Telefonnummer eingeben und die zweite Authentifizierungsstufe selbst abschließen. Das Ändern einer bestätigten Nummer muss in einem separaten Prozess zur Kontoverwaltung erfolgen, der idealerweise durch eine Überprüfung anhand der bestehenden Nummer geschützt ist.
Bei einer neuen Nummer schreibt die Anwendung auch noch nichts an VerifiedPhone . Der übermittelte Wert bleibt in der Sitzung gespeichert, bis der an diese Nummer gesendete Code verifiziert wurde.
Die Code-Eingabeansicht
Die zweite Ansicht überprüft den Code:
class CheckCodeView(LoginRequiredMixin, View):
template_name = "stepup/check.html"
def post(self, request):
request_id = session.pending_request_id(request.session)
if not request_id:
messages.error(request, "That verification is no longer active. Request a new code.")
return redirect(_with_next(reverse("stepup:start"), session.safe_next(request)))
form = CodeForm(request.POST)
if not form.is_valid():
return render(request, self.template_name, self._context(request, form))
try:
client.check_code(request_id, form.cleaned_data["code"])
except client.VerificationError as err:
messages.error(request, err.message)
if err.restart:
session.clear(request.session)
return redirect(_with_next(reverse("stepup:start"), session.safe_next(request)))
return render(request, self.template_name, self._context(request, form))
number = session.pending_number(request.session)
redirect_to = session.safe_next(request)
# Rotate the session key on a successful second factor, the same way Django
# does on login. A session ID captured before the step-up is useless after it.
request.session.cycle_key()
_remember_number(request.user, number)
session.mark_verified(request.session, request_id)
return redirect(redirect_to)Der Aufruf von cycle_key() verdient etwas Aufmerksamkeit. Django wechselt den Sitzungsschlüssel während der Anmeldung, um vor einer Sitzungsfixierung zu schützen. Sollte es einem Angreifer gelingen, vor der Authentifizierung eine bekannte Sitzungs-ID zu ermitteln, wird diese alte ID nach der Anmeldung des Benutzers unbrauchbar. Eine erfolgreiche Zwei-Faktor-Verifizierung stellt eine weitere Authentifizierungsgrenze dar, daher wechselt das Beispiel den Sitzungsschlüssel auch an dieser Stelle.
Feinheiten und Fallstricke
Verbesserung der Eingabe von Einmalcodes auf Mobilgeräten
Das Überprüfungsformular gibt dem Browser zudem einige nützliche Hinweise:
code = forms.CharField(
min_length=4,
max_length=10,
label="Verification code",
widget=forms.TextInput(
attrs={
"inputmode": "numeric", # Lets iOS and Android offer the code straight from the SMS.
"autocomplete": "one-time-code",
"autofocus": "autofocus",
}
),
)inputmode="numeric" regt mobile Browser dazu an, eine Zifferntastatur anzuzeigen. autocomplete="one-time-code" ermöglicht es unterstützten mobilen Plattformen, den eingehenden SMS-Code direkt über die Benachrichtigung bereitzustellen. Es handelt sich um eine kleine Ergänzung, die den mühsamsten Teil des Verifizierungsschritts überflüssig macht.
Zwei Fallstricke bei Django, die man vermeiden sollte
Beim Erstellen des Beispiels traten zwei Probleme auf, die eher auf Django selbst als auf die Verify API zurückzuführen waren. Beide sind nicht kompliziert, sobald man weiß, was vor sich geht, aber beide lassen sich leicht übersehen.
Normalisieren Sie die Telefonnummer vor der Feldvalidierung
Oft geben Nutzer Numbers mit Leerzeichen oder einem vorangestellten Pluszeichen ein:
+44 7700 900000
Das ist ein guter Eingabewert für ein Formular, auch wenn der Wert, den wir letztendlich an Vonage senden, ausschließlich aus Ziffern bestehen sollte. Mein erster Gedanke war, ihn in clean_number(). Das Problem ist jedoch, dass Django die Feldvalidatoren vor der clean_<field> ausgeführt werden. Das bedeutet, dass der E.164-Regex-Ausdruck den ursprünglichen Wert einschließlich der + Leerzeichen, und lehnt ihn mit der Validierungsmeldung („Bitte geben Sie Ihre Nummer im internationalen Format ein, nur Ziffern“) ab, bevor clean_number() die Gelegenheit hat, irgendetwas zu ändern.
Die Methode eines benutzerdefinierten Feldes to_python() wird früher ausgeführt:
# stepup/forms.py
# Everything people type around a phone number that is decoration, not digits.
DECORATION = re.compile(r"[\s()\-.]")
class PhoneNumberField(forms.CharField):
def to_python(self, value):
value = super().to_python(value)
if value in self.empty_values:
return value
return DECORATION.sub("", value).lstrip("+")Wenn die Validatoren ausgeführt werden, erhalten sie die normalisierte Darstellung.
Geschützte Objekte nicht in laden dispatch()
In der ersten Version der Ticket-Übertragungsansicht wurde das Ticket in dispatch():
def dispatch(self, request, *args, **kwargs):
self.ticket = get_object_or_404(Ticket, pk=kwargs["pk"], owner=request.user.pk)
return super().dispatch(request, *args, **kwargs)Bei anonymen Anfragen verhält sich dies fehlerhaft. Bei einem anonymen Benutzer request.user.pk ist None. Die Ticketabfrage schlägt fehl und gibt einen 404-Fehler zurück, bevor LoginRequiredMixin, was ebenfalls über dispatch()die Möglichkeit erhält, den Benutzer auf die Anmeldeseite umzuleiten.
Ein besserer Ansatz besteht darin, die Zugriffs-Mixins das Laden übernehmen zu lassen dispatch() und das Objekt später laden:
@cached_property
def ticket(self):
return get_object_or_404(
Ticket.objects.select_related("event"),
pk=self.kwargs["pk"],
owner=self.request.user.pk,
)Die Ansicht kann dann self.ticket aus get() und post() nachdem die Authentifizierung bereits überprüft wurde.
Überprüfung nach der Übertragung durchführen
Eine kürzlich durchgeführte Überprüfung leitet den Benutzer zur Ansicht für die Ticketübertragung weiter, doch es muss noch eine weitere richtlinienbezogene Entscheidung getroffen werden. Soll diese Überprüfung jede sensible Aktion autorisieren, die während der fünfminütigen TTL durchgeführt wird, oder nur die Aktion, die der Benutzer ursprünglich ausführen wollte?
In diesem Beispiel berechtigt eine Überprüfung zu einer Übertragung. Die Übertragung selbst erfolgt im Rahmen einer Datenbanktransaktion:
with transaction.atomic():
Transfer.objects.create(
ticket=self.ticket,
sender=request.user,
recipient=form.recipient,
verification_request_id=session.last_request_id(request.session),
)
self.ticket.owner = form.recipient
self.ticket.save(update_fields=["owner"])
# One verification, one transfer.
# Leaving the session verified would let a
# single code authorize every ticket in the account.
session.clear(request.session)Sobald die Übertragung erfolgreich abgeschlossen ist, wird der „Step-up“-Status aus der Sitzung entfernt. Möchte der Benutzer ein weiteres Ticket übertragen, muss er den Verifizierungsablauf erneut durchlaufen. Das ist strenger, als sich allein auf die TTL von fünf Minuten zu verlassen, entspricht aber der von uns umgesetzten Richtlinie: Der Code hat einen Vorgang mit hohem Risiko genehmigt.
Der Transfer Datensatz speichert außerdem die ID der Verify-Anfrage, die mit der Aktion verknüpft ist. Dadurch erhält die Anwendung einen Prüfpfad, der die Übertragung mit einer bestimmten Verify-Anfrage verknüpft. Sollte die Übertragung später angefochten werden, enthält der Datensatz aussagekräftigere Belege als ein bloßer boolescher Wert, der angibt, dass die Sitzung zu diesem Zeitpunkt zufällig verifiziert wurde.
Authentication can help secure important actions for your users, like transferring tickets.
Testen ohne SMS zu versenden
Das Projekt umfasst 47 Tests. Keiner davon greift auf das Netzwerk zu, benötigt Anmeldedaten oder sendet eine Nachricht.
Das funktioniert, weil Vonage-HTTP-Ausnahmen eine requests.Response. Ein Test kann eine solche Ausnahmeklasse manuell erstellen und an dieselbe Ausnahmeklasse übergeben, die das SDK ausgelöst hätte:
# stepup/tests/support.py
from json import dumps
from requests import Response
from vonage_http_client.errors import HttpRequestError, NotFoundError, RateLimitedError
def http_error(status_code, title="error"):
"""Build the exception the Vonage SDK would raise for a given status code."""
response = Response()
response.status_code = status_code
response.url = "https://api.nexmo.com/v2/verify"
response._content = dumps({"title": title}).encode()
if status_code == 404:
return NotFoundError(response)
if status_code == 429:
return RateLimitedError(response)
return HttpRequestError(response)Füge diese http_error() Factory in start_verification() oder check_code() und jeder Zweig der Fehlerzuordnung wird zu einem dreizeiligen Test: falscher Code, abgelaufene Anfrage, zu viele Versuche, Ratenbegrenzung, gleichzeitige Anfragen und ungültige Anmeldedaten. Das Repository enthält außerdem die Tests, die die Sicherheitsrichtlinien selbst überprüfen, darunter die Überprüfung, dass ein bestätigter Nutzer Codes nicht an eine neue Nummer weiterleiten kann.
Führen Sie die gesamte Testsuite mit folgendem Befehl aus:
python manage.py test Nächste Schritte
Die stepup App ist bewusst klein und in sich geschlossen gehalten. Um dasselbe Muster an anderer Stelle zu verwenden, fügen Sie VerificationRequiredMixin zu den Ansichten hinzu, die eine strengere Authentifizierung erfordern. Dazu könnten Passwortänderungen, das Hinzufügen einer Auszahlungsmethode, das Löschen eines Accounts, das Rotieren eines API-Schlüssels oder die Offenlegung eines Geheimnisses gehören.
Es lohnt sich, auch sensible Lesezugriffe in diese Überprüfung einzubeziehen. Die Anzeige eines API-Schlüssels oder von Wiederherstellungsdaten kann ebenso schwerwiegende Folgen haben wie deren Änderung.
Es gibt einige Aspekte, die in diesem Beispiel bewusst ausgelassen wurden. Der erste betrifft die Wiederherstellung des Accounts. So wie es hier beschrieben ist, bedeutet der Verlust des Zugriffs auf das bestätigte Mobiltelefon auch, dass keine Tickets mehr übertragen werden können. Eine echte Anwendung würde in der Regel Sicherheitscodes, einen vom Support unterstützten Wiederherstellungsprozess oder einen weiteren Wiederherstellungsfaktor erfordern.
Der zweite Schritt ist die Änderung der bestätigten Telefonnummer. Dies sollte in einem eigenen Ablauf erfolgen, der in der Regel durch eine Überprüfung anhand der bestehenden Nummer gesichert ist.
Die Anwendung sollte außerdem eigene Ratenbeschränkungen für ihre Endpunkte festlegen. Verify schützt zwar die API-Seite des Verifizierungsprozesses, verhindert jedoch nicht, dass jemand wiederholt Anfragen an Ihren Django-Endpunkt sendet.
Schließlich unterstützt Verify die stille Authentifizierung. Bei der stillen Authentifizierung kann eine Telefonnummer über den Mobilfunkanbieter verifiziert werden, ohne dass der Nutzer einen Code eingeben muss; SMS steht als Ausweichlösung zur Verfügung, falls eine stille Verifizierung nicht möglich ist. Der gesamte Anwendungsablauf bleibt ähnlich, aber die Benutzerinteraktion kann reibungsloser verlaufen.
Der vollständige Quellcode ist verfügbar unter GitHub.
Zusammenfassung
In diesem Tutorial wurde ein mehrstufiger Authentifizierungsablauf in Django von Grund auf erstellt. Die fertige Implementierung umfasst Folgendes:
Die Vonage Verify API übernimmt die Codegenerierung, die Zustellung, die Gültigkeitsdauer, die Wiederholungslimits und die Durchsetzung der Versuchsbeschränkungen. Die Anwendung muss lediglich eine Verifizierungsanfrage starten und anschließend den übermittelten Code überprüfen.
VerificationRequiredMixinmacht den Schutz jeder klassenbasierten Ansicht zu einer Einzeiler-Lösung. Es wird anLoginRequiredMixin, sodass anonyme Benutzer zur Anmeldung weitergeleitet und authentifizierte, aber noch nicht verifizierte Benutzer zum Verifizierungsablauf weitergeleitet werden.Sitzungsbasierter Status mit kurzer TTL beschränkt die Überprüfung auf ein einzelnes Gerät und ein Zeitfenster von fünf Minuten. Kein geräteübergreifender Datenverlust, keine langfristig gültigen Berechtigungen.
Eine Verifizierung, eine Aktion. Die Nutzung des „Step-up“-Zustands nach der Überweisung bedeutet, dass ein einzelner Code nicht unbemerkt jedes Ticket auf dem Account autorisieren kann.
Sichere Weiterleitungen, Sitzungsrotation und Sperrung von Telefonnummern schließen die häufigsten Sicherheitslücken bei der Implementierung: Open-Redirect-Angriffe, Session-Fixation und die Umleitung von Anmeldedaten.
47 Tests, keine echten SMS-Nachrichten. Die
http_error()Factory sorgt dafür, dass jeder Fehlerzweig durch das Einfügen einer gefälschten HTTP-Ausnahme getestet wird, wodurch die Testsuite schnell und ohne Zugangsdaten auskommt.
Haben Sie eine Frage oder möchten Sie uns mitteilen, was Sie gerade bauen?
Abonnieren Sie den Entwickler-Newsletter
Folgen Sie uns auf X (früher Twitter) für Updates
Lieber ein Video? Schau dir Tutorials auf unserem YouTube-Kanal an
Verbinden Sie sich mit uns auf der Vonage Entwickler-Seite auf LinkedIn
Helfen Sie uns, unsere Entwicklererfahrung zu verbessern, indem Sie unser Feedback-Formular „Voice of the Developer“ aus.
Bleiben Sie auf dem Laufenden und halten Sie sich über die neuesten Nachrichten, Tipps und Veranstaltungen für Entwickler auf dem Laufenden.
Teilen Sie:
Benjamin Aronov ist ein Entwickler-Befürworter bei Vonage. Er ist ein bewährter Community Builder mit einem Hintergrund in Ruby on Rails. Benjamin genießt die Strände von Tel Aviv, das er sein Zuhause nennt. Von Tel Aviv aus kann er einige der besten Startup-Gründer der Welt treffen und von ihnen lernen. Außerhalb der Tech-Branche reist Benjamin gerne um die Welt auf der Suche nach dem perfekten Pain au Chocolat.