Skip to content

Commit eb9e843

Browse files
authored
feat: add support for MFA using TOTP and recovery codes (#109)
1 parent 1ea55b1 commit eb9e843

File tree

15 files changed

+401
-46
lines changed

15 files changed

+401
-46
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ defaults for new projects.
2222
## Features
2323

2424
- 📱 Mobile-friendly design
25-
- 💄 Configurable themes
25+
- 💄 [Configurable themes](https://daisyui.com/docs/themes/)
26+
- 🕵️ Support for [Allauth User Sessions](https://docs.allauth.org/en/latest/usersessions/index.html)
27+
- 📱Support for [Multi-Factor Authentication](https://docs.allauth.org/en/latest/mfa/index.html)
2628
- 🗣️ Translations
2729
- 🇪🇸 Spanish
2830
- 🇫🇷 French

allauth_ui/templates/allauth/layouts/base.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{# djlint:off H006 #}
12
{% load i18n %}
23
{% load static %}
34
{% load allauth_ui %}

allauth_ui/templates/components/form.html

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
{% load widget_tweaks slippers %}
1+
{% load slippers %}
2+
{% load allauth_ui %}
3+
{% load widget_tweaks %}
24
{% var render_fields=render_fields|default:"true" %}
35
{% var use_default_button=use_default_button|default:"true" %}
46
<form class="py-3" method="post" action="{{ url }}">
@@ -8,17 +10,8 @@
810
{% if render_fields == "true" %}
911
{% for field in form.visible_fields %}
1012
{% if field.name != "remember" %}
11-
<label class="label" for="{{ field.id_for_label }}">
12-
<span class="label-text">{{ field.label }}</span>
13-
</label>
14-
{% if field.errors %}
15-
{% render_field field placeholder="" class="w-full input input-bordered text-primary input-error" %}
16-
{% else %}
17-
{% render_field field placeholder="" class="w-full input input-bordered text-primary" %}
18-
{% endif %}
19-
{% for error in field.errors %}
20-
<span class="flex items-center max-w-xs mt-1 ml-1 text-xs font-medium tracking-wide text-error">{{ error }}</span>
21-
{% endfor %}
13+
{% #form_field field=field %}
14+
{% /form_field %}
2215
{% endif %}
2316
{% endfor %}
2417
{% endif %}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{% load widget_tweaks %}
2+
<label class="label" for="{{ field.id_for_label }}">
3+
<span class="label-text">{{ field.label }}</span>
4+
</label>
5+
{% if field.errors %}
6+
{% render_field field placeholder="" class="w-full input input-bordered text-primary input-error" %}
7+
{% else %}
8+
{% render_field field placeholder="" class="w-full input input-bordered text-primary" %}
9+
{% endif %}
10+
{% for error in field.errors %}
11+
<span class="flex items-center max-w-xs mt-1 ml-1 text-xs font-medium tracking-wide text-error">{{ error }}</span>
12+
{% endfor %}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{% extends "mfa/index.html" %}
2+
{% load i18n %}
3+
{% load allauth_ui %}
4+
{% block content %}
5+
{% trans "Two-Factor Authentication" as heading %}
6+
{% #container heading=heading %}
7+
{% if "totp" in MFA_SUPPORTED_TYPES %}
8+
<h2 class="mt-6 mb-3 text-lg">{% translate "Authenticator App" %}</h2>
9+
{% if authenticators.totp %}
10+
<p class="my-2">
11+
{% translate "Authentication using an authenticator app is active." %}
12+
<div>
13+
<a href="{% url "mfa_activate_totp" %}"
14+
role="button"
15+
class="my-3 btn btn-warning">{% translate "Deactivate" %}</a>
16+
</div>
17+
</p>
18+
{% else %}
19+
<p class="my-2">{% translate "An authenticator app is not active." %}</p>
20+
<a href="{% url "mfa_activate_totp" %}"
21+
role="button"
22+
class="btn btn-neutral">{% translate "Activate" %}</a>
23+
{% endif %}
24+
{% endif %}
25+
{% if "webauthn" in MFA_SUPPORTED_TYPES %}
26+
<div class="divider divider-neutral"></div>
27+
<h2 class="mt-6 mb-3 text-lg">{% translate "Security Keys" %}</h2>
28+
{% if authenticators.webauthn|length %}
29+
<p class="my-2">
30+
{% blocktranslate count count=authenticators.webauthn|length %}You have added {{ count }} security key.{% plural %}You have added {{ count }} security keys.{% endblocktranslate %}
31+
</p>
32+
<a href="{% url "mfa_list_webauthn" %}"
33+
role="button"
34+
class="btn btn-neutral">{% translate "Manage" %}</a>
35+
{% else %}
36+
<p class="my-2">{% translate "No security keys have been added." %}</p>
37+
<a href="{% url "mfa_add_webauthn" %}"
38+
role="button"
39+
class="btn btn-neutral">{% translate "Add" %}</a>
40+
{% endif %}
41+
{% endif %}
42+
{% if "recovery_codes" in MFA_SUPPORTED_TYPES %}
43+
<div class="divider divider-neutral"></div>
44+
{% with total_count=authenticators.recovery_codes.generate_codes|length unused_count=authenticators.recovery_codes.get_unused_codes|length %}
45+
<h2 class="mt-6 mb-3 text-lg">{% translate "Recovery Codes" %}</h2>
46+
<p>
47+
{% if authenticators.recovery_codes %}
48+
{% blocktranslate count unused_count=unused_count %}There is {{ unused_count }} out of {{ total_count }} recovery codes available.{% plural %}There are {{ unused_count }} out of {{ total_count }} recovery codes available.{% endblocktranslate %}
49+
{% else %}
50+
{% translate "No recovery codes set up." %}
51+
{% endif %}
52+
</p>
53+
{% if is_mfa_enabled %}
54+
<div class="flex flex-col justify-evenly my-3 md:flex-row gap-1">
55+
{% if authenticators.recovery_codes %}
56+
{% if unused_count > 0 %}
57+
<a href="{% url "mfa_view_recovery_codes" %}"
58+
role="button"
59+
class="btn btn-neutral">{% translate "View" %}</a>
60+
<a href="{% url "mfa_download_recovery_codes" %}"
61+
role="button"
62+
class="btn btn-neutral">{% translate "Download" %}</a>
63+
{% endif %}
64+
{% endif %}
65+
<a href="{% url "mfa_generate_recovery_codes" %}"
66+
role="button"
67+
class="btn btn-neutral">{% translate "Generate" %}</a>
68+
</div>
69+
{% endif %}
70+
{% endwith %}
71+
{% endif %}
72+
{% /container %}
73+
{% endblock content %}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{% extends "mfa/recovery_codes/index.html" %}
2+
{% load i18n %}
3+
{% load allauth %}
4+
{% load allauth_ui %}
5+
{% block content %}
6+
{% translate "Recovery Codes" as heading %}
7+
{% blocktranslate asvar subheading %}You are about to generate a new set of recovery codes for your account.{% endblocktranslate %}
8+
{% #container heading=heading subheading=subheading %}
9+
<p class="py-3">
10+
{% if unused_code_count %}
11+
{% blocktranslate %}This action will invalidate your existing codes.{% endblocktranslate %}
12+
{% endif %}
13+
{% blocktranslate %}Are you sure?{% endblocktranslate %}
14+
</p>
15+
{% url 'mfa_generate_recovery_codes' as action_url %}
16+
{% trans "Generate" as button_text %}
17+
{% #form url=action_url form=form button_text=button_text %}
18+
{% csrf_token %}
19+
{% /form %}
20+
{% /container %}
21+
{% endblock content %}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{% extends "mfa/recovery_codes/index.html" %}
2+
{% load i18n %}
3+
{% load allauth %}
4+
{% load allauth_ui %}
5+
{% block content %}
6+
{% translate "Recovery Codes" as heading %}
7+
{% blocktranslate asvar subheading count unused_count=unused_codes|length %}There is {{ unused_count }} out of {{ total_count }} recovery codes available.{% plural %}There are {{ unused_count }} out of {{ total_count }} recovery codes available.{% endblocktranslate %}
8+
{% #container heading=heading subheading=subheading %}
9+
<div class="my-3">
10+
<label class="label" for="codes">
11+
<span class="label-text">{% translate "Unused codes" %}</span>
12+
</label>
13+
<textarea id="codes" class="w-full m-3 mx-auto textarea" rows="{{unused_codes|length}}" disabled readonly>
14+
{% for code in unused_codes %}{{ code }}
15+
{% endfor %}
16+
</textarea>
17+
</div>
18+
<div class="flex flex-col mx-auto sm:w-8/12 xl:w-7/12">
19+
{% if unused_codes %}
20+
<a href="{% url "mfa_download_recovery_codes" %}"
21+
class="md:w-full btn btn-neutral">{% trans "Download codes" %}</a>
22+
{% endif %}
23+
<a href="{% url "mfa_generate_recovery_codes" %}"
24+
class="mt-4 md:w-full btn btn-neutral">{% trans "Generate new codes" %}</a>
25+
</div>
26+
{% /container %}
27+
{% endblock content %}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{# djlint:off H006 #}
2+
{% extends "mfa/totp/activate_form.html" %}
3+
{% load allauth %}
4+
{% load allauth_ui %}
5+
{% load i18n %}
6+
{% block content %}
7+
{% translate "Activate Authenticator App" as heading %}
8+
{% blocktranslate asvar subheading %}To protect your account with two-factor authentication, scan the QR code below with your authenticator app. Then, input the verification code generated by the app below.{% endblocktranslate %}
9+
{% #container heading=heading subheading=subheading %}
10+
{% translate "Activate" as button_text %}
11+
{% url 'mfa_activate_totp' as action_url %}
12+
{% #form form=form method="post" url=action_url button_text=button_text render_fields="false" %}
13+
<img src="{{ totp_svg_data_uri }}"
14+
alt="{{ form.secret }}"
15+
class="mx-auto my-5" />
16+
{% #form_field field=form.code %}
17+
{% /form_field %}
18+
<div class="my-3">
19+
<label class="label" for="authenticator_secret">
20+
<span class="label-text">{% translate "Authenticator secret" %}</span>
21+
</label>
22+
<p class="text-xs mb-2">
23+
{% translate "You can store this secret and use it to reinstall your authenticator app at a later time." %}
24+
</p>
25+
<input type="text" id="authenticator_secret"" value="{{ form.secret }}" disabled
26+
class="w-full input input-bordered text-primary"/>
27+
</div>
28+
{% csrf_token %}
29+
{% /form %}
30+
{% /container %}
31+
{% endblock content %}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{% extends "mfa/totp/deactivate_form.html" %}
2+
{% load i18n %}
3+
{% load allauth_ui %}
4+
{% block content %}
5+
{% trans "Deactivate Authenticator App" as heading %}
6+
{% blocktranslate asvar subheading %}You are about to deactivate authenticator app based authentication. Are you sure?{% endblocktranslate %}
7+
{% #container heading=heading subheading=subheading %}
8+
{% url 'mfa_deactivate_totp' as action_url %}
9+
{% #form form=form url=action_url button_text=button_text use_default_button="false"%}
10+
{% csrf_token %}
11+
<button type="submit" class="btn btn-warning">{% trans "Deactivate" %}</button>
12+
{% /form %}
13+
{% /container %}
14+
{% endblock content %}

allauth_ui/templates/usersessions/usersession_list.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<h1 class="mb-3 text-2xl">{{ heading }}</h1>
1010
{% if session_count > 1 %}
1111
{% url 'usersessions_list' as action_url %}
12-
{% translate "Sign Out Other Sessions" as button_text %}
12+
{% translate "Sign Out Other Sessions" as button_text %}
1313
{% else %}
1414
{% url 'account_logout' as action_url %}
1515
{% translate "Sign Out" as button_text %}

0 commit comments

Comments
 (0)