Skip to content

Commit 059c7a3

Browse files
committed
Merged v3.1
1 parent f6c8c84 commit 059c7a3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1564
-1327
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Change Log
22

3+
## 3.1
4+
5+
* Upgraded to fido==1.2.0
6+
* Added: CSP Compliance (optional), thanks to @lvanbuiten, to under CSP refer to [van Buiten](https://github.com/mkalioby/django-mfa2/pull/93#issuecomment-2847112870)
7+
* Fix: issue when finding the user based on credential id.
8+
* Fix: Move key delete to be POST call. Thanks to @AndreasDickow
9+
10+
## 3.0.1
11+
12+
* Fix: Issue with some usernames that crashes FIDO2 registration.
13+
314
## 3.0
415

516
This is a major cleanup and CSS adjustments so please test before deployment.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ function some_func() {
224224
* [ezrajrice](https://github.com/ezrajrice)
225225
* [Spitfireap](https://github.com/Spitfireap)
226226
* [peterthomassen](https://github.com/peterthomassen)
227+
* [lvanbuiten](https://github.com/lvanbuiten)
227228

228229

229230
# Security contact information

example/example-ssl-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
django-sslserver
2+
django-csp~=3.8

example/example/settings.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"django.contrib.messages",
4242
"django.contrib.staticfiles",
4343
"mfa",
44+
'csp',
4445
"sslserver",
4546
]
4647

@@ -49,6 +50,7 @@
4950
"django.contrib.sessions.middleware.SessionMiddleware",
5051
"django.middleware.common.CommonMiddleware",
5152
"django.middleware.csrf.CsrfViewMiddleware",
53+
"csp.middleware.CSPMiddleware",
5254
"django.contrib.auth.middleware.AuthenticationMiddleware",
5355
"django.contrib.messages.middleware.MessageMiddleware",
5456
"django.middleware.clickjacking.XFrameOptionsMiddleware",
@@ -168,3 +170,13 @@
168170
"localhost" # Server rp id for FIDO2, it the full domain of your project
169171
)
170172
FIDO_SERVER_NAME = "TestApp"
173+
174+
175+
CSP = ("'self'", f'*.localhost')
176+
CSP_DEFAULT_SRC = CSP
177+
CSP_IMG_SRC = CSP + ('data:', 'blob:')
178+
CSP_FONT_SRC = CSP + ('data:',)
179+
CSP_SCRIPT_SRC = CSP
180+
CSP_STYLE_SRC = CSP
181+
CSP_CONNECT_SRC = CSP
182+
CSP_FORM_ACTION = CSP

example/example/templates/logout.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<link href="{% static 'vendor/fontawesome-free/css/all.min.css'%}" rel="stylesheet" type="text/css">
1717

1818
<!-- Custom styles for this template-->
19-
<link href="{% static 'css/sb-admin.css'%}" rel="stylesheet">
19+
<link href="{% static 'css/sb-admin.min.css'%}" rel="stylesheet">
2020

2121
</head>
2222

example/example/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
urlpatterns = [
2222
path("admin/", admin.site.urls),
23-
path("mfa/", include("mfa.urls")),
23+
path("mfa2/", include("mfa.urls")),
2424
path("auth/login", auth.loginView, name="login"),
2525
path("auth/logout", auth.logoutView, name="logout"),
2626
path("devices/add/", TrustedDevice.add, name="add_trusted_device"),

mfa/FIDO2.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from base64 import urlsafe_b64encode
23

34
from fido2.server import Fido2Server, PublicKeyCredentialRpEntity
45
from fido2.webauthn import RegistrationResponse
@@ -58,7 +59,7 @@ def begin_registeration(request):
5859
user_verification = getattr(settings, "MFA_FIDO2_USER_VERIFICATION", None)
5960
registration_data, state = server.register_begin(
6061
{
61-
"id": request.user.username.encode("utf8"),
62+
"id":urlsafe_b64encode(request.user.username.encode("utf8")),
6263
"name": request.user.username,
6364
"displayName": request.user.username,
6465
},
@@ -103,8 +104,9 @@ def complete_reg(request):
103104
}
104105
uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False)
105106
uk.key_type = "FIDO2"
106-
if auth_data.credential_data.credential_id:
107-
uk.user_handle = auth_data.credential_data.credential_id
107+
if data.get("id"):
108+
uk.user_handle = data.get('id')
109+
108110
uk.save()
109111
if (
110112
getattr(settings, "MFA_ENFORCE_RECOVERY_METHOD", False)
@@ -189,7 +191,7 @@ def authenticate_complete(request):
189191
username = request.user.username
190192
server = getServer()
191193
data = json.loads(request.body)
192-
userHandle = data.get("response", {}).get("userHandle")
194+
userHandle = data['id']
193195
credential_id = data["id"]
194196

195197
if userHandle:
@@ -204,12 +206,16 @@ def authenticate_complete(request):
204206
websafe_decode(keys[0].properties["device"])
205207
)
206208
]
209+
else:
210+
credentials = getUserCredentials(username)
207211
elif credential_id and username is None:
208212
keys = User_Keys.objects.filter(user_handle=credential_id)
209213
if keys.exists():
210214
credentials = [
211215
AttestedCredentialData(websafe_decode(keys[0].properties["device"]))
212216
]
217+
else:
218+
credentials = getUserCredentials(username)
213219
else:
214220
credentials = getUserCredentials(username)
215221

@@ -220,16 +226,13 @@ def authenticate_complete(request):
220226
response=data,
221227
)
222228
except ValueError:
223-
return (
224-
JsonResponse(
229+
return JsonResponse(
225230
{
226231
"status": "ERR",
227232
"message": "Wrong challenge received, make sure that this is your security and try again.",
228233
},
229234
status=400,
230235
),
231-
)
232-
233236
except Exception as excep:
234237
return JsonResponse({"status": "ERR", "message": str(excep)}, status=500)
235238

@@ -239,7 +242,7 @@ def authenticate_complete(request):
239242
return JsonResponse({"status": "OK"})
240243

241244
else:
242-
if keys is None:
245+
if keys is None or len(keys)==0:
243246
keys = User_Keys.objects.filter(
244247
username=username, key_type="FIDO2", enabled=1
245248
)

mfa/static/mfa/css/mfa.css

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
.text-center {
2+
text-align: center;
3+
}
4+
5+
.text-right {
6+
text-align: right;
7+
}
8+
9+
.padding-10 {
10+
padding: 10px;
11+
}
12+
13+
.padding-left-0 {
14+
padding-left: 0;
15+
}
16+
17+
.padding-left-15 {
18+
padding-left: 15px;
19+
}
20+
21+
.padding-left-25 {
22+
padding-left: 25px;
23+
}
24+
25+
.padding-top-10 {
26+
padding-top: 10px;
27+
}
28+
29+
.padding-right-30 {
30+
padding-right: 30px;
31+
}
32+
33+
#popUpModal {
34+
top: 40px;
35+
36+
& .modal-dialog {
37+
height: 80%;
38+
width: 80%;
39+
}
40+
}
41+
42+
.img-circle {
43+
padding: 3px;
44+
height: 50px;
45+
}
46+
47+
.success-message {
48+
color: green;
49+
}
50+
.error-message {
51+
color: red;
52+
}
53+
54+
.font-10 {
55+
font-size: 10px;
56+
}
57+
58+
.color-gray {
59+
color: #333333;
60+
}
61+
62+
.bg-white {
63+
background-color: #f0f0f0;
64+
}
65+
66+
.institution-code {
67+
font-size: 10pt;
68+
font-family: Calibri;
69+
height: 34px;width: 230px
70+
}
71+
72+
.td-digits {
73+
font-size: 16px;
74+
font-weight: bold;
75+
margin-left: 50px;
76+
}
77+
78+
#two-factor-steps {
79+
border: 1px solid #ccc;
80+
border-radius: 3px;
81+
padding: 15px;
82+
83+
& .row {
84+
margin: 0;
85+
align-items: center;
86+
}
87+
88+
& .toolbtn {
89+
border-radius: 7px;
90+
cursor: pointer;
91+
92+
& :hover {
93+
background-color: gray;
94+
transition: 0.2s;
95+
}
96+
97+
& :active {
98+
background-color: green;
99+
transition: 0.2s;
100+
}
101+
}
102+
103+
& #answer {
104+
display: inline;
105+
width: 95%
106+
}
107+
108+
& #second_step {
109+
display: none;
110+
text-align: center;
111+
align-content: center
112+
}
113+
114+
& .tokenrow {
115+
margin-top: 10px;
116+
margin-left: 5px;
117+
}
118+
}
119+
120+
.row.margin-3 {
121+
margin: 3px;
122+
}
123+
124+
.row.margin-left-15, .auth-card .row {
125+
margin-left: 15px;
126+
}

mfa/static/mfa/js/Email/recheck.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
$(document).ready(function () {
2+
const mode = JSON.parse(document.getElementById('mode').textContent);
3+
if (mode == "recheck") {
4+
$("#send_totp").click(function () {send_totp()});
5+
}
6+
})

mfa/static/mfa/js/FIDO2/add.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
var MakeCredReq = (makeCredReq) => {
2+
makeCredReq.publicKey.challenge = base64url.decode(makeCredReq.publicKey.challenge);
3+
makeCredReq.publicKey.user.id = base64url.decode(makeCredReq.publicKey.user.id);
4+
5+
for (let excludeCred of makeCredReq.publicKey.excludeCredentials) {
6+
excludeCred.id = base64url.decode(excludeCred.id);
7+
}
8+
9+
return makeCredReq
10+
}
11+
12+
function begin_reg() {
13+
const fido2_begin_reg = JSON.parse(document.getElementById('fido2_begin_reg').textContent);
14+
const fido2_complete_reg = JSON.parse(document.getElementById('fido2_complete_reg').textContent);
15+
const redirect_html = JSON.parse(document.getElementById('redirect_html').textContent);
16+
const reg_success_msg = JSON.parse(document.getElementById('reg_success_msg').textContent);
17+
const manage_recovery_codes = JSON.parse(document.getElementById('manage_recovery_codes').textContent);
18+
const RECOVERY_METHOD = JSON.parse(document.getElementById('RECOVERY_METHOD').textContent);
19+
const mfa_home = JSON.parse(document.getElementById('mfa_home').textContent);
20+
fetch(fido2_begin_reg, {}).then(function (response) {
21+
if (response.ok) {
22+
return response.json().then(function (req) {
23+
return MakeCredReq(req)
24+
});
25+
}
26+
throw new Error('Error getting registration data!');
27+
}).then(function (options) {
28+
//options.publicKey.attestation="direct"
29+
console.log(options)
30+
return navigator.credentials.create(options);
31+
}).then(function (attestation) {
32+
return fetch(fido2_complete_reg, {
33+
method: 'POST',
34+
body: JSON.stringify(publicKeyCredentialToJSON(attestation))
35+
});
36+
}).then(function (response) {
37+
var stat = response.ok ? 'successful' : 'unsuccessful';
38+
return response.json()
39+
}).then(function (res) {
40+
if (res["status"] == 'OK')
41+
$("#res").html("<div class='alert alert-success'>Registered Successfully, <a href='"+redirect_html+"'>"+reg_success_msg+"</a></div>")
42+
else if (res['status'] = "RECOVERY") {
43+
setTimeout(function () {
44+
location.href = manage_recovery_codes
45+
}, 2500)
46+
$("#res").html("<div class='alert alert-success'>Registered Successfully, but <a href='"+manage_recovery_codes+"'>redirecting to "+RECOVERY_METHOD+" method</a></div>")
47+
} else
48+
$("#res").html("<div class='alert alert-danger'>Registration Failed as " + res["message"] + ", <a href='#' id='failed_begin_reg'> try again or <a href='"+mfa_home+"'> Go to Security Home</a></div>")
49+
$("#failed_begin_reg").click(function() {begin_reg()});
50+
}, function (reason) {
51+
$("#res").html("<div class='alert alert-danger'>Registration Failed as " + reason + ", <a href='#' id='failed_begin_reg'> try again </a> or <a href='"+mfa_home+"'> Go to Security Home</a></div>")
52+
$("#failed_begin_reg").click(function() {begin_reg()});
53+
})
54+
}
55+
56+
$(document).ready(function () {
57+
ua = new UAParser().getResult()
58+
if (ua.browser.name == "Safari" || ua.browser.name == "Mobile Safari") {
59+
$("#res").html("<button class='btn btn-success' id='ua_begin_reg'>Start...</button>")
60+
$("#ua_begin_reg").click(function() { begin_reg(); });
61+
} else {
62+
setTimeout(begin_reg, 500)
63+
}
64+
})

0 commit comments

Comments
 (0)