├── .github └── workflows │ └── autopublish.yml ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── design ├── package-lock.json ├── package.json ├── postcss.config.js ├── src │ ├── all-layouts.html │ ├── multifactor.js │ └── multifactor.scss └── vite.config.js ├── logo.svg ├── logo3.png ├── multifactor ├── __init__.py ├── admin.py ├── app_settings.py ├── apps.py ├── common.py ├── decorators.py ├── factors │ ├── __init__.py │ ├── fallback.py │ ├── fido2.py │ └── totp.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20190823_2128.py │ ├── 0003_userkey_name.py │ ├── 0004_alter_userkey_key_type.py │ └── __init__.py ├── mixins.py ├── models.py ├── static │ └── multifactor │ │ ├── js │ │ ├── multifactor.js │ │ ├── qrcode.js │ │ ├── qrcode.min.js │ │ └── webauthn-json.browser-ponyfill.js │ │ ├── keys.svg │ │ ├── multifactor.css │ │ └── multifactor.js ├── templates │ └── multifactor │ │ ├── FIDO2 │ │ ├── add.html │ │ └── check.html │ │ ├── TOTP │ │ ├── add.html │ │ └── check.html │ │ ├── add.html │ │ ├── authenticate.html │ │ ├── base.html │ │ ├── brand.html │ │ ├── fallback │ │ ├── auth.html │ │ └── email.html │ │ ├── help.html │ │ ├── home.html │ │ └── userkey_form.html ├── urls.py └── views.py ├── poetry.lock ├── pyproject.toml └── testsite ├── manage.py ├── requirements.txt └── testsite ├── __init__.py ├── disable_csrf.py ├── settings.py └── urls.py /.github/workflows/autopublish.yml: -------------------------------------------------------------------------------- 1 | name: autopublish 2 | on: 3 | push: 4 | tags: 5 | - "v*.*.*" 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Build and publish to pypi 12 | uses: JRubics/poetry-publish@v1.16 13 | with: 14 | pypi_token: ${{ secrets.PYPI_TOKEN }} 15 | plugins: "poetry-dynamic-versioning[plugin]" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | example/venv 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | node_modules/ 109 | venv 110 | 111 | # for testing HTTPS locally - generate your own ;) 112 | testsite/cert.pem 113 | testsite/key.pem 114 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | install: 3 | - pip install poetry 4 | - pip install pytest 5 | - poetry install 6 | 7 | script: 8 | - pytest # I hope you're using this to run your tests instead of unittest 9 | 10 | after_success: 11 | - poetry build # need the build artifacts for deploy 12 | - | 13 | if [ -n "${TRAVIS_TAG}" ]; then 14 | # enter deploy command(s) you used to use here 15 | else 16 | echo "not on a tag, nothing else to do." 17 | fi -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "cbor", 4 | "collectstatic", 5 | "ctap", 6 | "keychain", 7 | "loginas", 8 | "multifactor", 9 | "passwordless", 10 | "TOTP", 11 | "totp", 12 | "urlpatterns", 13 | "webauthn" 14 | ] 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Oli Warner 4 | Copyright (c) 2019 Mohamed El-Kalioby 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![django-multifactor - Easy multi-factor authentication for Django](https://raw.githubusercontent.com/oliwarner/django-multifactor/master/logo3.png) 2 | 3 | Probably the easiest multi-factor for Django. Ships with standalone views, opinionated defaults 4 | and a very simple integration pathway to retrofit onto mature sites. Supports [FIDO2/WebAuthn](https://en.wikipedia.org/wiki/WebAuthn) and [TOTP authenticators](https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm), with removable fallback options for email, SMS, carrier pigeon, or whatever other token exchange you can think of. U2F has been removed in 0.6. 5 | 6 | This is ***not*** a passwordless authentication system. django-multifactor is a second layer of defence. 7 | 8 | [![PyPI version](https://badge.fury.io/py/django-multifactor.svg)](https://badge.fury.io/py/django-multifactor) 9 | 10 | FIDO2/WebAuthn is the big-ticket item for MFA. It allows the browser to interface with a myriad of biometric and secondary authentication factors. 11 | 12 | * **Security keys** (Firefox 60+, Chrome 67+, Edge 18+), 13 | * **Windows Hello** (Firefox 67+, Chrome 72+ , Edge) , 14 | * **Apple's Touch ID** (Chrome 70+ on Mac OS X ), 15 | * **android-safetynet** (Chrome 70+) 16 | * **NFC devices using PCSC** (Not Tested, but as supported in fido2) 17 | 18 | # Python and Django Support 19 | This project targets modern stacks, officially supporting Python 3.8+ and Django 3.2+. Please refer to the [Django documentation](https://docs.djangoproject.com/en/dev/faq/install/) for more 20 | 21 | | **Python/Django** | **2.2** |**3.2** | **4.0** | **4.1** | **4.2** | **5.0** | **5.1** | **5.2** | 22 | |-------------------|---------|--------|---------|---------|---------|---------|---------|---------| 23 | | 3.8 | Y | Y | Y | Y | N/A | N/A | N/A | N/A | 24 | | 3.9 | Y | Y | Y | Y | N/A | N/A | N/A | N/A | 25 | | 3.10 | N | Y | Y | Y | N/A | Y | Y | Y | 26 | | 3.11 | N | N | N | Y | Y | Y | Y | Y | 27 | | 3.12 | N | N | N | N | Y | Y | Y | Y | 28 | | 3.13 | N | N | N | N | N | N | Y | Y | 29 | 30 | ## Installation: 31 | 32 | Install the package: 33 | 34 | pip install django-multifactor 35 | 36 | Add `multifactor` to `settings.INSTALLED_APPS` and override whichever setting you need. 37 | 38 | MULTIFACTOR = { 39 | 'LOGIN_CALLBACK': False, # False, or dotted import path to function to process after successful authentication 40 | 'RECHECK': True, # Invalidate previous authorisations at random intervals 41 | 'RECHECK_MIN': 60 * 60 * 3, # No rechecks before 3 hours 42 | 'RECHECK_MAX': 60 * 60 * 6, # But within 6 hours 43 | 44 | 'FIDO_SERVER_ID': 'example.com', # Server ID for FIDO request 45 | 'FIDO_SERVER_NAME': 'Django App', # Human-readable name for FIDO request 46 | 'TOKEN_ISSUER_NAME': 'Django App', # TOTP token issuing name (to be shown in authenticator) 47 | 48 | # Optional Keys - Only include these keys if you wish to deviate from the default actions 49 | 'LOGIN_MESSAGE': 'Manage multifactor settings.', # {OPTIONAL} When set overloads the default post-login message. 50 | 'SHOW_LOGIN_MESSAGE': False, # {OPTIONAL} Set to False to not create a post-login message 51 | } 52 | 53 | Ensure that [`django.contrib.messages`](https://docs.djangoproject.com/en/2.2/ref/contrib/messages/) is installed. 54 | 55 | Include `multifactor.urls` in your URLs. You can do this anywhere but I suggest somewhere similar to your login URLs, or underneath them, eg: 56 | 57 | urlpatterns = [ 58 | path('admin/multifactor/', include('multifactor.urls')), 59 | path('admin/', admin.site.urls), 60 | ... 61 | ] 62 | 63 | 64 | And don't forget to run a `./manage.py collectstatic` before restarting Django. 65 | 66 | 67 | ## Usage 68 | 69 | At this stage any authenticated user can add a secondary factor to their account by visiting (eg) `/admin/multifactor/`, but no view will *require* secondary authentication. django-multifactor gives you granular control to conditionally require certain users need a secondary factor on certain views. This is accomplished through the `multifactor.decorators.multifactor_protected` decorator. 70 | 71 | from multifactor.decorators import multifactor_protected 72 | 73 | @multifactor_protected(factors=0, user_filter=None, max_age=0, advertise=False) 74 | def my_view(request): 75 | ... 76 | 77 | - `factors` is the minimum number of active, authenticated secondary factors. 0 will mean users will only be prompted if they have keys. It can also accept a lambda/function with one request argument that returns a number. This allows you to tune whether factors are required based on custom logic (eg if local IP return 0 else return 1) 78 | - `user_filter` can be a dictionary to be passed to `User.objects.filter()` to see if the current user matches these conditions. If empty or None, it will match all users. 79 | - `max_age=600` will ensure the the user has authenticated with their secondary factor within 10 minutes. You can tweak this for higher security at the cost of inconvenience. 80 | - `advertise=True` will send an info-level message via django.contrib.messages with a link to the main django-multifactor page that allows them to add factors for future use. This is useful to increase optional uptake when introducing multifactor to an organisation. 81 | 82 | 83 | You can also wrap entire branches of your URLs using [`django-decorator-include`](https://pypi.org/project/django-decorator-include/): 84 | 85 | from decorator_include import decorator_include 86 | from multifactor.decorators import multifactor_protected 87 | 88 | urlpatterns = [ 89 | path('admin/multifactor/', include('multifactor.urls')), 90 | path('admin/', decorator_include(multifactor_protected(factors=1), admin.site.urls)), 91 | ... 92 | ] 93 | 94 | 95 | ## Don't want to allow TOTP? Turn them off. 96 | 97 | You can control the factors users can pick from in `settings.MULTIFACTOR`: 98 | 99 | MULTIFACTOR = { 100 | # ... 101 | 'FACTORS': ['FIDO2', 'TOTP'], # <- this is the default 102 | } 103 | 104 | 105 | ## Extending OTP fallback with custom transports 106 | 107 | `django-multifactor` has a fallback system that allows the user to be contacted via a number of sub-secure methods **simultaneously**. The rationale is that if somebody hacks their email account, they'll still know something is going on when they get an SMS. Providing sane options for your users is critical to security here. A poor fallback can undermine otherwise solid factors. 108 | 109 | The included fallback uses `user.email` to send an email. This now sends as plain+HTML. You can send just plain by setting `settings.MULTIFACTOR.HTML_EMAIL` to `False`. 110 | 111 | You can plumb in additional functions to carry the OTP message over any 112 | other system you like. The function should look something like: 113 | 114 | def send_carrier_pigeon(user, message): 115 | bird = find_bird() 116 | bird.attach(message) 117 | bird.send(user.address) 118 | return True # to indicate it sent 119 | 120 | Then hook that into `settings.MULTIFACTOR`: 121 | 122 | MULTIFACTOR = { 123 | # ... 124 | 'FALLBACKS': { 125 | 'email': (lambda user: user, 'multifactor.factors.fallback.send_email'), 126 | 'pigeon': (lambda user: user.address, 'path.to.send_carrier_pigeon'), 127 | } 128 | } 129 | 130 | Now if the user selects the fallback option, they will receive an email *and* a pigeon. You can remove email by omitting that line. You can disable fallback entirely by setting FALLBACKS to an empty dict. 131 | 132 | 133 | ## Conditional bypass 134 | 135 | It's sometimes useful to be able to be able to conditionally bypass multifactor requirements. You might be in local testing, you might be in automated testing or impersonating other users. Deactivating a security layer has obvious risks but that's between you and your gods. 136 | 137 | `settings.MULTIFACTOR.BYPASS` accepts a single path to a function accepting a request. If that returns True, multifactor will bypass its normal checks on a page. 138 | 139 | MULTIFACTOR = { 140 | # ... 141 | 'BYPASS': 'path.to.bypass_when_impersonating' 142 | } 143 | 144 | These are relatively easy to implement: 145 | 146 | def bypass_when_impersonating(request): 147 | from loginas.utils import is_impersonated_session 148 | if is_impersonated_session(request): 149 | return True 150 | 151 | def bypass_when_debug(request): 152 | from django.config import settings 153 | return settings.DEBUG 154 | 155 | 156 | ## UserAdmin integration 157 | 158 | It's often useful to monitor which of your users is using django-multifactor and, in emergencies, critical to be able to turn their secondary factors off. We ship a opinionated mixin class that you can add to your existing UserAdmin definition. 159 | 160 | from multifactor.admin import MultifactorUserAdmin 161 | 162 | @admin.register(User) 163 | class StaffAdmin(UserAdmin, MultifactorUserAdmin): 164 | ... 165 | 166 | It adds a column to show if that user has active factors, a filter to just show those with or without, and an inline to allow admins to turn certain keys off for their users. 167 | 168 | 169 | ## Branding 170 | 171 | If you want to use the styles and form that django-multifactor supplies, your users may think they're on another site. To help there is an empty placeholder template `multifactor/brand.html` that you can override in your project. This slots in just before the h1 title tag and has `text-align: centre` as standard. 172 | 173 | If you use HTML emails for your email fallback, you can create a `multifactor/email.html` template (accepting user, message context variables). 174 | 175 | You can use this to include your product logo, or an explanation. 176 | -------------------------------------------------------------------------------- /design/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multifactor-design", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "vite build", 8 | "watch": "vite build --watch" 9 | }, 10 | "author": "Oli Warner", 11 | "license": "MIT", 12 | "private": true, 13 | "dependencies": { 14 | "bulma": "^0.9.4", 15 | "sass": "^1.76.0", 16 | "vite": "^6.3.4" 17 | }, 18 | "devDependencies": { 19 | "@fullhuman/postcss-purgecss": "^6.0.0", 20 | "postcss": "^8.4.38", 21 | "postcss-variable-compress": "^3.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /design/postcss.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | plugins: [ 4 | require('@fullhuman/postcss-purgecss')({ 5 | content: [ 6 | 'src/all-layouts.html', 7 | '../multifactor/templates/**/*html', 8 | ], 9 | variables: true, 10 | }), 11 | require('postcss-variable-compress'), 12 | ], 13 | } -------------------------------------------------------------------------------- /design/src/all-layouts.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block title %}Multi-Factor Verification{% endblock %} 10 | 11 | 12 | 13 | 14 |
15 | 16 |
success test
17 |
info test
18 |
warning test
19 |
danger test
20 |
error test
21 | 22 | 23 |
24 |
25 |

Test card

26 |
27 |
28 | 29 | 41 | 42 |

To keep your account secure, you can add one or more secondary authentication factors. These are commonly USB keys, or codes acquired through a secure mechanism like an authenticator.

43 | 44 | 45 | 46 | 47 | 53 | 64 | 65 | 66 | 70 | 78 | 79 | 80 |
48 |

49 | {{ key.display_name }} 50 |

51 |

Added {{ key.added_on.date }}. raaa

52 |
54 | 🔓 55 | 🔒 56 |
57 | 58 |
59 |
60 | 61 |
62 | 63 |
67 | Fallback verification factors 68 |

Consider turning these off for higher security.

69 |
71 |
72 | 76 |
77 |
81 | 82 | What is multi-factor authentication? Where do I start? 83 | 84 |
85 | 142 | 143 | 144 | 145 | 146 |
147 |
148 |

Test card

149 |
150 |
151 |

You are logged in but we need to verify one of your secondary factors to continue. Please pick one.

152 | U2F Security Key → 153 | TOTP Authenticator → 154 | FIDO2 Security Device → 155 |
156 |
157 | 158 | 159 | 160 |
161 |
162 |

Test card

163 |
164 |
165 |

This step is completely optional. Give your TOTP Authenticator a better name to help you distinguish it from your other keys.

166 | 167 |
168 | 169 | 170 |
171 | 172 |
173 | 174 | 175 |
176 |
177 |
178 | 179 | 180 |
181 |
182 |

Test card

183 |
184 |
185 |

Start by downloading an Authenticator App on your phone. Google Authenticator for Android or Authy for iPhones.

186 | 187 |
188 | 189 |

{{secret_key}}

190 |
191 | 192 |

Scan the barcode above with your Authenticator. It will give you a rotating 6-digit code.

193 |

Copy that code into the box below and click Verify.

194 | {% endblock preform %} 195 | 196 |
197 | {% csrf_token %} 198 | 199 | 200 |
201 | 202 |
203 | 204 | 205 |
206 | 207 |
208 |
209 | 210 | 211 |
212 |
213 |

U2F Security Key

214 |
215 |
U2F only works under HTTPS
216 |
217 | 218 | 219 |
220 |
221 |

U2F Security Key

222 |
223 |
U2F only works under HTTPS
224 |
225 | 226 |
227 |
228 |

Which factor would you like to authenticate with?

229 |
230 |
231 |

You are logged in but we need to verify one of your secondary factors to continue. Please pick one.

232 | U2F Security Key → 233 | TOTP Authenticator → 234 | FIDO2 Security Device → 235 |
236 | 242 |
243 | 244 | 245 |
246 | -------------------------------------------------------------------------------- /design/src/multifactor.js: -------------------------------------------------------------------------------- 1 | import design from './multifactor.scss' 2 | 3 | function display_message(level, msg) { 4 | document.getElementById('card').classList.add(`has-background-${level}-dark`, 'has-text-white', 'has-text-centered') 5 | document.getElementById('content').innerHTML = msg 6 | } 7 | window.display_error = function(msg) { display_message('danger', msg) } 8 | window.display_succcess = function(msg) { display_message('success', msg) } 9 | 10 | 11 | document.body.addEventListener('click', function (ev) { 12 | if (ev.target.classList.contains('delete-button')) { 13 | let carryOn = confirm('Are you sure you want to delete this factor?') 14 | if (!carryOn) 15 | ev.preventDefault() 16 | } 17 | }, false) -------------------------------------------------------------------------------- /design/src/multifactor.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | @import url('https://fonts.googleapis.com/css?family=Assistant:400,600'); 4 | $family-sans-serif: "Assistant", sans-serif; 5 | $weight-normal: 400; 6 | $weight-bold: 600; 7 | 8 | $primary: #33A9FF; 9 | $success: #54BC2B; 10 | $info: #0287FC; 11 | $warning: #FFF028; 12 | $danger: #ed1400; 13 | 14 | $body-background-color: #EEE; 15 | $control-border-width: 3px; 16 | 17 | $custom-colors: ("error": $danger,); 18 | 19 | @import "~/bulma/bulma.sass"; 20 | 21 | html, body {min-height:100vh;} 22 | body { 23 | margin: 50px 20px 50px; 24 | display: flex; 25 | 26 | @include until($tablet) { 27 | margin-top: 0 !important; 28 | :last-child {margin-bottom:0 !important;} 29 | } 30 | @include from($tablet) { 31 | align-items: center; 32 | justify-content: center; 33 | } 34 | } 35 | body > .column { 36 | padding-bottom:0; 37 | 38 | @include until($tablet) { 39 | flex-grow: 1; 40 | display: flex; 41 | flex-direction: column; 42 | padding-top:0; 43 | } 44 | 45 | @include from($tablet) { 46 | max-width: 1020px; 47 | margin-left:20px; 48 | margin-right: 20px; 49 | &.curt {max-width: 450px} 50 | } 51 | 52 | 53 | > .notification { 54 | margin: 1.5rem 0 0 0 !important; 55 | text-align: center; 56 | 57 | @include until($tablet) { 58 | border-radius: 0; 59 | margin: 0 !important; 60 | } 61 | flex-grow: 0; 62 | } 63 | 64 | > .card { 65 | @include until($tablet) { 66 | flex-grow: 1; 67 | display: flex; 68 | flex-direction: column; 69 | > .card-content {flex-grow: 1} 70 | &, > .card-header {box-shadow: none !important;} 71 | } 72 | @include from($tablet) { 73 | min-height: 0; 74 | margin-top:1rem; 75 | margin-bottom:10%; 76 | } 77 | :last-child, .card-content :last-child {margin-bottom: 0 !important;} 78 | } 79 | } 80 | 81 | .card { 82 | border-radius: 3px; 83 | overflow: hidden; 84 | } 85 | 86 | .card-header { 87 | box-shadow: none; 88 | margin-top: 0.75rem; 89 | margin-bottom: -.75rem; 90 | flex-direction: column; 91 | text-align: center; 92 | 93 | h1, h2, h3, h4, h5 { 94 | text-align:center; 95 | font-size: 1.4rem; 96 | margin:0; 97 | } 98 | } 99 | .card-footer { 100 | border-top: 0; 101 | 102 | .card {height:100%} 103 | 104 | // authenticate full-width (flex) button 105 | > .button { 106 | border-radius: 0; 107 | padding: 0.75rem 1rem; 108 | } 109 | } 110 | 111 | .card.has-background-danger-dark { 112 | text-align: center; 113 | color: $white; 114 | .card-header-title {color: $white;} 115 | } 116 | 117 | p {margin-bottom: 1em;} 118 | 119 | .table { 120 | margin-left: -12px; 121 | margin-right: -12px; 122 | width: 100%; 123 | td { 124 | vertical-align: middle; 125 | :last-child { 126 | margin-bottom:0; 127 | } 128 | &:last-child { 129 | text-align: right; 130 | width:10%; 131 | white-space: nowrap; 132 | } 133 | form { 134 | display: inline; 135 | } 136 | } 137 | 138 | @include until($tablet) { 139 | td { 140 | display: block; 141 | padding-bottom: 0.5rem; 142 | width: 100%; 143 | } 144 | tr, td {border: 0;} 145 | } 146 | 147 | h3 {font-weight: $weight-bold} 148 | } 149 | 150 | .button-ml { 151 | display: inline-block; 152 | height: auto; 153 | span, small, strong { 154 | display: block 155 | } 156 | } 157 | 158 | .is-100 {width: 100%} 159 | 160 | .field {margin-bottom:1rem;} 161 | .button.is-fullwidth {margin-bottom:1rem;} 162 | 163 | h4 { 164 | font-size: 1.4rem; 165 | border-bottom: 1px solid $hr-background-color; 166 | padding-bottom: 4px; 167 | margin-bottom: 4px; 168 | } 169 | 170 | 171 | .button.is-multilayer { 172 | flex-direction: column; 173 | height:auto; 174 | 175 | small { 176 | display: block; 177 | font-size: 0.9rem; 178 | } 179 | } 180 | 181 | .home-add { 182 | float:right; 183 | margin: 0 0 1rem 1rem; 184 | } 185 | 186 | .qr-block { 187 | margin-bottom:1rem; 188 | text-align: center; 189 | > div { 190 | margin-bottom: 0.5rem; 191 | img {margin: auto;} 192 | } 193 | } 194 | 195 | #content .columns > .column { 196 | > .card { 197 | height: 100%; 198 | display: flex; 199 | flex-direction: column; 200 | } 201 | 202 | @include from($tablet) { 203 | padding-bottom:0; 204 | } 205 | .card-content {flex-grow: 1} 206 | } 207 | 208 | .button.is-toggle { 209 | position: relative; 210 | padding-left: 1.25em; 211 | padding-right: 1.25em; 212 | opacity: 1; 213 | transition: background-color 0.5s, color 0.5s; 214 | 215 | &:before{ 216 | content: " "; 217 | display: block; 218 | width: 12px; 219 | background: rgba(255, 255, 255, 0.8); 220 | border: 2px solid #ccc; 221 | border-radius: 4px; 222 | position: absolute; 223 | top: 0px; 224 | bottom: 0px; 225 | transition: left 0.7s, right 0.7s; 226 | } 227 | &:hover { 228 | } 229 | &.is-toggled-on { 230 | @extend .is-success; 231 | &:before { 232 | left: 0px; 233 | } 234 | &:hover { 235 | background-color: #eee !important; 236 | border-color: transparent !important; 237 | color: #363636 !important; 238 | } 239 | &:hover { 240 | // @extend .button .is-light:hover; 241 | } 242 | &:hover:before { 243 | left: calc(100% - 12px); 244 | } 245 | } 246 | &.is-toggled-off { 247 | // @extend .button .is-light; 248 | &:before { 249 | right: 0px; 250 | } 251 | &:hover { 252 | // @extend .button, .is-success:hover; 253 | } 254 | &:hover:before { 255 | right: calc(100% - 12px); 256 | } 257 | } 258 | } 259 | 260 | #authtype.automatic button {display: none;} 261 | #authtype.manual p {display: none;} 262 | 263 | .delete-button { 264 | span { display: none; } 265 | &:after { 266 | content: "✖" 267 | } 268 | } 269 | 270 | .is-error { 271 | @extend .is-danger 272 | } -------------------------------------------------------------------------------- /design/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { fileURLToPath } from 'url' 3 | 4 | export default defineConfig({ 5 | plugins: [], 6 | resolve: { 7 | alias: { 8 | '@': fileURLToPath(new URL('./src', import.meta.url)), 9 | '~': fileURLToPath(new URL('./node_modules/', import.meta.url)), 10 | } 11 | }, 12 | build: { 13 | outDir: '../multifactor/static/multifactor', 14 | emptyOutDir: false, 15 | rollupOptions: { 16 | input: { 17 | multifactor: fileURLToPath(new URL('./src/multifactor.js', import.meta.url)), 18 | }, 19 | output: { 20 | entryFileNames: `[name].js`, 21 | assetFileNames: `[name].[ext]` 22 | } 23 | }, 24 | } 25 | }) -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 58 | 63 | 68 | drop in multi-factor authentication for django 81 | 82 | -------------------------------------------------------------------------------- /logo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oliwarner/django-multifactor/e8c97f6aceaefcd0a234ccba8893754797b8b97e/logo3.png -------------------------------------------------------------------------------- /multifactor/__init__.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | 4 | if django.VERSION < (3, 2): 5 | default_app_config = 'multifactor.apps.MultifactorConfig' 6 | -------------------------------------------------------------------------------- /multifactor/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.db.models import OuterRef, Exists 3 | 4 | from .models import UserKey 5 | 6 | 7 | class HasMultifactorFilter(admin.SimpleListFilter): 8 | title = 'Using Multifactor authentication?' 9 | parameter_name = 'multifactor' 10 | 11 | def lookups(self, request, model_admin): 12 | return [ 13 | (True, 'Yes'), 14 | (False, 'No'), 15 | ] 16 | 17 | def queryset(self, request, queryset): 18 | if self.value(): 19 | return queryset.filter(has_multifactors=self.value()) 20 | 21 | 22 | class MultiFactorInline(admin.TabularInline): 23 | model = UserKey 24 | readonly_fields = ('key_type',) 25 | fields = ('key_type', 'enabled') 26 | max_num = 0 27 | 28 | 29 | class MultifactorUserAdmin(admin.ModelAdmin): 30 | multifactor_filter = True 31 | multifactor_list_display = True 32 | multifactor_inline = True 33 | 34 | def get_queryset(self, request): 35 | keys = UserKey.objects.filter(user=OuterRef('pk'), enabled=True) 36 | return super().get_queryset(request).annotate(has_multifactors=Exists(keys)) 37 | 38 | def get_list_display(self, request): 39 | if not self.multifactor_list_display: 40 | return super().get_list_display(request) 41 | 42 | return ( 43 | *super().get_list_display(request), 44 | 'multifactor', 45 | ) 46 | 47 | def get_list_filter(self, request): 48 | if not self.multifactor_filter: 49 | return super().get_list_filter(request) 50 | 51 | return ( 52 | *super().get_list_filter(request), 53 | HasMultifactorFilter, 54 | ) 55 | 56 | def multifactor(self, obj): 57 | return obj.has_multifactors 58 | multifactor.admin_order_field = 'has_multifactors' 59 | multifactor.boolean = True 60 | 61 | def get_inline_instances(self, request, obj=None): 62 | if self.multifactor_inline and MultiFactorInline not in self.inlines: 63 | self.inlines = ( 64 | *self.inlines, 65 | MultiFactorInline, 66 | ) 67 | 68 | return super().get_inline_instances(request, obj) 69 | -------------------------------------------------------------------------------- /multifactor/app_settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | mf_settings = getattr(settings, 'MULTIFACTOR', {}) 4 | 5 | mf_settings['LOGIN_MESSAGE'] = mf_settings.get('LOGIN_MESSAGE', 'You are now multifactor-authenticated. Multifactor settings.') 6 | mf_settings['SHOW_LOGIN_MESSAGE'] = mf_settings.get('SHOW_LOGIN_MESSAGE', True) 7 | mf_settings['LOGIN_CALLBACK'] = mf_settings.get('LOGIN_CALLBACK', False) 8 | mf_settings['RECHECK'] = mf_settings.get('RECHECK', True) 9 | mf_settings['RECHECK_MIN'] = mf_settings.get('RECHECK_MIN', 60 * 60 * 3) 10 | mf_settings['RECHECK_MAX'] = mf_settings.get('RECHECK_MAX', 60 * 60 * 6) 11 | 12 | mf_settings['FIDO_SERVER_ID'] = mf_settings.get('FIDO_SERVER_ID', 'example.com') 13 | mf_settings['FIDO_SERVER_NAME'] = mf_settings.get('FIDO_SERVER_NAME', 'Django App') 14 | mf_settings['FIDO_SERVER_ICON'] = mf_settings.get('FIDO_SERVER_ICON', None) 15 | mf_settings['TOKEN_ISSUER_NAME'] = mf_settings.get('TOKEN_ISSUER_NAME', 'Django App') 16 | 17 | mf_settings['FACTORS'] = mf_settings.get('FACTORS', ['FIDO2', 'TOTP']) 18 | 19 | mf_settings['FALLBACKS'] = mf_settings.get('FALLBACKS', { 20 | 'email': (lambda user: user.email, 'multifactor.factors.fallback.send_email'), 21 | }) 22 | 23 | mf_settings['HTML_EMAIL'] = mf_settings.get('HTML_EMAIL', True) 24 | 25 | mf_settings['BYPASS'] = mf_settings.get('BYPASS', None) -------------------------------------------------------------------------------- /multifactor/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MultifactorConfig(AppConfig): 5 | default = True 6 | name = 'multifactor' 7 | verbose_name = 'Multifactor' 8 | default_auto_field = 'django.db.models.AutoField' 9 | -------------------------------------------------------------------------------- /multifactor/common.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib import messages 3 | from django.shortcuts import render as dj_render, redirect 4 | from django.urls import reverse 5 | from django.utils import timezone 6 | from django.utils.html import format_html 7 | from django.utils.module_loading import import_string 8 | 9 | import random 10 | 11 | from .app_settings import mf_settings 12 | from .models import UserKey, DisabledFallback 13 | 14 | 15 | def has_multifactor(request): 16 | return UserKey.objects.filter(user=request.user, enabled=True).exists() 17 | 18 | 19 | def active_factors(request): 20 | # automatically expire old factors 21 | now = timezone.now().timestamp() 22 | factors = request.session["multifactor"] = [ 23 | *filter( 24 | lambda tup: tup[3] == False or tup[3] > now, 25 | request.session.get('multifactor', []) 26 | ), 27 | ] 28 | return factors 29 | 30 | 31 | def disabled_fallbacks(request): 32 | return DisabledFallback.objects.filter(user=request.user).values_list('fallback', flat=True) 33 | 34 | 35 | def next_check(): 36 | return timezone.now().timestamp() + random.randint( 37 | mf_settings['RECHECK_MIN'], 38 | mf_settings['RECHECK_MAX'] 39 | ) 40 | 41 | 42 | def render(request, template_name, context, **kwargs): 43 | return dj_render(request, template_name, { 44 | **context 45 | }, **kwargs) 46 | 47 | 48 | def method_url(method): 49 | return f'multifactor:{method.lower()}_auth' 50 | 51 | 52 | def write_session(request, key): 53 | """Write the multifactor session with the verified key""" 54 | request.session["multifactor"] = [ 55 | ( 56 | key.key_type if key else None, 57 | key.id if key else None, 58 | timezone.now().timestamp(), 59 | next_check() if mf_settings["RECHECK"] else False 60 | ), 61 | *filter( 62 | lambda tup: not key or tup[1] != key.id, 63 | request.session.get('multifactor', []) 64 | ), 65 | ] 66 | 67 | if key: 68 | key.last_used = timezone.now() 69 | key.save() 70 | 71 | 72 | def login(request): 73 | if mf_settings['SHOW_LOGIN_MESSAGE']: 74 | messages.info(request, format_html(mf_settings['LOGIN_MESSAGE'], reverse('multifactor:home'))) 75 | 76 | if 'multifactor-next' in request.session: 77 | return redirect(request.session.pop('multifactor-next', 'multifactor:home')) 78 | 79 | callback = mf_settings['LOGIN_CALLBACK'] 80 | if callback: 81 | return import_string(callback)(request, username=request.session["base_username"]) 82 | 83 | # punch back to the login URL and let it decide what to do with you 84 | return redirect(settings.LOGIN_URL) 85 | 86 | 87 | def is_bypassed(request): 88 | bypass = mf_settings['BYPASS'] 89 | if bypass: 90 | return import_string(bypass)(request) 91 | 92 | return False -------------------------------------------------------------------------------- /multifactor/decorators.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.contrib import messages 3 | from django.contrib.auth import get_user_model 4 | from django.core.exceptions import PermissionDenied 5 | from django.shortcuts import redirect 6 | from django.urls import reverse 7 | from django.utils import timezone 8 | from django.utils.html import format_html 9 | 10 | import functools 11 | import time 12 | import inspect 13 | 14 | from .common import method_url, active_factors, has_multifactor, is_bypassed 15 | 16 | 17 | __all__ = ['multifactor_protected'] 18 | 19 | 20 | def multifactor_protected(factors=0, user_filter=None, max_age=0, advertise=False): 21 | """ 22 | Protect a view with multifactor authentication. 23 | 24 | Parameters 25 | ---------- 26 | factors : int, function 27 | Number of separate factors that must be currently activated. 28 | You can also pass in a function accepting the request to return this number. 29 | user_filter : None | dict 30 | User-class.objects.filter dictionary to try to match current user. 31 | max_age : int 32 | Number of seconds since last authentication this view requires. 33 | Zero means infinite (or until it expires) 34 | advertise : bool 35 | Advertise to the user that they can optionally add keys for factors=0 views. 36 | """ 37 | def _func_wrapper(view_func, *args, **kwargs): 38 | @functools.wraps(view_func) 39 | def _wrapped_view_func(request, *args, **kwargs): 40 | def baulk(): 41 | return view_func(request, *args, **kwargs) 42 | 43 | def force_authenticate(): 44 | if django.VERSION < (4, 0) and request.is_ajax(): 45 | raise PermissionDenied('Multifactor authentication required') 46 | request.session['multifactor-next'] = request.get_full_path() 47 | return redirect('multifactor:authenticate') 48 | 49 | if not request.user.is_authenticated: 50 | return baulk() 51 | 52 | if is_bypassed(request): 53 | return baulk() 54 | 55 | if user_filter is not None: 56 | # we're filtering for specific users, check that the current user fits that 57 | if not get_user_model().objects.filter(pk=request.user.pk, **user_filter).exists(): 58 | return baulk() 59 | 60 | active = active_factors(request) 61 | 62 | if has_multifactor(request): 63 | if not active: 64 | # has keys but isn't using them, tell them to authenticate 65 | return force_authenticate() 66 | 67 | elif max_age and active[0][3] + max_age < timezone.now().timestamp(): 68 | # has authenticated but not recently enough for this view 69 | messages.warning( 70 | request, 71 | f'This page requires secondary authentication every {max_age} seconds. ' 72 | 'Please re-authenticate.' 73 | ) 74 | return force_authenticate() 75 | 76 | required_factors = factors 77 | if inspect.isfunction(factors): 78 | required_factors = factors(request) 79 | 80 | if required_factors > len(active): 81 | # view needs more active factors than provided 82 | messages.warning( 83 | request, 84 | f'This page requires {required_factors} active security ' 85 | f'factor{"" if required_factors == 1 else "s"}.' 86 | ) 87 | return force_authenticate() 88 | 89 | if not active and advertise and 'multifactor-advertised' not in request.session: 90 | # tell them that they can add keys but it's entirely optional 91 | messages.info(request, format_html( 92 | 'Make your account more secure by adding a second security factor ' 93 | 'such as a USB Security Token, or an Authenticator App.', 94 | reverse('multifactor:home') 95 | )) 96 | request.session['multifactor-advertised'] = True 97 | 98 | return baulk() 99 | return _wrapped_view_func 100 | return _func_wrapper 101 | -------------------------------------------------------------------------------- /multifactor/factors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oliwarner/django-multifactor/e8c97f6aceaefcd0a234ccba8893754797b8b97e/multifactor/factors/__init__.py -------------------------------------------------------------------------------- /multifactor/factors/fallback.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.conf import settings 3 | from django.core.mail import EmailMultiAlternatives 4 | from django.utils.module_loading import import_string 5 | from django.shortcuts import redirect 6 | from django.template.loader import render_to_string 7 | from django.views.generic import TemplateView 8 | from django.contrib.auth.mixins import LoginRequiredMixin 9 | 10 | from random import randint 11 | import logging 12 | 13 | from ..common import write_session, login, disabled_fallbacks 14 | from ..app_settings import mf_settings 15 | 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | SESSION_KEY = 'multifactor-fallback-otp' 20 | SESSION_KEY_SUCCEEDED = 'multifactor-fallback-succeeded' 21 | 22 | 23 | class Auth(LoginRequiredMixin, TemplateView): 24 | template_name = "multifactor/fallback/auth.html" 25 | 26 | def get(self, request, generate=True): 27 | if generate: 28 | otp = request.session[SESSION_KEY] = request.session.get(SESSION_KEY, str(randint(0, 100000000))) 29 | message = f'Your one-time-password is: {otp}' 30 | if request.user.get_full_name(): 31 | message = f'Dear {request.user.get_full_name()},\n{message}' 32 | 33 | disabled = disabled_fallbacks(request) 34 | s = [] 35 | for name, (field, method) in mf_settings['FALLBACKS'].items(): 36 | if name in disabled or not field(request.user): 37 | continue 38 | 39 | try: 40 | imported_method = import_string(method) 41 | if imported_method(request.user, message): 42 | s.append(name) 43 | except: 44 | pass 45 | 46 | if not s: 47 | messages.error(request, 'No fallback one-time-password transport methods worked. Please contact an administrator.') 48 | return redirect('multifactor:home') 49 | 50 | request.session[SESSION_KEY_SUCCEEDED] = s[0] if len(s) == 1 else (', '.join(s[:-1]) + ' and ' + s[-1]) 51 | 52 | return super().get( 53 | request, 54 | succeeded=request.session[SESSION_KEY_SUCCEEDED], 55 | ) 56 | 57 | def post(self, request): 58 | if request.session[SESSION_KEY] == request.POST["otp"].strip(): 59 | request.session.pop(SESSION_KEY) 60 | write_session(request, key=None) 61 | return login(request) 62 | 63 | messages.error(request, 'That key was not correct. Please try again.') 64 | return self.get(request, generate=False) 65 | 66 | 67 | def send_email(user, message): 68 | try: 69 | msg = EmailMultiAlternatives( 70 | subject='One Time Password', 71 | body=message, 72 | from_email=settings.SERVER_EMAIL, 73 | to=[user.email] 74 | ) 75 | 76 | if mf_settings['HTML_EMAIL']: 77 | # add a HTML version if allowed 78 | html_message = render_to_string( 79 | 'multifactor/fallback/email.html', 80 | {'user': user, 'message': message} 81 | ) 82 | msg.attach_alternative(html_message, "text/html") 83 | 84 | msg.send() 85 | 86 | return "email" 87 | except Exception: 88 | logger.exception('Could not send email:', user) 89 | return False 90 | 91 | 92 | def debug_print_console(user, message): 93 | print(user, message) 94 | return "command line" 95 | -------------------------------------------------------------------------------- /multifactor/factors/fido2.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | from django.contrib import messages 3 | from django.views.decorators.csrf import csrf_exempt 4 | from django.contrib.auth.mixins import LoginRequiredMixin 5 | from django.views.generic import View 6 | 7 | from fido2.server import Fido2Server 8 | from fido2.webauthn import AttestedCredentialData, PublicKeyCredentialUserEntity 9 | from fido2.utils import websafe_decode, websafe_encode 10 | import fido2.features 11 | import logging 12 | 13 | from ..models import UserKey, KeyTypes 14 | from ..common import write_session, login 15 | from ..app_settings import mf_settings 16 | from ..mixins import PreferMultiAuthMixin 17 | 18 | import json 19 | 20 | fido2.features.webauthn_json_mapping.enabled = True 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | class FidoClass(View): 25 | @classmethod 26 | def as_view(cls, **initkwargs): 27 | view = super().as_view(**initkwargs) 28 | return csrf_exempt(view) 29 | 30 | @property 31 | def server(self): 32 | return Fido2Server(rp=dict( 33 | id=mf_settings['FIDO_SERVER_ID'], 34 | name=mf_settings['FIDO_SERVER_NAME'] 35 | )) 36 | 37 | def get_user_credentials(self): 38 | if not self.request.user.is_authenticated: 39 | return [] 40 | return [ 41 | AttestedCredentialData(websafe_decode(key.properties["device"])) 42 | for key in UserKey.objects.filter( 43 | user=self.request.user, 44 | key_type=str(KeyTypes.FIDO2), 45 | properties__domain=self.request.get_host().split(":")[0], 46 | enabled=True, 47 | ) 48 | ] 49 | 50 | 51 | class Register(PreferMultiAuthMixin, FidoClass): 52 | def get(self, request, *args, **kwargs): 53 | registration_data, state = self.server.register_begin( 54 | user=PublicKeyCredentialUserEntity( 55 | id=request.user.get_username().encode('utf-8'), 56 | name=f'{request.user.get_full_name()}', 57 | display_name=request.user.get_username(), 58 | ), 59 | credentials=self.get_user_credentials(), 60 | ) 61 | request.session['fido_state'] = state 62 | 63 | return JsonResponse({**registration_data}, safe=False) 64 | 65 | def post(self, request, *args, **kwargs): 66 | try: 67 | data = json.loads(request.body) 68 | auth_data = self.server.register_complete( 69 | request.session['fido_state'], data) 70 | 71 | encoded = websafe_encode(auth_data.credential_data) 72 | key = UserKey.objects.create( 73 | user=request.user, 74 | properties={ 75 | "device": encoded, 76 | "type": data['type'], 77 | "domain": self.server.rp.id, 78 | }, 79 | key_type=str(KeyTypes.FIDO2), 80 | ) 81 | write_session(request, key) 82 | messages.success(request, 'FIDO2 Token added!') 83 | return JsonResponse({'status': 'OK'}) 84 | 85 | except: 86 | logger.exception("Error completing FIDO2 registration.") 87 | return JsonResponse({ 88 | 'status': 'ERR', 89 | "message": "Error on server, please try again later", 90 | }) 91 | 92 | 93 | class Authenticate(LoginRequiredMixin, FidoClass): 94 | def get(self, request, *args, **kwargs): 95 | auth_data, state = self.server.authenticate_begin( 96 | credentials=self.get_user_credentials(), 97 | user_verification="discouraged", 98 | ) 99 | request.session['fido_state'] = state 100 | return JsonResponse({**auth_data}) 101 | 102 | def post(self, request, *args, **kwargs): 103 | data = json.loads(request.body) 104 | 105 | cred = self.server.authenticate_complete( 106 | request.session.pop('fido_state'), 107 | self.get_user_credentials(), 108 | data 109 | ) 110 | 111 | keys = UserKey.objects.filter( 112 | user=request.user, 113 | key_type=str(KeyTypes.FIDO2), 114 | enabled=True, 115 | ) 116 | 117 | for key in keys: 118 | if AttestedCredentialData(websafe_decode(key.properties["device"])).credential_id == cred.credential_id: 119 | write_session(request, key) 120 | res = login(request) 121 | return JsonResponse({'status': "OK", "redirect": res["location"]}) 122 | 123 | return JsonResponse({'status': "err"}) 124 | -------------------------------------------------------------------------------- /multifactor/factors/totp.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.shortcuts import redirect 3 | from django.views.generic import TemplateView 4 | from django.contrib.auth.mixins import LoginRequiredMixin 5 | 6 | import pyotp 7 | 8 | from ..models import UserKey, KeyTypes 9 | from ..common import write_session, login 10 | from ..app_settings import mf_settings 11 | from ..mixins import PreferMultiAuthMixin 12 | 13 | 14 | WINDOW = 60 15 | 16 | 17 | class Create(PreferMultiAuthMixin, TemplateView): 18 | template_name = "multifactor/TOTP/add.html" 19 | 20 | def dispatch(self, request, *args, **kwargs): 21 | self.secret_key = request.POST.get("key", pyotp.random_base32()) 22 | self.totp = pyotp.TOTP(self.secret_key) 23 | return super().dispatch(request, *args, **kwargs) 24 | 25 | def get_context_data(self, **kwargs): 26 | return { 27 | **super().get_context_data(**kwargs), 28 | "qr": self.totp.provisioning_uri( 29 | self.request.user.get_username(), 30 | issuer_name=mf_settings['TOKEN_ISSUER_NAME'] 31 | ), 32 | "secret_key": self.secret_key, 33 | } 34 | 35 | def post(self, request, *args, **kwargs): 36 | if self.totp.verify(request.POST["answer"], valid_window=WINDOW): 37 | key = UserKey.objects.create( 38 | user=request.user, 39 | properties={"secret_key": self.secret_key}, 40 | key_type=str(KeyTypes.TOTP) 41 | ) 42 | write_session(request, key) 43 | messages.success(request, 'TOTP Authenticator added.') 44 | return redirect("multifactor:home") 45 | 46 | messages.error(request, 'Could not validate key, please try again.') 47 | return super().get(request, *args, **kwargs) 48 | 49 | 50 | class Auth(LoginRequiredMixin, TemplateView): 51 | template_name = "multifactor/TOTP/check.html" 52 | 53 | def post(self, request, *args, **kwargs): 54 | key = self.verify_login(token=request.POST["answer"]) 55 | if key: 56 | write_session(request, key) 57 | return login(request) 58 | 59 | messages.error(request, 'Could not validate key, please try again.') 60 | return super().get(request, *args, **kwargs) 61 | 62 | def verify_login(self, token): 63 | for key in UserKey.objects.filter(user=self.request.user, key_type=str(KeyTypes.TOTP), enabled=True): 64 | if pyotp.TOTP(key.properties["secret_key"]).verify(token, valid_window=WINDOW): 65 | return key 66 | -------------------------------------------------------------------------------- /multifactor/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.3 on 2019-08-17 21:39 2 | # Modified by Oli since for Django 3.1 compat 3 | 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | try: 10 | from django.db.models import JSONField 11 | except ImportError: 12 | from django.contrib.postgres.fields.jsonb import JSONField 13 | 14 | 15 | class Migration(migrations.Migration): 16 | 17 | initial = True 18 | 19 | dependencies = [ 20 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 21 | ] 22 | 23 | operations = [ 24 | migrations.CreateModel( 25 | name='UserKey', 26 | fields=[ 27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('properties', JSONField(null=True)), 29 | ('key_type', models.CharField(choices=[('FIDO2', 'FIDO2 Security Device'), ('U2F', 'U2F Security Key'), ('TOTP', 'TOTP Authenticator'), ('Email', 'OTP-over-Email')], max_length=25)), 30 | ('enabled', models.BooleanField(default=True)), 31 | ('added_on', models.DateTimeField(auto_now_add=True)), 32 | ('expires', models.DateTimeField(blank=True, default=None, null=True)), 33 | ('last_used', models.DateTimeField(blank=True, default=None, null=True)), 34 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='multifactor_keys', to=settings.AUTH_USER_MODEL)), 35 | ], 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /multifactor/migrations/0002_auto_20190823_2128.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.3 on 2019-08-23 20:28 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 11 | ('multifactor', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='userkey', 17 | name='key_type', 18 | field=models.CharField(choices=[('FIDO2', 'FIDO2 Security Device'), ('U2F', 'U2F Security Key'), ('TOTP', 'TOTP Authenticator')], max_length=25), 19 | ), 20 | migrations.CreateModel( 21 | name='DisabledFallback', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('fallback', models.CharField(max_length=50)), 25 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /multifactor/migrations/0003_userkey_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.3 on 2019-08-25 20:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('multifactor', '0002_auto_20190823_2128'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='userkey', 15 | name='name', 16 | field=models.CharField(blank=True, help_text='Easy to remember name to distinguish from any other keys of this sort you own.', max_length=30, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /multifactor/migrations/0004_alter_userkey_key_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-10-05 09:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('multifactor', '0003_userkey_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='userkey', 15 | name='key_type', 16 | field=models.CharField(choices=[('FIDO2', 'FIDO2 Security Device'), ('TOTP', 'TOTP Authenticator')], max_length=25), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /multifactor/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oliwarner/django-multifactor/e8c97f6aceaefcd0a234ccba8893754797b8b97e/multifactor/migrations/__init__.py -------------------------------------------------------------------------------- /multifactor/mixins.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import redirect 2 | 3 | from .models import UserKey 4 | from .common import active_factors, is_bypassed 5 | 6 | 7 | class MultiFactorMixin: 8 | """Verify that the current user is multifactor-authenticated or has no factors yet.""" 9 | 10 | def setup(self, request, *args, **kwargs): 11 | super().setup(request, *args, **kwargs) 12 | 13 | if not request.user.is_authenticated: 14 | return 15 | 16 | self.active_factors = active_factors(request) 17 | self.factors = UserKey.objects.filter(user=request.user) 18 | self.has_multifactor = self.factors.filter(enabled=True).exists() 19 | self.bypass = is_bypassed(request) 20 | 21 | 22 | class RequireMultiAuthMixin(MultiFactorMixin): 23 | """Require Multifactor, force user to add factors if none on account.""" 24 | 25 | def dispatch(self, request, *args, **kwargs): 26 | if not self.active_factors and not self.bypass: 27 | request.session['multifactor-next'] = request.get_full_path() 28 | if self.has_multifactor: 29 | return redirect('multifactor:authenticate') 30 | 31 | return redirect('multifactor:add') 32 | 33 | return super().dispatch(request, *args, **kwargs) 34 | 35 | 36 | class PreferMultiAuthMixin(MultiFactorMixin): 37 | """Use Multifactor if user has active factors.""" 38 | 39 | def dispatch(self, request, *args, **kwargs): 40 | if not self.active_factors and not self.bypass and self.has_multifactor: 41 | request.session['multifactor-next'] = request.get_full_path() 42 | return redirect('multifactor:authenticate') 43 | 44 | return super().dispatch(request, *args, **kwargs) 45 | -------------------------------------------------------------------------------- /multifactor/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | 4 | try: 5 | from django.db.models import JSONField 6 | except ImportError: 7 | from django.contrib.postgres.fields import JSONField 8 | 9 | 10 | 11 | class KeyTypes(models.TextChoices): 12 | FIDO2 = 'FIDO2', "FIDO2 Security Device" 13 | TOTP = 'TOTP', "TOTP Authenticator" 14 | 15 | 16 | # keys that can only be used on one domain 17 | DOMAIN_KEYS = (KeyTypes.FIDO2) 18 | 19 | 20 | class UserKey(models.Model): 21 | user = models.ForeignKey(settings.AUTH_USER_MODEL, models.CASCADE, related_name='multifactor_keys') 22 | name = models.CharField(max_length=30, help_text="Easy to remember name to distinguish from any other keys of this sort you own.", blank=True, null=True) 23 | properties = JSONField(null=True) 24 | key_type = models.CharField(max_length=25, choices=KeyTypes.choices) 25 | enabled = models.BooleanField(default=True) 26 | 27 | added_on = models.DateTimeField(auto_now_add=True) 28 | expires = models.DateTimeField(null=True, default=None, blank=True) 29 | last_used = models.DateTimeField(null=True, default=None, blank=True) 30 | 31 | def __str__(self): 32 | if self.name: 33 | return f"{self.get_key_type_display()}, aka \"{self.name}\" for {self.user}" 34 | return f"{self.get_key_type_display()} for {self.user}" 35 | 36 | def display_name(self): 37 | if self.name: 38 | return f"{self.name} ({self.key_type})" 39 | return self.get_key_type_display() 40 | 41 | @property 42 | def device(self): 43 | if self.key_type == KeyTypes.FIDO2: 44 | return self.properties.get("type", "----") 45 | return "" 46 | 47 | @property 48 | def auth_url(self): 49 | from .common import method_url 50 | return method_url(self.key_type) 51 | 52 | 53 | class DisabledFallback(models.Model): 54 | user = models.ForeignKey(settings.AUTH_USER_MODEL, models.CASCADE, related_name='+') 55 | fallback = models.CharField(max_length=50) 56 | -------------------------------------------------------------------------------- /multifactor/static/multifactor/js/multifactor.js: -------------------------------------------------------------------------------- 1 | function display_message(level, msg) { 2 | document.getElementById('card').classList.add(`has-background-${level}-dark`, 'has-text-white', 'has-text-centered') 3 | document.getElementById('content').innerHTML = msg 4 | } 5 | window.display_error = function(msg) { display_message('danger', msg) } 6 | window.display_succcess = function(msg) { display_message('success', msg) } 7 | 8 | 9 | document.body.addEventListener('click', function (ev) { 10 | if (ev.target.classList.contains('delete-button')) { 11 | let carryOn = confirm('Are you sure you want to delete this factor?') 12 | if (!carryOn) 13 | ev.preventDefault() 14 | } 15 | }, false) -------------------------------------------------------------------------------- /multifactor/static/multifactor/js/qrcode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * - Using the 'QRCode for Javascript library' 4 | * - Fixed dataset of 'QRCode for Javascript library' for support full-spec. 5 | * - this library has no dependencies. 6 | * 7 | * @author davidshimjs 8 | * @see http://www.d-project.com/ 9 | * @see http://jeromeetienne.github.com/jquery-qrcode/ 10 | */ 11 | var QRCode; 12 | 13 | (function () { 14 | //--------------------------------------------------------------------- 15 | // QRCode for JavaScript 16 | // 17 | // Copyright (c) 2009 Kazuhiko Arase 18 | // 19 | // URL: http://www.d-project.com/ 20 | // 21 | // Licensed under the MIT license: 22 | // http://www.opensource.org/licenses/mit-license.php 23 | // 24 | // The word "QR Code" is registered trademark of 25 | // DENSO WAVE INCORPORATED 26 | // http://www.denso-wave.com/qrcode/faqpatent-e.html 27 | // 28 | //--------------------------------------------------------------------- 29 | function QR8bitByte(data) { 30 | this.mode = QRMode.MODE_8BIT_BYTE; 31 | this.data = data; 32 | this.parsedData = []; 33 | 34 | // Added to support UTF-8 Characters 35 | for (var i = 0, l = this.data.length; i < l; i++) { 36 | var byteArray = []; 37 | var code = this.data.charCodeAt(i); 38 | 39 | if (code > 0x10000) { 40 | byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18); 41 | byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12); 42 | byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6); 43 | byteArray[3] = 0x80 | (code & 0x3F); 44 | } else if (code > 0x800) { 45 | byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12); 46 | byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6); 47 | byteArray[2] = 0x80 | (code & 0x3F); 48 | } else if (code > 0x80) { 49 | byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6); 50 | byteArray[1] = 0x80 | (code & 0x3F); 51 | } else { 52 | byteArray[0] = code; 53 | } 54 | 55 | this.parsedData.push(byteArray); 56 | } 57 | 58 | this.parsedData = Array.prototype.concat.apply([], this.parsedData); 59 | 60 | if (this.parsedData.length != this.data.length) { 61 | this.parsedData.unshift(191); 62 | this.parsedData.unshift(187); 63 | this.parsedData.unshift(239); 64 | } 65 | } 66 | 67 | QR8bitByte.prototype = { 68 | getLength: function (buffer) { 69 | return this.parsedData.length; 70 | }, 71 | write: function (buffer) { 72 | for (var i = 0, l = this.parsedData.length; i < l; i++) { 73 | buffer.put(this.parsedData[i], 8); 74 | } 75 | } 76 | }; 77 | 78 | function QRCodeModel(typeNumber, errorCorrectLevel) { 79 | this.typeNumber = typeNumber; 80 | this.errorCorrectLevel = errorCorrectLevel; 81 | this.modules = null; 82 | this.moduleCount = 0; 83 | this.dataCache = null; 84 | this.dataList = []; 85 | } 86 | 87 | QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);} 88 | return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row=7){this.setupTypeNumber(test);} 90 | if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);} 91 | this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}} 92 | return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;} 98 | for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}} 99 | for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}} 100 | this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex>>bitIndex)&1)==1);} 101 | var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;} 102 | this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}} 103 | row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;itotalDataCount*8){throw new Error("code length overflow. (" 106 | +buffer.getLengthInBits() 107 | +">" 108 | +totalDataCount*8 109 | +")");} 110 | if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);} 111 | while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);} 112 | while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;} 113 | buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;} 114 | buffer.put(QRCodeModel.PAD1,8);} 115 | return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r=0)?modPoly.get(modIndex):0;}} 117 | var totalCodeCount=0;for(var i=0;i=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));} 121 | return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));} 122 | return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;} 123 | return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i5){lostPoint+=(3+sameCount-5);}}} 129 | for(var row=0;row=256){n-=255;} 136 | return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);} 151 | if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));} 152 | this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]]; 153 | 154 | function _isSupportCanvas() { 155 | return typeof CanvasRenderingContext2D != "undefined"; 156 | } 157 | 158 | // android 2.x doesn't support Data-URI spec 159 | function _getAndroid() { 160 | var android = false; 161 | var sAgent = navigator.userAgent; 162 | 163 | if (/android/i.test(sAgent)) { // android 164 | android = true; 165 | var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i); 166 | 167 | if (aMat && aMat[1]) { 168 | android = parseFloat(aMat[1]); 169 | } 170 | } 171 | 172 | return android; 173 | } 174 | 175 | var svgDrawer = (function() { 176 | 177 | var Drawing = function (el, htOption) { 178 | this._el = el; 179 | this._htOption = htOption; 180 | }; 181 | 182 | Drawing.prototype.draw = function (oQRCode) { 183 | var _htOption = this._htOption; 184 | var _el = this._el; 185 | var nCount = oQRCode.getModuleCount(); 186 | var nWidth = Math.floor(_htOption.width / nCount); 187 | var nHeight = Math.floor(_htOption.height / nCount); 188 | 189 | this.clear(); 190 | 191 | function makeSVG(tag, attrs) { 192 | var el = document.createElementNS('http://www.w3.org/2000/svg', tag); 193 | for (var k in attrs) 194 | if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]); 195 | return el; 196 | } 197 | 198 | var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight}); 199 | svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); 200 | _el.appendChild(svg); 201 | 202 | svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"})); 203 | svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"})); 204 | 205 | for (var row = 0; row < nCount; row++) { 206 | for (var col = 0; col < nCount; col++) { 207 | if (oQRCode.isDark(row, col)) { 208 | var child = makeSVG("use", {"x": String(col), "y": String(row)}); 209 | child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template") 210 | svg.appendChild(child); 211 | } 212 | } 213 | } 214 | }; 215 | Drawing.prototype.clear = function () { 216 | while (this._el.hasChildNodes()) 217 | this._el.removeChild(this._el.lastChild); 218 | }; 219 | return Drawing; 220 | })(); 221 | 222 | var useSVG = document.documentElement.tagName.toLowerCase() === "svg"; 223 | 224 | // Drawing in DOM by using Table tag 225 | var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () { 226 | var Drawing = function (el, htOption) { 227 | this._el = el; 228 | this._htOption = htOption; 229 | }; 230 | 231 | /** 232 | * Draw the QRCode 233 | * 234 | * @param {QRCode} oQRCode 235 | */ 236 | Drawing.prototype.draw = function (oQRCode) { 237 | var _htOption = this._htOption; 238 | var _el = this._el; 239 | var nCount = oQRCode.getModuleCount(); 240 | var nWidth = Math.floor(_htOption.width / nCount); 241 | var nHeight = Math.floor(_htOption.height / nCount); 242 | var aHTML = ['']; 243 | 244 | for (var row = 0; row < nCount; row++) { 245 | aHTML.push(''); 246 | 247 | for (var col = 0; col < nCount; col++) { 248 | aHTML.push(''); 249 | } 250 | 251 | aHTML.push(''); 252 | } 253 | 254 | aHTML.push('
'); 255 | _el.innerHTML = aHTML.join(''); 256 | 257 | // Fix the margin values as real size. 258 | var elTable = _el.childNodes[0]; 259 | var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2; 260 | var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2; 261 | 262 | if (nLeftMarginTable > 0 && nTopMarginTable > 0) { 263 | elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px"; 264 | } 265 | }; 266 | 267 | /** 268 | * Clear the QRCode 269 | */ 270 | Drawing.prototype.clear = function () { 271 | this._el.innerHTML = ''; 272 | }; 273 | 274 | return Drawing; 275 | })() : (function () { // Drawing in Canvas 276 | function _onMakeImage() { 277 | this._elImage.src = this._elCanvas.toDataURL("image/png"); 278 | this._elImage.style.display = "block"; 279 | this._elCanvas.style.display = "none"; 280 | } 281 | 282 | // Android 2.1 bug workaround 283 | // http://code.google.com/p/android/issues/detail?id=5141 284 | if (this._android && this._android <= 2.1) { 285 | var factor = 1 / window.devicePixelRatio; 286 | var drawImage = CanvasRenderingContext2D.prototype.drawImage; 287 | CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) { 288 | if (("nodeName" in image) && /img/i.test(image.nodeName)) { 289 | for (var i = arguments.length - 1; i >= 1; i--) { 290 | arguments[i] = arguments[i] * factor; 291 | } 292 | } else if (typeof dw == "undefined") { 293 | arguments[1] *= factor; 294 | arguments[2] *= factor; 295 | arguments[3] *= factor; 296 | arguments[4] *= factor; 297 | } 298 | 299 | drawImage.apply(this, arguments); 300 | }; 301 | } 302 | 303 | /** 304 | * Check whether the user's browser supports Data URI or not 305 | * 306 | * @private 307 | * @param {Function} fSuccess Occurs if it supports Data URI 308 | * @param {Function} fFail Occurs if it doesn't support Data URI 309 | */ 310 | function _safeSetDataURI(fSuccess, fFail) { 311 | var self = this; 312 | self._fFail = fFail; 313 | self._fSuccess = fSuccess; 314 | 315 | // Check it just once 316 | if (self._bSupportDataURI === null) { 317 | var el = document.createElement("img"); 318 | var fOnError = function() { 319 | self._bSupportDataURI = false; 320 | 321 | if (self._fFail) { 322 | self._fFail.call(self); 323 | } 324 | }; 325 | var fOnSuccess = function() { 326 | self._bSupportDataURI = true; 327 | 328 | if (self._fSuccess) { 329 | self._fSuccess.call(self); 330 | } 331 | }; 332 | 333 | el.onabort = fOnError; 334 | el.onerror = fOnError; 335 | el.onload = fOnSuccess; 336 | el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data. 337 | return; 338 | } else if (self._bSupportDataURI === true && self._fSuccess) { 339 | self._fSuccess.call(self); 340 | } else if (self._bSupportDataURI === false && self._fFail) { 341 | self._fFail.call(self); 342 | } 343 | }; 344 | 345 | /** 346 | * Drawing QRCode by using canvas 347 | * 348 | * @constructor 349 | * @param {HTMLElement} el 350 | * @param {Object} htOption QRCode Options 351 | */ 352 | var Drawing = function (el, htOption) { 353 | this._bIsPainted = false; 354 | this._android = _getAndroid(); 355 | 356 | this._htOption = htOption; 357 | this._elCanvas = document.createElement("canvas"); 358 | this._elCanvas.width = htOption.width; 359 | this._elCanvas.height = htOption.height; 360 | el.appendChild(this._elCanvas); 361 | this._el = el; 362 | this._oContext = this._elCanvas.getContext("2d"); 363 | this._bIsPainted = false; 364 | this._elImage = document.createElement("img"); 365 | this._elImage.alt = "Scan me!"; 366 | this._elImage.style.display = "none"; 367 | this._el.appendChild(this._elImage); 368 | this._bSupportDataURI = null; 369 | }; 370 | 371 | /** 372 | * Draw the QRCode 373 | * 374 | * @param {QRCode} oQRCode 375 | */ 376 | Drawing.prototype.draw = function (oQRCode) { 377 | var _elImage = this._elImage; 378 | var _oContext = this._oContext; 379 | var _htOption = this._htOption; 380 | 381 | var nCount = oQRCode.getModuleCount(); 382 | var nWidth = _htOption.width / nCount; 383 | var nHeight = _htOption.height / nCount; 384 | var nRoundedWidth = Math.round(nWidth); 385 | var nRoundedHeight = Math.round(nHeight); 386 | 387 | _elImage.style.display = "none"; 388 | this.clear(); 389 | 390 | for (var row = 0; row < nCount; row++) { 391 | for (var col = 0; col < nCount; col++) { 392 | var bIsDark = oQRCode.isDark(row, col); 393 | var nLeft = col * nWidth; 394 | var nTop = row * nHeight; 395 | _oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; 396 | _oContext.lineWidth = 1; 397 | _oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; 398 | _oContext.fillRect(nLeft, nTop, nWidth, nHeight); 399 | 400 | // 안티 앨리어싱 방지 처리 401 | _oContext.strokeRect( 402 | Math.floor(nLeft) + 0.5, 403 | Math.floor(nTop) + 0.5, 404 | nRoundedWidth, 405 | nRoundedHeight 406 | ); 407 | 408 | _oContext.strokeRect( 409 | Math.ceil(nLeft) - 0.5, 410 | Math.ceil(nTop) - 0.5, 411 | nRoundedWidth, 412 | nRoundedHeight 413 | ); 414 | } 415 | } 416 | 417 | this._bIsPainted = true; 418 | }; 419 | 420 | /** 421 | * Make the image from Canvas if the browser supports Data URI. 422 | */ 423 | Drawing.prototype.makeImage = function () { 424 | if (this._bIsPainted) { 425 | _safeSetDataURI.call(this, _onMakeImage); 426 | } 427 | }; 428 | 429 | /** 430 | * Return whether the QRCode is painted or not 431 | * 432 | * @return {Boolean} 433 | */ 434 | Drawing.prototype.isPainted = function () { 435 | return this._bIsPainted; 436 | }; 437 | 438 | /** 439 | * Clear the QRCode 440 | */ 441 | Drawing.prototype.clear = function () { 442 | this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height); 443 | this._bIsPainted = false; 444 | }; 445 | 446 | /** 447 | * @private 448 | * @param {Number} nNumber 449 | */ 450 | Drawing.prototype.round = function (nNumber) { 451 | if (!nNumber) { 452 | return nNumber; 453 | } 454 | 455 | return Math.floor(nNumber * 1000) / 1000; 456 | }; 457 | 458 | return Drawing; 459 | })(); 460 | 461 | /** 462 | * Get the type by string length 463 | * 464 | * @private 465 | * @param {String} sText 466 | * @param {Number} nCorrectLevel 467 | * @return {Number} type 468 | */ 469 | function _getTypeNumber(sText, nCorrectLevel) { 470 | var nType = 1; 471 | var length = _getUTF8Length(sText); 472 | 473 | for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) { 474 | var nLimit = 0; 475 | 476 | switch (nCorrectLevel) { 477 | case QRErrorCorrectLevel.L : 478 | nLimit = QRCodeLimitLength[i][0]; 479 | break; 480 | case QRErrorCorrectLevel.M : 481 | nLimit = QRCodeLimitLength[i][1]; 482 | break; 483 | case QRErrorCorrectLevel.Q : 484 | nLimit = QRCodeLimitLength[i][2]; 485 | break; 486 | case QRErrorCorrectLevel.H : 487 | nLimit = QRCodeLimitLength[i][3]; 488 | break; 489 | } 490 | 491 | if (length <= nLimit) { 492 | break; 493 | } else { 494 | nType++; 495 | } 496 | } 497 | 498 | if (nType > QRCodeLimitLength.length) { 499 | throw new Error("Too long data"); 500 | } 501 | 502 | return nType; 503 | } 504 | 505 | function _getUTF8Length(sText) { 506 | var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a'); 507 | return replacedText.length + (replacedText.length != sText ? 3 : 0); 508 | } 509 | 510 | /** 511 | * @class QRCode 512 | * @constructor 513 | * @example 514 | * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie"); 515 | * 516 | * @example 517 | * var oQRCode = new QRCode("test", { 518 | * text : "http://naver.com", 519 | * width : 128, 520 | * height : 128 521 | * }); 522 | * 523 | * oQRCode.clear(); // Clear the QRCode. 524 | * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode. 525 | * 526 | * @param {HTMLElement|String} el target element or 'id' attribute of element. 527 | * @param {Object|String} vOption 528 | * @param {String} vOption.text QRCode link data 529 | * @param {Number} [vOption.width=256] 530 | * @param {Number} [vOption.height=256] 531 | * @param {String} [vOption.colorDark="#000000"] 532 | * @param {String} [vOption.colorLight="#ffffff"] 533 | * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H] 534 | */ 535 | QRCode = function (el, vOption) { 536 | this._htOption = { 537 | width : 256, 538 | height : 256, 539 | typeNumber : 4, 540 | colorDark : "#000000", 541 | colorLight : "#ffffff", 542 | correctLevel : QRErrorCorrectLevel.H 543 | }; 544 | 545 | if (typeof vOption === 'string') { 546 | vOption = { 547 | text : vOption 548 | }; 549 | } 550 | 551 | // Overwrites options 552 | if (vOption) { 553 | for (var i in vOption) { 554 | this._htOption[i] = vOption[i]; 555 | } 556 | } 557 | 558 | if (typeof el == "string") { 559 | el = document.getElementById(el); 560 | } 561 | 562 | if (this._htOption.useSVG) { 563 | Drawing = svgDrawer; 564 | } 565 | 566 | this._android = _getAndroid(); 567 | this._el = el; 568 | this._oQRCode = null; 569 | this._oDrawing = new Drawing(this._el, this._htOption); 570 | 571 | if (this._htOption.text) { 572 | this.makeCode(this._htOption.text); 573 | } 574 | }; 575 | 576 | /** 577 | * Make the QRCode 578 | * 579 | * @param {String} sText link data 580 | */ 581 | QRCode.prototype.makeCode = function (sText) { 582 | this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel); 583 | this._oQRCode.addData(sText); 584 | this._oQRCode.make(); 585 | this._el.title = sText; 586 | this._oDrawing.draw(this._oQRCode); 587 | this.makeImage(); 588 | }; 589 | 590 | /** 591 | * Make the Image from Canvas element 592 | * - It occurs automatically 593 | * - Android below 3 doesn't support Data-URI spec. 594 | * 595 | * @private 596 | */ 597 | QRCode.prototype.makeImage = function () { 598 | if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) { 599 | this._oDrawing.makeImage(); 600 | } 601 | }; 602 | 603 | /** 604 | * Clear the QRCode 605 | */ 606 | QRCode.prototype.clear = function () { 607 | this._oDrawing.clear(); 608 | }; 609 | 610 | /** 611 | * @name QRCode.CorrectLevel 612 | */ 613 | QRCode.CorrectLevel = QRErrorCorrectLevel; 614 | })(); 615 | -------------------------------------------------------------------------------- /multifactor/static/multifactor/js/qrcode.min.js: -------------------------------------------------------------------------------- 1 | var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); -------------------------------------------------------------------------------- /multifactor/static/multifactor/js/webauthn-json.browser-ponyfill.js: -------------------------------------------------------------------------------- 1 | // src/webauthn-json/base64url.ts 2 | function base64urlToBuffer(baseurl64String) { 3 | const padding = "==".slice(0, (4 - baseurl64String.length % 4) % 4); 4 | const base64String = baseurl64String.replace(/-/g, "+").replace(/_/g, "/") + padding; 5 | const str = atob(base64String); 6 | const buffer = new ArrayBuffer(str.length); 7 | const byteView = new Uint8Array(buffer); 8 | for (let i = 0; i < str.length; i++) { 9 | byteView[i] = str.charCodeAt(i); 10 | } 11 | return buffer; 12 | } 13 | function bufferToBase64url(buffer) { 14 | const byteView = new Uint8Array(buffer); 15 | let str = ""; 16 | for (const charCode of byteView) { 17 | str += String.fromCharCode(charCode); 18 | } 19 | const base64String = btoa(str); 20 | const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); 21 | return base64urlString; 22 | } 23 | 24 | // src/webauthn-json/convert.ts 25 | var copyValue = "copy"; 26 | var convertValue = "convert"; 27 | function convert(conversionFn, schema, input) { 28 | if (schema === copyValue) { 29 | return input; 30 | } 31 | if (schema === convertValue) { 32 | return conversionFn(input); 33 | } 34 | if (schema instanceof Array) { 35 | return input.map((v) => convert(conversionFn, schema[0], v)); 36 | } 37 | if (schema instanceof Object) { 38 | const output = {}; 39 | for (const [key, schemaField] of Object.entries(schema)) { 40 | if (schemaField.derive) { 41 | const v = schemaField.derive(input); 42 | if (v !== void 0) { 43 | input[key] = v; 44 | } 45 | } 46 | if (!(key in input)) { 47 | if (schemaField.required) { 48 | throw new Error(`Missing key: ${key}`); 49 | } 50 | continue; 51 | } 52 | if (input[key] == null) { 53 | output[key] = null; 54 | continue; 55 | } 56 | output[key] = convert(conversionFn, schemaField.schema, input[key]); 57 | } 58 | return output; 59 | } 60 | } 61 | function derived(schema, derive) { 62 | return { 63 | required: true, 64 | schema, 65 | derive 66 | }; 67 | } 68 | function required(schema) { 69 | return { 70 | required: true, 71 | schema 72 | }; 73 | } 74 | function optional(schema) { 75 | return { 76 | required: false, 77 | schema 78 | }; 79 | } 80 | 81 | // src/webauthn-json/basic/schema.ts 82 | var publicKeyCredentialDescriptorSchema = { 83 | type: required(copyValue), 84 | id: required(convertValue), 85 | transports: optional(copyValue) 86 | }; 87 | var simplifiedExtensionsSchema = { 88 | appid: optional(copyValue), 89 | appidExclude: optional(copyValue), 90 | credProps: optional(copyValue) 91 | }; 92 | var simplifiedClientExtensionResultsSchema = { 93 | appid: optional(copyValue), 94 | appidExclude: optional(copyValue), 95 | credProps: optional(copyValue) 96 | }; 97 | var credentialCreationOptions = { 98 | publicKey: required({ 99 | rp: required(copyValue), 100 | user: required({ 101 | id: required(convertValue), 102 | name: required(copyValue), 103 | displayName: required(copyValue) 104 | }), 105 | challenge: required(convertValue), 106 | pubKeyCredParams: required(copyValue), 107 | timeout: optional(copyValue), 108 | excludeCredentials: optional([publicKeyCredentialDescriptorSchema]), 109 | authenticatorSelection: optional(copyValue), 110 | attestation: optional(copyValue), 111 | extensions: optional(simplifiedExtensionsSchema) 112 | }), 113 | signal: optional(copyValue) 114 | }; 115 | var publicKeyCredentialWithAttestation = { 116 | type: required(copyValue), 117 | id: required(copyValue), 118 | rawId: required(convertValue), 119 | authenticatorAttachment: optional(copyValue), 120 | response: required({ 121 | clientDataJSON: required(convertValue), 122 | attestationObject: required(convertValue), 123 | transports: derived(copyValue, (response) => { 124 | var _a; 125 | return ((_a = response.getTransports) == null ? void 0 : _a.call(response)) || []; 126 | }) 127 | }), 128 | clientExtensionResults: derived(simplifiedClientExtensionResultsSchema, (pkc) => pkc.getClientExtensionResults()) 129 | }; 130 | var credentialRequestOptions = { 131 | mediation: optional(copyValue), 132 | publicKey: required({ 133 | challenge: required(convertValue), 134 | timeout: optional(copyValue), 135 | rpId: optional(copyValue), 136 | allowCredentials: optional([publicKeyCredentialDescriptorSchema]), 137 | userVerification: optional(copyValue), 138 | extensions: optional(simplifiedExtensionsSchema) 139 | }), 140 | signal: optional(copyValue) 141 | }; 142 | var publicKeyCredentialWithAssertion = { 143 | type: required(copyValue), 144 | id: required(copyValue), 145 | rawId: required(convertValue), 146 | authenticatorAttachment: optional(copyValue), 147 | response: required({ 148 | clientDataJSON: required(convertValue), 149 | authenticatorData: required(convertValue), 150 | signature: required(convertValue), 151 | userHandle: required(convertValue) 152 | }), 153 | clientExtensionResults: derived(simplifiedClientExtensionResultsSchema, (pkc) => pkc.getClientExtensionResults()) 154 | }; 155 | 156 | // src/webauthn-json/basic/api.ts 157 | function createRequestFromJSON(requestJSON) { 158 | return convert(base64urlToBuffer, credentialCreationOptions, requestJSON); 159 | } 160 | function createResponseToJSON(credential) { 161 | return convert(bufferToBase64url, publicKeyCredentialWithAttestation, credential); 162 | } 163 | function getRequestFromJSON(requestJSON) { 164 | return convert(base64urlToBuffer, credentialRequestOptions, requestJSON); 165 | } 166 | function getResponseToJSON(credential) { 167 | return convert(bufferToBase64url, publicKeyCredentialWithAssertion, credential); 168 | } 169 | 170 | // src/webauthn-json/basic/supported.ts 171 | function supported() { 172 | return !!(navigator.credentials && navigator.credentials.create && navigator.credentials.get && window.PublicKeyCredential); 173 | } 174 | 175 | // src/webauthn-json/browser-ponyfill.ts 176 | async function create(options) { 177 | const response = await navigator.credentials.create(options); 178 | response.toJSON = () => createResponseToJSON(response); 179 | return response; 180 | } 181 | async function get(options) { 182 | const response = await navigator.credentials.get(options); 183 | response.toJSON = () => getResponseToJSON(response); 184 | return response; 185 | } 186 | export { 187 | create, 188 | get, 189 | createRequestFromJSON as parseCreationOptionsFromJSON, 190 | getRequestFromJSON as parseRequestOptionsFromJSON, 191 | supported 192 | }; 193 | //# sourceMappingURL=webauthn-json.browser-ponyfill.js.map 194 | -------------------------------------------------------------------------------- /multifactor/static/multifactor/keys.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 44 | 45 | -------------------------------------------------------------------------------- /multifactor/static/multifactor/multifactor.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";@import"https://fonts.googleapis.com/css?family=Assistant:400,600";/*! bulma.io v0.9.4 | MIT License | github.com/jgthms/bulma */.select select,.input,.button{-moz-appearance:none;-webkit-appearance:none;align-items:center;border:3px solid transparent;border-radius:4px;box-shadow:none;display:inline-flex;font-size:1rem;height:2.5em;justify-content:flex-start;line-height:1.5;padding-bottom:calc(.5em - 3px);padding-left:calc(.75em - 3px);padding-right:calc(.75em - 3px);padding-top:calc(.5em - 3px);position:relative;vertical-align:top}.select select:focus,.input:focus,.button:focus,.select select:active,.input:active,.button:active{outline:none}.select select[disabled],[disabled].input,[disabled].button{cursor:not-allowed}.button{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.select:not(.is-multiple):not(.is-loading):after{border:3px solid transparent;border-radius:2px;border-right:0;border-top:0;content:" ";display:block;height:.625em;margin-top:-.4375em;pointer-events:none;position:absolute;top:50%;transform:rotate(-45deg);transform-origin:center;width:.625em}.message:not(:last-child),.block:not(:last-child),.title:not(:last-child),.table:not(:last-child),.notification:not(:last-child),.content:not(:last-child),.box:not(:last-child){margin-bottom:1.5rem}/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */html,body,p,ul,li,h1,h3,h4,h5{margin:0;padding:0}h1,h3,h4,h5{font-size:100%;font-weight:400}ul{list-style:none}button,input,select{margin:0}html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}table{border-collapse:collapse;border-spacing:0}td{padding:0}td:not([align]){text-align:inherit}html{background-color:#eee;font-size:16px;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;min-width:300px;overflow-x:hidden;overflow-y:scroll;text-rendering:optimizeLegibility;text-size-adjust:100%}body,button,input,select{font-family:Assistant,sans-serif}code{-moz-osx-font-smoothing:auto;-webkit-font-smoothing:auto;font-family:monospace}body{color:#4a4a4a;font-size:1em;font-weight:400;line-height:1.5}a{color:#485fc7;cursor:pointer;text-decoration:none}a strong{color:currentColor}a:hover{color:#363636}code{background-color:#f5f5f5;color:#da1039;font-size:.875em;font-weight:400;padding:.25em .5em}img{height:auto;max-width:100%}small{font-size:.875em}span{font-style:inherit;font-weight:inherit}strong{color:#363636;font-weight:600}table td{vertical-align:top}table td:not([align]){text-align:inherit}@keyframes spinAround{0%{transform:rotate(0)}to{transform:rotate(359deg)}}.box{background-color:#fff;border-radius:6px;box-shadow:0 .5em 1em -.125em #0a0a0a1a,0 0 0 1px #0a0a0a05;color:#4a4a4a;display:block;padding:1.25rem}a.box:hover,a.box:focus{box-shadow:0 .5em 1em -.125em #0a0a0a1a,0 0 0 1px #485fc7}a.box:active{box-shadow:inset 0 1px 2px #0a0a0a33,0 0 0 1px #485fc7}.button{background-color:#fff;border-color:#dbdbdb;border-width:3px;color:#363636;cursor:pointer;justify-content:center;padding-bottom:calc(.5em - 3px);padding-left:1em;padding-right:1em;padding-top:calc(.5em - 3px);text-align:center;white-space:nowrap}.button strong{color:inherit}.button:hover{border-color:#b5b5b5;color:#363636}.button:focus{border-color:#485fc7;color:#363636}.button:focus:not(:active){box-shadow:0 0 0 .125em #485fc740}.button:active{border-color:#4a4a4a;color:#363636}.button.is-light{background-color:#f5f5f5;border-color:transparent;color:#000000b3}.button.is-light:hover{background-color:#eee;border-color:transparent;color:#000000b3}.button.is-light:focus{border-color:transparent;color:#000000b3}.button.is-light:focus:not(:active){box-shadow:0 0 0 .125em #f5f5f540}.button.is-light:active{background-color:#e8e8e8;border-color:transparent;color:#000000b3}.button.is-light[disabled]{background-color:#f5f5f5;border-color:#f5f5f5;box-shadow:none}.button.is-primary{background-color:#33a9ff;border-color:transparent;color:#fff}.button.is-primary:hover{background-color:#26a4ff;border-color:transparent;color:#fff}.button.is-primary:focus{border-color:transparent;color:#fff}.button.is-primary:focus:not(:active){box-shadow:0 0 0 .125em #33a9ff40}.button.is-primary:active{background-color:#1a9eff;border-color:transparent;color:#fff}.button.is-primary[disabled]{background-color:#33a9ff;border-color:#33a9ff;box-shadow:none}.button.is-primary.is-light{background-color:#ebf6ff;color:#0067b3}.button.is-primary.is-light:hover{background-color:#def1ff;border-color:transparent;color:#0067b3}.button.is-primary.is-light:active{background-color:#d1ecff;border-color:transparent;color:#0067b3}.button.is-info{background-color:#0287fc;border-color:transparent;color:#fff}.button.is-info:hover{background-color:#0280ef;border-color:transparent;color:#fff}.button.is-info:focus{border-color:transparent;color:#fff}.button.is-info:focus:not(:active){box-shadow:0 0 0 .125em #0287fc40}.button.is-info:active{background-color:#0279e3;border-color:transparent;color:#fff}.button.is-info[disabled]{background-color:#0287fc;border-color:#0287fc;box-shadow:none}.button.is-info.is-light{background-color:#ebf5ff;color:#0272d5}.button.is-info.is-light:hover{background-color:#deefff;border-color:transparent;color:#0272d5}.button.is-info.is-light:active{background-color:#d1e9ff;border-color:transparent;color:#0272d5}.button.is-success,.button.is-toggle.is-toggled-on{background-color:#54bc2b;border-color:transparent;color:#fff}.button.is-success:hover,.button.is-toggle.is-toggled-on:hover{background-color:#4fb229;border-color:transparent;color:#fff}.button.is-success:focus,.button.is-toggle.is-toggled-on:focus{border-color:transparent;color:#fff}.button.is-success:focus:not(:active),.button.is-toggle.is-toggled-on:focus:not(:active){box-shadow:0 0 0 .125em #54bc2b40}.button.is-success:active,.button.is-toggle.is-toggled-on:active{background-color:#4ba726;border-color:transparent;color:#fff}.button.is-success[disabled],.button[disabled].is-toggle.is-toggled-on{background-color:#54bc2b;border-color:#54bc2b;box-shadow:none}.button.is-success.is-light,.button.is-light.is-toggle.is-toggled-on{background-color:#f2fbee;color:#3f8d20}.button.is-success.is-light:hover,.button.is-light.is-toggle.is-toggled-on:hover{background-color:#eaf9e4;border-color:transparent;color:#3f8d20}.button.is-success.is-light:active,.button.is-light.is-toggle.is-toggled-on:active{background-color:#e2f6da;border-color:transparent;color:#3f8d20}.button.is-warning{background-color:#fff028;border-color:transparent;color:#000000b3}.button.is-warning:hover{background-color:#ffef1b;border-color:transparent;color:#000000b3}.button.is-warning:focus{border-color:transparent;color:#000000b3}.button.is-warning:focus:not(:active){box-shadow:0 0 0 .125em #fff02840}.button.is-warning:active{background-color:#ffee0f;border-color:transparent;color:#000000b3}.button.is-warning[disabled]{background-color:#fff028;border-color:#fff028;box-shadow:none}.button.is-warning.is-light{background-color:#fffeeb;color:#948a00}.button.is-warning.is-light:hover{background-color:#fffdde;border-color:transparent;color:#948a00}.button.is-warning.is-light:active{background-color:#fffcd1;border-color:transparent;color:#948a00}.button.is-danger,.button.is-error{background-color:#ed1400;border-color:transparent;color:#fff}.button.is-danger:hover,.button.is-error:hover{background-color:#e01300;border-color:transparent;color:#fff}.button.is-danger:focus,.button.is-error:focus{border-color:transparent;color:#fff}.button.is-danger:focus:not(:active),.button.is-error:focus:not(:active){box-shadow:0 0 0 .125em #ed140040}.button.is-danger:active,.button.is-error:active{background-color:#d41200;border-color:transparent;color:#fff}.button.is-danger[disabled],.button[disabled].is-error{background-color:#ed1400;border-color:#ed1400;box-shadow:none}.button.is-danger.is-light,.button.is-light.is-error{background-color:#ffeceb;color:#f01400}.button.is-danger.is-light:hover,.button.is-light.is-error:hover{background-color:#ffe1de;border-color:transparent;color:#f01400}.button.is-danger.is-light:active,.button.is-light.is-error:active{background-color:#ffd5d1;border-color:transparent;color:#f01400}.button.is-error{background-color:#ed1400;border-color:transparent;color:#fff}.button.is-error:hover{background-color:#e01300;border-color:transparent;color:#fff}.button.is-error:focus{border-color:transparent;color:#fff}.button.is-error:focus:not(:active){box-shadow:0 0 0 .125em #ed140040}.button.is-error:active{background-color:#d41200;border-color:transparent;color:#fff}.button.is-error[disabled]{background-color:#ed1400;border-color:#ed1400;box-shadow:none}.button.is-error.is-light{background-color:#ffeceb;color:#f01400}.button.is-error.is-light:hover{background-color:#ffe1de;border-color:transparent;color:#f01400}.button.is-error.is-light:active{background-color:#ffd5d1;border-color:transparent;color:#f01400}.button.is-small{font-size:.75rem}.button.is-small:not(.is-rounded){border-radius:2px}.button.is-large{font-size:1.5rem}.button[disabled]{background-color:#fff;border-color:#dbdbdb;box-shadow:none;opacity:.5}.button.is-fullwidth{display:flex;width:100%}.content li+li{margin-top:.25em}.content p:not(:last-child),.content ul:not(:last-child),.content table:not(:last-child){margin-bottom:1em}.content h1,.content h3,.content h4,.content h5{color:#363636;font-weight:600;line-height:1.125}.content h1{font-size:2em;margin-bottom:.5em}.content h1:not(:first-child){margin-top:1em}.content h3{font-size:1.5em;margin-bottom:.6666em}.content h3:not(:first-child){margin-top:1.3333em}.content h4{font-size:1.25em;margin-bottom:.8em}.content h5{font-size:1.125em;margin-bottom:.8888em}.content ul{list-style:disc outside;margin-left:2em;margin-top:1em}.content ul ul{list-style-type:circle;margin-top:.5em}.content ul ul ul{list-style-type:square}.content table{width:100%}.content table td{border:1px solid hsl(0,0%,86%);border-width:0 0 1px;padding:.5em .75em;vertical-align:top}.content table tbody tr:last-child td{border-bottom-width:0}.content.is-small{font-size:.75rem}.content.is-large{font-size:1.5rem}.notification{background-color:#f5f5f5;border-radius:4px;position:relative;padding:1.25rem 2.5rem 1.25rem 1.5rem}.notification a:not(.button):not(.dropdown-item){color:currentColor;text-decoration:underline}.notification strong{color:currentColor}.notification code{background:#fff}.notification .title,.notification .content{color:currentColor}.notification.is-light{background-color:#f5f5f5;color:#000000b3}.notification.is-primary{background-color:#33a9ff;color:#fff}.notification.is-primary.is-light{background-color:#ebf6ff;color:#0067b3}.notification.is-info{background-color:#0287fc;color:#fff}.notification.is-info.is-light{background-color:#ebf5ff;color:#0272d5}.notification.is-success,.notification.button.is-toggle.is-toggled-on{background-color:#54bc2b;color:#fff}.notification.is-success.is-light,.notification.is-light.button.is-toggle.is-toggled-on{background-color:#f2fbee;color:#3f8d20}.notification.is-warning{background-color:#fff028;color:#000000b3}.notification.is-warning.is-light{background-color:#fffeeb;color:#948a00}.notification.is-danger,.notification.is-error{background-color:#ed1400;color:#fff}.notification.is-danger.is-light,.notification.is-light.is-error{background-color:#ffeceb;color:#f01400}.notification.is-error{background-color:#ed1400;color:#fff}.notification.is-error.is-light{background-color:#ffeceb;color:#f01400}@keyframes moveIndeterminate{0%{background-position:200% 0}to{background-position:-200% 0}}.table{background-color:#fff;color:#363636}.table td{border:1px solid hsl(0,0%,86%);border-width:0 0 1px;padding:.5em .75em;vertical-align:top}.table td.is-light{background-color:#f5f5f5;border-color:#f5f5f5;color:#000000b3}.table td.is-primary{background-color:#33a9ff;border-color:#33a9ff;color:#fff}.table td.is-info{background-color:#0287fc;border-color:#0287fc;color:#fff}.table td.is-success,.table td.button.is-toggle.is-toggled-on{background-color:#54bc2b;border-color:#54bc2b;color:#fff}.table td.is-warning{background-color:#fff028;border-color:#fff028;color:#000000b3}.table td.is-danger,.table td.is-error{background-color:#ed1400;border-color:#ed1400;color:#fff}.table tbody{background-color:transparent}.table tbody tr:last-child td{border-bottom-width:0}.table.is-fullwidth{width:100%}.table.is-hoverable tbody tr:not(.is-selected):hover{background-color:#fafafa}.table.is-hoverable.is-striped tbody tr:not(.is-selected):hover{background-color:#fafafa}.table.is-hoverable.is-striped tbody tr:not(.is-selected):hover:nth-child(2n){background-color:#f5f5f5}.table.is-striped tbody tr:not(.is-selected):nth-child(2n){background-color:#fafafa}.tags{align-items:center;display:flex;flex-wrap:wrap;justify-content:flex-start}.tags:last-child{margin-bottom:-.5rem}.tags:not(:last-child){margin-bottom:1rem}.tags.is-centered{justify-content:center}.tags.is-right{justify-content:flex-end}.title{word-break:break-word}.title em,.title span{font-weight:inherit}.title{color:#363636;font-size:2rem;font-weight:600;line-height:1.125}.title strong{color:inherit;font-weight:inherit}.select select,.input{background-color:#fff;border-color:#dbdbdb;border-radius:4px;color:#363636}.select select::-moz-placeholder,.input::-moz-placeholder{color:#3636364d}.select select::-webkit-input-placeholder,.input::-webkit-input-placeholder{color:#3636364d}.select select:-moz-placeholder,.input:-moz-placeholder{color:#3636364d}.select select:-ms-input-placeholder,.input:-ms-input-placeholder{color:#3636364d}.select select:hover,.input:hover{border-color:#b5b5b5}.select select:focus,.input:focus,.select select:active,.input:active{border-color:#485fc7;box-shadow:0 0 0 .125em #485fc740}.select select[disabled],[disabled].input{background-color:#f5f5f5;border-color:#f5f5f5;box-shadow:none;color:#7a7a7a}.select select[disabled]::-moz-placeholder,[disabled].input::-moz-placeholder{color:#7a7a7a4d}.select select[disabled]::-webkit-input-placeholder,[disabled].input::-webkit-input-placeholder{color:#7a7a7a4d}.select select[disabled]:-moz-placeholder,[disabled].input:-moz-placeholder{color:#7a7a7a4d}.select select[disabled]:-ms-input-placeholder,[disabled].input:-ms-input-placeholder{color:#7a7a7a4d}.input{box-shadow:inset 0 .0625em .125em #0a0a0a0d;max-width:100%;width:100%}.is-light.input{border-color:#f5f5f5}.is-light.input:focus,.is-light.input:active{box-shadow:0 0 0 .125em #f5f5f540}.is-primary.input{border-color:#33a9ff}.is-primary.input:focus,.is-primary.input:active{box-shadow:0 0 0 .125em #33a9ff40}.is-info.input{border-color:#0287fc}.is-info.input:focus,.is-info.input:active{box-shadow:0 0 0 .125em #0287fc40}.is-success.input,.input.button.is-toggle.is-toggled-on{border-color:#54bc2b}.is-success.input:focus,.input.button.is-toggle.is-toggled-on:focus,.is-success.input:active,.input.button.is-toggle.is-toggled-on:active{box-shadow:0 0 0 .125em #54bc2b40}.is-warning.input{border-color:#fff028}.is-warning.input:focus,.is-warning.input:active{box-shadow:0 0 0 .125em #fff02840}.is-danger.input,.input.is-error{border-color:#ed1400}.is-danger.input:focus,.input.is-error:focus,.is-danger.input:active,.input.is-error:active{box-shadow:0 0 0 .125em #ed140040}.is-error.input{border-color:#ed1400}.is-error.input:focus,.is-error.input:active{box-shadow:0 0 0 .125em #ed140040}.is-small.input{border-radius:2px;font-size:.75rem}.is-large.input{font-size:1.5rem}.is-fullwidth.input{display:block;width:100%}.select{display:inline-block;max-width:100%;position:relative;vertical-align:top}.select:not(.is-multiple){height:2.5em}.select:not(.is-multiple):not(.is-loading):after{border-color:#485fc7;right:1.125em;z-index:4}.select select{cursor:pointer;display:block;font-size:1em;max-width:100%;outline:none}.select select::-ms-expand{display:none}.select select[disabled]:hover{border-color:#f5f5f5}.select select:not([multiple]){padding-right:2.5em}.select:not(.is-multiple):not(.is-loading):hover:after{border-color:#363636}.select.is-light:not(:hover):after{border-color:#f5f5f5}.select.is-light select{border-color:#f5f5f5}.select.is-light select:hover{border-color:#e8e8e8}.select.is-light select:focus,.select.is-light select:active{box-shadow:0 0 0 .125em #f5f5f540}.select.is-primary:not(:hover):after{border-color:#33a9ff}.select.is-primary select{border-color:#33a9ff}.select.is-primary select:hover{border-color:#1a9eff}.select.is-primary select:focus,.select.is-primary select:active{box-shadow:0 0 0 .125em #33a9ff40}.select.is-info:not(:hover):after{border-color:#0287fc}.select.is-info select{border-color:#0287fc}.select.is-info select:hover{border-color:#0279e3}.select.is-info select:focus,.select.is-info select:active{box-shadow:0 0 0 .125em #0287fc40}.select.is-success:not(:hover):after,.select.button.is-toggle.is-toggled-on:not(:hover):after{border-color:#54bc2b}.select.is-success select,.select.button.is-toggle.is-toggled-on select{border-color:#54bc2b}.select.is-success select:hover,.select.button.is-toggle.is-toggled-on select:hover{border-color:#4ba726}.select.is-success select:focus,.select.button.is-toggle.is-toggled-on select:focus,.select.is-success select:active,.select.button.is-toggle.is-toggled-on select:active{box-shadow:0 0 0 .125em #54bc2b40}.select.is-warning:not(:hover):after{border-color:#fff028}.select.is-warning select{border-color:#fff028}.select.is-warning select:hover{border-color:#ffee0f}.select.is-warning select:focus,.select.is-warning select:active{box-shadow:0 0 0 .125em #fff02840}.select.is-danger:not(:hover):after,.select.is-error:not(:hover):after{border-color:#ed1400}.select.is-danger select,.select.is-error select{border-color:#ed1400}.select.is-danger select:hover,.select.is-error select:hover{border-color:#d41200}.select.is-danger select:focus,.select.is-error select:focus,.select.is-danger select:active,.select.is-error select:active{box-shadow:0 0 0 .125em #ed140040}.select.is-error:not(:hover):after{border-color:#ed1400}.select.is-error select{border-color:#ed1400}.select.is-error select:hover{border-color:#d41200}.select.is-error select:focus,.select.is-error select:active{box-shadow:0 0 0 .125em #ed140040}.select.is-small{border-radius:2px;font-size:.75rem}.select.is-large{font-size:1.5rem}.select.is-fullwidth,.select.is-fullwidth select{width:100%}.label{color:#363636;display:block;font-size:1rem;font-weight:600}.label:not(:last-child){margin-bottom:.5em}.label.is-small{font-size:.75rem}.label.is-large{font-size:1.5rem}.help{display:block;font-size:.75rem;margin-top:.25rem}.help.is-light{color:#f5f5f5}.help.is-primary{color:#33a9ff}.help.is-info{color:#0287fc}.help.is-success,.help.button.is-toggle.is-toggled-on{color:#54bc2b}.help.is-warning{color:#fff028}.help.is-danger,.help.is-error{color:#ed1400}.field:not(:last-child){margin-bottom:.75rem}.control{box-sizing:border-box;clear:both;font-size:1rem;position:relative;text-align:inherit}.card{background-color:#fff;border-radius:.25rem;box-shadow:0 .5em 1em -.125em #0a0a0a1a,0 0 0 1px #0a0a0a05;color:#4a4a4a;max-width:100%;position:relative}.card-footer:first-child,.card-content:first-child,.card-header:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card-footer:last-child,.card-content:last-child,.card-header:last-child{border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem}.card-header{background-color:transparent;align-items:stretch;box-shadow:0 .125em .25em #0a0a0a1a;display:flex}.card-header-title{align-items:center;color:#363636;display:flex;flex-grow:1;font-weight:600;padding:.75rem 1rem}.card-header-title.is-centered{justify-content:center}.card-content{background-color:transparent;padding:1.5rem}.card-footer{background-color:transparent;border-top:1px solid hsl(0,0%,93%);align-items:stretch;display:flex}.dropdown{display:inline-flex;position:relative;vertical-align:top}.dropdown.is-hoverable:hover .dropdown-menu{display:block}.dropdown.is-right .dropdown-menu{left:auto;right:0}.dropdown-menu{display:none;left:0;min-width:12rem;padding-top:4px;position:absolute;top:100%;z-index:20}.dropdown-content{background-color:#fff;border-radius:4px;box-shadow:0 .5em 1em -.125em #0a0a0a1a,0 0 0 1px #0a0a0a05;padding-bottom:.5rem;padding-top:.5rem}.dropdown-item{color:#4a4a4a;display:block;font-size:.875rem;line-height:1.5;padding:.375rem 1rem;position:relative}a.dropdown-item,button.dropdown-item{padding-right:3rem;text-align:inherit;white-space:nowrap;width:100%}a.dropdown-item:hover,button.dropdown-item:hover{background-color:#f5f5f5;color:#0a0a0a}.menu{font-size:1rem}.menu.is-small{font-size:.75rem}.menu.is-large{font-size:1.5rem}.message{background-color:#f5f5f5;border-radius:4px;font-size:1rem}.message strong{color:currentColor}.message a:not(.button):not(.tag):not(.dropdown-item){color:currentColor;text-decoration:underline}.message.is-small{font-size:.75rem}.message.is-large{font-size:1.5rem}.message.is-light{background-color:#fafafa}.message.is-primary{background-color:#ebf6ff}.message.is-info{background-color:#ebf5ff}.message.is-success,.message.button.is-toggle.is-toggled-on{background-color:#f2fbee}.message.is-warning{background-color:#fffeeb}.message.is-danger,.message.is-error{background-color:#ffeceb}.column{display:block;flex-basis:0;flex-grow:1;flex-shrink:1;padding:.75rem}.columns{margin-left:-.75rem;margin-right:-.75rem;margin-top:-.75rem}.columns:last-child{margin-bottom:-.75rem}.columns:not(:last-child){margin-bottom:.75rem}.columns.is-centered{justify-content:center}@media screen and (min-width: 769px),print{.columns:not(.is-desktop){display:flex}}.has-text-white{color:#fff!important}a.has-text-white:hover,a.has-text-white:focus{color:#e6e6e6!important}.has-background-danger{background-color:#ed1400!important}.has-background-danger-dark{background-color:#f01400!important}.has-text-centered{text-align:center!important}html,body{min-height:100vh}body{margin:50px 20px;display:flex}@media screen and (max-width: 768px){body{margin-top:0!important}body :last-child{margin-bottom:0!important}}@media screen and (min-width: 769px){body{align-items:center;justify-content:center}}body>.column{padding-bottom:0}@media screen and (max-width: 768px){body>.column{flex-grow:1;display:flex;flex-direction:column;padding-top:0}}@media screen and (min-width: 769px){body>.column{max-width:1020px;margin-left:20px;margin-right:20px}body>.column.curt{max-width:450px}}body>.column>.notification{margin:1.5rem 0 0!important;text-align:center;flex-grow:0}@media screen and (max-width: 768px){body>.column>.notification{border-radius:0;margin:0!important}}@media screen and (max-width: 768px){body>.column>.card{flex-grow:1;display:flex;flex-direction:column}body>.column>.card>.card-content{flex-grow:1}body>.column>.card,body>.column>.card>.card-header{box-shadow:none!important}}@media screen and (min-width: 769px){body>.column>.card{min-height:0;margin-top:1rem;margin-bottom:10%}}body>.column>.card :last-child,body>.column>.card .card-content :last-child{margin-bottom:0!important}.card{border-radius:3px;overflow:hidden}.card-header{box-shadow:none;margin-top:.75rem;margin-bottom:-.75rem;flex-direction:column;text-align:center}.card-header h1,.card-header h3,.card-header h4,.card-header h5{text-align:center;font-size:1.4rem;margin:0}.card-footer{border-top:0}.card-footer .card{height:100%}.card-footer>.button{border-radius:0;padding:.75rem 1rem}.card.has-background-danger-dark{text-align:center;color:#fff}.card.has-background-danger-dark .card-header-title{color:#fff}p{margin-bottom:1em}.table{margin-left:-12px;margin-right:-12px;width:100%}.table td{vertical-align:middle}.table td :last-child{margin-bottom:0}.table td:last-child{text-align:right;width:10%;white-space:nowrap}.table td form{display:inline}@media screen and (max-width: 768px){.table td{display:block;padding-bottom:.5rem;width:100%}.table tr,.table td{border:0}}.table h3{font-weight:600}.button-ml{display:inline-block;height:auto}.button-ml span,.button-ml small,.button-ml strong{display:block}.is-100{width:100%}.field,.button.is-fullwidth{margin-bottom:1rem}h4{font-size:1.4rem;border-bottom:1px solid hsl(0,0%,96%);padding-bottom:4px;margin-bottom:4px}.button.is-multilayer{flex-direction:column;height:auto}.button.is-multilayer small{display:block;font-size:.9rem}.home-add{float:right;margin:0 0 1rem 1rem}.qr-block{margin-bottom:1rem;text-align:center}.qr-block>div{margin-bottom:.5rem}.qr-block>div img{margin:auto}#content .columns>.column>.card{height:100%;display:flex;flex-direction:column}@media screen and (min-width: 769px){#content .columns>.column{padding-bottom:0}}#content .columns>.column .card-content{flex-grow:1}.button.is-toggle{position:relative;padding-left:1.25em;padding-right:1.25em;opacity:1;transition:background-color .5s,color .5s}.button.is-toggle:before{content:" ";display:block;width:12px;background:#fffc;border:2px solid #ccc;border-radius:4px;position:absolute;top:0;bottom:0;transition:left .7s,right .7s}.button.is-toggle.is-toggled-on:before{left:0}.button.is-toggle.is-toggled-on:hover{background-color:#eee!important;border-color:transparent!important;color:#363636!important}.button.is-toggle.is-toggled-on:hover:before{left:calc(100% - 12px)}.button.is-toggle.is-toggled-off:before{right:0}.button.is-toggle.is-toggled-off:hover:before{right:calc(100% - 12px)}#authtype.automatic button,#authtype.manual p,.delete-button span{display:none}.delete-button:after{content:"✖"} 2 | -------------------------------------------------------------------------------- /multifactor/static/multifactor/multifactor.js: -------------------------------------------------------------------------------- 1 | function n(e,t){document.getElementById("card").classList.add(`has-background-${e}-dark`,"has-text-white","has-text-centered"),document.getElementById("content").innerHTML=t}window.display_error=function(e){n("danger",e)};window.display_succcess=function(e){n("success",e)};document.body.addEventListener("click",function(e){e.target.classList.contains("delete-button")&&(confirm("Are you sure you want to delete this factor?")||e.preventDefault())},!1); 2 | -------------------------------------------------------------------------------- /multifactor/templates/multifactor/FIDO2/add.html: -------------------------------------------------------------------------------- 1 | {% extends "multifactor/base.html" %}{% load static %} 2 | 3 | {% block card_title %}FIDO2 Security Key{% endblock %} 4 | 5 | {% block content %} 6 |
7 |

Follow your browser's instructions to continue.

8 | 9 |
10 | {% endblock %} 11 | 12 | {% block head %} 13 | {{ block.super }} 14 | 15 | {% block fido_scripting %} 16 | 59 | {% endblock fido_scripting %} 60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /multifactor/templates/multifactor/FIDO2/check.html: -------------------------------------------------------------------------------- 1 | {% extends "multifactor/FIDO2/add.html" %}{% load static %} 2 | 3 | {% block fido_scripting %} 4 | 48 | {% endblock fido_scripting %} -------------------------------------------------------------------------------- /multifactor/templates/multifactor/TOTP/add.html: -------------------------------------------------------------------------------- 1 | {% extends "multifactor/base.html" %}{% load static %} 2 | 3 | {% block card_title %}Add TOTP Authenticator{% endblock %} 4 | 5 | {% block content %} 6 | {% block preform %} 7 |

Start by downloading an Authenticator App on your phone. Google Authenticator for Android or Authy for iPhones. Use it to scan in this QR code.

8 | 9 |
10 |
11 |

{{secret_key}}

12 |
13 | 14 |

Once scanned, your Authenticator will give you a 6-digit, rotating code. Copy that code into the box below and click Verify.

15 | {% endblock preform %} 16 | 17 |
18 | {% csrf_token %} 19 | 20 | 21 |
22 | 24 |
25 | 26 | 27 |
28 | {% endblock %} 29 | 30 | {% block head %} 31 | 32 | 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /multifactor/templates/multifactor/TOTP/check.html: -------------------------------------------------------------------------------- 1 | {% extends "multifactor/TOTP/add.html" %}{% load static %} 2 | 3 | {% block card_title %}Verify Authenticator{% endblock %} 4 | 5 | {% block preform %} 6 |

Enter the current code from one of your authenticators.

7 | {% endblock %} 8 | {% block head %}{% endblock %} -------------------------------------------------------------------------------- /multifactor/templates/multifactor/add.html: -------------------------------------------------------------------------------- 1 | {% extends "multifactor/base.html" %} 2 | {% load static %} 3 | 4 | {% block card_title %}Add a New Factor{% endblock %} 5 | {% block container_class %}{% endblock %} 6 | 7 | {% block content %} 8 | 9 |

To protect your account and this system from intrusion we request a second verification factor.

10 | 11 |

There are a range of options. TOTP authenticators are a good first factor because most people always have their phone with them. USB and on-device options might be more secure but have more limited portability.

12 | 13 |
14 |
15 |
16 |
FIDO2 Device
17 |
18 |
19 |

A broad range of devices: Windows Hello (fingerprint, facial recognition), Google Chrome, Google Android (via biometrics or location), or FIDO2 USB and NFC keys. 20 |

On-device options are convenient but will lock you to that device. USB keys are more portable.

21 |
22 | 25 |
26 |
27 |
28 |
TOTP Authenticator
29 |
30 |
31 |

Install an authenticator from your phone's marketplace and get access to a secure rotating passcode. Once linked, verification is just a matter of copying that code back into here when prompted.

32 |

Benefits: free, portable.

33 |
34 | 37 |
38 |
39 | 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /multifactor/templates/multifactor/authenticate.html: -------------------------------------------------------------------------------- 1 | {% extends "multifactor/base.html" %} 2 | 3 | {% block card_title %}Multi-Factor Authentication{% endblock %} 4 | 5 | {% block content %} 6 |

You are logged in but you need to verify one of your secondary factors to continue. Which factor would you like to authenticate with?

7 | {% for link, label, factors in methods %} 8 | 9 | {{label}} 10 | {{factors}} 11 | 12 | {% endfor %} 13 | 14 | {% if fallbacks %} 15 |
16 |