Initial mfa support (for internal users) #93591

Merged
Oleg-Komarov merged 46 commits from mfa into main 2024-08-29 11:44:06 +02:00
8 changed files with 90 additions and 24 deletions
Showing only changes of commit 0913f60deb - Show all commits

View File

@ -0,0 +1,28 @@
{% load add_form_classes from forms %}
{% load static %}
<div class="bid box">
<div>
<h2>Multi-factor Authentication</h2>
</div>
{% with form=form|add_form_classes %}
<form role="login" action="" method="POST">{% csrf_token %}
<fieldset>
{% with field=form.otp_device %}
{% include "components/forms/field.html" %}
{% endwith %}
{% with field=form.otp_token %}
{% include "components/forms/field.html" %}
{% endwith %}
{% with field=form.otp_trust_agent %}
{% include "components/forms/field.html" %}
{% endwith %}
{% if form.errors %}
<p class="text-danger">{{ form.errors }}</p>
{% endif %}
</fieldset>
<button class="btn btn-block btn-accent" id="register">Continue</button>
</form>
{% endwith %}
</div>

View File

@ -139,6 +139,9 @@ Profile
<a class="btn" href="{% url 'bid_main:active_sessions' %}">
<span>Active Sessions</span>
</a>
<a class="btn" href="{% url 'bid_main:mfa' %}">
<span>Multi-factor Authentication</span>
</a>
</div>
<div class="btn-row-fluid mt-3">
<a class="btn" href="{% url 'bid_main:password_change' %}">

View File

@ -3,5 +3,9 @@
{% block page_title %}Sign in{% endblock %}
{% block form %}
{% if is_authentication_form %}
{% include 'bid_main/components/login_form.html' %}
{% elif is_mfa_form %}
{% include 'bid_main/components/mfa_form.html' %}
{% endif %}
{% endblock form %}

View File

@ -0,0 +1,9 @@
{% extends 'layout.html' %}
{% load pipeline static %}
{% block page_title %}
MFA Setup
{% endblock %}
{% block body %}
{% for d in devices %}{{d}}{% endfor %}
{% endblock %}

View File

@ -1,6 +1,7 @@
from django.conf import settings
from django.urls import reverse_lazy, path, re_path
from django.contrib.auth import views as auth_views
from otp_agents.decorators import otp_required
from . import forms
from .views import normal_pages, registration_email, json_api, developer_applications
@ -146,6 +147,15 @@ urlpatterns = [
normal_pages.TerminateSessionView.as_view(),
name='terminate_session',
),
path(
'mfa/',
otp_required(
accept_trusted_agent=True,
if_configured=True,
view=normal_pages.MfaView.as_view(),
),
name='mfa',
),
]
# Only enable this on a dev server:

View File

@ -20,13 +20,14 @@ from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import TemplateView, FormView
from django.views.generic.base import View
from django.views.generic.edit import UpdateView
from django_otp import devices_for_user
from otp_agents.forms import OTPTokenForm
import loginas.utils
import oauth2_provider.models as oauth2_models
import otp_agents.views
from .. import forms, email
from . import mixins
@ -72,35 +73,22 @@ class IndexView(LoginRequiredMixin, mixins.PageIdMixin, TemplateView):
}
class LoginView(mixins.RedirectToPrivacyAgreeMixin, mixins.PageIdMixin, auth_views.LoginView):
class LoginView(mixins.RedirectToPrivacyAgreeMixin, mixins.PageIdMixin, otp_agents.views.LoginView):
"""Shows the login view."""
otp_authentication_form = forms.AuthenticationForm
page_id = "login"
template_name = "bid_main/login.html"
authentication_form = forms.AuthenticationForm
redirect_authenticated_user = True
redirect_authenticated_user = False
success_url_allowed_hosts = settings.NEXT_REDIR_AFTER_LOGIN_ALLOWED_HOSTS
template_name = "bid_main/login.html"
authorize_url = reverse_lazy("oauth2_provider:authorize")
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_exempt)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
"""Don't check CSRF token when already authenticated."""
if self.redirect_authenticated_user and self.request.user.is_authenticated:
redirect_to = self.get_success_url()
if redirect_to == self.request.path:
raise ValueError(
"Redirection loop for authenticated user detected. Check that "
"your LOGIN_REDIRECT_URL doesn't point to a login page."
)
return HttpResponseRedirect(redirect_to)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs) -> dict:
ctx = super().get_context_data(**kwargs)
self.find_oauth_flow(ctx)
ctx["is_authentication_form"] = isinstance(self.get_form(), forms.AuthenticationForm)
ctx["is_mfa_form"] = isinstance(self.get_form(), OTPTokenForm)
return ctx
def find_oauth_flow(self, ctx: dict):
@ -453,3 +441,12 @@ class TerminateSessionView(LoginRequiredMixin, View):
user_session.terminate()
return redirect('bid_main:active_sessions')
return HttpResponseNotFound("session not found")
class MfaView(LoginRequiredMixin, TemplateView):
template_name = "bid_main/mfa_setup.html"
def get_context_data(self, **kwargs):
return {
'devices': list(devices_for_user(self.request.user)),
}

View File

@ -56,6 +56,11 @@ INSTALLED_APPS = [
"django.contrib.staticfiles",
"django.contrib.sites",
"django.contrib.flatpages",
"django_otp",
"django_otp.plugins.otp_totp",
"django_otp.plugins.otp_hotp",
"django_otp.plugins.otp_static",
"django_agent_trust",
"oauth2_provider",
"pipeline",
"sorl.thumbnail",
@ -73,6 +78,8 @@ MIDDLEWARE = [
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django_agent_trust.middleware.AgentMiddleware",
"django_otp.middleware.OTPMiddleware",
"bid_main.middleware.user_session_middleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
@ -264,6 +271,10 @@ NEXT_REDIR_AFTER_LOGIN_ALLOWED_HOSTS = {
"blender.community",
}
AGENT_COOKIE_SECURE = True
AGENT_TRUST_DAYS = 30
AGENT_INACTIVITY_DAYS = 7
CSRF_COOKIE_SECURE = True
CSRF_FAILURE_VIEW = "bid_main.views.errors.csrf_failure"
CSRF_TRUSTED_ORIGINS = ['https://*.blender.org']
@ -349,6 +360,7 @@ if TESTING:
# For Debug Toolbar, extend with whatever address you use to connect
# to your dev server.
if DEBUG:
AGENT_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
INSTALLED_APPS += ['debug_toolbar']
INTERNAL_IPS = ["127.0.0.1"]

View File

@ -8,15 +8,18 @@ colorama==0.4.6 ; python_version >= "3.8" and python_version < "4" and platform_
cryptography==41.0.0 ; python_version >= "3.8" and python_version < "4"
csscompressor==0.9.5 ; python_version >= "3.8" and python_version < "4"
deprecated==1.2.14 ; python_version >= "3.8" and python_version < "4"
dj-database-url==2.2.0
django==4.2.13 ; python_version >= "3.8" and python_version < "4"
django-admin-select2==1.0.1 ; python_version >= "3.8" and python_version < "4"
django-agent-trust==1.1.0
django-background-tasks-updated @ git+https://projects.blender.org/infrastructure/django-background-tasks.git@1.2.10
django[bcrypt]==4.2.13 ; python_version >= "3.8" and python_version < "4"
django-compat==1.0.15 ; python_version >= "3.8" and python_version < "4"
django-loginas==0.3.11 ; python_version >= "3.8" and python_version < "4"
django-oauth-toolkit @ git+https://projects.blender.org/Oleg-Komarov/django-oauth-toolkit.git@0b056a99ca943771615b859f48aaff0e12357f22 ; python_version >= "3.8" and python_version < "4"
django-otp==1.5.1
django-otp-agents==1.0.1
django-pipeline==3.1.0 ; python_version >= "3.8" and python_version < "4"
django==4.2.13 ; python_version >= "3.8" and python_version < "4"
django[bcrypt]==4.2.13 ; python_version >= "3.8" and python_version < "4"
dj-database-url==2.2.0
docutils==0.14 ; python_version >= "3.8" and python_version < "4"
htmlmin==0.1.12 ; python_version >= "3.8" and python_version < "4"
idna==2.8 ; python_version >= "3.8" and python_version < "4"