Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • hansegucker/AlekSIS-Core
  • pinguin/AlekSIS-Core
  • AlekSIS/official/AlekSIS-Core
  • sunweaver/AlekSIS-Core
  • sggua/AlekSIS-Core
  • edward/AlekSIS-Core
  • magicfelix/AlekSIS-Core
7 results
Show changes
Commits on Source (13)
Showing
with 481 additions and 143 deletions
......@@ -29,6 +29,7 @@ Added
* GraphQL queries and mutations for core data management
* [Dev] Introduce new mechanism to register classes over all apps.
* Data template for `room` model used for haystack search indexing moved to core.
* Support for two factor authentication via email codes and Webauthn.
Changed
~~~~~~~
......@@ -49,6 +50,8 @@ Changed
* Get dashboard widgets and data checks from apps with new registration mechanism.
* Use write-through cache for sessions to retain on clear_cache
* Better error page with redirect option to login page when user has no permission to access a route.
* Users now can setup as many 2FA devices as they want.
* The 2FA profile overview was completely redesigned.
Fixed
~~~~~
......
......@@ -5,7 +5,11 @@
>
<h1 class="text-h2">{{ $t(shortErrorMessageKey) }}</h1>
<div>{{ $t(longErrorMessageKey) }}</div>
<v-btn color="secondary" :to="{ name: redirectRouteName }" v-if="!hideButton">
<v-btn
color="secondary"
:to="{ name: redirectRouteName }"
v-if="!hideButton"
>
<v-icon left>{{ redirectButtonIcon }}</v-icon>
{{ $t(redirectButtonTextKey) }}
</v-btn>
......
<template>
<div>
<h1 class="mb-4">{{ $t("accounts.two_factor.title") }}</h1>
<div v-if="$apollo.queries.twoFactor.loading">
<v-skeleton-loader type="card,card"></v-skeleton-loader>
</div>
<div v-else-if="twoFactor && twoFactor.activated">
<v-card class="mb-4">
<v-card-title>
{{ $t("accounts.two_factor.primary_device_title") }}
</v-card-title>
<v-card-text>
{{ $t("accounts.two_factor.primary_device_description") }}
</v-card-text>
<v-list three-line>
<two-factor-device :device="twoFactor.defaultDevice" primary />
</v-list>
</v-card>
<v-card class="mb-4">
<v-card-title>
{{ $t("accounts.two_factor.other_devices_title") }}
</v-card-title>
<v-card-text>
{{ $t("accounts.two_factor.other_devices_description") }}
</v-card-text>
<v-list three-line>
<div v-for="(device, index) in twoFactor.otherDevices" :key="index">
<two-factor-device :device="device" />
<v-divider />
</div>
<two-factor-device-base icon="mdi-backup-restore">
<template #title>
{{ $t("accounts.two_factor.backup_codes_title") }}
</template>
<template #subtitles>
<v-list-item-subtitle>
{{ $t("accounts.two_factor.backup_codes_description") }}
</v-list-item-subtitle>
<v-list-item-subtitle>
{{
$tc(
"accounts.two_factor.backup_codes_count",
twoFactor.backupTokensCount,
{ counter: twoFactor.backupTokensCount }
)
}}
</v-list-item-subtitle>
</template>
<template #action>
<v-btn icon :to="{ name: 'core.twoFactor.backupTokens' }">
<v-icon>mdi-chevron-right</v-icon>
</v-btn>
</template>
</two-factor-device-base>
</v-list>
<v-card-actions>
<v-btn text color="primary" :to="{ name: 'core.twoFactor.add' }">
<v-icon left>mdi-key-plus</v-icon>
{{ $t("accounts.two_factor.add_authentication_method") }}
</v-btn>
</v-card-actions>
</v-card>
<v-card class="mb-4">
<v-card-title>{{
$t("accounts.two_factor.disable_title")
}}</v-card-title>
<v-card-text>
{{ $t("accounts.two_factor.disable_description") }}
</v-card-text>
<v-card-actions>
<v-btn
color="red"
class="white--text"
:to="{ name: 'core.twoFactor.disable' }"
>
<v-icon left>mdi-power</v-icon>
{{ $t("accounts.two_factor.disable_button") }}
</v-btn>
</v-card-actions>
</v-card>
</div>
<div v-else>
<v-card class="mb-4">
<v-card-title>
{{ $t("accounts.two_factor.enable_title") }}
</v-card-title>
<v-card-text>
{{ $t("accounts.two_factor.enable_description") }}
</v-card-text>
<v-card-actions>
<v-btn
color="green"
class="white--text"
:to="{ name: 'core.twoFactor.setup' }"
>
<v-icon left>mdi-power</v-icon>
{{ $t("accounts.two_factor.enable_button") }}
</v-btn>
</v-card-actions>
</v-card>
</div>
</div>
</template>
<script>
import gqlTwoFactor from "./twoFactor.graphql";
import TwoFactorDevice from "./TwoFactorDevice.vue";
import TwoFactorDeviceBase from "./TwoFactorDeviceBase.vue";
export default {
name: "TwoFactor",
components: { TwoFactorDeviceBase, TwoFactorDevice },
apollo: {
twoFactor: {
query: gqlTwoFactor,
fetchPolicy: "network-only",
},
},
};
</script>
<template>
<two-factor-device-base :icon="icon">
<template #title>{{ device.methodVerboseName }}</template>
<template #subtitles>
<v-list-item-subtitle>
{{ $t("accounts.two_factor.methods." + device.methodCode) }}
</v-list-item-subtitle>
<v-list-item-subtitle
v-if="device.methodCode === 'call' || device.methodCode === 'sms'"
class="black--text"
>
{{ device.verboseName }}
<v-icon class="ml-1" color="green" small v-if="device.confirmed">
mdi-check-circle-outline
</v-icon>
</v-list-item-subtitle>
</template>
<template #action>
<!-- <v-btn icon color="red" v-if="!primary">-->
<!-- <v-icon>mdi-delete</v-icon>-->
<!-- </v-btn>-->
</template>
</two-factor-device-base>
</template>
<script>
import TwoFactorDeviceBase from "./TwoFactorDeviceBase.vue";
const iconMap = {
sms: "mdi-message-text-outline",
call: "mdi-phone-outline",
webauthn: "mdi-key-outline",
email: "mdi-email-outline",
yubikey: "mdi-key",
};
export default {
name: "TwoFactorDevice",
components: { TwoFactorDeviceBase },
computed: {
icon() {
if (this.device && this.device.methodCode in iconMap) {
return iconMap[this.device.methodCode];
}
return "mdi-two-factor-authentication";
},
},
props: {
device: {
type: Object,
required: true,
},
primary: {
type: Boolean,
default: false,
},
},
};
</script>
<template>
<v-list-item>
<v-list-item-icon>
<v-icon color="grey darken-2" x-large>{{ icon }}</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="font-weight-medium"
><slot name="title"
/></v-list-item-title>
<slot name="subtitles" />
</v-list-item-content>
<v-list-item-action>
<slot name="action" />
</v-list-item-action>
</v-list-item>
</template>
<script>
export default {
name: "TwoFactorDeviceBase",
props: {
icon: {
type: String,
required: true,
},
},
};
</script>
{
twoFactor {
activated
backupTokensCount
defaultDevice {
persistentId
name
confirmed
action
verboseAction
verboseName
methodCode
methodVerboseName
}
otherDevices {
persistentId
name
confirmed
action
verboseAction
verboseName
methodCode
methodVerboseName
}
}
}
......@@ -45,7 +45,30 @@
"menu_title": "Third-party Accounts"
},
"two_factor": {
"menu_title": "2FA"
"menu_title": "2FA",
"title": "Two-Factor Authentication",
"primary_device_title": "Primary Authentication Device",
"primary_device_description": "While logging in, AlekSIS will ask you to confirm the login with the following device. If this device is not available, you can use a backup device.",
"other_devices_title": "Other Authentication Devices",
"other_devices_description": "If your primary authentication device is not available during logging in, you can use one of these devices:",
"methods": {
"generator": "You generate one-time codes using a code generator.",
"email": "We will send you one-time codes to your e-mail address.",
"sms": "We will send you one-time codes to your mobile phone number.",
"call": "We will call you at your mobile phone and tell you a one-time code.",
"webauthn": "You use a security key (either as external device or integrated in your personal device).",
"yubikey": "You use a Yubikey to generate one-time codes."
},
"add_authentication_method": "Add Authentication Method",
"backup_codes_title": "Backup Codes",
"backup_codes_description": "If you can't use any of your devices, you can access your account using backup codes.",
"backup_codes_count": "You have no backup codes remaining.|You have only one backup code remaining.|You have {counter} backup codes remaining.",
"disable_title": "Disable Two-Factor Authentication",
"disable_description": "However we strongly discourage you to do so, you can also disable two-factor authentication for your account.",
"disable_button": "Disable Two-Factor Authentication",
"enable_title": "Two-Factor Authentication Currently Disabled",
"enable_description": "Two-factor authentication is not enabled for your account. Enable two-factor authentication for enhanced account security.",
"enable_button": "Enable Two-Factor Authentication"
}
},
"actions": {
......
......@@ -729,10 +729,7 @@ const routes = [
},
{
path: "/account/two_factor/",
component: () => import("./components/LegacyBaseTemplate.vue"),
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
component: () => import("./components/two_factor/TwoFactor.vue"),
name: "core.twoFactor",
meta: {
inAccountMenu: true,
......@@ -749,6 +746,14 @@ const routes = [
},
name: "core.twoFactor.setup",
},
{
path: "/account/two_factor/add/",
component: () => import("./components/LegacyBaseTemplate.vue"),
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
name: "core.twoFactor.add",
},
{
path: "/account/two_factor/qrcode/",
component: () => import("./components/LegacyBaseTemplate.vue"),
......
......@@ -24,6 +24,7 @@ from .person import PersonMutation, PersonType
from .school_term import SchoolTermType # noqa
from .search import SearchResultType
from .system_properties import SystemPropertiesType
from .two_factor import TwoFactorType
from .user import UserType
......@@ -56,6 +57,8 @@ class Query(graphene.ObjectType):
dynamic_routes = graphene.List(DynamicRouteType)
two_factor = graphene.Field(TwoFactorType)
def resolve_ping(root, info, payload) -> str:
return payload
......@@ -149,6 +152,11 @@ class Query(graphene.ObjectType):
return dynamic_routes
def resolve_two_factor(root, info, **kwargs):
if info.context.user.is_anonymous:
return None
return info.context.user
class Mutation(graphene.ObjectType):
update_person = PersonMutation.Field()
......
import graphene
from graphene import ObjectType
from two_factor.plugins.email.utils import mask_email
from two_factor.plugins.phonenumber.utils import (
backup_phones,
format_phone_number,
get_available_phone_methods,
mask_phone_number,
)
from two_factor.plugins.registry import registry
from two_factor.utils import default_device
class TwoFactorDeviceType(ObjectType):
persistent_id = graphene.ID()
name = graphene.String()
method_code = graphene.String()
verbose_name = graphene.String()
confirmed = graphene.Boolean()
method_verbose_name = graphene.String()
action = graphene.String()
verbose_action = graphene.String()
def get_method(root, info, **kwargs):
if getattr(root, "method", None):
return registry.get_method(root.method)
return registry.method_from_device(root)
def resolve_action(root, info, **kwargs):
method = TwoFactorDeviceType.get_method(root, info, **kwargs)
return method.get_action(root)
def resolve_verbose_action(root, info, **kwargs):
method = TwoFactorDeviceType.get_method(root, info, **kwargs)
return method.get_verbose_action(root)
def resolve_verbose_name(root, info, **kwargs):
method = TwoFactorDeviceType.get_method(root, info, **kwargs)
if method.code in ["sms", "call"]:
return mask_phone_number(format_phone_number(root.number))
elif method.code == "email":
email = root.email or root.user.email
if email:
return mask_email(email)
return method.verbose_name
def resolve_method_verbose_name(root, info, **kwargs):
method = TwoFactorDeviceType.get_method(root, info, **kwargs)
return method.verbose_name
def resolve_method_code(root, info, **kwargs):
method = TwoFactorDeviceType.get_method(root, info, **kwargs)
return method.code
class PhoneTwoFactorDeviceType(TwoFactorDeviceType):
number = graphene.String
class TwoFactorType(ObjectType):
activated = graphene.Boolean()
default_device = graphene.Field(TwoFactorDeviceType)
backup_phones = graphene.List(PhoneTwoFactorDeviceType)
other_devices = graphene.List(TwoFactorDeviceType)
backup_tokens_count = graphene.Int()
phone_methods_available = graphene.Boolean()
def resolve_backup_tokens_count(root, info, **kwargs):
try:
backup_tokens = root.staticdevice_set.all()[0].token_set.count()
except Exception:
backup_tokens = 0
return backup_tokens
def resolve_phone_methods_available(root, info, **kwargs):
return bool(get_available_phone_methods())
def resolve_default_device(root, info, **kwargs):
return default_device(root)
def resolve_activated(root, info, **kwargs):
return bool(default_device(root))
def resolve_other_devices(root, info, **kwargs):
main_device = TwoFactorType.resolve_default_device(root, info, **kwargs)
other_devices = []
for method in registry.get_methods():
other_devices += list(method.get_other_authentication_devices(root, main_device))
return other_devices
def resolve_backup_phones(root, info, **kwargs):
return backup_phones(root)
......@@ -122,6 +122,7 @@ INSTALLED_APPS = [
"html2text",
"django_otp.plugins.otp_totp",
"django_otp.plugins.otp_static",
"django_otp.plugins.otp_email",
"django_otp",
"otp_yubikey",
"aleksis.core",
......@@ -139,7 +140,10 @@ INSTALLED_APPS = [
"dynamic_preferences.users.apps.UserPreferencesConfig",
"impersonate",
"two_factor",
"two_factor.plugins.email",
"two_factor.plugins.phonenumber",
"two_factor.plugins.yubikey",
"two_factor.plugins.webauthn",
"material",
"ckeditor",
"ckeditor_uploader",
......@@ -756,6 +760,8 @@ if _settings.get("twilio.sid", None):
TWILIO_AUTH_TOKEN = _settings.get("twilio.token")
TWILIO_CALLER_ID = _settings.get("twilio.callerid")
TWO_FACTOR_WEBAUTHN_RP_NAME = _settings.get("2fa.webauthn.rp_name", "AlekSIS")
CELERY_BROKER_URL = _settings.get("celery.broker", REDIS_URL)
CELERY_RESULT_BACKEND = "django-db"
CELERY_CACHE_BACKEND = "django-cache"
......
{# -*- engine:django -*- #}
{% extends "two_factor/_base_focus.html" %}
{% load i18n phonenumber account socialaccount %}
{% load i18n phonenumber account socialaccount two_factor_tags %}
{% block browser_title %}
{% trans "Login" %}
{% endblock %}
{% block extra_head %}
{{ wizard.form.media.css }}
{% endblock %}
{% block content %}
{% get_providers as socialaccount_providers %}
......@@ -68,13 +72,19 @@
{% endblocktrans %}
{% elif device.method == 'sms' %}
{% blocktrans %}
We sent you a text message, please enter the tokens we
sent.
We sent you a text message, please enter the code we sent.
{% endblocktrans %}
{% elif device.method == 'email' %}
{% blocktrans %}
We sent you an email, please enter the code we sent.
{% endblocktrans %}
{% elif device.method == 'webauthn' %}
{% blocktrans %}
Please use your Webauthn-compatible device to authenticate.
{% endblocktrans %}
{% else %}
{% blocktrans %}
Please enter the tokens generated by your token
generator.
Please enter the code generated by your code generator.
{% endblocktrans %}
{% endif %}
{% elif wizard.steps.current == 'backup' %}
......@@ -108,12 +118,13 @@
<div class="card-content">
<div class="card-title">{% trans "Device currently not available?" %}</div>
{% if other_devices %}
<p>{% trans "Or, alternatively, use one of your backup phones:" %}</p>
<p>{% trans "Alternatively, use one of your other authentication methods:" %}</p>
<p>
{% for other in other_devices %}
<button name="challenge_device" value="{{ other.persistent_id }}" class="btn margin-bottom"
<button name="challenge_device" value="{{ other.persistent_id }}"
class="btn waves-effect waves-light margin-bottom"
type="submit">
{{ other|device_action }}
{{ other|as_action }}
</button>
{% endfor %}
</p>
......@@ -121,7 +132,7 @@
{% if backup_tokens %}
<p>{% trans "As a last resort, you can use a backup token:" %}</p>
<p>
<button name="wizard_goto_step" type="submit" value="backup" class="btn">
<button name="wizard_goto_step" type="submit" value="backup" class="btn waves-effect waves-light">
{% trans "Use Backup Token" %}
</button>
</p>
......@@ -143,4 +154,5 @@
</div>
</form>
{{ wizard.form.media.js }}
{% endblock %}
......@@ -7,18 +7,15 @@
<div class="card-content white-text">
<i class="material-icons small left">error_outline</i>
<span class="card-title">{% blocktrans %}Permission Denied{% endblocktrans %}</span>
<p>{% blocktrans %}The page you requested, enforces users to verify using
two-factor authentication for security reasons. You need to enable these
security features in order to access this page.{% endblocktrans %}</p>
<p>{% blocktrans %}The page you requested enforces users to verify using
two-factor authentication for security reasons. You need to enable this
security feature in order to access this page.{% endblocktrans %}</p>
<p>{% blocktrans %}Two-factor authentication is not enabled for your
account. Enable two-factor authentication for enhanced account
security.{% endblocktrans %}</p>
<p>
<a href="javascript:history.go(-1)" class="pull-right btn waves-effect waves-light">
{% trans "Go back" %}
</a>
<a href="{% url 'two_factor:setup' %}" class="btn green waves-effect waves-light">
<a href="{% url 'setup_two_factor_auth' %}" class="btn green waves-effect waves-light">
{% trans "Enable Two-Factor Authentication" %}</a>
</p>
</div>
......
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block extra_head %}
{{ wizard.form.media.css }}
{% endblock %}
{% block content %}
<h1>{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>
<h1>{% block title %}{% trans "Add Two-Factor Authentication Method" %}{% endblock %}</h1>
{% if wizard.steps.current == 'welcome' %}
<p class="flow-text">
{% blocktrans %}
You are about to take your account security to the
next level. Follow the steps in this wizard to enable two-factor
authentication.
next level. Follow the steps in this wizard to add
a two-factor authentication method to your account.
{% endblocktrans %}
</p>
{% elif wizard.steps.current == 'method' %}
......@@ -21,9 +25,9 @@
{% elif wizard.steps.current == 'generator' %}
<p>
{% blocktrans %}
To start using a token generator, please use your
favourite two factor authentication (TOTP) app to scan the QR code below.
Then, enter the token generated by the app.
To start using a code generator, please use your
favourite two-factor authentication (TOTP) app to scan the QR code below.
Then enter the token generated by the app.
{% endblocktrans %}
</p>
<p>
......@@ -43,6 +47,12 @@
This number will be validated in the next step.
{% endblocktrans %}
</p>
{% elif wizard.steps.current == 'email' %}
<p>
{% blocktrans %}
We sent you an email, please enter the token we sent.
{% endblocktrans %}
</p>
{% elif wizard.steps.current == 'validation' %}
{% if challenge_succeeded %}
{% if device.method == 'call' %}
......@@ -54,19 +64,19 @@
{% elif device.method == 'sms' %}
<p>
{% blocktrans %}
We sent you a text message, please enter the tokens we sent.
We sent you a text message, please enter the code we sent.
{% endblocktrans %}
</p>
{% endif %}
{% else %}
<p class="alert warning" role="alert">
<figure class="alert warning">
{% blocktrans %}
We've encountered an issue with the selected authentication method. Please
go back and verify that you entered your information correctly, try
again, or use a different authentication method instead. If the issue
persists, contact the site administrator.
{% endblocktrans %}
</p>
</figure>
{% endif %}
{% elif wizard.steps.current == 'yubikey' %}
<p>
......@@ -90,4 +100,6 @@
{% include "two_factor/_wizard_actions.html" %}
</form>
{{ wizard.form.media.js }}
{% endblock %}
......@@ -8,14 +8,12 @@
{% block content %}
<h1>{% block title %}{% trans "Two-Factor Authentication successfully enabled" %}{% endblock %}</h1>
<div class="alert success">
<p>
<figure class="alert success">
<i class="material-icons iconify left" data-icon="mdi:check-circle-outline"></i>
{% blocktrans %}
Congratulations, you've successfully enabled two-factor authentication.
{% endblocktrans %}
</p>
</div>
</figure>
{% if not phone_methods %}
<a href="{% url 'two_factor:profile' %}"
......@@ -28,16 +26,14 @@
{% trans "Generate backup codes" %}
</a>
{% else %}
<div class="warning">
<p>
<figure class="alert warning">
<i class="material-icons iconify left" data-icon="mdi:alert-outline"></i>
{% blocktrans %}
However, it might happen that you don't have access to
your primary token device. To enable account recovery, generate backup codes
or add a phone number.
your primary device. To enable account recovery, generate backup codes
or add other authentication methods.
{% endblocktrans %}
</p>
</div>
</figure>
<a href="{% url 'two_factor:profile' %}"
class="btn btn-primary waves-effect waves-light">
<i class="material-icons iconify left" data-icon="mdi:arrow-left"></i>
......@@ -47,9 +43,9 @@
<i class="material-icons iconify left" data-icon="mdi:key-outline"></i>
{% trans "Generate backup codes" %}
</a>
<a href="{% url 'two_factor:phone_create' %}" class="btn green waves-effect waves-light">
<i class="material-icons iconify left" data-icon="mdi:phone-plus"></i>
{% trans "Add Phone Number" %}
<a href="{% url 'setup_two_factor_auth' %}" class="btn green waves-effect waves-light">
<i class="material-icons iconify left" data-icon="mdi:key-plus"></i>
{% trans "Add Another Authentication Method" %}
</a>
{% endif %}
......
{% extends "two_factor/_base_focus.html" %}
{% load i18n phonenumber %}
{% block browser_title %}
{% trans "Account Security" %}
{% endblock %}
{% block content %}
<h1>
{% block title %}{% trans "Account Security" %}{% endblock %}
</h1>
{% if default_device %}
{% if default_device_type == 'TOTPDevice' %}
<p>{% trans "Tokens will be generated by your token generator." %}</p>
{% elif default_device_type == 'PhoneDevice' %}
<p>{% blocktrans with primary=default_device|device_action %}Primary method: {{ primary }}{% endblocktrans %}</p>
{% elif default_device_type == 'RemoteYubikeyDevice' %}
<p>{% blocktrans %}Tokens will be generated by your YubiKey.{% endblocktrans %}</p>
{% endif %}
{% if available_phone_methods %}
<h2>{% trans "Backup Phone Numbers" %}</h2>
<p>{% blocktrans %}If your primary method is not available, we are able to
send backup tokens to the phone numbers listed below.{% endblocktrans %}</p>
<ul class="collection">
{% for phone in backup_phones %}
<li class="collection-item">
{{ phone|device_action }}
<form method="post" action="{% url 'two_factor:phone_delete' phone.id %}"
onsubmit="return confirm('Are you sure?')">
{% csrf_token %}
<button class="btn-flat red-text waves-effect waves-red" type="submit">{% trans "Unregister" %}</button>
</form>
</li>
{% endfor %}
</ul>
<p>
<a href="{% url 'two_factor:phone_create' %}" class="btn green waves-effect waves-light">
<i class="material-icons iconify left" data-icon="mdi:phone-plus"></i>
{% trans "Add Phone Number" %}
</a>
</p>
{% endif %}
<h2>{% trans "Backup Tokens" %}</h2>
<p>
{% blocktrans %}If you don't have any device with you, you can access
your account using backup tokens.{% endblocktrans %}
{% blocktrans count counter=backup_tokens %}
You have only one backup token remaining.
{% plural %}
You have {{ counter }} backup tokens remaining.
{% endblocktrans %}
</p>
<p>
<a href="{% url 'two_factor:backup_tokens' %}" class="btn primary waves-effect waves-light">
<i class="material-icons iconify left" data-icon="mdi:key-outline"></i>
{% trans "Show Codes" %}
</a>
</p>
<h2>{% trans "Disable Two-Factor Authentication" %}</h2>
<p>
{% blocktrans %}
However we strongly discourage you to do so, you can
also disable two-factor authentication for your account.
{% endblocktrans %}
</p>
<p>
<a class="btn red waves-effect waves-light" href="{% url 'two_factor:disable' %}">
<i class="material-icons iconify left" data-icon="mdi:power"></i>
{% trans "Disable Two-Factor Authentication" %}
</a>
</p>
{% else %}
<p class="flow-text">
{% blocktrans %}
Two-factor authentication is not enabled for your
account. Enable two-factor authentication for enhanced account
security.
{% endblocktrans %}
</p>
<p>
<a href="{% url 'two_factor:setup' %}" class="green btn waves-effect waves-light ">
<i class="material-icons iconify left" data-icon="mdi:key-outline"></i>
{% trans "Enable Two-Factor Authentication" %}
</a>
</p>
{% endif %}
{% endblock %}
......@@ -81,6 +81,12 @@ urlpatterns = [
path("invitations/", include("invitations.urls")),
path("status/", views.SystemStatus.as_view(), name="system_status"),
path("", include(tf_urls)),
path("account/login/", views.TwoFactorLoginView.as_view()),
path(
"account/two_factor/add/",
views.TwoFactorSetupView.as_view(),
name="setup_two_factor_auth",
),
path("school_terms/", views.SchoolTermListView.as_view(), name="school_terms"),
path(
"school_terms/create/",
......
......@@ -58,6 +58,8 @@ from oauth2_provider.views import AuthorizationView
from reversion import set_user
from reversion.views import RevisionMixin
from rules.contrib.views import PermissionRequiredMixin, permission_required
from two_factor import views as two_factor_views
from two_factor.utils import devices_for_user
from two_factor.views.core import LoginView as AllAuthLoginView
from aleksis.core.data_checks import DataCheck, check_data
......@@ -1513,3 +1515,31 @@ class CustomAuthorizationView(AuthorizationView):
context = super().get_context_data(**kwargs)
context["no_menu"] = True
return context
class TwoFactorSetupView(two_factor_views.SetupView):
def get(self, request, *args, **kwargs):
return super(two_factor_views.SetupView, self).get(request, *args, **kwargs)
def get_device(self, **kwargs):
device = super().get_device(**kwargs)
# Ensure that the device is named "backup" if it is a phone device
# to ensure compatibility with django_two_factor_auth
method = self.get_method()
if device and method.code in ("call", "sms"):
device.name = "backup"
return device
class TwoFactorLoginView(two_factor_views.LoginView):
def get_devices(self):
user = self.get_user()
return devices_for_user(user)
def get_other_devices(self, main_device):
other_devices = self.get_devices()
other_devices = list(filter(lambda x: not isinstance(x, type(main_device)), other_devices))
return other_devices
......@@ -242,9 +242,11 @@ export default defineConfig({
if (response.ok) {
return response;
}
throw new Error(`${response.status} ${response.statusText}`);
throw new Error(
`${response.status} ${response.statusText}`
);
},
}
},
],
},
},
......
......@@ -35,11 +35,6 @@ Example configuration file::
[maintenance]
debug = true
# Two factor authentication with yubikey enabled, optional
[2fa]
enabled = true
yubikey = { enabled = true }
# Authentication via LDAP, optional
[ldap]
uri = "ldaps://ldap.myschool.edu"
......