1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/accounts/utils.py
catborise c42f74b4ee fix(accounts): an OTP mail links to the QR code instead of holding its secret
The Lost OTP? page, open without a login, and the admin's Email OTP QR
code mailed the QR code itself: the TOTP secret then stayed in the
mailbox and on every server the mail passed. The mail now carries a
signed link, good for an hour, to a page that shows the QR code (not
cached); the page shows nothing for an old or altered link or a user
without a device. tr and zh too.
2026-11-05 18:17:00 +03:00

82 lines
2.8 KiB
Python

import base64
import binascii
import struct
from decimal import Decimal
import segno
from django.core.mail import send_mail
from django.core.signing import TimestampSigner
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from django_otp import devices_for_user
from django_otp.plugins.otp_totp.models import TOTPDevice
def get_existing_totp_device(user):
"""The user's confirmed TOTP device, or None. Unlike get_user_totp_device
it never creates one, so an unauthenticated caller cannot turn a
password-only account into an OTP-required one."""
for device in devices_for_user(user, confirmed=True):
if isinstance(device, TOTPDevice):
return device
return None
def get_user_totp_device(user):
devices = devices_for_user(user)
for device in devices:
if isinstance(device, TOTPDevice):
return device
device = user.totpdevice_set.create()
return device
def validate_ssh_key(key):
# An OpenSSH public key is a type, a key and an optional comment, which may hold spaces
array = key.strip().encode().split(maxsplit=2)
if len(array) < 2:
return False
typeofkey = array[0]
string = array[1]
# must have only valid rsa-ssh key characters ie binascii characters
try:
data = base64.decodebytes(string)
except binascii.Error:
return False
# unpack the contents of data, from data[:4] , property of ssh key .
try:
str_len = struct.unpack(">I", data[:4])[0]
except struct.error:
return False
# data[4:str_len] must have string which matches with the typeofkey, another ssh key property.
if data[4 : 4 + str_len] != typeofkey:
return False
return True
# the link a mail carries to its user's OTP QR code: the mail holds no secret
OTP_LINK = TimestampSigner(salt="accounts.otp_qr")
OTP_LINK_MAX_AGE = 3600
def send_email_with_otp(request, user):
"""Mail the user a link to the page of their OTP QR code, good for
OTP_LINK_MAX_AGE: the QR code's secret is never in a mailbox."""
url = request.build_absolute_uri(reverse("accounts:otp_qr", args=[OTP_LINK.sign(str(user.pk))]))
send_mail(
_("OTP QR Code"),
_("Open this link within an hour to see the OTP QR code of account '%(user)s':\n\n%(url)s")
% {"user": user, "url": url},
None,
[user.email],
fail_silently=False,
)
def otp_qr_svg(device):
"""The device's QR code as inline SVG: 1.8 mm modules, error correction M, a 4-module border."""
qr = segno.make(device.config_url, error="m", mode="byte", encoding="utf-8", micro=False)
return mark_safe(qr.svg_inline(scale=Decimal("1.8"), unit="mm", border=4, dark="black", light="white"))