Commit e22fd31a authored by mh's avatar mh
Browse files

add webauthn support to users interface

additionally add icon to totp URI
parent d5436426
......@@ -8,7 +8,14 @@ gem 'rails', '~> 5.2.0'
# Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.3', '< 1.4'
# Use Puma as the app server
# TODO remove once released
# https://github.com/puma/puma/issues/1670
# https://github.com/puma/puma/issues/1758
if ENV['PUMA_SSL_FIX'].to_i == 1
gem 'puma', git: 'https://github.com/puma/puma', ref: '27a09c4e7c0e04bc25a515388b6b8a55fa332ad5'
else
gem 'puma', '~> 3.11'
end
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
......
function ab2str(buf) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buf))).replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '');
}
function str2ab(enc) {
var str = atob(enc.replace(/_/g, '/').replace(/-/g, '+'));
var buf = new ArrayBuffer(str.length);
var bufView = new Uint8Array(buf);
for (var i=0, strLen=str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.
//= require base64.js
......@@ -84,7 +84,7 @@
margin-top: 20px;
font-size: 9pt;
font-family: monospace;
width: 260px;
width: 280px;
}
.qr-field table {
......
.webauthn-form div {
float: left;
padding-right: 15px;
}
.webauthn-form table {
padding-top: 5px;
}
......@@ -9,7 +9,7 @@ class AppPasswordsController < ApplicationController
if session[:app_pw]
@app_pw = session[:app_pw]
begin
@qr = RQRCode::QRCode.new(@app_pw, :size => 10, :level => :h)
@qr = RQRCode::QRCode.new(@app_pw, :size => 13, :level => :h)
rescue => e
@qr = ''
puts "qr token generation failed #{e}"
......
......@@ -71,6 +71,16 @@ class ApplicationController < ActionController::Base
end
helper_method :recovery_email_set?
def requires_2fa?
session[:requires_2fa]
end
helper_method :requires_2fa?
def tfa_enabled?
requires_2fa? || recovery_email_set?
end
helper_method :tfa_enabled?
def resource_enabled?(resource)
(session[:possible_resources]||{})[resource].present?
end
......@@ -152,4 +162,8 @@ class ApplicationController < ActionController::Base
def api_token
session[:api_token]
end
def jsb64_tob64(str)
Base64.strict_encode64(Base64.urlsafe_decode64(str))
end
end
......@@ -237,6 +237,31 @@ module ApiBackend
post(['users', 'delete_totp'], {'name' => name, 'password' => password })
end
def init_webauthn_registration(password)
post(['users', 'init_webauthn_registration'],{'password' => password })
end
def verify_webauthn_registration(password, webauthn_name, attestation_object , client_data_json, current_challenge)
post(['users','verify_webauthn_registration'],{
'password' => password,
'webauthn_name' => webauthn_name,
'attestation_object' => attestation_object,
'client_data_json' => client_data_json,
'current_challenge' => current_challenge,
})
end
def delete_webauthn_credential(password, webauthn_name)
post(['users','delete_webauthn_credential'],{
'password' => password,
'webauthn_name' => webauthn_name,
})
end
def webauthn_credentials
get(['users','webauthn_credentials'])
end
def mail_crypt_token(pw)
post(['users', 'generate_recovery_token'], {"password" => pw})
end
......
......@@ -64,7 +64,7 @@ class JabberController < ApplicationController
def qr
@qr ||= if @jid.present? && @jid['password'].present?
RQRCode::QRCode.new(@jid['password'], :size => 10, :level => :h)
RQRCode::QRCode.new(@jid['password'], :size => 13, :level => :h)
else
nil
end
......
......@@ -31,7 +31,7 @@ class MailCryptController < ApplicationController
res = api.mail_crypt_token(params[:pass])
recovery_token = res['mail_crypt_recovery_token']
begin
@qr = RQRCode::QRCode.new(recovery_token, :size => 12, :level => :h)
@qr = RQRCode::QRCode.new(recovery_token, :size => 13, :level => :h)
rescue => e
@qr = ''
puts "qr token generation failed #{e}"
......
......@@ -11,7 +11,7 @@ class RecoveryEmailController < ApplicationController
render 'show' and return
end
api.set_recovery_email(@recovery_email, @email_recovery_token, params[:pass])
api.set_recovery_email(@recovery_email, @email_recovery_token, params[:password])
flash[:notice] = :success
session[:recovery_email_set] = true
redirect_to '/'
......
......@@ -56,6 +56,7 @@ class SessionsController < ApplicationController
session[:mail_crypt_enabled] = res['mail_crypt_enabled']
session[:recovery_email_set] = res['recovery_email_set']
session[:requires_2fa] = res['requires_2fa']
session[:possible_resources] = res['possible_resources']
if res['locked']
......
......@@ -65,7 +65,7 @@ class SignupController < ApplicationController
if @keep_recovery_token == 'show'
@mail_crypt_recovery_token = res['mail_crypt_recovery_token']
begin
@qr = RQRCode::QRCode.new(@mail_crypt_recovery_token, :size => 12, :level => :h)
@qr = RQRCode::QRCode.new(@mail_crypt_recovery_token, :size => 13, :level => :h)
rescue => e
puts "qr token generation failed #{e}"
@qr = ''
......
......@@ -3,8 +3,11 @@ require 'rotp'
class TfaController < ApplicationController
include ApiBackend
before_action :require_tfa_enabled
def show
end
def create_totp
if [:name, :password, :secret].all?{|e| params[e].present? }
begin
......@@ -39,16 +42,68 @@ class TfaController < ApplicationController
redirect_to tfa_path
end
protected
helper_method :tfa_activated?, :totp_qr, :totp_secret, :existing_totps
def tfa_activated?
existing_totps.present?
def new_webauthn
render 'new_webauthn'
end
# either verify or init
def create_webauthn
if [:response, :challenge, :name, :password].all?{|e| params[e].present? }
begin
response = JSON.parse(params[:response])
attestation_object = jsb64_tob64(response.fetch("attestationObject"))
client_data_json = jsb64_tob64(response.fetch("clientDataJSON"))
api.verify_webauthn_registration(
params[:password],
params[:name],
attestation_object,
client_data_json,
params[:challenge],
)
flash[:notice] = :success
redirect_to tfa_path
rescue ApiBackend::ApiError
flash[:notice] = :failed
return render 'new_webauthn'
end
elsif [:name, :password].all?{|e| params[e].present? }
begin
@options = api.init_webauthn_registration(params[:password])['credential_options']
render 'create_webauthn'
rescue ApiBackend::ApiError
flash[:notice] = :failed
return render 'new_webauthn'
end
else
flash[:notice] = t(:all_fields_required)
return render 'new_webauthn'
end
end
def delete_webauthn
if params[:name].present?
begin
api.delete_webauthn_credential(params[:password], params[:name])
flash[:notice] = :success
rescue ApiBackend::ApiError
flash[:notice] = :failed
end
else
flash[:notice] = t(:name_must_be_set)
end
redirect_to tfa_path
end
protected
helper_method :totp_qr, :totp_secret, :existing_totps,
:existing_webauthn_credentials
def totp_qr
totp = ROTP::TOTP.new(totp_secret, issuer: "immerda.ch")
totp = ROTP::TOTP.new(totp_secret, issuer: "immerda.ch", image: "https://www.immerda.ch/layout/images/immerda_logo.png")
url = totp.provisioning_uri(current_user)
RQRCode::QRCode.new(url, :size => 12, :level => :h)
RQRCode::QRCode.new(url, :size => 13, :level => :h)
end
def totp_secret
......@@ -63,4 +118,20 @@ class TfaController < ApplicationController
rescue ApiBackend::ApiError
flash[:notice] = :failed
end
def existing_webauthn_credentials
@existing_webauthn_credentials ||= begin
res = api.webauthn_credentials
res['credentials']
end
rescue ApiBackend::ApiError
flash[:notice] = :failed
end
def require_tfa_enabled
unless tfa_enabled?
flash[:error] = t(:tfa_not_available) + ' - ' + t(:requires_recovery_email)
redirect_to root_path
end
end
end
......@@ -69,6 +69,8 @@ class UsersController < AdminController
[:verify_recovery_email, params[:verify_recovery_email]]
elsif params[:unlock]
[:unlock, true]
elsif params[:disable_2fa]
[:disable_2fa, true]
elsif params[:lock]
[:lock, true]
elsif params[:delete]
......
......@@ -21,8 +21,7 @@
<%= t(:backup_recovery_token_email) %>
<% end %>
<br />
<%= label_tag(:pass, t(:your_main_pw)) %><%= password_field_tag(:password, nil, placeholder: t(:enter_your_current
_pw)) %>
<%= label_tag(:password, t(:your_main_pw)) %><%= password_field_tag(:password, nil, placeholder: t(:enter_your_current_pw)) %>
<br />
<%= submit_tag(recovery_email_set? ? t(:overwrite) : t(:submit)) %>
<%= link_to t(:back), root_path %>
......
<h3><%= @page_title = t(:certify_webauthn) %></h3>
<p>
<%= t(:certify_webauthn_description) %> | <%= link_to t(:back), tfa_path %>
</p>
<p>
<%= t(:name) %>: <%= params[:name] %>
</p>
<p>
<%= image_tag 'webauthn.png' %>
</p>
<%= form_tag(tfa_webauthn_path, method: "post") do %>
<%= hidden_field('', :password, :value => params[:password]) %>
<%= hidden_field('', :name, :value => params[:name]) %>
<%= hidden_field('', :challenge, :value => @options['challenge']) %>
<%= hidden_field_tag("response") %>
<% end %>
<script>
// render requests from server into Javascript format
var options = <%= @options.to_json.html_safe %>
function do_register() {
options.challenge = str2ab(options.challenge);
for (var i = 0; i < options.exclude_credentials.length; i++) {
options.exclude_credentials[i].id = str2ab(options.exclude_credentials[i].id);
}
options.user.id = str2ab(options.user.id);
navigator.credentials.create({
publicKey: options,
}).then((credential) => {
var r = credential.response;
var registerResponse = {
id: credential.id,
clientDataJSON: ab2str(r.clientDataJSON),
attestationObject: ab2str(r.attestationObject)
};
form = document.forms[0];
response = document.querySelector('[name=response]');
response.value = JSON.stringify(registerResponse);
form.submit();
}, (reason) => {
return alert("Registration error: " + reason);
});
}
do_register();
</script>
<h3><%= @page_title = t(:add_webauthn) %></h3>
<p>
<%= t(:register_webauthn_description) %>
</p>
<h4><%= t(:register_new_webauthn) %></h4>
<div class="webauthn-form">
<div>
<%= image_tag 'webauthn.png' %>
</div>
<%= form_tag(tfa_webauthn_path, method: "post") do %>
<table>
<tr><td>
<%= label_tag(:name, (t :name)) %>
</td><td>
<%= text_field_tag(:name, '', value: (existing_webauthn_credentials.keys.any?{|t| t == 'default' } ? "default-#{Time.now.strftime("%Y%m%d%H%M")}" : 'default'), maxlength: 255, autocomplete: 'off') %>
</td></tr>
<tr><td>
<%= label_tag(:password, t(:your_main_pw)) %>
</td><td>
<%= password_field_tag(:password, nil, placeholder: t(:enter_your_current_pw)) %>
</td></tr>
</table>
<%= submit_tag(t(:register)) %>
<%= link_to t(:back), tfa_path %>
<% end -%>
</div>
<h3><%= @page_title = t(:tfa_enable) %></h3>
<p>
<%= t(:status) %>: <%= tfa_activated? ? "<b>#{t(:activated)}</b>".html_safe : t(:disabled) %> | <%= link_to(t(:back), root_path) %>
<%= t(:status) %>: <%= requires_2fa? ? "<b>#{t(:activated)}</b>".html_safe : t(:disabled) %> | <%= link_to(t(:back), root_path) %>
</p>
<p>
......@@ -35,3 +35,30 @@
<p>
<%= link_to(t(:add_totp), tfa_totp_path) %>
</p>
<h4><%= t(:webauthn) %></h4>
<p>
<%= t(:webauthn_description) %>
</p>
<h5><%= t(:my_webauthn_credentials) %></h5>
<% if existing_webauthn_credentials.present? -%>
<ul>
<% existing_webauthn_credentials.each do |name, pubkey| -%>
<li>
<%= name %>: <%= pubkey %>&nbsp;
<a href="#" onclick="toggle_visibility('delete_form_<%= URI.encode(name) %>')"/><%= t(:delete) %></a>
<div id="delete_form_<%= URI.encode(name) %>" style="display:none">
<br />
<%= form_tag("/tfa/webauthn/#{URI.encode(name)}/delete", method: "post") do %>
<%= label_tag(:pass, t(:your_main_pw)) %><%= password_field_tag(:password, nil, placeholder: t(:enter_your_current_pw)) %>
<%= submit_tag t(:delete) %>
<% end -%>
</div>
<% end -%>
</ul>
<% else -%>
<ul>
<li><%= t(:no_existing_webauthn_credentials) %></li>
</ul>
<% end -%>
<p>
<%= link_to(t(:add_webauthn), tfa_webauthn_path) %>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment