Skip to content

Commit 7471020

Browse files
committed
Upgraded OIDC
1 parent 1fd8dbd commit 7471020

File tree

11 files changed

+297
-51
lines changed

11 files changed

+297
-51
lines changed

app/settings.py

+21-27
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
'axes',
6666
'django_password_validators',
6767
'django_password_validators.password_history',
68+
'rest_framework',
6869
'user',
6970
'application',
7071
'review',
@@ -144,29 +145,29 @@
144145
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
145146

146147
AUTH_PASSWORD_VALIDATORS = [
147-
{
148-
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
149-
},
150-
{
151-
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
152-
},
153148
{
154149
'NAME': 'django_password_validators.password_history.password_validation.UniquePasswordsValidator',
155-
},
156-
{
157-
'NAME': 'django_password_validators.password_character_requirements.password_validation.'
158-
'PasswordCharacterValidator',
159150
'OPTIONS': {
160-
'min_length_digit': 1,
161-
'min_length_alpha': 1,
162-
'min_length_special': 1,
163-
'min_length_lower': 1,
164-
'min_length_upper': 1,
165-
'special_characters': ",.-~!@#$%^&*()_+{}\":;'[]"
151+
# How many recently entered passwords matter.
152+
# Passwords out of range are deleted.
153+
# Default: 0 - All passwords entered by the user. All password hashes are stored.
154+
'last_passwords': 5 # Only the last 5 passwords entered by the user
166155
}
167156
},
168157
]
169158

159+
# Validations are js because server does not get the password
160+
PASSWORD_VALIDATORS = {
161+
'min_length_digit': 1,
162+
'min_length_special': 1,
163+
'min_length_lower': 1,
164+
'min_length_upper': 1,
165+
'min_characters': 8,
166+
# regex format
167+
'special_characters': "/[`~!@#$%\^\*\(\),\.\-=_+\\\\\[\]{}/\?]/g",
168+
169+
}
170+
170171
AUTHENTICATION_BACKENDS = [
171172
# AxesStandaloneBackend should be the first backend in the AUTHENTICATION_BACKENDS list.
172173
'axes.backends.AxesStandaloneBackend',
@@ -234,17 +235,10 @@
234235
LOGIN_URL = '/auth/login'
235236

236237
# JWT settings
237-
JWT_CLIENT = {
238-
'OPENID2_URL': os.environ.get('OPENID_CLIENT_ID', 'http://localhost:8000/openid'), # Required
239-
'CLIENT_ID': os.environ.get('OPENID_CLIENT_ID', 'client_id'), # Required
240-
'TYPE': 'fake' if DEBUG else 'local', # Required
241-
'RESPONSE_TYPE': 'id_token', # Required
242-
'RENAME_ATTRIBUTES': {'sub': 'email', 'groups': 'get_groups'}, # Optional
243-
244-
}
245-
JWT_SERVER = {
246-
'JWK_EXPIRATION_TIME': 3600, # Optional
247-
'JWT_EXPIRATION_TIME': 14400 # Optional
238+
JWT_OIDC = {
239+
'TYPE': 'provider', # Required
240+
'DISCOVERY_ENDPOINT': os.environ.get('OIDC_DISCOVERY_ENDPOINT',
241+
'http://localhost:8000/openid/.well-known/openid-configuration'),
248242
}
249243

250244
DJANGO_TABLES2_TEMPLATE = 'django_tables2/bootstrap-responsive.html'

app/static/img/logo-no-text.png

20.2 KB
Loading

app/template.py

+1
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,5 @@ def app_variables(request):
5959
'theme': get_theme(request),
6060
'captcha_site_key': getattr(settings, 'GOOGLE_RECAPTCHA_SITE_KEY', ''),
6161
'socialaccount_providers': getattr(settings, 'SOCIALACCOUNT_PROVIDERS', {}),
62+
'auth_password_validators': getattr(settings, 'PASSWORD_VALIDATORS', {})
6263
}

app/templates/base.html

+23-10
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,30 @@
4646
</div>
4747
</nav>
4848
{% endblock %}
49-
{% block container %}
50-
<div class="container-lg mb-4 mt-4">
51-
{% if tabs %}
52-
{% include 'components/tabs.html' %}
53-
{% endif %}
54-
<div class="bg-{{ theme }} text-{% if theme == 'dark' %}white{% else %}black{% endif %}">
55-
<div class="p-4">
56-
{% block content %}
57-
{% endblock %}
49+
<div id="js-disabled">
50+
<div class="row justify-content-md-center" style="margin: 0">
51+
<div class="col-lg-4 mb-4 mt-4">
52+
<div class="bg-{{ theme }} text-{% if theme == 'dark' %}white{% else %}black{% endif %}">
53+
<h3 class="text-center p-5">{{ app_name }} does not work without Javascript. Enable it please!</h3>
5854
</div>
5955
</div>
6056
</div>
61-
{% endblock %}
57+
</div>
58+
<div id="js-enabled" style="display: none">
59+
{% block container %}
60+
<div class="container-lg mb-4 mt-4">
61+
{% if tabs %}
62+
{% include 'components/tabs.html' %}
63+
{% endif %}
64+
<div class="bg-{{ theme }} text-{% if theme == 'dark' %}white{% else %}black{% endif %}">
65+
<div class="p-4">
66+
{% block content %}
67+
{% endblock %}
68+
</div>
69+
</div>
70+
</div>
71+
{% endblock %}
72+
</div>
6273
{% include 'components/toast.html' %}
6374
{% block footer %}
6475
{% include 'components/footer.html' %}
@@ -68,6 +79,8 @@
6879
{% endblock %}
6980
<script nonce="{{ CSP_NONCE }}">
7081
$(document).ready(() => {
82+
$('#js-disabled').hide()
83+
$('#js-enabled').show()
7184
// Timezone settings
7285
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // e.g. "America/New_York"
7386
document.cookie = "django_timezone=" + timezone;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{% extends 'base.html' %}
2+
{% load i18n %}
3+
{% load static %}
4+
{% block subtitle %}{% translate 'Authorize' %} {{ web.get_name }}{% endblock %}
5+
{% block navbar_main_menu %}{% endblock %}
6+
{% block container %}
7+
<div class="row justify-content-md-center" style="margin: 0">
8+
<div class="col-lg-4 mb-4 mt-4">
9+
<div class="bg-{{ theme }} text-{% if theme == 'dark' %}white{% else %}black{% endif %}">
10+
<div class="content p-4">
11+
<div class="d-flex align-items-center justify-content-center" style="height: 10vh">
12+
<img style="width: 10vh" class="rounded-circle border border-2" src="{{ web.get_logo_as_base64 }}" alt="{{ web.get_name }}">
13+
<div style="position: relative; width: 10vh; height: 100%">
14+
<div class="border border-right" style="position: absolute; width: 100%; top: 50%; z-index: 0"></div>
15+
<div style="position: absolute; width: 100%; z-index: 1; top: 50%; transform: translateY(-48%)" class="h3 text-center"><i class="bi bi-check-circle-fill bg-{{ theme }} text-success"></i></div>
16+
</div>
17+
<img style="width: 10vh" class="rounded-circle border border-2" src="{% static 'img/logo-no-text.png' %}" alt="{{ app_name }}">
18+
</div>
19+
<h2 class="text-center mt-4">{% translate 'Authorize' %} {{ web.get_name }}</h2>
20+
<h4 class="text-center">{% translate 'to access your information about:' %}</h4>
21+
<ul class="mt-3">
22+
{% for scope in accepted_scopes %}
23+
<li><h5>{{ scope|title }} <i class="text-success bi bi-check-circle-fill bg-{{ theme }}" title="{% translate 'Previously accepted' %}"></i></h5></li>
24+
{% endfor %}
25+
{% for scope in not_accepted_scopes %}
26+
<li><h5>{{ scope|title }}</h5></li>
27+
{% endfor %}
28+
</ul>
29+
{% if denied %}
30+
<div class="toast align-items-center fade show bg-danger" role="alert" aria-live="assertive" aria-atomic="true">
31+
<div class="d-flex">
32+
<div class="toast-body">You denied access to {{ web.get_name }}</div>
33+
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
34+
</div>
35+
</div>
36+
{% endif %}
37+
<form method="post">
38+
{% csrf_token %}
39+
<div class="row justify-content-around">
40+
<div class="col-12 col-lg-6 d-grid d-md-block mt-2">
41+
<button type="submit" name="confirmation" value="false" class="btn btn-danger col-12">{% translate 'Deny' %}</button>
42+
</div>
43+
<div class="col-12 col-lg-6 d-grid d-md-block mt-2">
44+
<button type="submit" name="confirmation" value="true" class="btn btn-primary col-12">{% translate 'Accept' %}</button>
45+
</div>
46+
</div>
47+
<p class="text-center mt-3">{% translate 'Accepting will redirect to' %} {{ web.host }}</p>
48+
</form>
49+
</div>
50+
</div>
51+
</div>
52+
</div>
53+
{% endblock %}

app/urls.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
path('privacy_and_cookies/', views.PrivacyCookies.as_view(), name='privacy_and_cookies'),
2828

2929
path('admin/', include('admin_honeypot.urls', namespace='admin_honeypot')),
30+
path('openid/', include('django_jwt.urls')),
3031
path(getattr(settings, 'ADMIN_URL', 'secret/'), admin.site.urls),
3132
path('auth/', include('user.urls')),
3233
path('application/', include('application.urls')),
@@ -38,9 +39,3 @@
3839

3940
if is_installed("friends"):
4041
urlpatterns.append(path('friends/', include('friends.urls')))
41-
42-
# JWT fake login on DEBUG for development purposes
43-
if getattr(settings, 'DEBUG', True):
44-
urlpatterns.append(path('openid/', include('django_jwt.urls')))
45-
else:
46-
urlpatterns.append(path('openid/', include('django_jwt.server.urls')))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.0.8 on 2023-04-06 08:13
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('event_messages', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='announcement',
15+
name='services',
16+
field=models.CharField(blank=True, help_text='Messages will be sent to the services you select', max_length=200),
17+
),
18+
]

requirements.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ django-crontab==0.7.1
2424
django-csp==3.7
2525
django-filter==22.1
2626
django-ipware==4.0.2
27-
django-jwt-oidc==0.3.9
27+
django-jwt-oidc==1.0.3
2828
django-libsass==0.9
2929
django-password-validators==1.7.0
3030
django-recaptcha==3.0.0
3131
django-tables2==2.4.1
32+
djangorestframework==3.14.0
3233
filetype==1.1.0
3334
flake8==4.0.1
3435
frozenlist==1.3.3

user/templates/auth.html

+91-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{% extends 'base.html' %}
22
{% load i18n %}
33
{% load socialaccount %}
4+
{% block head %}
5+
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js" integrity="sha512-E8QSvWZ0eCLGk4km3hxSsNmGWbLtSCSUcewDQPQWZF6pEU8GlT8a5fF32wOl1i8ftdMhssTrF/OhyGWwonTcXA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
6+
{% endblock %}
47
{% block subtitle %}{{ auth|title }}{% endblock %}
58
{% block container %}
69
<div class="row justify-content-md-center" style="margin: 0">
@@ -10,7 +13,7 @@
1013
{% endif %}
1114
<div class="bg-{{ theme }} text-{% if theme == 'dark' %}white{% else %}black{% endif %}">
1215
{% if not blocked_message %}
13-
<form method="post">
16+
<form method="post" id="auth-form">
1417
{% csrf_token %}
1518
<div class="content p-4">
1619
{% if auth != 'register' %}
@@ -55,4 +58,91 @@
5558
</div>
5659
</div>
5760
</div>
61+
<script>
62+
$(document).ready(() => {
63+
{% if auth == 'register' %}
64+
let min_chars = {{ auth_password_validators.min_characters|default:8 }};
65+
let min_digits = {{ auth_password_validators.min_length_digit|default:0 }};
66+
let min_special = {{ auth_password_validators.min_length_special|default:0 }};
67+
let special = '{{ auth_password_validators.special_characters|default:'' }}';
68+
let min_lower = {{ auth_password_validators.min_length_lower|default:0 }};
69+
let min_upper = {{ auth_password_validators.min_length_upper|default:0 }};
70+
function check_password_requirements(password_input, password_text) {
71+
let password = password_input.val()
72+
if (password.length < min_chars ||
73+
(password.match('[0-9]')?.length ?? 0) < min_digits ||
74+
(password.match({{ auth_password_validators.special_characters|default:'' }})?.length ?? 0) < min_special ||
75+
(password.match('[a-z]')?.length ?? 0) < min_lower ||
76+
(password.match('[A-Z]')?.length ?? 0) < min_upper) {
77+
password_input.addClass('is-invalid')
78+
password_text.removeClass('text-success list-check')
79+
password_text.addClass('text-danger list-cross')
80+
return false
81+
}
82+
password_input.removeClass('is-invalid')
83+
password_text.addClass('text-success list-check')
84+
password_text.removeClass('text-danger list-cross')
85+
return true
86+
}
87+
let special_to_see = special.slice(special.indexOf('[') + 1, special.lastIndexOf(']'))
88+
let help_text = $(`<ul><li>This password be ${min_chars} must contain at least ${min_digits} digit, ${min_lower} lower case letter, ${min_upper} upper case letter, ${min_special} special character, such as ${special_to_see}.</li><ul>`)
89+
$('.form-text > ul').css('margin', '0')
90+
$('.form-text').append(help_text)
91+
let password_input = $('#id_password1')
92+
let password_input_repeat = $('#id_password2')
93+
password_input.keyup(() => {
94+
check_password_requirements(password_input, help_text)
95+
if (password_input.val() !== password_input_repeat.val()) {
96+
password_input_repeat.addClass('is-invalid')
97+
} else {
98+
password_input_repeat.removeClass('is-invalid')
99+
}
100+
})
101+
password_input_repeat.keyup(() => {
102+
if (password_input.val() !== password_input_repeat.val()) {
103+
password_input_repeat.addClass('is-invalid')
104+
} else {
105+
password_input_repeat.removeClass('is-invalid')
106+
}
107+
})
108+
{% endif %}
109+
async function digestMessage(message) {
110+
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
111+
const hashBuffer = await crypto.subtle.digest("SHA-512", msgUint8); // hash the message
112+
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
113+
const hashHex = hashArray
114+
.map((b) => b.toString(16).padStart(2, "0"))
115+
.join(""); // convert bytes to hex string
116+
return hashHex;
117+
}
118+
async function hash_password_inputs(password_inputs_id) {
119+
for (const password_input_id of password_inputs_id) {
120+
let password_input = $('#' + password_input_id)
121+
if (password_input.length > 0) {
122+
let text = window.location.hostname + ':' + password_input.val()
123+
let hash = await digestMessage(text)
124+
password_input.val(hash)
125+
}
126+
}
127+
}
128+
let hashed = false
129+
$('#auth-form').submit((event) => {
130+
{% if auth == 'register' %}
131+
if (!check_password_requirements(password_input, help_text) || password_input.val() !== password_input_repeat.val()) {
132+
event.preventDefault()
133+
return
134+
}
135+
{% endif %}
136+
if (hashed) {
137+
hashed = false
138+
return
139+
}
140+
event.preventDefault()
141+
hash_password_inputs(['id_password', 'id_password2', 'id_password1']).then(() => {
142+
hashed = true
143+
event.target.submit()
144+
})
145+
})
146+
})
147+
</script>
58148
{% endblock %}

user/templates/mails/password_reset.html

-5
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@
77
</p>
88

99
{% include 'mails/components/button.html' with url=url text='Reset password' %}
10-
<p>
11-
If clicking the link above doesn't work, please copy and paste the URL in a new browser
12-
window instead.
13-
</p>
14-
<p style="text-align: center;">{{ url|urlize }}</p>
1510

1611
<p>Have any other questions? Email us at {{ app_contact|urlize }}.</p>
1712

0 commit comments

Comments
 (0)