Skip to content

Commit 265c2a0

Browse files
committed
experiment: configure allauth with mfa
1 parent ea39286 commit 265c2a0

File tree

9 files changed

+100
-13
lines changed

9 files changed

+100
-13
lines changed

python/nav/django/defaults.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
PUBLIC_URLS = [
2+
'/api/', # No auth/different auth system
3+
'/doc/', # No auth/different auth system
4+
'/about/',
5+
'/index/login/',
6+
'/index/audit-logging-modal/',
7+
'/refresh_session',
8+
'/accounts/',
9+
'/accounts/2fa/authenticate/',
10+
]
11+
NAV_LOGIN_URL = '/index/login/'
12+
ALLAUTH_LOGIN_URL = '/accounts/login/'
13+
14+
LOGIN_URL = ALLAUTH_LOGIN_URL

python/nav/django/settings.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
'nav.django.legacy.LegacyCleanupMiddleware',
138138
'django.contrib.messages.middleware.MessageMiddleware',
139139
'django_htmx.middleware.HtmxMiddleware',
140+
'allauth.account.middleware.AccountMiddleware',
140141
)
141142

142143
SESSION_SERIALIZER = 'nav.web.session_serializer.PickleSerializer'
@@ -236,12 +237,21 @@
236237
'nav.portadmin.napalm',
237238
'nav.web.portadmin',
238239
'django.contrib.postgres',
240+
'allauth',
241+
'allauth.account',
242+
'allauth.mfa',
243+
'allauth.socialaccount',
244+
# noqa: Needs to be a setting
245+
'allauth.socialaccount.providers.dataporten',
239246
)
240247

241248
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
242249
AUTH_USER_MODEL = 'nav_models.Account'
243250

244-
AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend']
251+
AUTHENTICATION_BACKENDS = [
252+
'django.contrib.auth.backends.ModelBackend',
253+
"allauth.account.auth_backends.AuthenticationBackend",
254+
]
245255
LOGIN_REDIRECT_URL = '/'
246256
LOGIN_URL = '/index/login/'
247257

@@ -318,3 +328,17 @@
318328
'JWT_ISSUERS': _issuers_setting,
319329
'JWT_AUTH_HEADER_PREFIX': 'Bearer',
320330
}
331+
332+
# Allauth settings
333+
334+
# ACCOUNT_ADAPTER = "argus.auth.allauth.adapter.ArgusAccountAdapter"
335+
ACCOUNT_USER_MODEL_USERNAME_FIELD = 'login'
336+
ACCOUNT_ALLOW_SIGNUPS = False
337+
ACCOUNT_MAX_EMAIL_ADDRESSES = 1
338+
LOGIN_URL = '/accounts/login/'
339+
MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = True # allow localhost
340+
MFA_TOTP_ISSUER = 'NAV'
341+
MFA_TOTP_TOLERANCE = 0
342+
MFA_SUPPORTED_TYPES = ['totp', 'recovery_codes']
343+
SOCIALACCOUNT_AUTO_SIGNUP = True
344+
# SOCIALACCOUNT_ADAPTER = 'argus.auth.allauth.adapter.ArgusSocialAccountAdapter'

python/nav/django/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
path('refresh_session/', refresh_session, name='refresh-session'),
6767
path('auditlog/', include('nav.auditlog.urls')),
6868
path('interfaces/', include('nav.web.interface_browser.urls')),
69+
path('accounts/', include('allauth.urls')),
6970
path('500/', force_500),
7071
]
7172

python/nav/web/auth/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from django.http import HttpRequest
2727
from django.urls import reverse
2828

29+
from nav.django.defaults import LOGIN_URL
2930
from nav.auditlog.models import LogEntry
3031
from nav.models.profiles import Account, AccountGroup
3132
from nav.web.auth import ldap, remote_user
@@ -42,7 +43,7 @@
4243
# This may seem like redundant information, but it seems django's reverse
4344
# will hang under some usages of these middleware classes - so until we figure
4445
# out what's going on, we'll hardcode this here.
45-
LOGIN_URL = '/index/login/'
46+
# LOGIN_URL = '/accounts/login/'
4647
# The local logout url, redirects to '/' after logout
4748
# If the entire site is protected via remote_user, this link must be outside
4849
# that protection!

python/nav/web/auth/utils.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from django.contrib.auth import get_user as django_get_user
2525
from django.core.cache import cache
2626

27+
from nav.django.defaults import PUBLIC_URLS
2728
from nav.models.profiles import Account
2829

2930

@@ -103,15 +104,7 @@ def authorization_not_required(fullpath):
103104
Should the user be able to decide this? Currently not.
104105
105106
"""
106-
auth_not_required = [
107-
'/api/',
108-
'/doc/', # No auth/different auth system
109-
'/about/',
110-
'/index/login/',
111-
'/index/audit-logging-modal/',
112-
'/refresh_session',
113-
]
114-
for url in auth_not_required:
107+
for url in PUBLIC_URLS:
115108
if fullpath.startswith(url):
116109
_logger.debug('authorization_not_required: %s', url)
117110
return True
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{% extends "base.html" %}
2+
{% load socialaccount %}
3+
{% load i18n %}
4+
{% block base_content %}
5+
{% if request.user.is_authenticated %}
6+
<nav>
7+
<ul>
8+
{% url 'account_email' as email_url_ %}
9+
{% if email_url_ %}
10+
<li>
11+
<a href="{{ email_url_ }}">{% trans "Change Email" %}</a>
12+
</li>
13+
{% endif %}
14+
{% url 'account_change_password' as change_password_url_ %}
15+
{% if change_password_url_ %}
16+
<li>
17+
<a href="{{ change_password_url_ }}">{% trans "Change Password" %}</a>
18+
</li>
19+
{% endif %}
20+
{% url 'socialaccount_connections' as connections_url_ %}
21+
{% if connections_url_ %}
22+
<li>
23+
<a href="{{ connections_url_ }}">{% trans "Account Connections" %}</a>
24+
</li>
25+
{% endif %}
26+
{% url 'mfa_index' as mfa_url_ %}
27+
{% if mfa_url_ %}
28+
<li>
29+
<a href="{{ mfa_url_ }}">{% trans "Two-Factor Authentication" %}</a>
30+
</li>
31+
{% endif %}
32+
{% url 'usersessions_list' as usersessions_list_url_ %}
33+
{% if usersessions_list_url_ %}
34+
<li>
35+
<a href="{{ usersessions_list_url_ }}">{% trans "Sessions" %}</a>
36+
</li>
37+
{% endif %}
38+
{% url 'account_logout' as logout_url_ %}
39+
</ul>
40+
</nav>
41+
{% endif %}
42+
<section>
43+
<div>
44+
{% block content %}
45+
{% endblock content %}
46+
</div>
47+
</section>
48+
{% endblock base_content %}

requirements/base.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,5 @@ service-identity==21.1.0
4747
requests
4848

4949
pyjwt>=2.6.0
50+
51+
django-allauth[mfa,socialaccount]

tests/functional/conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
22
import subprocess
33

4+
from nav.django.default import NAV_LOGIN_URL as LOGIN_URL
5+
46
import pytest
57
from selenium.webdriver.common.by import By
68
from selenium.webdriver.support.ui import WebDriverWait
@@ -45,7 +47,7 @@ def selenium(selenium, base_url, admin_username, admin_password):
4547
wait = WebDriverWait(selenium, 10)
4648

4749
# visit the login page and submit the login form
48-
selenium.get(f"{base_url}/index/login")
50+
selenium.get(f"{base_url}/{LOGIN_URL}")
4951
wait.until(EC.text_to_be_present_in_element((By.TAG_NAME, "label"), "Username"))
5052

5153
username = selenium.find_element(By.ID, "id_username")

tests/integration/web/crawler_test.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
urlunparse,
3636
)
3737

38+
from nav.django.settings import LOGIN_URL
39+
3840

3941
TIMEOUT = 90 # seconds?
4042

@@ -165,7 +167,7 @@ def _queue_links_from(self, content, base_url):
165167
self.queue.append('%s://%s%s' % (url.scheme, url.netloc, url.path))
166168

167169
def login(self):
168-
login_url = urljoin(self.base_url, '/index/login/')
170+
login_url = urljoin(self.base_url, LOGIN_URL)
169171
opener = build_opener(HTTPCookieProcessor())
170172
data = urlencode({'username': self.username, 'password': self.password})
171173
opener.open(login_url, data.encode('utf-8'), TIMEOUT)

0 commit comments

Comments
 (0)