Show multifactor setup key in addition to QR code
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index c1689be..aa558c0 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -19,6 +19,7 @@
 from __future__ import absolute_import
 import logging
 import os
+from base64 import b32encode
 from datetime import datetime
 import re
 import warnings
@@ -765,12 +766,14 @@
             totp = totp_service.Totp(key)
 
         qr = totp_service.get_qr_code(totp, c.user)
+        key_b32 = b32encode(totp.key).decode('ascii')
         h.auditlog_user('Visited multifactor new TOTP page')
         provider = plugin.AuthenticationProvider.get(request)
 
         return dict(
             menu=provider.account_navigation(),
             qr=qr,
+            key_b32=key_b32,
             setup=True,
         )
 
@@ -784,11 +787,13 @@
         totp_service = TotpService.get()
         totp = totp_service.get_totp(c.user)
         qr = totp_service.get_qr_code(totp, c.user)
+        key_b32 = b32encode(totp.key).decode('ascii')
         h.auditlog_user('Viewed multifactor TOTP config page')
         provider = plugin.AuthenticationProvider.get(request)
 
         return dict(
             qr=qr,
+            key_b32=key_b32,
             setup=False,
             menu=provider.account_navigation(),
         )
diff --git a/Allura/allura/templates/user_totp.html b/Allura/allura/templates/user_totp.html
index 6ec9bee..ce4ae72 100644
--- a/Allura/allura/templates/user_totp.html
+++ b/Allura/allura/templates/user_totp.html
@@ -45,18 +45,21 @@
         </form>
     {% endif %}
 
-    <h2>Scan this barcode with your app</h2>
+    <h2>Scan this with your app:</h2>
     <img class="qrcode" src="{{ h.base64uri(qr) }}"/>
+    <p style="margin-left:1rem">
+        Or enter setup key: {{ key_b32 }}
+    </p>
 
     {% if setup %}
         <h2>Enter the code</h2>
         <form method="POST" action="totp_set" id="totp_set">
         <p>
-            Enter the {{ config['auth.multifactor.totp.length'] }}-digit code to confirm it is set up correctly:<br>
+            Enter the {{ config['auth.multifactor.totp.length'] }}-digit code from the app, to confirm it is set up correctly:<br>
             {% if c.form_errors['code'] %}
                 <span class="fielderror">{{ c.form_errors['code'] }}</span><br>
             {% endif %}
-            <input type="text" name="code" autofocus autocomplete="off"/>
+            <input type="text" name="code" autofocus autocomplete="off" maxlength="{{ config['auth.multifactor.totp.length']|int + 1 }}"/>
             {{ lib.csrf_token() }}
             <br>
             <input type="submit" value="Submit">
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index 05e11e2..3be1355 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -19,6 +19,7 @@
 from __future__ import print_function
 from __future__ import absolute_import
 import calendar
+from base64 import b32encode
 from datetime import datetime, time, timedelta
 from time import time as time_time
 import json
@@ -2389,6 +2390,7 @@
 class TestTwoFactor(TestController):
 
     sample_key = b'\x00K\xda\xbfv\xc2B\xaa\x1a\xbe\xa5\x96b\xb2\xa0Z:\xc9\xcf\x8a'
+    sample_b32 = 'ABF5VP3WYJBKUGV6UWLGFMVALI5MTT4K'
 
     def _init_totp(self, username='test-admin'):
         user = M.User.query.get(username=username)
@@ -2468,7 +2470,8 @@
         with audits('Visited multifactor new TOTP page', user=True):
             r.form['password'] = 'foo'
             r = r.form.submit()
-            assert_in('Scan this barcode', r)
+            assert_in('Scan this', r)
+            assert_in('Or enter setup key: ', r)
 
         first_key_shown = r.session['totp_new_key']
 
@@ -2477,6 +2480,7 @@
             form['code'] = ''
             r = form.submit()
             assert_in('Invalid', r)
+            assert_in(f'Or enter setup key: {b32encode(first_key_shown).decode()}', r)
             assert_equal(first_key_shown, r.session['totp_new_key'])  # different keys on each pageload would be bad!
 
         new_totp = TotpService().Totp(r.session['totp_new_key'])
@@ -2513,7 +2517,8 @@
         r = r.form.submit()
 
         # confirm warning message, and key is not changed yet
-        assert_in('Scan this barcode', r)
+        assert_in('Scan this', r)
+        assert_in('Or enter setup key: ', r)
         assert_in('this will invalidate your previous', r)
         current_key = TotpService.get().get_secret_key(M.User.query.get(username='test-admin'))
         assert_equal(self.sample_key, current_key)
@@ -2760,7 +2765,8 @@
         with audits('Viewed multifactor TOTP config page', user=True):
             r.form['password'] = 'foo'
             r = r.form.submit()
-            assert_in('Scan this barcode', r)
+            assert_in('Scan this', r)
+            assert_in(f'Or enter setup key: {self.sample_b32}', r)
 
     def test_view_recovery_codes_and_regen(self):
         self._init_totp()