Add userpass authentication
authorLunar <lunar@anargeek.net>
Mon, 19 Dec 2016 12:51:03 +0000 (13:51 +0100)
committerLunar <lunar@anargeek.net>
Tue, 20 Dec 2016 12:38:13 +0000 (13:38 +0100)
The `userpass` authentication mechanism prompts for a user and password
to perform an upload. The credentials are stored as pairs of login/password in
the local configuration. Password are stored in an encrypted form using
bcrypt().

`userpass` configured with a single account can be used instead of `simplepass`
to allow users to make their browser retain the upload credentials.

Based on a patch from Rowan Thorpe.

14 files changed:
Gemfile.lock
INSTALL
conf/settings-userpass.yml [new file with mode: 0644]
coquelicot.gemspec
features/auth/userpass.feature [new file with mode: 0644]
features/step_definitions/auth/userpass.rb [new file with mode: 0644]
lib/coquelicot/auth/userpass.rb [new file with mode: 0644]
po/coquelicot.pot
po/de/coquelicot.po
po/es/coquelicot.po
po/fr/coquelicot.po
public/javascripts/coquelicot.auth.userpass.js [new file with mode: 0644]
spec/coquelicot/auth/userpass_spec.rb [new file with mode: 0644]
views/auth/userpass.haml [new file with mode: 0644]

index 681669f..9a3c580 100644 (file)
@@ -28,6 +28,7 @@ GEM
     addressable (2.5.0)
       public_suffix (~> 2.0, >= 2.0.2)
     backports (3.6.8)
+    bcrypt (3.1.11)
     builder (3.2.2)
     capybara (2.11.0)
       addressable
@@ -133,6 +134,7 @@ PLATFORMS
 
 DEPENDENCIES
   activesupport
+  bcrypt
   capybara
   coquelicot!
   cucumber
diff --git a/INSTALL b/INSTALL
index afebb3f..54e1b8f 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -146,6 +146,9 @@ Further settings example:
  * `conf/settings-simplepass.yml`: shows how to change the default
    password for the "simplepass" mechanism.
 
+ * `conf/settings-imap.yml`: necessary configuration for the "userpass"
+   authentication mechanism.
+
  * `conf/settings-imap.yml`: necessary configuration for the "imap"
    authentication mechanism.
 
@@ -155,6 +158,9 @@ Further settings example:
 You can copy one of these examples to `conf/settings.yml` and adjust
 them according to your environment.
 
+Using the "userpass" authentication method requires the `bcrypt` gem to
+be installed manually.
+
 Using the LDAP authentication method requires the `net-ldap` gem
 to be installed manually.
 
diff --git a/conf/settings-userpass.yml b/conf/settings-userpass.yml
new file mode 100644 (file)
index 0000000..8cb031f
--- /dev/null
@@ -0,0 +1,19 @@
+# Settings for the 'userpass' authentication method
+# -------------------------------------------
+#
+# When using the 'userpass' authentication method, users will
+# be asked for their username and a pre-shared password.
+
+authentication_method:
+  name: "userpass"
+
+  # Passwords are stored using BCrypt
+  #
+  #     You can compute them using:
+  #
+  #         $ echo 'secret' | ruby -rbcrypt -p -e 'puts BCrypt::Password.create($_).to_s'
+  #
+  credentials:
+    fred: "$2a$10$Xe8F9F.4vNBmuA6V/5hvK.Glw0ab4pJgcVPQlUN8dP/uhf4QCWAFW"
+    jenny: "$2a$10$mBAOGjcsIL9wyaAIGDdxf.rOyJEfCYeG2pAaXCrwjhXbis7k/Vcs."
+    abdul: "$2a$10$8NXS8SQdhkUlC7b2vog.FOm9Nob4t38v146rQHlAVoyClNJPiCnVa"
index 8985b6c..9151c5c 100644 (file)
@@ -54,6 +54,7 @@ Gem::Specification.new do |s|
   s.add_development_dependency 'tzinfo'
   s.add_development_dependency 'net-ldap'
   s.add_development_dependency 'gettext', '~>3'
+  s.add_development_dependency 'bcrypt'
 
   s.add_runtime_dependency 'sinatra', '~>1.4'
   s.add_runtime_dependency 'sinatra-contrib', '~>1.4'
diff --git a/features/auth/userpass.feature b/features/auth/userpass.feature
new file mode 100644 (file)
index 0000000..8600217
--- /dev/null
@@ -0,0 +1,21 @@
+Feature: Uploads can be limited to accounts registred in a configuration file
+
+ Background:
+   Given the admin has configured the "userpass" authentication method
+   And the config file describes an account "user" identified with "secret"
+
+ Scenario: Uploads are denied without a login
+   When I try to upload a file without a login
+   Then I'm denied the upload
+
+ Scenario: Uploads are denied with a wrong login
+   Given I have entered "unknown" as user login
+   And I have entered "secret" as user password
+   When I try to upload a file
+   Then I'm denied the upload
+
+ Scenario: Uploads are accepted with the right password
+   Given I have entered "user" as user login
+   And I have entered "secret" as user password
+   When I try to upload a file
+   Then the upload is accepted
diff --git a/features/step_definitions/auth/userpass.rb b/features/step_definitions/auth/userpass.rb
new file mode 100644 (file)
index 0000000..17de45b
--- /dev/null
@@ -0,0 +1,34 @@
+# -*- coding: UTF-8 -*-
+# Coquelicot: "one-click" file sharing with a focus on users' privacy.
+# Copyright © 2016 potager.org <jardiniers@potager.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+require 'bcrypt'
+
+Given(/^the config file describes an account "([^"]*)" identified with "([^"]*)"$/) do |user, password|
+  @credentials = {} if @credentials.nil?
+
+  allow(Coquelicot.settings).to receive_messages(
+      :credentials => { user => BCrypt::Password.create(password).to_s })
+end
+
+Given(/^I have entered "([^"]*)" as user login$/) do |login|
+  visit '/'
+  fill_in :upload_user, :with => login
+end
+
+Given(/^I have entered "([^"]*)" as user password$/) do |password|
+  fill_in :upload_password, :with => password
+end
diff --git a/lib/coquelicot/auth/userpass.rb b/lib/coquelicot/auth/userpass.rb
new file mode 100644 (file)
index 0000000..cd9f281
--- /dev/null
@@ -0,0 +1,44 @@
+# -*- coding: UTF-8 -*-
+# Coquelicot: "one-click" file sharing with a focus on users' privacy.
+# Copyright © 2012-2016 potager.org <jardiniers@potager.org>
+#           ©      2016 Rowan Thorpe <rowan@rowanthorpe.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+require 'bcrypt'
+
+module Coquelicot
+  module Auth
+    class UserpassAuthenticator < AbstractAuthenticator
+      EMPTY_PASSWORD = BCrypt::Password.create('')
+
+      def authenticate(params)
+        upload_user = params[:upload_user] || ''
+        upload_password = params[:upload_password] || ''
+        return false if upload_user.empty? || upload_password.empty?
+
+       # Use the empty password—we just disallowed it—for unknown users
+        # in order to get constant time.
+        reference_password = settings.credentials.fetch(upload_user, EMPTY_PASSWORD)
+        return BCrypt::Password.new(reference_password) == upload_password
+      rescue NoMethodError => ex
+        if :credentials == ex.name
+          raise Coquelicot::Auth::Error.new("Missing 'credentials' attribute in 'userpass' configuration.")
+        else
+          raise
+        end
+      end
+    end
+  end
+end
index cf1fadc..b26abbe 100644 (file)
@@ -41,10 +41,14 @@ msgstr ""
 msgid "The requested URL %s was not found on this server."
 msgstr ""
 
-#: views/auth/simplepass.haml:19
+#: views/auth/simplepass.haml:19 views/auth/userpass.haml:23
 msgid "Upload password:"
 msgstr ""
 
+#: views/auth/userpass.haml:20
+msgid "Upload user:"
+msgstr ""
+
 #: views/auth/imap.haml:20
 msgid "E-mail User:"
 msgstr ""
index ce9e739..e86c120 100644 (file)
@@ -40,10 +40,14 @@ msgstr "Nit gefunden"
 msgid "The requested URL %s was not found on this server."
 msgstr "Die angeforderte URL %s wurde nicht auf dem Server gefunden."
 
-#: views/auth/simplepass.haml:19
+#: views/auth/simplepass.haml:19 views/auth/userpass.haml:23
 msgid "Upload password:"
 msgstr "Upload Passwort:"
 
+#: views/auth/userpass.haml:20
+msgid "Upload user:"
+msgstr "Upload user:"
+
 #: views/auth/imap.haml:20
 msgid "E-mail User:"
 msgstr "Email User:"
index f2954aa..afa8ebb 100644 (file)
@@ -40,10 +40,14 @@ msgstr "No se encuentra"
 msgid "The requested URL %s was not found on this server."
 msgstr "El URL %s no se encuentra en este servidor."
 
-#: views/auth/simplepass.haml:19
+#: views/auth/simplepass.haml:19 views/auth/userpass.haml:23
 msgid "Upload password:"
 msgstr "Contraseña para el envío:"
 
+#: views/auth/userpass.haml:20
+msgid "Upload user:"
+msgstr ""
+
 #: views/auth/imap.haml:20
 msgid "E-mail User:"
 msgstr "Correo electrónico:"
index 9e8e9e0..3d487e5 100644 (file)
@@ -40,10 +40,14 @@ msgstr "Introuvable"
 msgid "The requested URL %s was not found on this server."
 msgstr "L'URL %s demandé est introuvable sur ce serveur."
 
-#: views/auth/simplepass.haml:19
+#: views/auth/simplepass.haml:19 views/auth/userpass.haml:23
 msgid "Upload password:"
 msgstr "Mot de passe pour envoyer :"
 
+#: views/auth/userpass.haml:20
+msgid "Upload user:"
+msgstr "Compte pour envoyer :"
+
 #: views/auth/imap.haml:20
 msgid "E-mail User:"
 msgstr "Compte email :"
diff --git a/public/javascripts/coquelicot.auth.userpass.js b/public/javascripts/coquelicot.auth.userpass.js
new file mode 100644 (file)
index 0000000..336180c
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Coquelicot: "one-click" file sharing with a focus on users' privacy.
+ * Copyright © 2012-2016 potager.org <jardiniers@potager.org>
+ *           ©      2016 Rowan Thorpe <rowan@rowanthorpe.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var authentication = {
+  getData: function() {
+    return {
+      upload_user: $('#upload_user').val(),
+      upload_password: $('#upload_password').val()
+    };
+  },
+  focus: function() {
+    $('#upload_user').focus();
+  },
+  handleReject: function() {
+    $('#upload_user').val('');
+    $('#upload_password').val('');
+  },
+};
+
+$(document).ready(function() {
+  $('#upload-auth-submit').remove();
+  var submit = $('<input type="submit" />');
+  submit.attr('value', 'Login');
+  submit.attr('id', 'upload-auth-submit');
+  $('#upload-authentication').append(
+    $('<div class="field" />').append(
+      $('<div class="submit" />').append(
+        submit)));
+});
diff --git a/spec/coquelicot/auth/userpass_spec.rb b/spec/coquelicot/auth/userpass_spec.rb
new file mode 100644 (file)
index 0000000..74bcefe
--- /dev/null
@@ -0,0 +1,69 @@
+# -*- coding: UTF-8 -*-
+# Coquelicot: "one-click" file sharing with a focus on users' privacy.
+# Copyright © 2016 potager.org <jardiniers@potager.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+require 'spec_helper'
+require 'bcrypt'
+require 'coquelicot/auth/userpass'
+
+describe Coquelicot::Auth::UserpassAuthenticator do
+  include_context 'with Coquelicot::Application'
+
+  before(:each) do
+    app.set :authentication_method, :name => :userpass
+  end
+
+  def authenticate(params)
+    Coquelicot.settings.authenticator.authenticate(params)
+  end
+
+  describe '.authenticate' do
+    context 'when no credentials are configured' do
+      it 'should raise an error' do
+        expect { authenticate(:upload_user => 'user', :upload_password => 'password') }.to raise_error(Coquelicot::Auth::Error)
+      end
+    end
+
+    context 'when credentials are configured' do
+      before(:each) do
+        allow(Coquelicot.settings).to receive_messages(
+            :credentials => { 'ada' => BCrypt::Password.create('lovelace'),
+                              'emma' => BCrypt::Password.create('goldman') } ) 
+      end
+
+      it 'should return false if the login is empty' do
+        expect(authenticate(:upload_login => '', :upload_password => 'something')).to be_falsy
+      end
+
+      it 'should return false if the password is empty' do
+        expect(authenticate(:upload_login => 'something', :upload_password => '')).to be_falsy
+      end
+
+      it 'should return false if the user is unknown' do
+        expect(authenticate(:upload_login => 'random', :upload_password => 'password')).to be_falsy
+      end
+
+      it 'should return false if the password is wrong' do
+        expect(authenticate(:upload_login => 'ada', :upload_password => 'goldman')).to be_falsy
+      end
+
+      it 'should return false if the user and password are right' do
+        expect(authenticate(:upload_login => 'emma', :upload_password => 'goldman')).to be_falsy
+      end
+    end
+  end
+end
+
diff --git a/views/auth/userpass.haml b/views/auth/userpass.haml
new file mode 100644 (file)
index 0000000..dff0726
--- /dev/null
@@ -0,0 +1,24 @@
+-# -*- coding: UTF-8 -*-
+-# Coquelicot: "one-click" file sharing with a focus on users' privacy.
+-# Copyright © 2012-2016 potager.org <jardiniers@potager.org>
+-#           ©      2016 Rowan Thorpe <rowan@rowanthorpe.com>
+-#
+-# This program is free software: you can redistribute it and/or modify
+-# it under the terms of the GNU Affero General Public License as
+-# published by the Free Software Foundation, either version 3 of the
+-# License, or (at your option) any later version.
+-#
+-# This program is distributed in the hope that it will be useful,
+-# but WITHOUT ANY WARRANTY; without even the implied warranty of
+-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-# GNU Affero General Public License for more details.
+-#
+-# You should have received a copy of the GNU Affero General Public License
+-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+.field
+  %label{ :for => 'upload_user' } Upload user:
+  %input.input{ :type => 'text', :id => 'upload_user', :name => 'upload_user' }
+.field
+  %label{ :for => 'upload_password' } Upload password:
+  %input.input{ :type => 'password', :id => 'upload_password', :name => 'upload_password' }