├── Pipfile
├── Pipfile.lock
├── Procfile
├── README.md
├── accounts
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
├── templates
│ ├── index.html
│ ├── login.html
│ ├── mfa_auth_base.html
│ └── register.html
├── tests.py
├── urls.py
├── utils.py
└── views.py
├── db.sqlite3
├── django_mfa2_example
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
├── manage.py
├── mfa
├── ApproveLogin.py
├── Common.py
├── Email.py
├── FIDO2.py
├── TrustedDevice.py
├── U2F.py
├── __init__.py
├── admin.py
├── apps.py
├── helpers.py
├── middleware.py
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
├── static
│ └── mfa
│ │ ├── css
│ │ └── bootstrap-toggle.min.css
│ │ └── js
│ │ ├── bootstrap-toggle.min.js
│ │ ├── cbor.js
│ │ ├── qrious.min.js
│ │ ├── u2f-api.js
│ │ └── ua-parser.min.js
├── templates
│ ├── ApproveLogin
│ │ └── Add.html
│ ├── Email
│ │ ├── Add.html
│ │ ├── Auth.html
│ │ ├── mfa_email_token_template.html
│ │ └── recheck.html
│ ├── FIDO2
│ │ ├── Add.html
│ │ ├── Auth.html
│ │ └── recheck.html
│ ├── MFA.html
│ ├── TOTP
│ │ ├── Add.html
│ │ ├── Auth.html
│ │ └── recheck.html
│ ├── TrustedDevices
│ │ ├── Add.html
│ │ ├── Done.html
│ │ ├── email.html
│ │ ├── start.html
│ │ └── user-agent.html
│ ├── U2F
│ │ ├── Add.html
│ │ ├── Auth.html
│ │ └── recheck.html
│ ├── mfa_check.html
│ ├── modal.html
│ └── select_mfa_method.html
├── tests.py
├── totp.py
├── urls.py
└── views.py
├── requirements.txt
├── static
├── css
│ ├── bootstrap
│ │ ├── css
│ │ │ ├── bootstrap.min.css
│ │ │ └── bootstrap.min.css.map
│ │ └── js
│ │ │ ├── bootstrap.bundle.min.js
│ │ │ ├── bootstrap.bundle.min.js.map
│ │ │ └── bootstrap.min.js
│ └── custom.css
├── images
│ └── logo.png
└── js
│ └── jquery-3.1.1.min.js
└── templates
└── base.html
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | django = "*"
8 | whitenoise = "*"
9 | gunicorn = "*"
10 | python-decouple = "*"
11 | psycopg2 = "*"
12 | dj-database-url = "*"
13 | jsonfield = "*"
14 | python-jose = "*"
15 | django-mfa2 = "*"
16 |
17 | [dev-packages]
18 |
19 | [requires]
20 | python_version = "3.8"
21 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "edffc00b245250f40cf97556743a1632bc9ddfe51e3781f11c4245c92802cbe4"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.8"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "asgiref": {
20 | "hashes": [
21 | "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
22 | "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
23 | ],
24 | "markers": "python_version >= '3.6'",
25 | "version": "==3.3.4"
26 | },
27 | "cffi": {
28 | "hashes": [
29 | "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813",
30 | "sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373",
31 | "sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69",
32 | "sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f",
33 | "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06",
34 | "sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05",
35 | "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea",
36 | "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee",
37 | "sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0",
38 | "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396",
39 | "sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7",
40 | "sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f",
41 | "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73",
42 | "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315",
43 | "sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76",
44 | "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1",
45 | "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49",
46 | "sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed",
47 | "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892",
48 | "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482",
49 | "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058",
50 | "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5",
51 | "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53",
52 | "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045",
53 | "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3",
54 | "sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55",
55 | "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5",
56 | "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e",
57 | "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c",
58 | "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369",
59 | "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827",
60 | "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053",
61 | "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa",
62 | "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4",
63 | "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322",
64 | "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132",
65 | "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62",
66 | "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa",
67 | "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0",
68 | "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396",
69 | "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e",
70 | "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991",
71 | "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6",
72 | "sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc",
73 | "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1",
74 | "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406",
75 | "sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333",
76 | "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d",
77 | "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"
78 | ],
79 | "version": "==1.14.5"
80 | },
81 | "cryptography": {
82 | "hashes": [
83 | "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d",
84 | "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959",
85 | "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6",
86 | "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873",
87 | "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2",
88 | "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713",
89 | "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1",
90 | "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177",
91 | "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250",
92 | "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca",
93 | "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d",
94 | "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"
95 | ],
96 | "markers": "python_version >= '3.6'",
97 | "version": "==3.4.7"
98 | },
99 | "dj-database-url": {
100 | "hashes": [
101 | "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163",
102 | "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9"
103 | ],
104 | "index": "pypi",
105 | "version": "==0.5.0"
106 | },
107 | "django": {
108 | "hashes": [
109 | "sha256:13ac78dbfd189532cad8f383a27e58e18b3d33f80009ceb476d7fcbfc5dcebd8",
110 | "sha256:7e0a1393d18c16b503663752a8b6790880c5084412618990ce8a81cc908b4962"
111 | ],
112 | "index": "pypi",
113 | "version": "==3.2.3"
114 | },
115 | "django-mfa2": {
116 | "hashes": [
117 | "sha256:f13be96323ddb3521ccf77ff792a5c88bce2ae223e09417b55f1e37849b192bb"
118 | ],
119 | "index": "pypi",
120 | "version": "==2.1.2"
121 | },
122 | "ecdsa": {
123 | "hashes": [
124 | "sha256:64c613005f13efec6541bb0a33290d0d03c27abab5f15fbab20fb0ee162bdd8e",
125 | "sha256:e108a5fe92c67639abae3260e43561af914e7fd0d27bae6d2ec1312ae7934dfe"
126 | ],
127 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
128 | "version": "==0.14.1"
129 | },
130 | "fido2": {
131 | "hashes": [
132 | "sha256:8680ee25238e2307596eb3900a0f8c0d9cc91189146ed8039544f1a3a69dfe6e"
133 | ],
134 | "markers": "python_version not in '3.0, 3.1, 3.2, 3.3' and python_full_version >= '2.7.6'",
135 | "version": "==0.9.1"
136 | },
137 | "gunicorn": {
138 | "hashes": [
139 | "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e",
140 | "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"
141 | ],
142 | "index": "pypi",
143 | "version": "==20.1.0"
144 | },
145 | "jsonfield": {
146 | "hashes": [
147 | "sha256:7e4e84597de21eeaeeaaa7cc5da08c61c48a9b64d0c446b2d71255d01812887a",
148 | "sha256:df857811587f252b97bafba42e02805e70a398a7a47870bc6358a0308dd689ed"
149 | ],
150 | "index": "pypi",
151 | "version": "==3.1.0"
152 | },
153 | "jsonlookup": {
154 | "hashes": [
155 | "sha256:95bb1cd51e04e21c1b5a70ec5faa597b8435cb94da868fbbd5d1550fc7c88432"
156 | ],
157 | "version": "==0.9.0"
158 | },
159 | "psycopg2": {
160 | "hashes": [
161 | "sha256:00195b5f6832dbf2876b8bf77f12bdce648224c89c880719c745b90515233301",
162 | "sha256:068115e13c70dc5982dfc00c5d70437fe37c014c808acce119b5448361c03725",
163 | "sha256:26e7fd115a6db75267b325de0fba089b911a4a12ebd3d0b5e7acb7028bc46821",
164 | "sha256:2c93d4d16933fea5bbacbe1aaf8fa8c1348740b2e50b3735d1b0bf8154cbf0f3",
165 | "sha256:56007a226b8e95aa980ada7abdea6b40b75ce62a433bd27cec7a8178d57f4051",
166 | "sha256:56fee7f818d032f802b8eed81ef0c1232b8b42390df189cab9cfa87573fe52c5",
167 | "sha256:6a3d9efb6f36f1fe6aa8dbb5af55e067db802502c55a9defa47c5a1dad41df84",
168 | "sha256:a49833abfdede8985ba3f3ec641f771cca215479f41523e99dace96d5b8cce2a",
169 | "sha256:ad2fe8a37be669082e61fb001c185ffb58867fdbb3e7a6b0b0d2ffe232353a3e",
170 | "sha256:b8cae8b2f022efa1f011cc753adb9cbadfa5a184431d09b273fb49b4167561ad",
171 | "sha256:d160744652e81c80627a909a0e808f3c6653a40af435744de037e3172cf277f5",
172 | "sha256:d5062ae50b222da28253059880a871dc87e099c25cb68acf613d9d227413d6f7",
173 | "sha256:f22ea9b67aea4f4a1718300908a2fb62b3e4276cf00bd829a97ab5894af42ea3",
174 | "sha256:f974c96fca34ae9e4f49839ba6b78addf0346777b46c4da27a7bf54f48d3057d",
175 | "sha256:fb23f6c71107c37fd667cb4ea363ddeb936b348bbd6449278eb92c189699f543"
176 | ],
177 | "index": "pypi",
178 | "version": "==2.8.6"
179 | },
180 | "pyasn1": {
181 | "hashes": [
182 | "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
183 | "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
184 | "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
185 | "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
186 | "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
187 | "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
188 | "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
189 | "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
190 | "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
191 | "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
192 | "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
193 | "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
194 | "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
195 | ],
196 | "version": "==0.4.8"
197 | },
198 | "pycparser": {
199 | "hashes": [
200 | "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
201 | "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
202 | ],
203 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
204 | "version": "==2.20"
205 | },
206 | "pyotp": {
207 | "hashes": [
208 | "sha256:9d144de0f8a601d6869abe1409f4a3f75f097c37b50a36a3bf165810a6e23f28",
209 | "sha256:d28ddfd40e0c1b6a6b9da961c7d47a10261fb58f378cb00f05ce88b26df9c432"
210 | ],
211 | "version": "==2.6.0"
212 | },
213 | "python-decouple": {
214 | "hashes": [
215 | "sha256:2e5adb0263a4f963b58d7407c4760a2465d464ee212d733e2a2c179e54c08d8f",
216 | "sha256:a8268466e6389a639a20deab9d880faee186eb1eb6a05e54375bdf158d691981"
217 | ],
218 | "index": "pypi",
219 | "version": "==3.4"
220 | },
221 | "python-jose": {
222 | "hashes": [
223 | "sha256:4e4192402e100b5fb09de5a8ea6bcc39c36ad4526341c123d401e2561720335b",
224 | "sha256:67d7dfff599df676b04a996520d9be90d6cdb7e6dd10b4c7cacc0c3e2e92f2be"
225 | ],
226 | "index": "pypi",
227 | "version": "==3.2.0"
228 | },
229 | "python-u2flib-server": {
230 | "hashes": [
231 | "sha256:b5e1712bf8f703c6fc9bac6643efb2d57e6c9d9f0b9ab0c0df74981b2c349632"
232 | ],
233 | "version": "==5.0.1"
234 | },
235 | "pytz": {
236 | "hashes": [
237 | "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
238 | "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
239 | ],
240 | "version": "==2021.1"
241 | },
242 | "rsa": {
243 | "hashes": [
244 | "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2",
245 | "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"
246 | ],
247 | "markers": "python_version >= '3.5' and python_version < '4'",
248 | "version": "==4.7.2"
249 | },
250 | "simplejson": {
251 | "hashes": [
252 | "sha256:034550078a11664d77bc1a8364c90bb7eef0e44c2dbb1fd0a4d92e3997088667",
253 | "sha256:05b43d568300c1cd43f95ff4bfcff984bc658aa001be91efb3bb21df9d6288d3",
254 | "sha256:0dd9d9c738cb008bfc0862c9b8fa6743495c03a0ed543884bf92fb7d30f8d043",
255 | "sha256:10fc250c3edea4abc15d930d77274ddb8df4803453dde7ad50c2f5565a18a4bb",
256 | "sha256:2862beabfb9097a745a961426fe7daf66e1714151da8bb9a0c430dde3d59c7c0",
257 | "sha256:292c2e3f53be314cc59853bd20a35bf1f965f3bc121e007ab6fd526ed412a85d",
258 | "sha256:2d3eab2c3fe52007d703a26f71cf649a8c771fcdd949a3ae73041ba6797cfcf8",
259 | "sha256:2e7b57c2c146f8e4dadf84977a83f7ee50da17c8861fd7faf694d55e3274784f",
260 | "sha256:311f5dc2af07361725033b13cc3d0351de3da8bede3397d45650784c3f21fbcf",
261 | "sha256:344e2d920a7f27b4023c087ab539877a1e39ce8e3e90b867e0bfa97829824748",
262 | "sha256:3fabde09af43e0cbdee407555383063f8b45bfb52c361bc5da83fcffdb4fd278",
263 | "sha256:42b8b8dd0799f78e067e2aaae97e60d58a8f63582939af60abce4c48631a0aa4",
264 | "sha256:4b3442249d5e3893b90cb9f72c7d6ce4d2ea144d2c0d9f75b9ae1e5460f3121a",
265 | "sha256:55d65f9cc1b733d85ef95ab11f559cce55c7649a2160da2ac7a078534da676c8",
266 | "sha256:5c659a0efc80aaaba57fcd878855c8534ecb655a28ac8508885c50648e6e659d",
267 | "sha256:72d8a3ffca19a901002d6b068cf746be85747571c6a7ba12cbcf427bfb4ed971",
268 | "sha256:75ecc79f26d99222a084fbdd1ce5aad3ac3a8bd535cd9059528452da38b68841",
269 | "sha256:76ac9605bf2f6d9b56abf6f9da9047a8782574ad3531c82eae774947ae99cc3f",
270 | "sha256:7d276f69bfc8c7ba6c717ba8deaf28f9d3c8450ff0aa8713f5a3280e232be16b",
271 | "sha256:7f10f8ba9c1b1430addc7dd385fc322e221559d3ae49b812aebf57470ce8de45",
272 | "sha256:8042040af86a494a23c189b5aa0ea9433769cc029707833f261a79c98e3375f9",
273 | "sha256:813846738277729d7db71b82176204abc7fdae2f566e2d9fcf874f9b6472e3e6",
274 | "sha256:845a14f6deb124a3bcb98a62def067a67462a000e0508f256f9c18eff5847efc",
275 | "sha256:869a183c8e44bc03be1b2bbcc9ec4338e37fa8557fc506bf6115887c1d3bb956",
276 | "sha256:8acf76443cfb5c949b6e781c154278c059b09ac717d2757a830c869ba000cf8d",
277 | "sha256:8f713ea65958ef40049b6c45c40c206ab363db9591ff5a49d89b448933fa5746",
278 | "sha256:934115642c8ba9659b402c8bdbdedb48651fb94b576e3b3efd1ccb079609b04a",
279 | "sha256:9551f23e09300a9a528f7af20e35c9f79686d46d646152a0c8fc41d2d074d9b0",
280 | "sha256:9a2b7543559f8a1c9ed72724b549d8cc3515da7daf3e79813a15bdc4a769de25",
281 | "sha256:a55c76254d7cf8d4494bc508e7abb993a82a192d0db4552421e5139235604625",
282 | "sha256:ad8f41c2357b73bc9e8606d2fa226233bf4d55d85a8982ecdfd55823a6959995",
283 | "sha256:af4868da7dd53296cd7630687161d53a7ebe2e63814234631445697bd7c29f46",
284 | "sha256:afebfc3dd3520d37056f641969ce320b071bc7a0800639c71877b90d053e087f",
285 | "sha256:b59aa298137ca74a744c1e6e22cfc0bf9dca3a2f41f51bc92eb05695155d905a",
286 | "sha256:bc00d1210567a4cdd215ac6e17dc00cb9893ee521cee701adfd0fa43f7c73139",
287 | "sha256:c1cb29b1fced01f97e6d5631c3edc2dadb424d1f4421dad079cb13fc97acb42f",
288 | "sha256:c94dc64b1a389a416fc4218cd4799aa3756f25940cae33530a4f7f2f54f166da",
289 | "sha256:ceaa28a5bce8a46a130cd223e895080e258a88d51bf6e8de2fc54a6ef7e38c34",
290 | "sha256:cff6453e25204d3369c47b97dd34783ca820611bd334779d22192da23784194b",
291 | "sha256:d0b64409df09edb4c365d95004775c988259efe9be39697d7315c42b7a5e7e94",
292 | "sha256:d4813b30cb62d3b63ccc60dd12f2121780c7a3068db692daeb90f989877aaf04",
293 | "sha256:da3c55cdc66cfc3fffb607db49a42448785ea2732f055ac1549b69dcb392663b",
294 | "sha256:e058c7656c44fb494a11443191e381355388443d543f6fc1a245d5d238544396",
295 | "sha256:fed0f22bf1313ff79c7fc318f7199d6c2f96d4de3234b2f12a1eab350e597c06",
296 | "sha256:ffd4e4877a78c84d693e491b223385e0271278f5f4e1476a4962dca6824ecfeb"
297 | ],
298 | "markers": "python_version >= '2.5' and python_version not in '3.0, 3.1, 3.2, 3.3'",
299 | "version": "==3.17.2"
300 | },
301 | "six": {
302 | "hashes": [
303 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
304 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
305 | ],
306 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
307 | "version": "==1.16.0"
308 | },
309 | "sqlparse": {
310 | "hashes": [
311 | "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
312 | "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
313 | ],
314 | "markers": "python_version >= '3.5'",
315 | "version": "==0.4.1"
316 | },
317 | "ua-parser": {
318 | "hashes": [
319 | "sha256:46ab2e383c01dbd2ab284991b87d624a26a08f72da4d7d413f5bfab8b9036f8a",
320 | "sha256:47b1782ed130d890018d983fac37c2a80799d9e0b9c532e734c67cf70f185033"
321 | ],
322 | "version": "==0.10.0"
323 | },
324 | "user-agents": {
325 | "hashes": [
326 | "sha256:a98c4dc72ecbc64812c4534108806fb0a0b3a11ec3fd1eafe807cee5b0a942e7",
327 | "sha256:d36d25178db65308d1458c5fa4ab39c9b2619377010130329f3955e7626ead26"
328 | ],
329 | "version": "==2.2.0"
330 | },
331 | "whitenoise": {
332 | "hashes": [
333 | "sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7",
334 | "sha256:05d00198c777028d72d8b0bbd234db605ef6d60e9410125124002518a48e515d"
335 | ],
336 | "index": "pypi",
337 | "version": "==5.2.0"
338 | }
339 | },
340 | "develop": {}
341 | }
342 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: gunicorn django_mfa2_example.wsgi --log-file -
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # django_mfa2_example
2 |
3 | Fingerprint-based authentication and authorization system in Python (Django). This can be integrated with e-voting systems and other applications that should be very secure.
4 |
5 | A walk-through of this repository can be found on [dev.to](https://dev.to/) in this tutorial-like article [Fingerprint-based authentication and authorization in Python(Django) web applications](
6 | https://dev.to/sirneij/fingerprint-based-authentication-and-authorization-in-python-django-web-applications-2c6l).
7 | This example application uses [Django-mfa2](https://github.com/mkalioby/django-mfa2) to implement a password-less fingerprint-based authentication and authorization system. It's live and can be accessed [here](https://django-mfa2-example.herokuapp.com/).
8 | ## Run locally
9 |
10 | - clone this report:
11 | ```
12 | git clone https://github.com/Sirneij/django_mfa2_example.git
13 | ```
14 | - create and activate virtual environment (I used `pipenv` but you can stick with `venv`, `virtualenv` or `poetry`):
15 | ```
16 | pipenv shell
17 | pipenv install
18 | ```
19 | - makemigrations and migrate:
20 | ```
21 | python manage.py makemigrations
22 | python manage.py migrate
23 | ```
24 | - optionally, createsuperuser:
25 | ```
26 | python manage.py createsuperuser
27 | ```
28 |
--------------------------------------------------------------------------------
/accounts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sirneij/django_mfa2_example/d9505213e7e914481f06ea3426a515f5e0d26bc2/accounts/__init__.py
--------------------------------------------------------------------------------
/accounts/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import User
3 | from django.contrib.auth.admin import UserAdmin
4 |
5 | class CustomUserAdmin(UserAdmin):
6 | model = User
7 | readonly_fields = ['date_joined', ]
8 | actions = ['activate_users', ]
9 | list_display = ('username','display_name', 'email', 'first_name', 'last_name',
10 | 'is_staff',)
11 |
12 | def get_inline_instances(self, request, obj=None):
13 | if not obj:
14 | return list()
15 | return super(CustomUserAdmin, self).get_inline_instances(request, obj)
16 |
17 | def get_form(self, request, obj=None, **kwargs):
18 | form = super().get_form(request, obj, **kwargs)
19 | is_superuser = request.user.is_superuser
20 | disabled_fields = set()
21 |
22 | if not is_superuser:
23 | disabled_fields |= {
24 | 'username',
25 | 'is_superuser',
26 | }
27 | # Prevent non-superusers from editing their own permissions
28 | if (
29 | not is_superuser
30 | and obj is not None
31 | and obj == request.user
32 | ):
33 | disabled_fields |= {
34 | 'is_staff',
35 | 'is_superuser',
36 | 'groups',
37 | 'user_permissions',
38 | }
39 | for f in disabled_fields:
40 | if f in form.base_fields:
41 | form.base_fields[f].disabled = True
42 |
43 | return form
44 |
45 | def activate_users(self, request, queryset):
46 | cannot = queryset.filter(is_active=False).update(is_active=True)
47 | self.message_user(request, 'Activated {} users.'.format(cannot))
48 | activate_users.short_description = 'Activate Users' # type: ignore
49 |
50 | def get_actions(self, request):
51 | actions = super().get_actions(request)
52 | if not request.user.has_perm('auth.change_user'):
53 | del actions['activate_users']
54 | return actions
55 |
56 |
57 | admin.site.register(User, CustomUserAdmin)
58 |
--------------------------------------------------------------------------------
/accounts/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class AccountsConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'accounts'
7 |
--------------------------------------------------------------------------------
/accounts/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.3 on 2021-05-26 09:05
2 |
3 | import django.contrib.auth.models
4 | import django.contrib.auth.validators
5 | from django.db import migrations, models
6 | import django.utils.timezone
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | ('auth', '0012_alter_user_first_name_max_length'),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='User',
20 | fields=[
21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22 | ('password', models.CharField(max_length=128, verbose_name='password')),
23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
25 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
26 | ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
27 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
28 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
29 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
30 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
31 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
32 | ('display_name', models.CharField(max_length=32)),
33 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
34 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
35 | ],
36 | options={
37 | 'verbose_name': 'user',
38 | 'verbose_name_plural': 'users',
39 | 'abstract': False,
40 | },
41 | managers=[
42 | ('objects', django.contrib.auth.models.UserManager()),
43 | ],
44 | ),
45 | ]
46 |
--------------------------------------------------------------------------------
/accounts/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sirneij/django_mfa2_example/d9505213e7e914481f06ea3426a515f5e0d26bc2/accounts/migrations/__init__.py
--------------------------------------------------------------------------------
/accounts/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import AbstractUser
3 |
4 | class User(AbstractUser):
5 | display_name = models.CharField(max_length=32)
6 |
7 |
--------------------------------------------------------------------------------
/accounts/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% load static %}
4 |
5 | {% block title %} {{page_title}} {% endblock title %}
6 |
7 | {% block css %}
8 |
9 | {% endblock css %}
10 |
11 | {% block content %}
12 |
13 |
14 |
15 |
16 |
Cover
17 |
18 | Home
24 |
25 | {% if request.user.is_authenticated %}
26 | Logout
27 | {% else %}
28 | Register
29 | Login
30 | {% endif %}
31 |
32 |
33 |
34 |
35 |
36 | {% if request.user.is_authenticated %}
37 |
38 | You are logged in!!!!
39 |
40 | You have successfully used Django-mfa2 biometric authentication to log
41 | in. Hurray!
42 |
43 |
44 | Learn more
49 |
50 | {% else %}
51 |
52 | Cover your page.
53 |
54 | Cover is a one-page template for building simple and beautiful home
55 | pages. Download, edit the text, and add your own fullscreen background
56 | photo to make it your own.
57 |
58 |
59 | Learn more
64 |
65 | {% endif %}
66 |
67 |
68 |
69 |
70 | Cover template for
71 | Bootstrap , by
72 | @mdo .
73 |
74 |
75 |
76 |
77 |
78 | {% endblock content %}
79 |
--------------------------------------------------------------------------------
/accounts/templates/login.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% load static %}
4 |
5 | {% block css %}
6 |
60 | {% endblock css %}
61 |
62 |
63 | {% block title %} {{page_title}} {% endblock title %}
64 |
65 |
66 | {% block content %}
67 |
68 |
69 |
99 |
100 |
101 | {% endblock content %}
102 |
--------------------------------------------------------------------------------
/accounts/templates/mfa_auth_base.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
--------------------------------------------------------------------------------
/accounts/templates/register.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 |
4 | {% block title %} {{page_title}} {% endblock title %}
5 |
6 |
7 | {% load static %}
8 |
9 | {% block css %}
10 |
64 | {% endblock css %}
65 |
66 | {% block content %}
67 |
68 |
69 |
114 |
115 |
116 | {% endblock content %}
117 |
--------------------------------------------------------------------------------
/accounts/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/accounts/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 | from django.contrib.auth import views as auth_views
4 |
5 | app_name = 'accounts'
6 |
7 | urlpatterns = [
8 | path('', views.index, name='index'),
9 | path('login/', views.login, name='login'),
10 | path('register/', views.register, name='register'),
11 | path('logout/', auth_views.LogoutView.as_view(), name='logout'),
12 | ]
13 |
--------------------------------------------------------------------------------
/accounts/utils.py:
--------------------------------------------------------------------------------
1 | import six
2 |
3 | USERNAME_MAX_LENGTH = 32
4 | DISPLAY_NAME_MAX_LENGTH = 65
5 |
6 | def validate_username(username):
7 | if not isinstance(username, six.string_types):
8 | return False
9 |
10 | if len(username) > USERNAME_MAX_LENGTH:
11 | return False
12 |
13 | if not username.isalnum():
14 | return False
15 |
16 | if not username.lower().startswith("cpe"):
17 | return False
18 |
19 | return True
20 |
21 |
22 | def validate_display_name(display_name):
23 | if not isinstance(display_name, six.string_types):
24 | return False
25 |
26 | if len(display_name) > DISPLAY_NAME_MAX_LENGTH:
27 | return False
28 |
29 | if not display_name.replace(' ', '').isalnum():
30 | return False
31 |
32 | return True
--------------------------------------------------------------------------------
/accounts/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render, redirect
2 | from django.urls import reverse
3 | from django.contrib import auth
4 | from django.utils import timezone
5 | from django.conf import settings
6 | from .models import User
7 | from . import utils
8 |
9 | def login_user_in(request, username):
10 | user=User.objects.get(username=username)
11 | user.backend='django.contrib.auth.backends.ModelBackend'
12 | auth.login(request, user)
13 | if "redirect" in request.POST:
14 | return redirect(request.POST["redirect"])
15 | else:
16 | return redirect(reverse('accounts:index'))
17 |
18 | def login(request):
19 | if request.method == "POST":
20 | username = request.POST.get('username').replace('/', '')
21 | user = User.objects.filter(username=username).first()
22 | err=""
23 | if user is not None:
24 | if user.is_active:
25 | if "mfa" in settings.INSTALLED_APPS:
26 | from mfa.helpers import has_mfa
27 | res = has_mfa(request,username=username)
28 | if res: return res
29 | return login_user_in(request, username)
30 | else:
31 | err="This student is NOT activated yet."
32 | else:
33 | err="No student with such matriculation number exists."
34 | return render(request, 'login.html', {"err":err})
35 | else:
36 | return render(request, 'login.html')
37 |
38 | def register(request):
39 | if request.method == "POST":
40 | error = ''
41 | username = request.POST.get('username').replace('/', '')
42 | display_name = request.POST.get('display-name')
43 | if not utils.validate_username(username):
44 | error = 'Invalid matriculation number'
45 | return render(request, 'register.html', context = {'page_title': "Register", 'error': error})
46 | if not utils.validate_display_name(display_name):
47 | error = 'Invalid display name'
48 | return render(request, 'register.html', context = {'page_title': "Register", 'error': error})
49 | if User.objects.filter(username=username).exists():
50 | error = 'Student already exists.'
51 | return render(request, 'register.html', context = {'page_title': "Register", 'error': error})
52 | else:
53 | u = User.objects.create(first_name = display_name, password='none', is_superuser=False, username=username, last_name='', display_name=display_name, email='none', is_staff=False, is_active=True,date_joined=timezone.now())
54 | u.backend = 'django.contrib.auth.backends.ModelBackend'
55 | auth.login(request,u)
56 | return redirect(reverse('start_fido2'))
57 | else:
58 | return render(request, 'register.html', context = {'page_title': "Register"})
59 |
60 | def index(request):
61 | return render(request, 'index.html', {"page_title": "Welcome home"})
--------------------------------------------------------------------------------
/db.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sirneij/django_mfa2_example/d9505213e7e914481f06ea3426a515f5e0d26bc2/db.sqlite3
--------------------------------------------------------------------------------
/django_mfa2_example/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sirneij/django_mfa2_example/d9505213e7e914481f06ea3426a515f5e0d26bc2/django_mfa2_example/__init__.py
--------------------------------------------------------------------------------
/django_mfa2_example/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for django_mfa2_example project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.asgi import get_asgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_mfa2_example.settings')
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/django_mfa2_example/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for django_mfa2_example project.
3 |
4 | Generated by 'django-admin startproject' using Django 3.2.3.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.2/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/3.2/ref/settings/
11 | """
12 |
13 | from pathlib import Path
14 | from decouple import config
15 |
16 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
17 | BASE_DIR = Path(__file__).resolve().parent.parent
18 |
19 |
20 | # Quick-start development settings - unsuitable for production
21 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
22 |
23 | # SECURITY WARNING: keep the secret key used in production secret!
24 | SECRET_KEY = config('SECRET_KEY', default='django-insecure-k+*^qwc1ud*t5uvsc3@j2&(3567-x+6p)4$rx7gpt$gi@&rx5d')
25 |
26 | # SECURITY WARNING: don't run with debug turned on in production!
27 | DEBUG = config('DEBUG', default=True, cast=bool)
28 |
29 | ALLOWED_HOSTS = ['*']
30 |
31 |
32 | # Application definition
33 |
34 | INSTALLED_APPS = [
35 | 'django.contrib.admin',
36 | 'django.contrib.auth',
37 | 'django.contrib.contenttypes',
38 | 'django.contrib.sessions',
39 | 'django.contrib.messages',
40 | 'django.contrib.staticfiles',
41 | 'mfa',
42 |
43 | 'accounts.apps.AccountsConfig',
44 | ]
45 |
46 | MIDDLEWARE = [
47 | 'django.middleware.security.SecurityMiddleware',
48 | 'django.contrib.sessions.middleware.SessionMiddleware',
49 | 'django.middleware.common.CommonMiddleware',
50 | 'django.middleware.csrf.CsrfViewMiddleware',
51 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
52 | 'django.contrib.messages.middleware.MessageMiddleware',
53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
54 | 'whitenoise.middleware.WhiteNoiseMiddleware',
55 | ]
56 |
57 | ROOT_URLCONF = 'django_mfa2_example.urls'
58 |
59 | TEMPLATES = [
60 | {
61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
62 | 'DIRS': [BASE_DIR / 'templates'],
63 | 'APP_DIRS': True,
64 | 'OPTIONS': {
65 | 'context_processors': [
66 | 'django.template.context_processors.debug',
67 | 'django.template.context_processors.request',
68 | 'django.contrib.auth.context_processors.auth',
69 | 'django.contrib.messages.context_processors.messages',
70 | ],
71 | },
72 | },
73 | ]
74 |
75 | WSGI_APPLICATION = 'django_mfa2_example.wsgi.application'
76 |
77 |
78 | # Database
79 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases
80 |
81 | DATABASES = {
82 | 'default': {
83 | 'ENGINE': 'django.db.backends.sqlite3',
84 | 'NAME': BASE_DIR / 'db.sqlite3',
85 | }
86 | }
87 |
88 |
89 | # Password validation
90 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
91 |
92 | AUTH_PASSWORD_VALIDATORS = [
93 | {
94 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
95 | },
96 | {
97 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
98 | },
99 | {
100 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
101 | },
102 | {
103 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
104 | },
105 | ]
106 |
107 |
108 | # Internationalization
109 | # https://docs.djangoproject.com/en/3.2/topics/i18n/
110 |
111 | LANGUAGE_CODE = 'en-us'
112 |
113 | TIME_ZONE = 'UTC'
114 |
115 | USE_I18N = True
116 |
117 | USE_L10N = True
118 |
119 | USE_TZ = True
120 |
121 |
122 | # Static files (CSS, JavaScript, Images)
123 | # https://docs.djangoproject.com/en/3.2/howto/static-files/
124 |
125 | STATIC_URL = '/static/'
126 | STATICFILES_DIRS = (BASE_DIR / 'static',)
127 | STATIC_ROOT = BASE_DIR / 'staticfiles'
128 |
129 |
130 | # Default primary key field type
131 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
132 |
133 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
134 | AUTH_USER_MODEL = 'accounts.User'
135 |
136 | LOGOUT_REDIRECT_URL='accounts:index'
137 |
138 |
139 |
140 | MFA_UNALLOWED_METHODS=() # Methods that shouldn't be allowed for the user
141 | MFA_LOGIN_CALLBACK="accounts.views.login_user_in" # A function that should be called by username to login the user in session
142 | MFA_RECHECK=True # Allow random rechecking of the user
143 | MFA_RECHECK_MIN=10 # Minimum interval in seconds
144 | MFA_RECHECK_MAX=30 # Maximum in seconds
145 | MFA_QUICKLOGIN=True
146 |
147 | TOKEN_ISSUER_NAME="django_mfa2_example" #TOTP Issuer name
148 |
149 | if DEBUG:
150 | U2F_APPID="https://localhost" #URL For U2F
151 | FIDO_SERVER_ID=u"localhost" # Server rp id for FIDO2, it the full domain of your project
152 | else:
153 | U2F_APPID="https://django-mfa2-example.herokuapp.com" #URL For U2F
154 | FIDO_SERVER_ID=u"django-mfa2-example.herokuapp.com" # Server rp id for FIDO2, it the full domain of your project
155 |
156 | FIDO_SERVER_NAME=u"django_mfa2_example"
157 |
158 | import dj_database_url
159 | db_from_env = dj_database_url.config(conn_max_age=500)
160 | DATABASES['default'].update(db_from_env)
161 |
--------------------------------------------------------------------------------
/django_mfa2_example/urls.py:
--------------------------------------------------------------------------------
1 | """django_mfa2_example URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.contrib import admin
17 | from django.conf import settings
18 | from django.conf.urls.static import static
19 | from django.urls import path, include
20 | import mfa
21 | import mfa.TrustedDevice
22 |
23 | urlpatterns = [
24 | path('admin/', admin.site.urls),
25 | path('mfa/', include('mfa.urls')),
26 | path('devices/add/', mfa.TrustedDevice.add,name="mfa_add_new_trusted_device"),
27 | path("", include('accounts.urls', namespace='accounts'))
28 | ]
29 |
30 | if settings.DEBUG:
31 | urlpatterns += static(settings.STATIC_URL,
32 | document_root=settings.STATIC_ROOT)
--------------------------------------------------------------------------------
/django_mfa2_example/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for django_mfa2_example project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_mfa2_example.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_mfa2_example.settings')
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == '__main__':
22 | main()
23 |
--------------------------------------------------------------------------------
/mfa/ApproveLogin.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sirneij/django_mfa2_example/d9505213e7e914481f06ea3426a515f5e0d26bc2/mfa/ApproveLogin.py
--------------------------------------------------------------------------------
/mfa/Common.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.core.mail import EmailMessage
3 |
4 | def send(to,subject,body):
5 | from_email_address = settings.EMAIL_HOST_USER
6 | if '@' not in from_email_address:
7 | from_email_address = settings.DEFAULT_FROM_EMAIL
8 | From = "%s <%s>" % (settings.EMAIL_FROM, from_email_address)
9 | email = EmailMessage(subject,body,From,to)
10 | email.content_subtype = "html"
11 | return email.send(False)
--------------------------------------------------------------------------------
/mfa/Email.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 | from django.views.decorators.cache import never_cache
3 | from django.template.context_processors import csrf
4 | import datetime,random
5 | from random import randint
6 | from .models import *
7 | #from django.template.context import RequestContext
8 | from .views import login
9 | from .Common import send
10 | def sendEmail(request,username,secret):
11 | from django.contrib.auth import get_user_model
12 | User = get_user_model()
13 | key = getattr(User, 'USERNAME_FIELD', 'username')
14 | kwargs = {key: username}
15 | user = User.objects.get(**kwargs)
16 | res=render(request,"mfa_email_token_template.html",{"request":request,"user":user,'otp':secret})
17 | return send([user.email],"OTP", res.content.decode())
18 |
19 | @never_cache
20 | def start(request):
21 | context = csrf(request)
22 | if request.method == "POST":
23 | if request.session["email_secret"] == request.POST["otp"]:
24 | uk=User_Keys()
25 | uk.username=request.user.username
26 | uk.key_type="Email"
27 | uk.enabled=1
28 | uk.save()
29 | from django.http import HttpResponseRedirect
30 | try:
31 | from django.core.urlresolvers import reverse
32 | except:
33 | from django.urls import reverse
34 | return HttpResponseRedirect(reverse('mfa_home'))
35 | context["invalid"] = True
36 | else:
37 | request.session["email_secret"] = str(randint(0,100000))
38 | if sendEmail(request, request.user.username, request.session["email_secret"]):
39 | context["sent"] = True
40 | return render(request,"Email/Add.html", context)
41 | @never_cache
42 | def auth(request):
43 | context=csrf(request)
44 | if request.method=="POST":
45 | if request.session["email_secret"]==request.POST["otp"].strip():
46 | uk = User_Keys.objects.get(username=request.session["base_username"], key_type="Email")
47 | mfa = {"verified": True, "method": "Email","id":uk.id}
48 | if getattr(settings, "MFA_RECHECK", False):
49 | mfa["next_check"] = datetime.datetime.timestamp(datetime.datetime.now() + datetime.timedelta(
50 | seconds = random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX)))
51 | request.session["mfa"] = mfa
52 |
53 | from django.utils import timezone
54 | uk.last_used=timezone.now()
55 | uk.save()
56 | return login(request)
57 | context["invalid"]=True
58 | else:
59 | request.session["email_secret"] = str(randint(0, 100000))
60 | if sendEmail(request, request.session["base_username"], request.session["email_secret"]):
61 | context["sent"] = True
62 | return render(request,"Email/Auth.html", context)
63 |
--------------------------------------------------------------------------------
/mfa/FIDO2.py:
--------------------------------------------------------------------------------
1 | from fido2.client import ClientData
2 | from fido2.server import Fido2Server, PublicKeyCredentialRpEntity
3 | from fido2.ctap2 import AttestationObject, AuthenticatorData
4 | from django.template.context_processors import csrf
5 | from django.views.decorators.csrf import csrf_exempt
6 | from django.shortcuts import render
7 | # from django.template.context import RequestContext
8 | import simplejson
9 | from fido2 import cbor
10 | from django.http import HttpResponse
11 | from django.conf import settings
12 | from .models import *
13 | from fido2.utils import websafe_decode, websafe_encode
14 | from fido2.ctap2 import AttestedCredentialData
15 | from .views import login, reset_cookie
16 | import datetime
17 | from django.utils import timezone
18 |
19 |
20 | def recheck(request):
21 | context = csrf(request)
22 | context["mode"] = "recheck"
23 | request.session["mfa_recheck"] = True
24 | return render(request, "FIDO2/recheck.html", context)
25 |
26 |
27 | def getServer():
28 | rp = PublicKeyCredentialRpEntity(settings.FIDO_SERVER_ID, settings.FIDO_SERVER_NAME)
29 | return Fido2Server(rp)
30 |
31 |
32 | def begin_registeration(request):
33 | server = getServer()
34 | registration_data, state = server.register_begin({
35 | u'id': request.user.username.encode("utf8"),
36 | u'name': (request.user.first_name + " " + request.user.last_name),
37 | u'displayName': request.user.username,
38 | }, getUserCredentials(request.user.username))
39 | request.session['fido_state'] = state
40 |
41 | return HttpResponse(cbor.encode(registration_data), content_type = 'application/octet-stream')
42 |
43 |
44 | @csrf_exempt
45 | def complete_reg(request):
46 | try:
47 | data = cbor.decode(request.body)
48 |
49 | client_data = ClientData(data['clientDataJSON'])
50 | att_obj = AttestationObject((data['attestationObject']))
51 | server = getServer()
52 | auth_data = server.register_complete(
53 | request.session['fido_state'],
54 | client_data,
55 | att_obj
56 | )
57 | encoded = websafe_encode(auth_data.credential_data)
58 | uk = User_Keys()
59 | uk.username = request.user.username
60 | uk.properties = {"device": encoded, "type": att_obj.fmt, }
61 | uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False)
62 | uk.key_type = "FIDO2"
63 | uk.save()
64 | return HttpResponse(simplejson.dumps({'status': 'OK'}))
65 | except Exception as exp:
66 | try:
67 | from raven.contrib.django.raven_compat.models import client
68 | client.captureException()
69 | except:
70 | pass
71 | return HttpResponse(simplejson.dumps({'status': 'ERR', "message": "Error on server, please try again later"}))
72 |
73 |
74 | def start(request):
75 | context = csrf(request)
76 | return render(request, "FIDO2/Add.html", context)
77 |
78 |
79 | def getUserCredentials(username):
80 | credentials = []
81 | for uk in User_Keys.objects.filter(username = username, key_type = "FIDO2"):
82 | credentials.append(AttestedCredentialData(websafe_decode(uk.properties["device"])))
83 | return credentials
84 |
85 |
86 | def auth(request):
87 | context = csrf(request)
88 | return render(request, "FIDO2/Auth.html", context)
89 |
90 |
91 | def authenticate_begin(request):
92 | server = getServer()
93 | credentials = getUserCredentials(request.session.get("base_username", request.user.username))
94 | auth_data, state = server.authenticate_begin(credentials)
95 | request.session['fido_state'] = state
96 | return HttpResponse(cbor.encode(auth_data), content_type = "application/octet-stream")
97 |
98 |
99 | @csrf_exempt
100 | def authenticate_complete(request):
101 | try:
102 | credentials = []
103 | username = request.session.get("base_username", request.user.username)
104 | server = getServer()
105 | credentials = getUserCredentials(username)
106 | data = cbor.decode(request.body)
107 | credential_id = data['credentialId']
108 | client_data = ClientData(data['clientDataJSON'])
109 | auth_data = AuthenticatorData(data['authenticatorData'])
110 | signature = data['signature']
111 | try:
112 | cred = server.authenticate_complete(
113 | request.session.pop('fido_state'),
114 | credentials,
115 | credential_id,
116 | client_data,
117 | auth_data,
118 | signature
119 | )
120 | except ValueError:
121 | return HttpResponse(simplejson.dumps({'status': "ERR",
122 | "message": "Wrong challenge received, make sure that this is your security and try again."}),
123 | content_type = "application/json")
124 | except Exception as excep:
125 | try:
126 | from raven.contrib.django.raven_compat.models import client
127 | client.captureException()
128 | except:
129 | pass
130 | return HttpResponse(simplejson.dumps({'status': "ERR",
131 | "message": excep.message}),
132 | content_type = "application/json")
133 |
134 | if request.session.get("mfa_recheck", False):
135 | import time
136 | request.session["mfa"]["rechecked_at"] = time.time()
137 | return HttpResponse(simplejson.dumps({'status': "OK"}),
138 | content_type = "application/json")
139 | else:
140 | import random
141 | keys = User_Keys.objects.filter(username = username, key_type = "FIDO2", enabled = 1)
142 | for k in keys:
143 | if AttestedCredentialData(websafe_decode(k.properties["device"])).credential_id == cred.credential_id:
144 | k.last_used = timezone.now()
145 | k.save()
146 | mfa = {"verified": True, "method": "FIDO2", 'id': k.id}
147 | if getattr(settings, "MFA_RECHECK", False):
148 | mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now() + datetime.timedelta(
149 | seconds = random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
150 | request.session["mfa"] = mfa
151 | try:
152 | authenticated = request.user.is_authenticated
153 | except:
154 | authenticated = request.user.is_authenticated()
155 | if not authenticated:
156 | res = login(request)
157 | if not "location" in res: return reset_cookie(request)
158 | return HttpResponse(simplejson.dumps({'status': "OK", "redirect": res["location"]}),
159 | content_type = "application/json")
160 | return HttpResponse(simplejson.dumps({'status': "OK"}),
161 | content_type = "application/json")
162 | except Exception as exp:
163 | return HttpResponse(simplejson.dumps({'status': "ERR", "message": exp.message}),
164 | content_type = "application/json")
165 |
--------------------------------------------------------------------------------
/mfa/TrustedDevice.py:
--------------------------------------------------------------------------------
1 | import string
2 | import random
3 | from django.shortcuts import render
4 | from django.http import HttpResponse
5 | from django.template.context import RequestContext
6 | from django.template.context_processors import csrf
7 | from .models import *
8 | import user_agents
9 | from django.utils import timezone
10 |
11 | def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
12 | x=''.join(random.choice(chars) for _ in range(size))
13 | if not User_Keys.objects.filter(properties__shas="$.key="+x).exists(): return x
14 | else: return id_generator(size,chars)
15 |
16 | def getUserAgent(request):
17 | id=id=request.session.get("td_id",None)
18 | if id:
19 | tk=User_Keys.objects.get(id=id)
20 | if tk.properties.get("user_agent","")!="":
21 | ua = user_agents.parse(tk.properties["user_agent"])
22 | res = render(None, "TrustedDevices/user-agent.html", context={"ua":ua})
23 | return HttpResponse(res)
24 | return HttpResponse("")
25 |
26 | def trust_device(request):
27 | tk = User_Keys.objects.get(id=request.session["td_id"])
28 | tk.properties["status"]="trusted"
29 | tk.save()
30 | del request.session["td_id"]
31 | return HttpResponse("OK")
32 |
33 | def checkTrusted(request):
34 | res = ""
35 | id=request.session.get("td_id","")
36 | if id!="":
37 | try:
38 | tk = User_Keys.objects.get(id=id)
39 | if tk.properties["status"] == "trusted": res = "OK"
40 | except:
41 | pass
42 | return HttpResponse(res)
43 |
44 | def getCookie(request):
45 | tk = User_Keys.objects.get(id=request.session["td_id"])
46 |
47 | if tk.properties["status"] == "trusted":
48 | context={"added":True}
49 | response = render(request,"TrustedDevices/Done.html", context)
50 | from datetime import datetime, timedelta
51 | expires = datetime.now() + timedelta(days=180)
52 | tk.expires=expires
53 | tk.save()
54 | response.set_cookie("deviceid", tk.properties["signature"], expires=expires)
55 | return response
56 |
57 | def add(request):
58 | context=csrf(request)
59 | if request.method=="GET":
60 | return render(request,"TrustedDevices/Add.html",context)
61 | else:
62 | key=request.POST["key"].replace("-","").replace(" ","").upper()
63 | context["username"] = request.POST["username"]
64 | context["key"] = request.POST["key"]
65 | trusted_keys=User_Keys.objects.filter(username=request.POST["username"],properties__has="$.key="+key)
66 | cookie=False
67 | if trusted_keys.exists():
68 | tk=trusted_keys[0]
69 | request.session["td_id"]=tk.id
70 | ua=request.META['HTTP_USER_AGENT']
71 | agent=user_agents.parse(ua)
72 | if agent.is_pc:
73 | context["invalid"]="This is a PC, it can't used as a trusted device."
74 | else:
75 | tk.properties["user_agent"]=ua
76 | tk.save()
77 | context["success"]=True
78 | # tk.properties["user_agent"]=ua
79 | # tk.save()
80 | # context["success"]=True
81 |
82 | else:
83 | context["invalid"]="The username or key is wrong, please check and try again."
84 |
85 | return render(request,"TrustedDevices/Add.html", context)
86 |
87 | def start(request):
88 | if User_Keys.objects.filter(username=request.user.username,key_type="Trusted Device").count()>= 2:
89 | return render(request,"TrustedDevices/start.html",{"not_allowed":True})
90 | td=None
91 | if not request.session.get("td_id",None):
92 | td=User_Keys()
93 | td.username=request.user.username
94 | td.properties={"key":id_generator(),"status":"adding"}
95 | td.key_type="Trusted Device"
96 | td.save()
97 | request.session["td_id"]=td.id
98 | try:
99 | if td==None: td=User_Keys.objects.get(id=request.session["td_id"])
100 | context={"key":td.properties["key"]}
101 | except:
102 | del request.session["td_id"]
103 | return start(request)
104 | return render(request,"TrustedDevices/start.html",context)
105 |
106 | def send_email(request):
107 | body=render(request,"TrustedDevices/email.html",{}).content
108 | from .Common import send
109 | e=request.user.email
110 | if e=="":
111 | e=request.session.get("user",{}).get("email","")
112 | if e=="":
113 | res = "User has no email on the system."
114 | elif send([e],"Add Trusted Device Link",body):
115 | res="Sent Successfully"
116 | else:
117 | res="Error occured, please try again later."
118 | return HttpResponse(res)
119 |
120 |
121 | def verify(request):
122 | if request.COOKIES.get('deviceid',None):
123 | from jose import jwt
124 | json= jwt.decode(request.COOKIES.get('deviceid'),settings.SECRET_KEY)
125 | if json["username"].lower()== request.session['base_username'].lower():
126 | try:
127 | uk = User_Keys.objects.get(username=request.POST["username"].lower(), properties__has="$.key=" + json["key"])
128 | if uk.enabled and uk.properties["status"] == "trusted":
129 | uk.last_used=timezone.now()
130 | uk.save()
131 | request.session["mfa"] = {"verified": True, "method": "Trusted Device","id":uk.id}
132 | return True
133 | except:
134 | return False
135 | return False
136 |
--------------------------------------------------------------------------------
/mfa/U2F.py:
--------------------------------------------------------------------------------
1 |
2 | from u2flib_server.u2f import (begin_registration, begin_authentication,
3 | complete_registration, complete_authentication)
4 | from cryptography import x509
5 | from cryptography.hazmat.backends import default_backend
6 | from cryptography.hazmat.primitives.serialization import Encoding
7 | from django.shortcuts import render
8 | import simplejson
9 | #from django.template.context import RequestContext
10 | from django.template.context_processors import csrf
11 | from django.conf import settings
12 | from django.http import HttpResponse
13 | from .models import *
14 | from .views import login
15 | import datetime
16 | from django.utils import timezone
17 |
18 | def recheck(request):
19 | context = csrf(request)
20 | context["mode"]="recheck"
21 | s = sign(request.user.username)
22 | request.session["_u2f_challenge_"] = s[0]
23 | context["token"] = s[1]
24 | request.session["mfa_recheck"]=True
25 | return render(request,"U2F/recheck.html", context)
26 |
27 | def process_recheck(request):
28 | x=validate(request,request.user.username)
29 | if x==True:
30 | import time
31 | request.session["mfa"]["rechecked_at"] = time.time()
32 | return HttpResponse(simplejson.dumps({"recheck":True}),content_type="application/json")
33 | return x
34 |
35 | def check_errors(request, data):
36 | if "errorCode" in data:
37 | if data["errorCode"] == 0: return True
38 | if data["errorCode"] == 4:
39 | return HttpResponse("Invalid Security Key")
40 | if data["errorCode"] == 1:
41 | return auth(request)
42 | return True
43 | def validate(request,username):
44 | import datetime, random
45 |
46 | data = simplejson.loads(request.POST["response"])
47 |
48 | res= check_errors(request,data)
49 | if res!=True:
50 | return res
51 |
52 | challenge = request.session.pop('_u2f_challenge_')
53 | device, c, t = complete_authentication(challenge, data, [settings.U2F_APPID])
54 |
55 | key=User_Keys.objects.get(username=username,properties__shas="$.device.publicKey=%s"%device["publicKey"])
56 | key.last_used=timezone.now()
57 | key.save()
58 | mfa = {"verified": True, "method": "U2F","id":key.id}
59 | if getattr(settings, "MFA_RECHECK", False):
60 | mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now()
61 | + datetime.timedelta(
62 | seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
63 | request.session["mfa"] = mfa
64 | return True
65 |
66 | def auth(request):
67 | context=csrf(request)
68 | s=sign(request.session["base_username"])
69 | request.session["_u2f_challenge_"]=s[0]
70 | context["token"]=s[1]
71 |
72 | return render(request,"U2F/Auth.html")
73 |
74 | def start(request):
75 | enroll = begin_registration(settings.U2F_APPID, [])
76 | request.session['_u2f_enroll_'] = enroll.json
77 | context=csrf(request)
78 | context["token"]=simplejson.dumps(enroll.data_for_client)
79 | return render(request,"U2F/Add.html",context)
80 |
81 |
82 | def bind(request):
83 | import hashlib
84 | enroll = request.session['_u2f_enroll_']
85 | data=simplejson.loads(request.POST["response"])
86 | device, cert = complete_registration(enroll, data, [settings.U2F_APPID])
87 | cert = x509.load_der_x509_certificate(cert, default_backend())
88 | cert_hash=hashlib.md5(cert.public_bytes(Encoding.PEM)).hexdigest()
89 | q=User_Keys.objects.filter(key_type="U2F", properties__icontains= cert_hash)
90 | if q.exists():
91 | return HttpResponse("This key is registered before, it can't be registered again.")
92 | User_Keys.objects.filter(username=request.user.username,key_type="U2F").delete()
93 | uk = User_Keys()
94 | uk.username = request.user.username
95 | uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False)
96 | uk.properties = {"device":simplejson.loads(device.json),"cert":cert_hash}
97 | uk.key_type = "U2F"
98 | uk.save()
99 | return HttpResponse("OK")
100 |
101 | def sign(username):
102 | u2f_devices=[d.properties["device"] for d in User_Keys.objects.filter(username=username,key_type="U2F")]
103 | challenge = begin_authentication(settings.U2F_APPID, u2f_devices)
104 | return [challenge.json,simplejson.dumps(challenge.data_for_client)]
105 |
106 | def verify(request):
107 | x= validate(request,request.session["base_username"])
108 | if x==True:
109 | return login(request)
110 | else: return x
111 |
--------------------------------------------------------------------------------
/mfa/__init__.py:
--------------------------------------------------------------------------------
1 | __version__="2.1.2"
2 |
--------------------------------------------------------------------------------
/mfa/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from mfa.models import User_Keys
3 |
4 |
5 | @admin.register(User_Keys)
6 | class User_KeysAdmin(admin.ModelAdmin):
7 | list_display = ('username', 'added_on', 'key_type', 'owned_by_enterprise',)
8 |
--------------------------------------------------------------------------------
/mfa/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | class myAppNameConfig(AppConfig):
3 | name = 'mfa'
4 | verbose_name = 'A Much Better Name'
--------------------------------------------------------------------------------
/mfa/helpers.py:
--------------------------------------------------------------------------------
1 | import pyotp
2 | from .models import *
3 | from . import TrustedDevice, U2F, FIDO2, totp
4 | import simplejson
5 | from django.shortcuts import HttpResponse
6 | from mfa.views import verify,goto
7 | def has_mfa(request,username):
8 | if User_Keys.objects.filter(username=username,enabled=1).count()>0:
9 | return verify(request, username)
10 | return False
11 |
12 | def is_mfa(request,ignore_methods=[]):
13 | if request.session.get("mfa",{}).get("verified",False):
14 | if not request.session.get("mfa",{}).get("method",None) in ignore_methods:
15 | return True
16 | return False
17 |
18 | def recheck(request):
19 | method=request.session.get("mfa",{}).get("method",None)
20 | if not method:
21 | return HttpResponse(simplejson.dumps({"res":False}),content_type="application/json")
22 | if method=="Trusted Device":
23 | return HttpResponse(simplejson.dumps({"res":TrustedDevice.verify(request)}),content_type="application/json")
24 | elif method=="U2F":
25 | return HttpResponse(simplejson.dumps({"html": U2F.recheck(request).content}), content_type="application/json")
26 | elif method == "FIDO2":
27 | return HttpResponse(simplejson.dumps({"html": FIDO2.recheck(request).content}), content_type="application/json")
28 | elif method=="TOTP":
29 | return HttpResponse(simplejson.dumps({"html": totp.recheck(request).content}), content_type="application/json")
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/mfa/middleware.py:
--------------------------------------------------------------------------------
1 | import time
2 | from django.http import HttpResponseRedirect
3 | from django.core.urlresolvers import reverse
4 | from django.conf import settings
5 | def process(request):
6 | next_check=request.session.get('mfa',{}).get("next_check",False)
7 | if not next_check: return None
8 | now=int(time.time())
9 | if now >= next_check:
10 | method=request.session["mfa"]["method"]
11 | path = request.META["PATH_INFO"]
12 | return HttpResponseRedirect(reverse(method+"_auth")+"?next=%s"%(settings.BASE_URL + path).replace("//", "/"))
13 | return None
--------------------------------------------------------------------------------
/mfa/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.3 on 2021-05-26 09:05
2 |
3 | from django.db import migrations, models
4 | import jsonfield.fields
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | initial = True
10 |
11 | dependencies = [
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='User_Keys',
17 | fields=[
18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19 | ('username', models.CharField(max_length=50)),
20 | ('properties', jsonfield.fields.JSONField(null=True)),
21 | ('added_on', models.DateTimeField(auto_now_add=True)),
22 | ('key_type', models.CharField(default='TOTP', max_length=25)),
23 | ('enabled', models.BooleanField(default=True)),
24 | ('expires', models.DateTimeField(blank=True, default=None, null=True)),
25 | ('last_used', models.DateTimeField(blank=True, default=None, null=True)),
26 | ('owned_by_enterprise', models.BooleanField(blank=True, default=None, null=True)),
27 | ],
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/mfa/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sirneij/django_mfa2_example/d9505213e7e914481f06ea3426a515f5e0d26bc2/mfa/migrations/__init__.py
--------------------------------------------------------------------------------
/mfa/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from jsonfield import JSONField
3 | from jose import jwt
4 | from django.conf import settings
5 | from jsonLookup import shasLookup, hasLookup
6 | JSONField.register_lookup(shasLookup)
7 | JSONField.register_lookup(hasLookup)
8 |
9 |
10 | class User_Keys(models.Model):
11 | username=models.CharField(max_length = 50)
12 | properties=JSONField(null = True)
13 | added_on=models.DateTimeField(auto_now_add = True)
14 | key_type=models.CharField(max_length = 25,default = "TOTP")
15 | enabled=models.BooleanField(default=True)
16 | expires=models.DateTimeField(null=True,default=None,blank=True)
17 | last_used=models.DateTimeField(null=True,default=None,blank=True)
18 | owned_by_enterprise=models.BooleanField(default=None,null=True,blank=True)
19 |
20 | def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
21 | if self.key_type == "Trusted Device" and self.properties.get("signature","") == "":
22 | self.properties["signature"]= jwt.encode({"username": self.username, "key": self.properties["key"]}, settings.SECRET_KEY)
23 | super(User_Keys, self).save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
24 |
25 | def __unicode__(self):
26 | return "%s -- %s"%(self.username,self.key_type)
27 |
28 | def __str__(self):
29 | return self.__unicode__()
30 |
31 | class Meta:
32 | app_label='mfa'
33 |
--------------------------------------------------------------------------------
/mfa/static/mfa/css/bootstrap-toggle.min.css:
--------------------------------------------------------------------------------
1 | /*! ========================================================================
2 | * Bootstrap Toggle: bootstrap-toggle.css v2.2.0
3 | * http://www.bootstraptoggle.com
4 | * ========================================================================
5 | * Copyright 2014 Min Hur, The New York Times Company
6 | * Licensed under MIT
7 | * ======================================================================== */
8 | .checkbox label .toggle,.checkbox-inline .toggle{margin-left:-20px;margin-right:5px}
9 | .toggle{position:relative;overflow:hidden}
10 | .toggle input[type=checkbox]{display:none}
11 | .toggle-group{position:absolute;width:200%;top:0;bottom:0;left:0;transition:left .35s;-webkit-transition:left .35s;-moz-user-select:none;-webkit-user-select:none}
12 | .toggle.off .toggle-group{left:-100%}
13 | .toggle-on{position:absolute;top:0;bottom:0;left:0;right:50%;margin:0;border:0;border-radius:0}
14 | .toggle-off{position:absolute;top:0;bottom:0;left:50%;right:0;margin:0;border:0;border-radius:0}
15 | .toggle-handle{position:relative;margin:0 auto;padding-top:0;padding-bottom:0;height:100%;width:0;border-width:0 1px}
16 | .toggle.btn{min-width:59px;min-height:34px}
17 | .toggle-on.btn{padding-right:24px}
18 | .toggle-off.btn{padding-left:24px}
19 | .toggle.btn-lg{min-width:79px;min-height:45px}
20 | .toggle-on.btn-lg{padding-right:31px}
21 | .toggle-off.btn-lg{padding-left:31px}
22 | .toggle-handle.btn-lg{width:40px}
23 | .toggle.btn-sm{min-width:50px;min-height:30px}
24 | .toggle-on.btn-sm{padding-right:20px}
25 | .toggle-off.btn-sm{padding-left:20px}
26 | .toggle.btn-xs{min-width:35px;min-height:22px}
27 | .toggle-on.btn-xs{padding-right:12px}
28 | .toggle-off.btn-xs{padding-left:12px}
--------------------------------------------------------------------------------
/mfa/static/mfa/js/bootstrap-toggle.min.js:
--------------------------------------------------------------------------------
1 | /*! ========================================================================
2 | * Bootstrap Toggle: bootstrap-toggle.js v2.2.0
3 | * http://www.bootstraptoggle.com
4 | * ========================================================================
5 | * Copyright 2014 Min Hur, The New York Times Company
6 | * Licensed under MIT
7 | * ======================================================================== */
8 | +function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.toggle"),f="object"==typeof b&&b;e||d.data("bs.toggle",e=new c(this,f)),"string"==typeof b&&e[b]&&e[b]()})}var c=function(b,c){this.$element=a(b),this.options=a.extend({},this.defaults(),c),this.render()};c.VERSION="2.2.0",c.DEFAULTS={on:"On",off:"Off",onstyle:"primary",offstyle:"default",size:"normal",style:"",width:null,height:null},c.prototype.defaults=function(){return{on:this.$element.attr("data-on")||c.DEFAULTS.on,off:this.$element.attr("data-off")||c.DEFAULTS.off,onstyle:this.$element.attr("data-onstyle")||c.DEFAULTS.onstyle,offstyle:this.$element.attr("data-offstyle")||c.DEFAULTS.offstyle,size:this.$element.attr("data-size")||c.DEFAULTS.size,style:this.$element.attr("data-style")||c.DEFAULTS.style,width:this.$element.attr("data-width")||c.DEFAULTS.width,height:this.$element.attr("data-height")||c.DEFAULTS.height}},c.prototype.render=function(){this._onstyle="btn-"+this.options.onstyle,this._offstyle="btn-"+this.options.offstyle;var b="large"===this.options.size?"btn-lg":"small"===this.options.size?"btn-sm":"mini"===this.options.size?"btn-xs":"",c=a('').html(this.options.on).addClass(this._onstyle+" "+b),d=a('').html(this.options.off).addClass(this._offstyle+" "+b+" active"),e=a('').addClass(b),f=a('').append(c,d,e),g=a('
').addClass(this.$element.prop("checked")?this._onstyle:this._offstyle+" off").addClass(b).addClass(this.options.style);this.$element.wrap(g),a.extend(this,{$toggle:this.$element.parent(),$toggleOn:c,$toggleOff:d,$toggleGroup:f}),this.$toggle.append(f);var h=this.options.width||Math.max(c.outerWidth(),d.outerWidth())+e.outerWidth()/2,i=this.options.height||Math.max(c.outerHeight(),d.outerHeight());c.addClass("toggle-on"),d.addClass("toggle-off"),this.$toggle.css({width:h,height:i}),this.options.height&&(c.css("line-height",c.height()+"px"),d.css("line-height",d.height()+"px")),this.update(!0),this.trigger(!0)},c.prototype.toggle=function(){this.$element.prop("checked")?this.off():this.on()},c.prototype.on=function(a){return this.$element.prop("disabled")?!1:(this.$toggle.removeClass(this._offstyle+" off").addClass(this._onstyle),this.$element.prop("checked",!0),void(a||this.trigger()))},c.prototype.off=function(a){return this.$element.prop("disabled")?!1:(this.$toggle.removeClass(this._onstyle).addClass(this._offstyle+" off"),this.$element.prop("checked",!1),void(a||this.trigger()))},c.prototype.enable=function(){this.$toggle.removeAttr("disabled"),this.$element.prop("disabled",!1)},c.prototype.disable=function(){this.$toggle.attr("disabled","disabled"),this.$element.prop("disabled",!0)},c.prototype.update=function(a){this.$element.prop("disabled")?this.disable():this.enable(),this.$element.prop("checked")?this.on(a):this.off(a)},c.prototype.trigger=function(b){this.$element.off("change.bs.toggle"),b||this.$element.change(),this.$element.on("change.bs.toggle",a.proxy(function(){this.update()},this))},c.prototype.destroy=function(){this.$element.off("change.bs.toggle"),this.$toggleGroup.remove(),this.$element.removeData("bs.toggle"),this.$element.unwrap()};var d=a.fn.bootstrapToggle;a.fn.bootstrapToggle=b,a.fn.bootstrapToggle.Constructor=c,a.fn.toggle.noConflict=function(){return a.fn.bootstrapToggle=d,this},a(function(){a("input[type=checkbox][data-toggle^=toggle]").bootstrapToggle()}),a(document).on("click.bs.toggle","div[data-toggle^=toggle]",function(b){var c=a(this).find("input[type=checkbox]");c.bootstrapToggle("toggle"),b.preventDefault()})}(jQuery);
9 | //# sourceMappingURL=bootstrap-toggle.min.js.map
--------------------------------------------------------------------------------
/mfa/static/mfa/js/cbor.js:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2014-2016 Patrick Gansterer
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | (function(global, undefined) { "use strict";
26 | var POW_2_24 = 5.960464477539063e-8,
27 | POW_2_32 = 4294967296,
28 | POW_2_53 = 9007199254740992;
29 |
30 | function encode(value) {
31 | var data = new ArrayBuffer(256);
32 | var dataView = new DataView(data);
33 | var lastLength;
34 | var offset = 0;
35 |
36 | function prepareWrite(length) {
37 | var newByteLength = data.byteLength;
38 | var requiredLength = offset + length;
39 | while (newByteLength < requiredLength)
40 | newByteLength <<= 1;
41 | if (newByteLength !== data.byteLength) {
42 | var oldDataView = dataView;
43 | data = new ArrayBuffer(newByteLength);
44 | dataView = new DataView(data);
45 | var uint32count = (offset + 3) >> 2;
46 | for (var i = 0; i < uint32count; ++i)
47 | dataView.setUint32(i << 2, oldDataView.getUint32(i << 2));
48 | }
49 |
50 | lastLength = length;
51 | return dataView;
52 | }
53 | function commitWrite() {
54 | offset += lastLength;
55 | }
56 | function writeFloat64(value) {
57 | commitWrite(prepareWrite(8).setFloat64(offset, value));
58 | }
59 | function writeUint8(value) {
60 | commitWrite(prepareWrite(1).setUint8(offset, value));
61 | }
62 | function writeUint8Array(value) {
63 | var dataView = prepareWrite(value.length);
64 | for (var i = 0; i < value.length; ++i)
65 | dataView.setUint8(offset + i, value[i]);
66 | commitWrite();
67 | }
68 | function writeUint16(value) {
69 | commitWrite(prepareWrite(2).setUint16(offset, value));
70 | }
71 | function writeUint32(value) {
72 | commitWrite(prepareWrite(4).setUint32(offset, value));
73 | }
74 | function writeUint64(value) {
75 | var low = value % POW_2_32;
76 | var high = (value - low) / POW_2_32;
77 | var dataView = prepareWrite(8);
78 | dataView.setUint32(offset, high);
79 | dataView.setUint32(offset + 4, low);
80 | commitWrite();
81 | }
82 | function writeTypeAndLength(type, length) {
83 | if (length < 24) {
84 | writeUint8(type << 5 | length);
85 | } else if (length < 0x100) {
86 | writeUint8(type << 5 | 24);
87 | writeUint8(length);
88 | } else if (length < 0x10000) {
89 | writeUint8(type << 5 | 25);
90 | writeUint16(length);
91 | } else if (length < 0x100000000) {
92 | writeUint8(type << 5 | 26);
93 | writeUint32(length);
94 | } else {
95 | writeUint8(type << 5 | 27);
96 | writeUint64(length);
97 | }
98 | }
99 |
100 | function encodeItem(value) {
101 | var i;
102 |
103 | if (value === false)
104 | return writeUint8(0xf4);
105 | if (value === true)
106 | return writeUint8(0xf5);
107 | if (value === null)
108 | return writeUint8(0xf6);
109 | if (value === undefined)
110 | return writeUint8(0xf7);
111 |
112 | switch (typeof value) {
113 | case "number":
114 | if (Math.floor(value) === value) {
115 | if (0 <= value && value <= POW_2_53)
116 | return writeTypeAndLength(0, value);
117 | if (-POW_2_53 <= value && value < 0)
118 | return writeTypeAndLength(1, -(value + 1));
119 | }
120 | writeUint8(0xfb);
121 | return writeFloat64(value);
122 |
123 | case "string":
124 | var utf8data = [];
125 | for (i = 0; i < value.length; ++i) {
126 | var charCode = value.charCodeAt(i);
127 | if (charCode < 0x80) {
128 | utf8data.push(charCode);
129 | } else if (charCode < 0x800) {
130 | utf8data.push(0xc0 | charCode >> 6);
131 | utf8data.push(0x80 | charCode & 0x3f);
132 | } else if (charCode < 0xd800) {
133 | utf8data.push(0xe0 | charCode >> 12);
134 | utf8data.push(0x80 | (charCode >> 6) & 0x3f);
135 | utf8data.push(0x80 | charCode & 0x3f);
136 | } else {
137 | charCode = (charCode & 0x3ff) << 10;
138 | charCode |= value.charCodeAt(++i) & 0x3ff;
139 | charCode += 0x10000;
140 |
141 | utf8data.push(0xf0 | charCode >> 18);
142 | utf8data.push(0x80 | (charCode >> 12) & 0x3f);
143 | utf8data.push(0x80 | (charCode >> 6) & 0x3f);
144 | utf8data.push(0x80 | charCode & 0x3f);
145 | }
146 | }
147 |
148 | writeTypeAndLength(3, utf8data.length);
149 | return writeUint8Array(utf8data);
150 |
151 | default:
152 | var length;
153 | if (Array.isArray(value)) {
154 | length = value.length;
155 | writeTypeAndLength(4, length);
156 | for (i = 0; i < length; ++i)
157 | encodeItem(value[i]);
158 | } else if (value instanceof Uint8Array) {
159 | writeTypeAndLength(2, value.length);
160 | writeUint8Array(value);
161 | } else {
162 | var keys = Object.keys(value);
163 | length = keys.length;
164 | writeTypeAndLength(5, length);
165 | for (i = 0; i < length; ++i) {
166 | var key = keys[i];
167 | encodeItem(key);
168 | encodeItem(value[key]);
169 | }
170 | }
171 | }
172 | }
173 |
174 | encodeItem(value);
175 |
176 | if ("slice" in data)
177 | return data.slice(0, offset);
178 |
179 | var ret = new ArrayBuffer(offset);
180 | var retView = new DataView(ret);
181 | for (var i = 0; i < offset; ++i)
182 | retView.setUint8(i, dataView.getUint8(i));
183 | return ret;
184 | }
185 |
186 | function decode(data, tagger, simpleValue) {
187 | var dataView = new DataView(data);
188 | var offset = 0;
189 |
190 | if (typeof tagger !== "function")
191 | tagger = function(value) { return value; };
192 | if (typeof simpleValue !== "function")
193 | simpleValue = function() { return undefined; };
194 |
195 | function commitRead(length, value) {
196 | offset += length;
197 | return value;
198 | }
199 | function readArrayBuffer(length) {
200 | return commitRead(length, new Uint8Array(data, offset, length));
201 | }
202 | function readFloat16() {
203 | var tempArrayBuffer = new ArrayBuffer(4);
204 | var tempDataView = new DataView(tempArrayBuffer);
205 | var value = readUint16();
206 |
207 | var sign = value & 0x8000;
208 | var exponent = value & 0x7c00;
209 | var fraction = value & 0x03ff;
210 |
211 | if (exponent === 0x7c00)
212 | exponent = 0xff << 10;
213 | else if (exponent !== 0)
214 | exponent += (127 - 15) << 10;
215 | else if (fraction !== 0)
216 | return (sign ? -1 : 1) * fraction * POW_2_24;
217 |
218 | tempDataView.setUint32(0, sign << 16 | exponent << 13 | fraction << 13);
219 | return tempDataView.getFloat32(0);
220 | }
221 | function readFloat32() {
222 | return commitRead(4, dataView.getFloat32(offset));
223 | }
224 | function readFloat64() {
225 | return commitRead(8, dataView.getFloat64(offset));
226 | }
227 | function readUint8() {
228 | return commitRead(1, dataView.getUint8(offset));
229 | }
230 | function readUint16() {
231 | return commitRead(2, dataView.getUint16(offset));
232 | }
233 | function readUint32() {
234 | return commitRead(4, dataView.getUint32(offset));
235 | }
236 | function readUint64() {
237 | return readUint32() * POW_2_32 + readUint32();
238 | }
239 | function readBreak() {
240 | if (dataView.getUint8(offset) !== 0xff)
241 | return false;
242 | offset += 1;
243 | return true;
244 | }
245 | function readLength(additionalInformation) {
246 | if (additionalInformation < 24)
247 | return additionalInformation;
248 | if (additionalInformation === 24)
249 | return readUint8();
250 | if (additionalInformation === 25)
251 | return readUint16();
252 | if (additionalInformation === 26)
253 | return readUint32();
254 | if (additionalInformation === 27)
255 | return readUint64();
256 | if (additionalInformation === 31)
257 | return -1;
258 | throw "Invalid length encoding";
259 | }
260 | function readIndefiniteStringLength(majorType) {
261 | var initialByte = readUint8();
262 | if (initialByte === 0xff)
263 | return -1;
264 | var length = readLength(initialByte & 0x1f);
265 | if (length < 0 || (initialByte >> 5) !== majorType)
266 | throw "Invalid indefinite length element";
267 | return length;
268 | }
269 |
270 | function appendUtf16Data(utf16data, length) {
271 | for (var i = 0; i < length; ++i) {
272 | var value = readUint8();
273 | if (value & 0x80) {
274 | if (value < 0xe0) {
275 | value = (value & 0x1f) << 6
276 | | (readUint8() & 0x3f);
277 | length -= 1;
278 | } else if (value < 0xf0) {
279 | value = (value & 0x0f) << 12
280 | | (readUint8() & 0x3f) << 6
281 | | (readUint8() & 0x3f);
282 | length -= 2;
283 | } else {
284 | value = (value & 0x0f) << 18
285 | | (readUint8() & 0x3f) << 12
286 | | (readUint8() & 0x3f) << 6
287 | | (readUint8() & 0x3f);
288 | length -= 3;
289 | }
290 | }
291 |
292 | if (value < 0x10000) {
293 | utf16data.push(value);
294 | } else {
295 | value -= 0x10000;
296 | utf16data.push(0xd800 | (value >> 10));
297 | utf16data.push(0xdc00 | (value & 0x3ff));
298 | }
299 | }
300 | }
301 |
302 | function decodeItem() {
303 | var initialByte = readUint8();
304 | var majorType = initialByte >> 5;
305 | var additionalInformation = initialByte & 0x1f;
306 | var i;
307 | var length;
308 |
309 | if (majorType === 7) {
310 | switch (additionalInformation) {
311 | case 25:
312 | return readFloat16();
313 | case 26:
314 | return readFloat32();
315 | case 27:
316 | return readFloat64();
317 | }
318 | }
319 |
320 | length = readLength(additionalInformation);
321 | if (length < 0 && (majorType < 2 || 6 < majorType))
322 | throw "Invalid length";
323 |
324 | switch (majorType) {
325 | case 0:
326 | return length;
327 | case 1:
328 | return -1 - length;
329 | case 2:
330 | if (length < 0) {
331 | var elements = [];
332 | var fullArrayLength = 0;
333 | while ((length = readIndefiniteStringLength(majorType)) >= 0) {
334 | fullArrayLength += length;
335 | elements.push(readArrayBuffer(length));
336 | }
337 | var fullArray = new Uint8Array(fullArrayLength);
338 | var fullArrayOffset = 0;
339 | for (i = 0; i < elements.length; ++i) {
340 | fullArray.set(elements[i], fullArrayOffset);
341 | fullArrayOffset += elements[i].length;
342 | }
343 | return fullArray;
344 | }
345 | return readArrayBuffer(length);
346 | case 3:
347 | var utf16data = [];
348 | if (length < 0) {
349 | while ((length = readIndefiniteStringLength(majorType)) >= 0)
350 | appendUtf16Data(utf16data, length);
351 | } else
352 | appendUtf16Data(utf16data, length);
353 | return String.fromCharCode.apply(null, utf16data);
354 | case 4:
355 | var retArray;
356 | if (length < 0) {
357 | retArray = [];
358 | while (!readBreak())
359 | retArray.push(decodeItem());
360 | } else {
361 | retArray = new Array(length);
362 | for (i = 0; i < length; ++i)
363 | retArray[i] = decodeItem();
364 | }
365 | return retArray;
366 | case 5:
367 | var retObject = {};
368 | for (i = 0; i < length || length < 0 && !readBreak(); ++i) {
369 | var key = decodeItem();
370 | retObject[key] = decodeItem();
371 | }
372 | return retObject;
373 | case 6:
374 | return tagger(decodeItem(), length);
375 | case 7:
376 | switch (length) {
377 | case 20:
378 | return false;
379 | case 21:
380 | return true;
381 | case 22:
382 | return null;
383 | case 23:
384 | return undefined;
385 | default:
386 | return simpleValue(length);
387 | }
388 | }
389 | }
390 |
391 | var ret = decodeItem();
392 | if (offset !== data.byteLength)
393 | throw "Remaining bytes";
394 | return ret;
395 | }
396 |
397 | var obj = { encode: encode, decode: decode };
398 |
399 | if (typeof define === "function" && define.amd)
400 | define("cbor/cbor", obj);
401 | else if (typeof module !== "undefined" && module.exports)
402 | module.exports = obj;
403 | else if (!global.CBOR)
404 | global.CBOR = obj;
405 |
406 | })(this);
407 |
--------------------------------------------------------------------------------
/mfa/static/mfa/js/qrious.min.js:
--------------------------------------------------------------------------------
1 | /*! QRious v4.0.2 | (C) 2017 Alasdair Mercer | GPL v3 License
2 | Based on jsqrencode | (C) 2010 tz@execpc.com | GPL v3 License
3 | */
4 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.QRious=e()}(this,function(){"use strict";function t(t,e){var n;return"function"==typeof Object.create?n=Object.create(t):(s.prototype=t,n=new s,s.prototype=null),e&&i(!0,n,e),n}function e(e,n,s,r){var o=this;return"string"!=typeof e&&(r=s,s=n,n=e,e=null),"function"!=typeof n&&(r=s,s=n,n=function(){return o.apply(this,arguments)}),i(!1,n,o,r),n.prototype=t(o.prototype,s),n.prototype.constructor=n,n.class_=e||o.class_,n.super_=o,n}function i(t,e,i){for(var n,s,a=0,h=(i=o.call(arguments,2)).length;a>1&1,n=0;n0;e--)n[e]=n[e]?n[e-1]^_.EXPONENT[v._modN(_.LOG[n[e]]+t)]:n[e-1];n[0]=_.EXPONENT[v._modN(_.LOG[n[0]]+t)]}for(t=0;t<=i;t++)n[t]=_.LOG[n[t]]},_checkBadness:function(){var t,e,i,n,s,r=0,o=this._badness,a=this.buffer,h=this.width;for(s=0;sh*h;)u-=h*h,c++;for(r+=c*v.N4,n=0;n=o-2&&(t=o-2,s>9&&t--);var a=t;if(s>9){for(r[a+2]=0,r[a+3]=0;a--;)e=r[a],r[a+3]|=255&e<<4,r[a+2]=e>>4;r[2]|=255&t<<4,r[1]=t>>4,r[0]=64|t>>12}else{for(r[a+1]=0,r[a+2]=0;a--;)e=r[a],r[a+2]|=255&e<<4,r[a+1]=e>>4;r[1]|=255&t<<4,r[0]=64|t>>4}for(a=t+3-(s<10);a=5&&(i+=v.N1+n[e]-5);for(e=3;et||3*n[e-3]>=4*n[e]||3*n[e+3]>=4*n[e])&&(i+=v.N3);return i},_finish:function(){this._stringBuffer=this.buffer.slice();var t,e,i=0,n=3e4;for(e=0;e<8&&(this._applyMask(e),(t=this._checkBadness())>=1)1&n&&(s[r-1-e+8*r]=1,e<6?s[8+r*e]=1:s[8+r*(e+1)]=1);for(e=0;e<7;e++,n>>=1)1&n&&(s[8+r*(r-7+e)]=1,e?s[6-e+8*r]=1:s[7+8*r]=1)},_interleaveBlocks:function(){var t,e,i=this._dataBlock,n=this._ecc,s=this._eccBlock,r=0,o=this._calculateMaxLength(),a=this._neccBlock1,h=this._neccBlock2,f=this._stringBuffer;for(t=0;t1)for(t=u.BLOCK[n],i=s-7;;){for(e=s-7;e>t-3&&(this._addAlignment(e,i),!(e6)for(t=d.BLOCK[r-7],e=17,i=0;i<6;i++)for(n=0;n<3;n++,e--)1&(e>11?r>>e-12:t>>e)?(s[5-i+o*(2-n+o-11)]=1,s[2-n+o-11+o*(5-i)]=1):(this._setMask(5-i,2-n+o-11),this._setMask(2-n+o-11,5-i))},_isMasked:function(t,e){var i=v._getMaskBit(t,e);return 1===this._mask[i]},_pack:function(){var t,e,i,n=1,s=1,r=this.width,o=r-1,a=r-1,h=(this._dataBlock+this._eccBlock)*(this._neccBlock1+this._neccBlock2)+this._neccBlock2;for(e=0;ee&&(i=t,t=e,e=i),i=e,i+=e*e,i>>=1,i+=t},_modN:function(t){for(;t>=255;)t=((t-=255)>>8)+(255&t);return t},N1:3,N2:3,N3:40,N4:10}),p=v,m=f.extend({draw:function(){this.element.src=this.qrious.toDataURL()},reset:function(){this.element.src=""},resize:function(){var t=this.element;t.width=t.height=this.qrious.size}}),g=h.extend(function(t,e,i,n){this.name=t,this.modifiable=Boolean(e),this.defaultValue=i,this._valueTransformer=n},{transform:function(t){var e=this._valueTransformer;return"function"==typeof e?e(t,this):t}}),k=h.extend(null,{abs:function(t){return null!=t?Math.abs(t):null},hasOwn:function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},noop:function(){},toUpperCase:function(t){return null!=t?t.toUpperCase():null}}),w=h.extend(function(t){this.options={},t.forEach(function(t){this.options[t.name]=t},this)},{exists:function(t){return null!=this.options[t]},get:function(t,e){return w._get(this.options[t],e)},getAll:function(t){var e,i=this.options,n={};for(e in i)k.hasOwn(i,e)&&(n[e]=w._get(i[e],t));return n},init:function(t,e,i){"function"!=typeof i&&(i=k.noop);var n,s;for(n in this.options)k.hasOwn(this.options,n)&&(s=this.options[n],w._set(s,s.defaultValue,e),w._createAccessor(s,e,i));this._setAll(t,e,!0)},set:function(t,e,i){return this._set(t,e,i)},setAll:function(t,e){return this._setAll(t,e)},_set:function(t,e,i,n){var s=this.options[t];if(!s)throw new Error("Invalid option: "+t);if(!s.modifiable&&!n)throw new Error("Option cannot be modified: "+t);return w._set(s,e,i)},_setAll:function(t,e,i){if(!t)return!1;var n,s=!1;for(n in t)k.hasOwn(t,n)&&this._set(n,t[n],e,i)&&(s=!0);return s}},{_createAccessor:function(t,e,i){var n={get:function(){return w._get(t,e)}};t.modifiable&&(n.set=function(n){w._set(t,n,e)&&i(n,t)}),Object.defineProperty(e,t.name,n)},_get:function(t,e){return e["_"+t.name]},_set:function(t,e,i){var n="_"+t.name,s=i[n],r=t.transform(null!=e?e:t.defaultValue);return i[n]=r,r!==s}}),M=w,b=h.extend(function(){this._services={}},{getService:function(t){var e=this._services[t];if(!e)throw new Error("Service is not being managed with name: "+t);return e},setService:function(t,e){if(this._services[t])throw new Error("Service is already managed with name: "+t);e&&(this._services[t]=e)}}),B=new M([new g("background",!0,"white"),new g("backgroundAlpha",!0,1,k.abs),new g("element"),new g("foreground",!0,"black"),new g("foregroundAlpha",!0,1,k.abs),new g("level",!0,"L",k.toUpperCase),new g("mime",!0,"image/png"),new g("padding",!0,null,k.abs),new g("size",!0,100,k.abs),new g("value",!0,"")]),y=new b,O=h.extend(function(t){B.init(t,this,this.update.bind(this));var e=B.get("element",this),i=y.getService("element"),n=e&&i.isCanvas(e)?e:i.createCanvas(),s=e&&i.isImage(e)?e:i.createImage();this._canvasRenderer=new c(this,n,!0),this._imageRenderer=new m(this,s,s===e),this.update()},{get:function(){return B.getAll(this)},set:function(t){B.setAll(t,this)&&this.update()},toDataURL:function(t){return this.canvas.toDataURL(t||this.mime)},update:function(){var t=new p({level:this.level,value:this.value});this._canvasRenderer.render(t),this._imageRenderer.render(t)}},{use:function(t){y.setService(t.getName(),t)}});Object.defineProperties(O.prototype,{canvas:{get:function(){return this._canvasRenderer.getElement()}},image:{get:function(){return this._imageRenderer.getElement()}}});var A=O,L=h.extend({getName:function(){}}).extend({createCanvas:function(){},createImage:function(){},getName:function(){return"element"},isCanvas:function(t){},isImage:function(t){}}).extend({createCanvas:function(){return document.createElement("canvas")},createImage:function(){return document.createElement("img")},isCanvas:function(t){return t instanceof HTMLCanvasElement},isImage:function(t){return t instanceof HTMLImageElement}});return A.use(new L),A});
5 |
6 | //# sourceMappingURL=qrious.min.js.map
--------------------------------------------------------------------------------
/mfa/static/mfa/js/u2f-api.js:
--------------------------------------------------------------------------------
1 | //Copyright 2014-2015 Google Inc. All rights reserved.
2 |
3 | //Use of this source code is governed by a BSD-style
4 | //license that can be found in the LICENSE file or at
5 | //https://developers.google.com/open-source/licenses/bsd
6 |
7 | /**
8 | * @fileoverview The U2F api.
9 | */
10 | 'use strict';
11 |
12 |
13 | /**
14 | * Namespace for the U2F api.
15 | * @type {Object}
16 | */
17 | var u2f = u2f || {};
18 |
19 | /**
20 | * FIDO U2F Javascript API Version
21 | * @number
22 | */
23 | var js_api_version;
24 |
25 | /**
26 | * The U2F extension id
27 | * @const {string}
28 | */
29 | // The Chrome packaged app extension ID.
30 | // Uncomment this if you want to deploy a server instance that uses
31 | // the package Chrome app and does not require installing the U2F Chrome extension.
32 | u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
33 | // The U2F Chrome extension ID.
34 | // Uncomment this if you want to deploy a server instance that uses
35 | // the U2F Chrome extension to authenticate.
36 | // u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
37 |
38 |
39 | /**
40 | * Message types for messsages to/from the extension
41 | * @const
42 | * @enum {string}
43 | */
44 | u2f.MessageTypes = {
45 | 'U2F_REGISTER_REQUEST': 'u2f_register_request',
46 | 'U2F_REGISTER_RESPONSE': 'u2f_register_response',
47 | 'U2F_SIGN_REQUEST': 'u2f_sign_request',
48 | 'U2F_SIGN_RESPONSE': 'u2f_sign_response',
49 | 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
50 | 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
51 | };
52 |
53 |
54 | /**
55 | * Response status codes
56 | * @const
57 | * @enum {number}
58 | */
59 | u2f.ErrorCodes = {
60 | 'OK': 0,
61 | 'OTHER_ERROR': 1,
62 | 'BAD_REQUEST': 2,
63 | 'CONFIGURATION_UNSUPPORTED': 3,
64 | 'DEVICE_INELIGIBLE': 4,
65 | 'TIMEOUT': 5
66 | };
67 |
68 |
69 | /**
70 | * A message for registration requests
71 | * @typedef {{
72 | * type: u2f.MessageTypes,
73 | * appId: ?string,
74 | * timeoutSeconds: ?number,
75 | * requestId: ?number
76 | * }}
77 | */
78 | u2f.U2fRequest;
79 |
80 |
81 | /**
82 | * A message for registration responses
83 | * @typedef {{
84 | * type: u2f.MessageTypes,
85 | * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
86 | * requestId: ?number
87 | * }}
88 | */
89 | u2f.U2fResponse;
90 |
91 |
92 | /**
93 | * An error object for responses
94 | * @typedef {{
95 | * errorCode: u2f.ErrorCodes,
96 | * errorMessage: ?string
97 | * }}
98 | */
99 | u2f.Error;
100 |
101 | /**
102 | * Data object for a single sign request.
103 | * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
104 | */
105 | u2f.Transport;
106 |
107 |
108 | /**
109 | * Data object for a single sign request.
110 | * @typedef {Array}
111 | */
112 | u2f.Transports;
113 |
114 | /**
115 | * Data object for a single sign request.
116 | * @typedef {{
117 | * version: string,
118 | * challenge: string,
119 | * keyHandle: string,
120 | * appId: string
121 | * }}
122 | */
123 | u2f.SignRequest;
124 |
125 |
126 | /**
127 | * Data object for a sign response.
128 | * @typedef {{
129 | * keyHandle: string,
130 | * signatureData: string,
131 | * clientData: string
132 | * }}
133 | */
134 | u2f.SignResponse;
135 |
136 |
137 | /**
138 | * Data object for a registration request.
139 | * @typedef {{
140 | * version: string,
141 | * challenge: string
142 | * }}
143 | */
144 | u2f.RegisterRequest;
145 |
146 |
147 | /**
148 | * Data object for a registration response.
149 | * @typedef {{
150 | * version: string,
151 | * keyHandle: string,
152 | * transports: Transports,
153 | * appId: string
154 | * }}
155 | */
156 | u2f.RegisterResponse;
157 |
158 |
159 | /**
160 | * Data object for a registered key.
161 | * @typedef {{
162 | * version: string,
163 | * keyHandle: string,
164 | * transports: ?Transports,
165 | * appId: ?string
166 | * }}
167 | */
168 | u2f.RegisteredKey;
169 |
170 |
171 | /**
172 | * Data object for a get API register response.
173 | * @typedef {{
174 | * js_api_version: number
175 | * }}
176 | */
177 | u2f.GetJsApiVersionResponse;
178 |
179 |
180 | //Low level MessagePort API support
181 |
182 | /**
183 | * Sets up a MessagePort to the U2F extension using the
184 | * available mechanisms.
185 | * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
186 | */
187 | u2f.getMessagePort = function(callback) {
188 | if (typeof chrome != 'undefined' && chrome.runtime) {
189 | // The actual message here does not matter, but we need to get a reply
190 | // for the callback to run. Thus, send an empty signature request
191 | // in order to get a failure response.
192 | var msg = {
193 | type: u2f.MessageTypes.U2F_SIGN_REQUEST,
194 | signRequests: []
195 | };
196 | chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
197 | if (!chrome.runtime.lastError) {
198 | // We are on a whitelisted origin and can talk directly
199 | // with the extension.
200 | u2f.getChromeRuntimePort_(callback);
201 | } else {
202 | // chrome.runtime was available, but we couldn't message
203 | // the extension directly, use iframe
204 | u2f.getIframePort_(callback);
205 | }
206 | });
207 | } else if (u2f.isAndroidChrome_()) {
208 | u2f.getAuthenticatorPort_(callback);
209 | } else if (u2f.isIosChrome_()) {
210 | u2f.getIosPort_(callback);
211 | } else {
212 | // chrome.runtime was not available at all, which is normal
213 | // when this origin doesn't have access to any extensions.
214 | u2f.getIframePort_(callback);
215 | }
216 | };
217 |
218 | /**
219 | * Detect chrome running on android based on the browser's useragent.
220 | * @private
221 | */
222 | u2f.isAndroidChrome_ = function() {
223 | var userAgent = navigator.userAgent;
224 | return userAgent.indexOf('Chrome') != -1 &&
225 | userAgent.indexOf('Android') != -1;
226 | };
227 |
228 | /**
229 | * Detect chrome running on iOS based on the browser's platform.
230 | * @private
231 | */
232 | u2f.isIosChrome_ = function() {
233 | return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1;
234 | };
235 |
236 | /**
237 | * Connects directly to the extension via chrome.runtime.connect.
238 | * @param {function(u2f.WrappedChromeRuntimePort_)} callback
239 | * @private
240 | */
241 | u2f.getChromeRuntimePort_ = function(callback) {
242 | var port = chrome.runtime.connect(u2f.EXTENSION_ID,
243 | {'includeTlsChannelId': true});
244 | setTimeout(function() {
245 | callback(new u2f.WrappedChromeRuntimePort_(port));
246 | }, 0);
247 | };
248 |
249 | /**
250 | * Return a 'port' abstraction to the Authenticator app.
251 | * @param {function(u2f.WrappedAuthenticatorPort_)} callback
252 | * @private
253 | */
254 | u2f.getAuthenticatorPort_ = function(callback) {
255 | setTimeout(function() {
256 | callback(new u2f.WrappedAuthenticatorPort_());
257 | }, 0);
258 | };
259 |
260 | /**
261 | * Return a 'port' abstraction to the iOS client app.
262 | * @param {function(u2f.WrappedIosPort_)} callback
263 | * @private
264 | */
265 | u2f.getIosPort_ = function(callback) {
266 | setTimeout(function() {
267 | callback(new u2f.WrappedIosPort_());
268 | }, 0);
269 | };
270 |
271 | /**
272 | * A wrapper for chrome.runtime.Port that is compatible with MessagePort.
273 | * @param {Port} port
274 | * @constructor
275 | * @private
276 | */
277 | u2f.WrappedChromeRuntimePort_ = function(port) {
278 | this.port_ = port;
279 | };
280 |
281 | /**
282 | * Format and return a sign request compliant with the JS API version supported by the extension.
283 | * @param {Array} signRequests
284 | * @param {number} timeoutSeconds
285 | * @param {number} reqId
286 | * @return {Object}
287 | */
288 | u2f.formatSignRequest_ =
289 | function(appId, challenge, registeredKeys, timeoutSeconds, reqId) {
290 | if (js_api_version === undefined || js_api_version < 1.1) {
291 | // Adapt request to the 1.0 JS API
292 | var signRequests = [];
293 | for (var i = 0; i < registeredKeys.length; i++) {
294 | signRequests[i] = {
295 | version: registeredKeys[i].version,
296 | challenge: challenge,
297 | keyHandle: registeredKeys[i].keyHandle,
298 | appId: appId
299 | };
300 | }
301 | return {
302 | type: u2f.MessageTypes.U2F_SIGN_REQUEST,
303 | signRequests: signRequests,
304 | timeoutSeconds: timeoutSeconds,
305 | requestId: reqId
306 | };
307 | }
308 | // JS 1.1 API
309 | return {
310 | type: u2f.MessageTypes.U2F_SIGN_REQUEST,
311 | appId: appId,
312 | challenge: challenge,
313 | registeredKeys: registeredKeys,
314 | timeoutSeconds: timeoutSeconds,
315 | requestId: reqId
316 | };
317 | };
318 |
319 | /**
320 | * Format and return a register request compliant with the JS API version supported by the extension..
321 | * @param {Array} signRequests
322 | * @param {Array} signRequests
323 | * @param {number} timeoutSeconds
324 | * @param {number} reqId
325 | * @return {Object}
326 | */
327 | u2f.formatRegisterRequest_ =
328 | function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
329 | if (js_api_version === undefined || js_api_version < 1.1) {
330 | // Adapt request to the 1.0 JS API
331 | for (var i = 0; i < registerRequests.length; i++) {
332 | registerRequests[i].appId = appId;
333 | }
334 | var signRequests = [];
335 | for (var i = 0; i < registeredKeys.length; i++) {
336 | signRequests[i] = {
337 | version: registeredKeys[i].version,
338 | challenge: registerRequests[0],
339 | keyHandle: registeredKeys[i].keyHandle,
340 | appId: appId
341 | };
342 | }
343 | return {
344 | type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
345 | signRequests: signRequests,
346 | registerRequests: registerRequests,
347 | timeoutSeconds: timeoutSeconds,
348 | requestId: reqId
349 | };
350 | }
351 | // JS 1.1 API
352 | return {
353 | type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
354 | appId: appId,
355 | registerRequests: registerRequests,
356 | registeredKeys: registeredKeys,
357 | timeoutSeconds: timeoutSeconds,
358 | requestId: reqId
359 | };
360 | };
361 |
362 |
363 | /**
364 | * Posts a message on the underlying channel.
365 | * @param {Object} message
366 | */
367 | u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
368 | this.port_.postMessage(message);
369 | };
370 |
371 |
372 | /**
373 | * Emulates the HTML 5 addEventListener interface. Works only for the
374 | * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
375 | * @param {string} eventName
376 | * @param {function({data: Object})} handler
377 | */
378 | u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
379 | function(eventName, handler) {
380 | var name = eventName.toLowerCase();
381 | if (name == 'message' || name == 'onmessage') {
382 | this.port_.onMessage.addListener(function(message) {
383 | // Emulate a minimal MessageEvent object
384 | handler({'data': message});
385 | });
386 | } else {
387 | console.error('WrappedChromeRuntimePort only supports onMessage');
388 | }
389 | };
390 |
391 | /**
392 | * Wrap the Authenticator app with a MessagePort interface.
393 | * @constructor
394 | * @private
395 | */
396 | u2f.WrappedAuthenticatorPort_ = function() {
397 | this.requestId_ = -1;
398 | this.requestObject_ = null;
399 | }
400 |
401 | /**
402 | * Launch the Authenticator intent.
403 | * @param {Object} message
404 | */
405 | u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
406 | var intentUrl =
407 | u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
408 | ';S.request=' + encodeURIComponent(JSON.stringify(message)) +
409 | ';end';
410 | document.location = intentUrl;
411 | };
412 |
413 | /**
414 | * Tells what type of port this is.
415 | * @return {String} port type
416 | */
417 | u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() {
418 | return "WrappedAuthenticatorPort_";
419 | };
420 |
421 |
422 | /**
423 | * Emulates the HTML 5 addEventListener interface.
424 | * @param {string} eventName
425 | * @param {function({data: Object})} handler
426 | */
427 | u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) {
428 | var name = eventName.toLowerCase();
429 | if (name == 'message') {
430 | var self = this;
431 | /* Register a callback to that executes when
432 | * chrome injects the response. */
433 | window.addEventListener(
434 | 'message', self.onRequestUpdate_.bind(self, handler), false);
435 | } else {
436 | console.error('WrappedAuthenticatorPort only supports message');
437 | }
438 | };
439 |
440 | /**
441 | * Callback invoked when a response is received from the Authenticator.
442 | * @param function({data: Object}) callback
443 | * @param {Object} message message Object
444 | */
445 | u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
446 | function(callback, message) {
447 | var messageObject = JSON.parse(message.data);
448 | var intentUrl = messageObject['intentURL'];
449 |
450 | var errorCode = messageObject['errorCode'];
451 | var responseObject = null;
452 | if (messageObject.hasOwnProperty('data')) {
453 | responseObject = /** @type {Object} */ (
454 | JSON.parse(messageObject['data']));
455 | }
456 |
457 | callback({'data': responseObject});
458 | };
459 |
460 | /**
461 | * Base URL for intents to Authenticator.
462 | * @const
463 | * @private
464 | */
465 | u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
466 | 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
467 |
468 | /**
469 | * Wrap the iOS client app with a MessagePort interface.
470 | * @constructor
471 | * @private
472 | */
473 | u2f.WrappedIosPort_ = function() {};
474 |
475 | /**
476 | * Launch the iOS client app request
477 | * @param {Object} message
478 | */
479 | u2f.WrappedIosPort_.prototype.postMessage = function(message) {
480 | var str = JSON.stringify(message);
481 | var url = "u2f://auth?" + encodeURI(str);
482 | location.replace(url);
483 | };
484 |
485 | /**
486 | * Tells what type of port this is.
487 | * @return {String} port type
488 | */
489 | u2f.WrappedIosPort_.prototype.getPortType = function() {
490 | return "WrappedIosPort_";
491 | };
492 |
493 | /**
494 | * Emulates the HTML 5 addEventListener interface.
495 | * @param {string} eventName
496 | * @param {function({data: Object})} handler
497 | */
498 | u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) {
499 | var name = eventName.toLowerCase();
500 | if (name !== 'message') {
501 | console.error('WrappedIosPort only supports message');
502 | }
503 | };
504 |
505 | /**
506 | * Sets up an embedded trampoline iframe, sourced from the extension.
507 | * @param {function(MessagePort)} callback
508 | * @private
509 | */
510 | u2f.getIframePort_ = function(callback) {
511 | // Create the iframe
512 | var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
513 | var iframe = document.createElement('iframe');
514 | iframe.src = iframeOrigin + '/u2f-comms.html';
515 | iframe.setAttribute('style', 'display:none');
516 | document.body.appendChild(iframe);
517 |
518 | var channel = new MessageChannel();
519 | var ready = function(message) {
520 | if (message.data == 'ready') {
521 | channel.port1.removeEventListener('message', ready);
522 | callback(channel.port1);
523 | } else {
524 | console.error('First event on iframe port was not "ready"');
525 | }
526 | };
527 | channel.port1.addEventListener('message', ready);
528 | channel.port1.start();
529 |
530 | iframe.addEventListener('load', function() {
531 | // Deliver the port to the iframe and initialize
532 | iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
533 | });
534 | };
535 |
536 |
537 | //High-level JS API
538 |
539 | /**
540 | * Default extension response timeout in seconds.
541 | * @const
542 | */
543 | u2f.EXTENSION_TIMEOUT_SEC = 30;
544 |
545 | /**
546 | * A singleton instance for a MessagePort to the extension.
547 | * @type {MessagePort|u2f.WrappedChromeRuntimePort_}
548 | * @private
549 | */
550 | u2f.port_ = null;
551 |
552 | /**
553 | * Callbacks waiting for a port
554 | * @type {Array}
555 | * @private
556 | */
557 | u2f.waitingForPort_ = [];
558 |
559 | /**
560 | * A counter for requestIds.
561 | * @type {number}
562 | * @private
563 | */
564 | u2f.reqCounter_ = 0;
565 |
566 | /**
567 | * A map from requestIds to client callbacks
568 | * @type {Object.}
570 | * @private
571 | */
572 | u2f.callbackMap_ = {};
573 |
574 | /**
575 | * Creates or retrieves the MessagePort singleton to use.
576 | * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
577 | * @private
578 | */
579 | u2f.getPortSingleton_ = function(callback) {
580 | if (u2f.port_) {
581 | callback(u2f.port_);
582 | } else {
583 | if (u2f.waitingForPort_.length == 0) {
584 | u2f.getMessagePort(function(port) {
585 | u2f.port_ = port;
586 | u2f.port_.addEventListener('message',
587 | /** @type {function(Event)} */ (u2f.responseHandler_));
588 |
589 | // Careful, here be async callbacks. Maybe.
590 | while (u2f.waitingForPort_.length)
591 | u2f.waitingForPort_.shift()(u2f.port_);
592 | });
593 | }
594 | u2f.waitingForPort_.push(callback);
595 | }
596 | };
597 |
598 | /**
599 | * Handles response messages from the extension.
600 | * @param {MessageEvent.} message
601 | * @private
602 | */
603 | u2f.responseHandler_ = function(message) {
604 | var response = message.data;
605 | var reqId = response['requestId'];
606 | if (!reqId || !u2f.callbackMap_[reqId]) {
607 | console.error('Unknown or missing requestId in response.');
608 | return;
609 | }
610 | var cb = u2f.callbackMap_[reqId];
611 | delete u2f.callbackMap_[reqId];
612 | cb(response['responseData']);
613 | };
614 |
615 | /**
616 | * Dispatches an array of sign requests to available U2F tokens.
617 | * If the JS API version supported by the extension is unknown, it first sends a
618 | * message to the extension to find out the supported API version and then it sends
619 | * the sign request.
620 | * @param {string=} appId
621 | * @param {string=} challenge
622 | * @param {Array} registeredKeys
623 | * @param {function((u2f.Error|u2f.SignResponse))} callback
624 | * @param {number=} opt_timeoutSeconds
625 | */
626 | u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
627 | if (js_api_version === undefined) {
628 | // Send a message to get the extension to JS API version, then send the actual sign request.
629 | u2f.getApiVersion(
630 | function (response) {
631 | js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
632 | console.log("Extension JS API Version: ", js_api_version);
633 | u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
634 | });
635 | } else {
636 | // We know the JS API version. Send the actual sign request in the supported API version.
637 | u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
638 | }
639 | };
640 |
641 | /**
642 | * Dispatches an array of sign requests to available U2F tokens.
643 | * @param {string=} appId
644 | * @param {string=} challenge
645 | * @param {Array} registeredKeys
646 | * @param {function((u2f.Error|u2f.SignResponse))} callback
647 | * @param {number=} opt_timeoutSeconds
648 | */
649 | u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
650 | u2f.getPortSingleton_(function(port) {
651 | var reqId = ++u2f.reqCounter_;
652 | u2f.callbackMap_[reqId] = callback;
653 | var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
654 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
655 | var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
656 | port.postMessage(req);
657 | });
658 | };
659 |
660 | /**
661 | * Dispatches register requests to available U2F tokens. An array of sign
662 | * requests identifies already registered tokens.
663 | * If the JS API version supported by the extension is unknown, it first sends a
664 | * message to the extension to find out the supported API version and then it sends
665 | * the register request.
666 | * @param {string=} appId
667 | * @param {Array} registerRequests
668 | * @param {Array} registeredKeys
669 | * @param {function((u2f.Error|u2f.RegisterResponse))} callback
670 | * @param {number=} opt_timeoutSeconds
671 | */
672 | u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
673 | console.log("appid",appId)
674 | console.log("registerRequests",registerRequests)
675 | console.log("registeredKeys",registeredKeys)
676 | if (js_api_version === undefined) {
677 | // Send a message to get the extension to JS API version, then send the actual register request.
678 | u2f.getApiVersion(
679 | function (response) {
680 | js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
681 | console.log("Extension JS API Version: ", js_api_version);
682 | u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
683 | callback, opt_timeoutSeconds);
684 | });
685 | } else {
686 | // We know the JS API version. Send the actual register request in the supported API version.
687 | u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
688 | callback, opt_timeoutSeconds);
689 | }
690 | };
691 |
692 | /**
693 | * Dispatches register requests to available U2F tokens. An array of sign
694 | * requests identifies already registered tokens.
695 | * @param {string=} appId
696 | * @param {Array} registerRequests
697 | * @param {Array} registeredKeys
698 | * @param {function((u2f.Error|u2f.RegisterResponse))} callback
699 | * @param {number=} opt_timeoutSeconds
700 | */
701 | u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
702 | u2f.getPortSingleton_(function(port) {
703 | var reqId = ++u2f.reqCounter_;
704 | u2f.callbackMap_[reqId] = callback;
705 | var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
706 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
707 | var req = u2f.formatRegisterRequest_(
708 | appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
709 | port.postMessage(req);
710 | });
711 | };
712 |
713 |
714 | /**
715 | * Dispatches a message to the extension to find out the supported
716 | * JS API version.
717 | * If the user is on a mobile phone and is thus using Google Authenticator instead
718 | * of the Chrome extension, don't send the request and simply return 0.
719 | * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
720 | * @param {number=} opt_timeoutSeconds
721 | */
722 | u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
723 | u2f.getPortSingleton_(function(port) {
724 | // If we are using Android Google Authenticator or iOS client app,
725 | // do not fire an intent to ask which JS API version to use.
726 | if (port.getPortType) {
727 | var apiVersion;
728 | switch (port.getPortType()) {
729 | case 'WrappedIosPort_':
730 | case 'WrappedAuthenticatorPort_':
731 | apiVersion = 1.1;
732 | break;
733 |
734 | default:
735 | apiVersion = 0;
736 | break;
737 | }
738 | callback({ 'js_api_version': apiVersion });
739 | return;
740 | }
741 | var reqId = ++u2f.reqCounter_;
742 | u2f.callbackMap_[reqId] = callback;
743 | var req = {
744 | type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
745 | timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
746 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
747 | requestId: reqId
748 | };
749 | port.postMessage(req);
750 | });
751 | };
752 |
--------------------------------------------------------------------------------
/mfa/static/mfa/js/ua-parser.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * UAParser.js v0.7.23
3 | * Lightweight JavaScript-based User-Agent string parser
4 | * https://github.com/faisalman/ua-parser-js
5 | *
6 | * Copyright © 2012-2019 Faisal Salman
7 | * Licensed under MIT License
8 | */
9 | (function(window,undefined){"use strict";var LIBVERSION="0.7.23",EMPTY="",UNKNOWN="?",FUNC_TYPE="function",UNDEF_TYPE="undefined",OBJ_TYPE="object",STR_TYPE="string",MAJOR="major",MODEL="model",NAME="name",TYPE="type",VENDOR="vendor",VERSION="version",ARCHITECTURE="architecture",CONSOLE="console",MOBILE="mobile",TABLET="tablet",SMARTTV="smarttv",WEARABLE="wearable",EMBEDDED="embedded";var util={extend:function(regexes,extensions){var mergedRegexes={};for(var i in regexes){if(extensions[i]&&extensions[i].length%2===0){mergedRegexes[i]=extensions[i].concat(regexes[i])}else{mergedRegexes[i]=regexes[i]}}return mergedRegexes},has:function(str1,str2){if(typeof str1==="string"){return str2.toLowerCase().indexOf(str1.toLowerCase())!==-1}else{return false}},lowerize:function(str){return str.toLowerCase()},major:function(version){return typeof version===STR_TYPE?version.replace(/[^\d\.]/g,"").split(".")[0]:undefined},trim:function(str){return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}};var mapper={rgx:function(ua,arrays){var i=0,j,k,p,q,matches,match;while(i0){if(q.length==2){if(typeof q[1]==FUNC_TYPE){this[q[0]]=q[1].call(this,match)}else{this[q[0]]=q[1]}}else if(q.length==3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){this[q[0]]=match?q[1].call(this,match,q[2]):undefined}else{this[q[0]]=match?match.replace(q[1],q[2]):undefined}}else if(q.length==4){this[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}}else{this[q]=match?match:undefined}}}}i+=2}},str:function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;j
2 |
3 |
4 |
5 | Title
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/mfa/templates/Email/Add.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block head %}
3 | {% endblock %}
4 | {% block content %}
5 |
6 |
7 |
8 |
9 |
10 | Activate Token by email
11 |
12 |
49 |
50 |
51 | {% endblock %}
--------------------------------------------------------------------------------
/mfa/templates/Email/Auth.html:
--------------------------------------------------------------------------------
1 | {% extends "mfa_auth_base.html" %}
2 | {% block head %}
3 |
8 | {% endblock %}
9 | {% block content %}
10 |
11 |
12 | {% include "Email/recheck.html" with mode='auth' %}
13 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/mfa/templates/Email/mfa_email_token_template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Dear {{ username }},
6 | Your OTP is: {{ otp }}
7 |
8 | Thanks
9 |
10 |
--------------------------------------------------------------------------------
/mfa/templates/Email/recheck.html:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 | Email One Time Password
22 |
23 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/mfa/templates/FIDO2/Add.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %} {% load static %} {% block head %}
2 |
6 |
10 |
75 |
76 | {% endblock %} {% block content %}
77 |
78 |
79 |
80 |
81 |
82 | FIDO2 Security Key
83 |
84 |
85 |
86 |
87 | Your browser should ask you to confirm you identity.
88 |
89 |
90 |
91 |
92 | {% include "modal.html" %} {% endblock %}
93 |
94 |
--------------------------------------------------------------------------------
/mfa/templates/FIDO2/Auth.html:
--------------------------------------------------------------------------------
1 | {% extends "mfa_auth_base.html" %}
2 | {% block content %}
3 | {% include 'FIDO2/recheck.html' with mode='auth' %}
4 | {% endblock %}
--------------------------------------------------------------------------------
/mfa/templates/FIDO2/recheck.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Security Key
10 |
11 |
12 |
13 |
14 |
15 | {% if mode == "auth" %}
16 | Welcome back {% comment %}
{% endcomment %} {{ request.session.base_username }}
17 |
Not me
18 |
19 |
20 | {% endif %}
21 |
22 |
please press the button on your security key to prove it is you.
23 |
24 |
25 | {% if mode == "auth" %}
26 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {% if request.session.mfa_methods|length > 1 %}
41 |
Select Another Method
42 | {% endif %}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/mfa/templates/MFA.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load static %}
3 | {% block head %}
4 |
38 |
39 |
40 | {% endblock %}
41 | {% block content %}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Add Method
50 |
51 |
68 |
69 |
70 |
71 |
72 |
106 |
107 |
108 | {% include "modal.html" %}
109 | {% endblock %}
--------------------------------------------------------------------------------
/mfa/templates/TOTP/Add.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "base.html" %}
3 | {% load static %}
4 | {% block head %}
5 |
15 |
16 |
62 | {% endblock %}
63 | {% block content %}
64 |
65 |
66 |
67 |
68 |
69 |
Adding Authenticator
70 |
71 |
72 |
73 |
Scan the image below with the two-factor authentication app on your phone/PC . If you can’t use a barcode,
74 | enter this text instead.
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
Enter the six-digit code from the application
87 |
After scanning the barcode image, the app will display a six-digit code that you can enter below.
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | Enable
99 |
100 |
103 |
104 |
105 |
106 |
107 |
108 | {% include "modal.html" %}
109 | {% endblock %}
--------------------------------------------------------------------------------
/mfa/templates/TOTP/Auth.html:
--------------------------------------------------------------------------------
1 | {% extends "mfa_auth_base.html" %}
2 | {% block head %}
3 |
8 | {% endblock %}
9 | {% block content %}
10 |
11 |
12 | {% include "TOTP/recheck.html" with mode='auth' %}
13 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/mfa/templates/TOTP/recheck.html:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 | One Time Password
22 |
23 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/mfa/templates/TrustedDevices/Add.html:
--------------------------------------------------------------------------------
1 | {% extends "mfa_auth_base.html" %}
2 | {% block head %}
3 |
26 | {% endblock %}
27 | {% block content %}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | Add Trusted Device
37 |
38 |
39 | {% if success %}
40 |
41 | Please check your PC window, to continue the process.
42 |
43 | {% elif added %}
44 |
45 | Your device is now trusted, please try to
login
46 |
47 |
48 | {% else %}
49 |
Please make sure you are not in private (incognito) mode
50 |
114 | {% endif %}
115 |
116 |
120 |
121 |
122 |
123 |
124 |
125 | {% endblock %}
126 |
--------------------------------------------------------------------------------
/mfa/templates/TrustedDevices/Done.html:
--------------------------------------------------------------------------------
1 | {% extends "mfa_auth_base.html" %}
2 | {% block head %}
3 | {% endblock %}
4 | {% block content %}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Add Trusted Device
13 |
14 |
15 |
16 | Your device is now trusted, please try to
login
17 |
18 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 | {% endblock %}
30 |
--------------------------------------------------------------------------------
/mfa/templates/TrustedDevices/email.html:
--------------------------------------------------------------------------------
1 |
Dear {{ request.user.last_name }}, {{ request.user.first_name }}
2 |
You requested the link to add a new trusted device, please follow the link below
3 | {{ HOST }}{% url 'mfa_add_new_trusted_device' %}
4 |
5 |
--------------------------------------------------------------------------------
/mfa/templates/TrustedDevices/start.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block head %}
3 |
13 |
14 |
71 | {% endblock %}
72 | {% block content %}
73 |
74 |
75 |
76 |
77 |
78 |
Add Trusted Device
79 |
80 |
81 |
82 | {% if not_allowed %}
83 |
You can't add any more devices, you need to remove previously trusted devices first.
84 | {% else %}
85 |
Allow access from mobile phone and tables.
86 |
Steps:
87 |
88 | Using your mobile/table, open Chrome/Firefox.
89 | Go to {{ HOST }}{{ BASE_URL }}devices/add
90 | Enter your username & following 6 digits
91 | {{ key|slice:":3" }} - {{ key|slice:"3:" }}
92 |
93 | This window will ask to confirm the device.
94 |
95 |
96 | {% endif %}
97 |
98 |
99 |
100 | {% include "modal.html" %}
101 | {% include 'mfa_check.html' %}
102 | {% endblock %}
--------------------------------------------------------------------------------
/mfa/templates/TrustedDevices/user-agent.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Browser:
4 | {{ ua.browser.family }}
5 |
6 |
7 | Version:
8 | {{ ua.browser.version_string }}
9 |
10 |
11 | Device:
12 | {{ ua.device.brand }} / {{ ua.device.model }}
13 |
14 |
--------------------------------------------------------------------------------
/mfa/templates/U2F/Add.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load static %}
3 | {% block head %}
4 |
14 |
15 |
35 | {% endblock %}
36 | {% block content %}
37 |
38 |
39 |
40 |
41 |
42 |
Adding Security Key
43 |
44 |
45 |
Your secure Key should be flashing now, please press on button.
46 |
47 |
48 |
49 |
50 | {% include "modal.html" %}
51 | {% endblock %}
--------------------------------------------------------------------------------
/mfa/templates/U2F/Auth.html:
--------------------------------------------------------------------------------
1 | {% extends "mfa_auth_base.html" %}
2 | {% block content %}
3 |
4 |
5 | {% include 'U2F/recheck.html' with mode='auth' %}
6 | {% endblock %}
--------------------------------------------------------------------------------
/mfa/templates/U2F/recheck.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 |
7 | Security Key
8 |
9 |
10 |
11 |
12 |
13 |
Your key should be flashing now, please press the button.
14 | {% if mode == "auth" %}
15 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {% if request.session.mfa_methods|length > 1 %}
30 |
Select Another Method
31 | {% endif %}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/mfa/templates/mfa_check.html:
--------------------------------------------------------------------------------
1 |
36 | {% include "modal.html" %}
--------------------------------------------------------------------------------
/mfa/templates/modal.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mfa/templates/select_mfa_method.html:
--------------------------------------------------------------------------------
1 | {% extends "mfa_auth_base.html" %}
2 | {% block content %}
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Select Second Verification Method
11 |
12 |
26 |
27 |
28 |
29 | {% endblock %}
--------------------------------------------------------------------------------
/mfa/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/mfa/totp.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 | from django.views.decorators.cache import never_cache
3 | from django.http import HttpResponse
4 | from .models import *
5 | from django.template.context_processors import csrf
6 | import simplejson
7 | from django.template.context import RequestContext
8 | from django.conf import settings
9 | import pyotp
10 | from .views import login
11 | import datetime
12 | from django.utils import timezone
13 | import random
14 | def verify_login(request,username,token):
15 | for key in User_Keys.objects.filter(username=username,key_type = "TOTP"):
16 | totp = pyotp.TOTP(key.properties["secret_key"])
17 | if totp.verify(token,valid_window = 30):
18 | key.last_used=timezone.now()
19 | key.save()
20 | return [True,key.id]
21 | return [False]
22 |
23 | def recheck(request):
24 | context = csrf(request)
25 | context["mode"]="recheck"
26 | if request.method == "POST":
27 | if verify_login(request,request.user.username, token=request.POST["otp"]):
28 | import time
29 | request.session["mfa"]["rechecked_at"] = time.time()
30 | return HttpResponse(simplejson.dumps({"recheck": True}), content_type="application/json")
31 | else:
32 | return HttpResponse(simplejson.dumps({"recheck": False}), content_type="application/json")
33 | return render(request,"TOTP/recheck.html", context)
34 |
35 | @never_cache
36 | def auth(request):
37 | context=csrf(request)
38 | if request.method=="POST":
39 | res=verify_login(request,request.session["base_username"],token = request.POST["otp"])
40 | if res[0]:
41 | mfa = {"verified": True, "method": "TOTP","id":res[1]}
42 | if getattr(settings, "MFA_RECHECK", False):
43 | mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now()
44 | + datetime.timedelta(
45 | seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
46 | request.session["mfa"] = mfa
47 | return login(request)
48 | context["invalid"]=True
49 | return render(request,"TOTP/Auth.html", context)
50 |
51 |
52 |
53 | def getToken(request):
54 | secret_key=pyotp.random_base32()
55 | totp = pyotp.TOTP(secret_key)
56 | request.session["new_mfa_answer"]=totp.now()
57 | return HttpResponse(simplejson.dumps({"qr":pyotp.totp.TOTP(secret_key).provisioning_uri(str(request.user.username), issuer_name = settings.TOKEN_ISSUER_NAME),
58 | "secret_key": secret_key}))
59 | def verify(request):
60 | answer=request.GET["answer"]
61 | secret_key=request.GET["key"]
62 | totp = pyotp.TOTP(secret_key)
63 | if totp.verify(answer,valid_window = 60):
64 | uk=User_Keys()
65 | uk.username=request.user.username
66 | uk.properties={"secret_key":secret_key}
67 | #uk.name="Authenticatior #%s"%User_Keys.objects.filter(username=user.username,type="TOTP")
68 | uk.key_type="TOTP"
69 | uk.save()
70 | return HttpResponse("Success")
71 | else: return HttpResponse("Error")
72 |
73 | @never_cache
74 | def start(request):
75 | return render(request,"TOTP/Add.html",{})
76 |
--------------------------------------------------------------------------------
/mfa/urls.py:
--------------------------------------------------------------------------------
1 | from . import views,totp,U2F,TrustedDevice,helpers,FIDO2,Email
2 | #app_name='mfa'
3 |
4 | try:
5 | from django.urls import re_path as url
6 | except:
7 | from django.conf.urls import url
8 | urlpatterns = [
9 | url(r'totp/start/', totp.start , name="start_new_otop"),
10 | url(r'totp/getToken', totp.getToken , name="get_new_otop"),
11 | url(r'totp/verify', totp.verify, name="verify_otop"),
12 | url(r'totp/auth', totp.auth, name="totp_auth"),
13 | url(r'totp/recheck', totp.recheck, name="totp_recheck"),
14 |
15 | url(r'email/start/', Email.start , name="start_email"),
16 | url(r'email/auth/', Email.auth , name="email_auth"),
17 |
18 | url(r'u2f/$', U2F.start, name="start_u2f"),
19 | url(r'u2f/bind', U2F.bind, name="bind_u2f"),
20 | url(r'u2f/auth', U2F.auth, name="u2f_auth"),
21 | url(r'u2f/process_recheck', U2F.process_recheck, name="u2f_recheck"),
22 | url(r'u2f/verify', U2F.verify, name="u2f_verify"),
23 |
24 | url(r'fido2/$', FIDO2.start, name="start_fido2"),
25 | url(r'fido2/auth', FIDO2.auth, name="fido2_auth"),
26 | url(r'fido2/begin_auth', FIDO2.authenticate_begin, name="fido2_begin_auth"),
27 | url(r'fido2/complete_auth', FIDO2.authenticate_complete, name="fido2_complete_auth"),
28 | url(r'fido2/begin_reg', FIDO2.begin_registeration, name="fido2_begin_reg"),
29 | url(r'fido2/complete_reg', FIDO2.complete_reg, name="fido2_complete_reg"),
30 | url(r'fido2/recheck', FIDO2.recheck, name="fido2_recheck"),
31 |
32 |
33 | url(r'td/$', TrustedDevice.start, name="start_td"),
34 | url(r'td/add', TrustedDevice.add, name="add_td"),
35 | url(r'td/send_link', TrustedDevice.send_email, name="td_sendemail"),
36 | url(r'td/get-ua', TrustedDevice.getUserAgent, name="td_get_useragent"),
37 | url(r'td/trust', TrustedDevice.trust_device, name="td_trust_device"),
38 | url(r'u2f/checkTrusted', TrustedDevice.checkTrusted, name="td_checkTrusted"),
39 | url(r'u2f/secure_device', TrustedDevice.getCookie, name="td_securedevice"),
40 |
41 | url(r'^$', views.index, name="mfa_home"),
42 | url(r'goto/(.*)', views.goto, name="mfa_goto"),
43 | url(r'selct_method', views.show_methods, name="mfa_methods_list"),
44 | url(r'recheck', helpers.recheck, name="mfa_recheck"),
45 | url(r'toggleKey', views.toggleKey, name="toggle_key"),
46 | url(r'delete', views.delKey, name="mfa_delKey"),
47 | url(r'reset', views.reset_cookie, name="mfa_reset_cookie"),
48 |
49 | ]
50 | # print(urlpatterns)
--------------------------------------------------------------------------------
/mfa/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 | from django.http import HttpResponse,HttpResponseRedirect
3 | from .models import *
4 | try:
5 | from django.urls import reverse
6 | except:
7 | from django.core.urlresolvers import reverse
8 | from django.template.context_processors import csrf
9 | from django.template.context import RequestContext
10 | from django.conf import settings
11 | from . import TrustedDevice
12 | from django.contrib.auth.decorators import login_required
13 | from user_agents import parse
14 |
15 | @login_required
16 | def index(request):
17 | keys=[]
18 | context={"keys":User_Keys.objects.filter(username=request.user.username),"UNALLOWED_AUTHEN_METHODS":settings.MFA_UNALLOWED_METHODS
19 | ,"HIDE_DISABLE":getattr(settings,"MFA_HIDE_DISABLE",[])}
20 | for k in context["keys"]:
21 | if k.key_type =="Trusted Device" :
22 | setattr(k,"device",parse(k.properties.get("user_agent","-----")))
23 | elif k.key_type == "FIDO2":
24 | setattr(k,"device",k.properties.get("type","----"))
25 | keys.append(k)
26 | context["keys"]=keys
27 | return render(request,"MFA.html",context)
28 |
29 | def verify(request,username):
30 | request.session["base_username"] = username
31 | #request.session["base_password"] = password
32 | keys=User_Keys.objects.filter(username=username,enabled=1)
33 | methods=list(set([k.key_type for k in keys]))
34 |
35 | if "Trusted Device" in methods and not request.session.get("checked_trusted_device",False):
36 | if TrustedDevice.verify(request):
37 | return login(request)
38 | methods.remove("Trusted Device")
39 | request.session["mfa_methods"] = methods
40 | if len(methods)==1:
41 | return HttpResponseRedirect(reverse(methods[0].lower()+"_auth"))
42 | return show_methods(request)
43 |
44 | def show_methods(request):
45 | return render(request,"select_mfa_method.html", {})
46 |
47 | def reset_cookie(request):
48 | response=HttpResponseRedirect(settings.LOGIN_URL)
49 | response.delete_cookie("base_username")
50 | return response
51 | def login(request):
52 | from django.contrib import auth
53 | from django.conf import settings
54 | callable_func = __get_callable_function__(settings.MFA_LOGIN_CALLBACK)
55 | return callable_func(request,username=request.session["base_username"])
56 |
57 |
58 | @login_required
59 | def delKey(request):
60 | key=User_Keys.objects.get(id=request.GET["id"])
61 | if key.username == request.user.username:
62 | key.delete()
63 | return HttpResponse("Deleted Successfully")
64 | else:
65 | return HttpResponse("Error: You own this token so you can't delete it")
66 |
67 | def __get_callable_function__(func_path):
68 | import importlib
69 | if not '.' in func_path:
70 | raise Exception("class Name should include modulename.classname")
71 |
72 | parsed_str = func_path.split(".")
73 | module_name , func_name = ".".join(parsed_str[:-1]) , parsed_str[-1]
74 | imported_module = importlib.import_module(module_name)
75 | callable_func = getattr(imported_module,func_name)
76 | if not callable_func:
77 | raise Exception("Module does not have requested function")
78 | return callable_func
79 |
80 | @login_required
81 | def toggleKey(request):
82 | id=request.GET["id"]
83 | q=User_Keys.objects.filter(username=request.user.username, id=id)
84 | if q.count()==1:
85 | key=q[0]
86 | if not key.key_type in settings.MFA_HIDE_DISABLE:
87 | key.enabled=not key.enabled
88 | key.save()
89 | return HttpResponse("OK")
90 | else:
91 | return HttpResponse("You can't change this method.")
92 | else:
93 | return HttpResponse("Error")
94 |
95 | def goto(request,method):
96 | return HttpResponseRedirect(reverse(method.lower()+"_auth"))
97 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | asgiref==3.3.4
2 | cffi==1.14.5
3 | cryptography==3.4.7
4 | dj-database-url==0.5.0
5 | Django==3.2.3
6 | ecdsa==0.14.1
7 | fido2==0.9.1
8 | gunicorn==20.1.0
9 | jsonfield==3.1.0
10 | jsonLookup==0.9.0
11 | psycopg2==2.8.6
12 | pyasn1==0.4.8
13 | pycparser==2.20
14 | pyotp==2.6.0
15 | python-decouple==3.4
16 | python-jose==3.2.0
17 | python-u2flib-server==5.0.1
18 | pytz==2021.1
19 | rsa==4.7.2
20 | simplejson==3.17.2
21 | six==1.16.0
22 | sqlparse==0.4.1
23 | ua-parser==0.10.0
24 | user-agents==2.2.0
25 | whitenoise==5.2.0
26 |
--------------------------------------------------------------------------------
/static/css/custom.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Globals
3 | */
4 |
5 | /* Custom default button */
6 | .btn-secondary,
7 | .btn-secondary:hover,
8 | .btn-secondary:focus {
9 | color: #333;
10 | text-shadow: none; /* Prevent inheritance from `body` */
11 | }
12 |
13 | .bd-placeholder-img {
14 | font-size: 1.125rem;
15 | text-anchor: middle;
16 | -webkit-user-select: none;
17 | -moz-user-select: none;
18 | user-select: none;
19 | }
20 |
21 | @media (min-width: 768px) {
22 | .bd-placeholder-img-lg {
23 | font-size: 3.5rem;
24 | }
25 | }
26 | /*
27 | * Base structure
28 | */
29 |
30 | body {
31 | text-shadow: 0 0.05rem 0.1rem rgba(0, 0, 0, 0.5);
32 | box-shadow: inset 0 0 5rem rgba(0, 0, 0, 0.5);
33 | }
34 |
35 | .cover-container {
36 | max-width: 42em;
37 | }
38 |
39 | /*
40 | * Header
41 | */
42 |
43 | .nav-masthead .nav-link {
44 | padding: 0.25rem 0;
45 | font-weight: 700;
46 | color: rgba(255, 255, 255, 0.5);
47 | background-color: transparent;
48 | border-bottom: 0.25rem solid transparent;
49 | }
50 |
51 | .nav-masthead .nav-link:hover,
52 | .nav-masthead .nav-link:focus {
53 | border-bottom-color: rgba(255, 255, 255, 0.25);
54 | }
55 |
56 | .nav-masthead .nav-link + .nav-link {
57 | margin-left: 1rem;
58 | }
59 |
60 | .nav-masthead .active {
61 | color: #fff;
62 | border-bottom-color: #fff;
63 | }
64 |
--------------------------------------------------------------------------------
/static/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sirneij/django_mfa2_example/d9505213e7e914481f06ea3426a515f5e0d26bc2/static/images/logo.png
--------------------------------------------------------------------------------
/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Django-mfa2 Example | {% block title %} {% endblock title %}
11 |
12 |
13 |
17 |
18 |
19 |
23 |
24 |
25 |
26 | {% block head %} {% endblock %} {% block css %} {% endblock css %}
27 |
28 |
29 | {% block content %} {% endblock content %}
30 |
31 |
--------------------------------------------------------------------------------