Move Factor instances to database
This commit is contained in:
parent
57e5996513
commit
59a15c988f
|
@ -17,6 +17,9 @@
|
|||
<li class="{% is_active 'passbook_admin:providers' 'passbook_admin:provider-create' 'passbook_admin:provider-update' 'passbook_admin:provider-delete' %}">
|
||||
<a href="{% url 'passbook_admin:providers' %}">{% trans 'Providers' %}</a>
|
||||
</li>
|
||||
<li class="{% is_active 'passbook_admin:factors' 'passbook_admin:factor-create' 'passbook_admin:factor-update' 'passbook_admin:factor-delete' %}">
|
||||
<a href="{% url 'passbook_admin:factors' %}">{% trans 'Factors' %}</a>
|
||||
</li>
|
||||
<li class="{% is_active 'passbook_admin:rules' 'passbook_admin:rule-create' 'passbook_admin:rule-update' 'passbook_admin:rule-delete' 'passbook_admin:rule-test' %}">
|
||||
<a href="{% url 'passbook_admin:rules' %}">{% trans 'Rules' %}</a>
|
||||
</li>
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load utils %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block title %}
|
||||
{% title %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>{% trans "Factors" %}</h1>
|
||||
<a href="{% url 'passbook_admin:factor-create' %}" class="btn btn-primary">
|
||||
{% trans 'Create...' %}
|
||||
</a>
|
||||
<hr>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'Name' %}</th>
|
||||
<th>{% trans 'Type' %}</th>
|
||||
<th>{% trans 'Order' %}</th>
|
||||
<th>{% trans 'Enabled?' %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for factor in object_list %}
|
||||
<tr>
|
||||
<td>{{ factor.name }} ({{ factor.slug }})</td>
|
||||
<td>{{ factor.type }}</td>
|
||||
<td>{{ factor.order }}</td>
|
||||
<td>{{ factor.enabled }}</td>
|
||||
<td>
|
||||
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:factor-update' pk=factor.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:factor-delete' pk=factor.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
{% get_links factor as links %}
|
||||
{% for name, href in links.items %}
|
||||
<a class="btn btn-default btn-sm" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -2,8 +2,9 @@
|
|||
from django.urls import include, path
|
||||
from rest_framework_swagger.views import get_swagger_view
|
||||
|
||||
from passbook.admin.views import (applications, audit, groups, invitations,
|
||||
overview, providers, rules, sources, users)
|
||||
from passbook.admin.views import (applications, audit, factors, groups,
|
||||
invitations, overview, providers, rules,
|
||||
sources, users)
|
||||
|
||||
schema_view = get_swagger_view(title='passbook Admin Internal API')
|
||||
|
||||
|
@ -38,6 +39,14 @@ urlpatterns = [
|
|||
providers.ProviderUpdateView.as_view(), name='provider-update'),
|
||||
path('providers/<int:pk>/delete/',
|
||||
providers.ProviderDeleteView.as_view(), name='provider-delete'),
|
||||
# Factors
|
||||
path('factors/', factors.FactorListView.as_view(), name='factors'),
|
||||
path('factors/create/',
|
||||
factors.FactorCreateView.as_view(), name='factor-create'),
|
||||
path('factors/<uuid:pk>/update/',
|
||||
factors.FactorUpdateView.as_view(), name='factor-update'),
|
||||
path('factors/<uuid:pk>/delete/',
|
||||
factors.FactorDeleteView.as_view(), name='factor-delete'),
|
||||
# Invitations
|
||||
path('invitations/', invitations.InvitationListView.as_view(), name='invitations'),
|
||||
path('invitations/create/',
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
"""passbook Factor administration"""
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
|
||||
|
||||
from passbook.admin.mixins import AdminRequiredMixin
|
||||
from passbook.core.forms.factor import FactorForm
|
||||
from passbook.core.models import Factor
|
||||
|
||||
|
||||
class FactorListView(AdminRequiredMixin, ListView):
|
||||
"""Show list of all factors"""
|
||||
|
||||
model = Factor
|
||||
template_name = 'administration/factor/list.html'
|
||||
ordering = 'order'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['types'] = {
|
||||
x.__name__: x._meta.verbose_name for x in Factor.__subclasses__()}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class FactorCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
|
||||
"""Create new Factor"""
|
||||
|
||||
template_name = 'generic/create.html'
|
||||
success_url = reverse_lazy('passbook_admin:factors')
|
||||
success_message = _('Successfully created Factor')
|
||||
form_class = FactorForm
|
||||
|
||||
|
||||
class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
|
||||
"""Update factor"""
|
||||
|
||||
model = Factor
|
||||
template_name = 'generic/update.html'
|
||||
success_url = reverse_lazy('passbook_admin:factors')
|
||||
success_message = _('Successfully updated Factor')
|
||||
form_class = FactorForm
|
||||
|
||||
|
||||
class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
|
||||
"""Delete factor"""
|
||||
|
||||
model = Factor
|
||||
template_name = 'generic/delete.html'
|
||||
success_url = reverse_lazy('passbook_admin:factors')
|
||||
success_message = _('Successfully updated Factor')
|
|
@ -4,8 +4,10 @@ from django.views.generic import FormView
|
|||
|
||||
from passbook.captcha_factor.forms import CaptchaForm
|
||||
from passbook.core.auth.factor import AuthenticationFactor
|
||||
from passbook.core.auth.factor_manager import MANAGER
|
||||
|
||||
|
||||
@MANAGER.factor()
|
||||
class CaptchaFactor(FormView, AuthenticationFactor):
|
||||
"""Simple captcha checker, logic is handeled in django-captcha module"""
|
||||
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
"""passbook core app config"""
|
||||
from importlib import import_module
|
||||
from logging import getLogger
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
from passbook.lib.config import CONFIG
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
class PassbookCoreConfig(AppConfig):
|
||||
"""passbook core app config"""
|
||||
|
@ -13,3 +17,10 @@ class PassbookCoreConfig(AppConfig):
|
|||
|
||||
def ready(self):
|
||||
import_module('passbook.core.rules')
|
||||
factors_to_load = CONFIG.y('passbook.factors', [])
|
||||
for factors_to_load in factors_to_load:
|
||||
try:
|
||||
import_module(factors_to_load)
|
||||
LOGGER.info("Loaded %s", factors_to_load)
|
||||
except ImportError as exc:
|
||||
LOGGER.debug(exc)
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
"""Authentication Factor Manager"""
|
||||
from logging import getLogger
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
class AuthenticationFactorManager:
|
||||
"""Manager to hold all Factors."""
|
||||
|
||||
__factors = []
|
||||
|
||||
def factor(self):
|
||||
"""Class decorator to register classes inline."""
|
||||
def inner_wrapper(cls):
|
||||
self.__factors.append(cls)
|
||||
LOGGER.debug("Registered factor '%s'", cls.__name__)
|
||||
return cls
|
||||
return inner_wrapper
|
||||
|
||||
@property
|
||||
def all(self):
|
||||
"""Get list of all registered factors"""
|
||||
return self.__factors
|
||||
|
||||
|
||||
MANAGER = AuthenticationFactorManager()
|
|
@ -8,13 +8,15 @@ from django.utils.translation import gettext as _
|
|||
from django.views.generic import FormView
|
||||
|
||||
from passbook.core.auth.factor import AuthenticationFactor
|
||||
from passbook.core.auth.mfa import MultiFactorAuthenticator
|
||||
from passbook.core.auth.factor_manager import MANAGER
|
||||
from passbook.core.auth.view import AuthenticationView
|
||||
from passbook.core.forms.authentication import AuthenticationBackendFactorForm
|
||||
from passbook.lib.config import CONFIG
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
@MANAGER.factor()
|
||||
class AuthenticationBackendFactor(FormView, AuthenticationFactor):
|
||||
"""Authentication factor which authenticates against django's AuthBackend"""
|
||||
|
||||
|
@ -34,7 +36,7 @@ class AuthenticationBackendFactor(FormView, AuthenticationFactor):
|
|||
if user:
|
||||
# User instance returned from authenticate() has .backend property set
|
||||
self.authenticator.pending_user = user
|
||||
self.request.session[MultiFactorAuthenticator.SESSION_USER_BACKEND] = user.backend
|
||||
self.request.session[AuthenticationView.SESSION_USER_BACKEND] = user.backend
|
||||
return self.authenticator.user_ok()
|
||||
# No user was found -> invalid credentials
|
||||
LOGGER.debug("Invalid credentials")
|
|
@ -2,10 +2,12 @@
|
|||
from logging import getLogger
|
||||
|
||||
from passbook.core.auth.factor import AuthenticationFactor
|
||||
from passbook.core.auth.factor_manager import MANAGER
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
@MANAGER.factor()
|
||||
class DummyFactor(AuthenticationFactor):
|
||||
"""Dummy factor for testing with multiple factors"""
|
||||
|
|
@ -1,20 +1,19 @@
|
|||
"""passbook multi-factor authentication engine"""
|
||||
from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import login
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.shortcuts import get_object_or_404, redirect, reverse
|
||||
from django.views.generic import View
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.core.models import Factor, User
|
||||
from passbook.core.views.utils import PermissionDeniedView
|
||||
from passbook.lib.utils.reflection import class_to_path, path_to_class
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class MultiFactorAuthenticator(UserPassesTestMixin, View):
|
||||
class AuthenticationView(UserPassesTestMixin, View):
|
||||
"""Wizard-like Multi-factor authenticator"""
|
||||
|
||||
SESSION_FACTOR = 'passbook_factor'
|
||||
|
@ -25,8 +24,6 @@ class MultiFactorAuthenticator(UserPassesTestMixin, View):
|
|||
pending_user = None
|
||||
pending_factors = []
|
||||
|
||||
factors = settings.AUTHENTICATION_FACTORS.copy()
|
||||
|
||||
_current_factor = None
|
||||
|
||||
# Allow only not authenticated users to login
|
||||
|
@ -34,29 +31,38 @@ class MultiFactorAuthenticator(UserPassesTestMixin, View):
|
|||
return self.request.user.is_authenticated is False
|
||||
|
||||
def handle_no_permission(self):
|
||||
# Function from UserPassesTestMixin
|
||||
if 'next' in self.request.GET:
|
||||
return redirect(self.request.GET.get('next'))
|
||||
return redirect(reverse('passbook_core:overview'))
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
# Extract pending user from session (only remember uid)
|
||||
if MultiFactorAuthenticator.SESSION_PENDING_USER in request.session:
|
||||
if AuthenticationView.SESSION_PENDING_USER in request.session:
|
||||
self.pending_user = get_object_or_404(
|
||||
User, id=self.request.session[MultiFactorAuthenticator.SESSION_PENDING_USER])
|
||||
User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER])
|
||||
else:
|
||||
# No Pending user, redirect to login screen
|
||||
return redirect(reverse('passbook_core:auth-login'))
|
||||
# Write pending factors to session
|
||||
if MultiFactorAuthenticator.SESSION_PENDING_FACTORS in request.session:
|
||||
self.pending_factors = request.session[MultiFactorAuthenticator.SESSION_PENDING_FACTORS]
|
||||
if AuthenticationView.SESSION_PENDING_FACTORS in request.session:
|
||||
self.pending_factors = request.session[AuthenticationView.SESSION_PENDING_FACTORS]
|
||||
else:
|
||||
self.pending_factors = self.factors.copy()
|
||||
# Get an initial list of factors which are currently enabled
|
||||
# and apply to the current user. We check rules here and block the request
|
||||
_all_factors = Factor.objects.filter(enabled=True)
|
||||
self.pending_factors = []
|
||||
for factor in _all_factors:
|
||||
if factor.passes(self.pending_user):
|
||||
self.pending_factors.append(_all_factors)
|
||||
# self.pending_factors = Factor
|
||||
# Read and instantiate factor from session
|
||||
factor_class = None
|
||||
if MultiFactorAuthenticator.SESSION_FACTOR not in request.session:
|
||||
if AuthenticationView.SESSION_FACTOR not in request.session:
|
||||
factor_class = self.pending_factors[0]
|
||||
else:
|
||||
factor_class = request.session[MultiFactorAuthenticator.SESSION_FACTOR]
|
||||
factor_class = request.session[AuthenticationView.SESSION_FACTOR]
|
||||
# Instantiate Next Factor and pass request
|
||||
factor = path_to_class(factor_class)
|
||||
self._current_factor = factor(self)
|
||||
self._current_factor.request = request
|
||||
|
@ -81,11 +87,11 @@ class MultiFactorAuthenticator(UserPassesTestMixin, View):
|
|||
next_factor = None
|
||||
if self.pending_factors:
|
||||
next_factor = self.pending_factors.pop()
|
||||
self.request.session[MultiFactorAuthenticator.SESSION_PENDING_FACTORS] = \
|
||||
self.request.session[AuthenticationView.SESSION_PENDING_FACTORS] = \
|
||||
self.pending_factors
|
||||
self.request.session[MultiFactorAuthenticator.SESSION_FACTOR] = next_factor
|
||||
self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor
|
||||
LOGGER.debug("Rendering Factor is %s", next_factor)
|
||||
return redirect(reverse('passbook_core:mfa'))
|
||||
return redirect(reverse('passbook_core:auth-process', kwargs={'factor': next_factor}))
|
||||
# User passed all factors
|
||||
LOGGER.debug("User passed all factors, logging in")
|
||||
return self._user_passed()
|
||||
|
@ -94,12 +100,12 @@ class MultiFactorAuthenticator(UserPassesTestMixin, View):
|
|||
"""Show error message, user cannot login.
|
||||
This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
|
||||
LOGGER.debug("User invalid")
|
||||
return redirect(reverse('passbook_core:mfa-denied'))
|
||||
return redirect(reverse('passbook_core:auth-denied'))
|
||||
|
||||
def _user_passed(self):
|
||||
"""User Successfully passed all factors"""
|
||||
# user = authenticate(request=self.request, )
|
||||
backend = self.request.session[MultiFactorAuthenticator.SESSION_USER_BACKEND]
|
||||
backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND]
|
||||
login(self.request, self.pending_user, backend=backend)
|
||||
LOGGER.debug("Logged in user %s", self.pending_user)
|
||||
# Cleanup
|
|
@ -0,0 +1,25 @@
|
|||
"""passbook administration forms"""
|
||||
from django import forms
|
||||
|
||||
from passbook.core.auth.factor_manager import MANAGER
|
||||
from passbook.core.models import Factor
|
||||
from passbook.lib.utils.reflection import class_to_path
|
||||
|
||||
|
||||
def get_factors():
|
||||
"""Return list of factors for Select Widget"""
|
||||
for factor in MANAGER.all:
|
||||
yield (class_to_path(factor), factor.__name__)
|
||||
|
||||
class FactorForm(forms.ModelForm):
|
||||
"""Form to create/edit Factors"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Factor
|
||||
fields = ['name', 'slug', 'order', 'rules', 'type', 'enabled']
|
||||
widgets = {
|
||||
'type': forms.Select(choices=get_factors()),
|
||||
'name': forms.TextInput(),
|
||||
'order': forms.NumberInput(),
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 2.1.7 on 2019-02-14 15:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0002_auto_20190208_1514'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Factor',
|
||||
fields=[
|
||||
('rulemodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.RuleModel')),
|
||||
('name', models.TextField()),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('order', models.IntegerField()),
|
||||
('type', models.TextField()),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('passbook_core.rulemodel',),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 2.1.7 on 2019-02-15 15:34
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0003_factor'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='factor',
|
||||
name='enabled',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='provider',
|
||||
field=models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='passbook_core.Provider'),
|
||||
),
|
||||
]
|
|
@ -64,6 +64,18 @@ class RuleModel(UUIDModel, CreatedUpdatedModel):
|
|||
return True
|
||||
|
||||
@reversion.register()
|
||||
class Factor(RuleModel):
|
||||
"""Authentication factor, multiple instances of the same Factor can be used"""
|
||||
|
||||
name = models.TextField()
|
||||
slug = models.SlugField(unique=True)
|
||||
order = models.IntegerField()
|
||||
type = models.TextField(unique=True)
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return "Factor %s" % self.slug
|
||||
|
||||
class Application(RuleModel):
|
||||
"""Every Application which uses passbook for authentication/identification/authorization
|
||||
needs an Application record. Other authentication types can subclass this Model to
|
||||
|
@ -73,7 +85,7 @@ class Application(RuleModel):
|
|||
slug = models.SlugField()
|
||||
launch_url = models.URLField(null=True, blank=True)
|
||||
icon_url = models.TextField(null=True, blank=True)
|
||||
provider = models.OneToOneField('Provider', null=True,
|
||||
provider = models.OneToOneField('Provider', null=True, blank=True,
|
||||
default=None, on_delete=models.SET_DEFAULT)
|
||||
skip_authorization = models.BooleanField(default=False)
|
||||
|
||||
|
|
|
@ -50,11 +50,6 @@ AUTHENTICATION_BACKENDS = [
|
|||
'django.contrib.auth.backends.ModelBackend',
|
||||
'passbook.oauth_client.backends.AuthorizedServiceBackend'
|
||||
]
|
||||
AUTHENTICATION_FACTORS = [
|
||||
'passbook.core.auth.backend_factor.AuthenticationBackendFactor',
|
||||
'passbook.core.auth.dummy.DummyFactor',
|
||||
'passbook.captcha_factor.factor.CaptchaFactor',
|
||||
]
|
||||
|
||||
# Application definition
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.contrib import admin
|
|||
from django.urls import include, path
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from passbook.core.auth import mfa
|
||||
from passbook.core.auth import view
|
||||
from passbook.core.views import authentication, overview, user
|
||||
from passbook.lib.utils.reflection import get_apps
|
||||
|
||||
|
@ -19,8 +19,9 @@ core_urls = [
|
|||
path('auth/login/', authentication.LoginView.as_view(), name='auth-login'),
|
||||
path('auth/logout/', authentication.LogoutView.as_view(), name='auth-logout'),
|
||||
path('auth/sign_up/', authentication.SignUpView.as_view(), name='auth-sign-up'),
|
||||
path('auth/mfa/', mfa.MultiFactorAuthenticator.as_view(), name='mfa'),
|
||||
path('auth/mfa/denied/', mfa.MFAPermissionDeniedView.as_view(), name='mfa-denied'),
|
||||
path('auth/process/', view.AuthenticationView.as_view(), name='auth-process'),
|
||||
path('auth/process/<slug:factor>/', view.AuthenticationView.as_view(), name='auth-process'),
|
||||
path('auth/process/denied/', view.MFAPermissionDeniedView.as_view(), name='auth-denied'),
|
||||
# User views
|
||||
path('user/', user.UserSettingsView.as_view(), name='user-settings'),
|
||||
path('user/delete/', user.UserDeleteView.as_view(), name='user-delete'),
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.translation import ugettext as _
|
|||
from django.views import View
|
||||
from django.views.generic import FormView
|
||||
|
||||
from passbook.core.auth.mfa import MultiFactorAuthenticator
|
||||
from passbook.core.auth.view import AuthenticationView
|
||||
from passbook.core.forms.authentication import LoginForm, SignUpForm
|
||||
from passbook.core.models import Invitation, Source, User
|
||||
from passbook.core.signals import invitation_used, user_signed_up
|
||||
|
@ -62,9 +62,9 @@ class LoginView(UserPassesTestMixin, FormView):
|
|||
if not pre_user:
|
||||
# No user found
|
||||
return self.invalid_login(self.request)
|
||||
if MultiFactorAuthenticator.SESSION_FACTOR in self.request.session:
|
||||
del self.request.session[MultiFactorAuthenticator.SESSION_FACTOR]
|
||||
self.request.session[MultiFactorAuthenticator.SESSION_PENDING_USER] = pre_user.pk
|
||||
if AuthenticationView.SESSION_FACTOR in self.request.session:
|
||||
del self.request.session[AuthenticationView.SESSION_FACTOR]
|
||||
self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk
|
||||
return redirect(reverse('passbook_core:mfa'))
|
||||
|
||||
def invalid_login(self, request: HttpRequest, disabled_user: User = None) -> HttpResponse:
|
||||
|
|
|
@ -59,6 +59,10 @@ passbook:
|
|||
uid_fields:
|
||||
- username
|
||||
- email
|
||||
factors:
|
||||
- passbook.core.auth.factors.backend
|
||||
- passbook.core.auth.factors.dummy
|
||||
- passbook.captcha_factor.factor
|
||||
session:
|
||||
remember_age: 2592000 # 60 * 60 * 24 * 30, one month
|
||||
# Provider-specific settings
|
||||
|
|
Reference in New Issue