[#8119] U2F multifactor
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 2009515..d997ad3 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -14,7 +14,7 @@
 #       KIND, either express or implied.  See the License for the
 #       specific language governing permissions and limitations
 #       under the License.
-
+import json
 import logging
 import os
 from datetime import datetime, timedelta
@@ -31,6 +31,7 @@
 from webob import exc as wexc
 from paste.deploy.converters import asbool
 from cryptography.hazmat.primitives.twofactor import InvalidToken
+from u2flib_server import u2f_v2 as u2f
 
 import allura.tasks.repo_tasks
 from allura import model as M
@@ -807,6 +808,30 @@
         tg.flash('Your recovery codes have been regenerated.  Save the new codes!')
         redirect('/auth/preferences/multifactor_recovery')
 
+    @expose('jinja:allura:templates/user_u2f.html')
+    @reconfirm_auth
+    @without_trailing_slash
+    def u2f(self, **kwargs):
+        appId = config.get('auth.multifactor.u2f.appId', config['base_url'])
+        register_request = u2f.start_register(appId)
+        session['u2f_reg_req'] = register_request
+        session.save()
+        return dict(
+            appId=appId,
+            register_request=register_request,
+        )
+
+    @expose()
+    @require_post()
+    @reconfirm_auth
+    def u2f_add(self, device_response, **kwargs):
+        register_request = session.pop('u2f_reg_req')
+        session.save()
+        appId = config.get('auth.multifactor.u2f.appId', config['base_url'])
+        registration, cert = u2f.complete_register(register_request, device_response)#, [appId])
+        return
+
+
 
 class UserInfoController(BaseController):
 
diff --git a/Allura/allura/model/multifactor.py b/Allura/allura/model/multifactor.py
index cf88ce6..1ec9d2b 100644
--- a/Allura/allura/model/multifactor.py
+++ b/Allura/allura/model/multifactor.py
@@ -54,3 +54,6 @@
     _id = FieldProperty(S.ObjectId)
     user_id = FieldProperty(S.ObjectId, required=True)
     code = FieldProperty(str, required=True)
+
+
+# U2F data is stored as user data, since it isn't confidential (just public keys) it doesn't warrant its own collection
\ No newline at end of file
diff --git a/Allura/allura/templates/user_prefs.html b/Allura/allura/templates/user_prefs.html
index c7a8677..0c60b1e 100644
--- a/Allura/allura/templates/user_prefs.html
+++ b/Allura/allura/templates/user_prefs.html
@@ -124,6 +124,7 @@
             {% if user_multifactor %}
                 <p><b class="fa fa-qrcode"></b> <a href="totp_view">View existing configuration</a></p>
                 <p><b class="fa fa-life-ring"></b> <a href="multifactor_recovery">View recovery codes</a></p>
+                <p><b class="fa fa-key"></b> <a href="u2f">Add or remove U2F hardware keys</a></p>
                 <form action="multifactor_disable" id="multifactor_disable" method="post">
                 <p>
                     <b class="fa fa-trash"></b> <a href="#" class="disable">Disable</a>
diff --git a/Allura/allura/templates/user_u2f.html b/Allura/allura/templates/user_u2f.html
new file mode 100644
index 0000000..40c8ff3
--- /dev/null
+++ b/Allura/allura/templates/user_u2f.html
@@ -0,0 +1,89 @@
+{#-
+       Licensed to the Apache Software Foundation (ASF) under one
+       or more contributor license agreements.  See the NOTICE file
+       distributed with this work for additional information
+       regarding copyright ownership.  The ASF licenses this file
+       to you under the Apache License, Version 2.0 (the
+       "License"); you may not use this file except in compliance
+       with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+       Unless required by applicable law or agreed to in writing,
+       software distributed under the License is distributed on an
+       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+       KIND, either express or implied.  See the License for the
+       specific language governing permissions and limitations
+       under the License.
+-#}
+{% set hide_left_bar = True %}
+{% extends "allura:templates/user_account_base.html" %}
+
+{% block title %}{{c.user.username}} / U2F Setup{% endblock %}
+
+{% block header %}U2F Hardware Key Setup for {{c.user.username}}{% endblock %}
+
+{% block content %}
+  {{ super() }}
+  <div class='grid-20'>
+    <h2>Intro</h2>
+    <p>asdfsadf
+    </p>
+
+      <form method="post" action="u2f_add">
+          <input type="hidden" name="device_response"/>
+          {{ lib.csrf_token() }}
+
+
+          DEBUG: {{ register_request }}
+      </form>
+
+    <p>
+      <a href="/auth/preferences/">Back</a>
+    </p>
+  </div>
+{% endblock %}
+
+{% block extra_css %}
+<style type="text/css">
+</style>
+{% endblock %}
+
+{% block extra_js %}
+
+{#  TODO feature detection...  u2f already there or chrome?
+
+always need to include one of these I think, Chrome only has low-level APIs,
+ and the function signatures are a little bit different between these and firefox plugin
+<script src="https://demo.yubico.com/js/u2f-api.js"></script>
+#}
+<script src="https://rawgit.com/google/u2f-ref-code/master/u2f-gae-demo/war/js/u2f-api.js"></script>
+
+<script type="text/javascript">
+$(function() {
+    if (window.u2f) {
+        window.u2f.register('{{ appId }}', [
+            {{ h.escape_json(register_request)|safe }}
+        ],
+        [],
+        function (data) {
+            if (data.errorCode) {
+                if (data.errorCode === u2f.ErrorCodes.TIMEOUT) {
+                    // FIXME do something to re-prompt
+                    $('#messages').notify({message: 'Timeout, please try again.', status:'info'});
+                } else {
+                    $('#messages').notify({message: 'An error occurred, please try again.', status:'error'});
+                }
+                console.log(data);
+                return;
+            }
+            var $input = $('input[name=device_response]');
+            $input.val(JSON.stringify(data));
+            $input.closest('form').submit();
+        });
+    }
+});
+
+
+</script>
+{% endblock %}
\ No newline at end of file
diff --git a/Allura/development.ini b/Allura/development.ini
index b160ef6..c059ed3 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -212,6 +212,9 @@
 ; length of each code.  Must be 8 for compatibility with "filesystem-googleauth" files
 auth.multifactor.recovery_code.length = 8
 
+; The default u2f appId will be Allura's base_url
+;auth.multifactor.u2f.appId = https://website.com
+; if testing with HTTP, see https://github.com/google/u2f-ref-code#option-2-use-the-built-in-chrome-support
 
 user_prefs_storage.method = local
 ; user_prefs_storage.method = ldap
diff --git a/requirements.txt b/requirements.txt
index fc32006..bd46792 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -36,6 +36,7 @@
 python-dateutil==1.5
 python-magic==0.4.3
 python-oembed==0.2.1
+python-u2flib-server==4.0.1
 pytz==2014.10
 qrcode==5.3
 requests==2.0.0