15 |
16 |
success test
17 |
info test
18 |
warning test
19 |
danger test
20 |
error test
21 |
22 |
23 |
24 |
27 |
28 |
29 |
30 |
31 |
32 | Add Factor ➕
33 |
34 |
35 |
40 |
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 |
48 |
51 | Added {{ key.added_on.date }}. raaa
52 |
53 |
54 | 🔓
55 | 🔒
56 |
59 |
62 |
63 |
64 |
65 |
66 |
67 | Fallback verification factors
68 | Consider turning these off for higher security.
69 |
70 |
71 |
77 |
78 |
79 |
80 |
81 |
82 |
What is multi-factor authentication? Where do I start?
83 |
84 |
85 |
142 |
143 |
144 |
145 |
146 |
157 |
158 |
159 |
160 |
161 |
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 |
176 |
177 |
178 |
179 |
180 |
181 |
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 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
215 |
U2F only works under HTTPS
216 |
217 |
218 |
219 |
220 |
223 |
U2F only works under HTTPS
224 |
225 |
226 |
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 |
Click here to Start
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 |
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 |
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 |
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 |