introduce authentication mechanism framework
authormh <mh@immerda.ch>
Mon, 2 May 2011 10:31:37 +0000 (12:31 +0200)
committermh <mh@immerda.ch>
Mon, 2 May 2011 11:22:57 +0000 (13:22 +0200)
We can now implement additional authentication methods, to
authenticate against other authentication mechanisms.

The requirements for these authentication mechanisms and how they work
is documented in the README file.

README
coquelicot_app.rb
lib/coquelicot/auth/simplepass.rb [new file with mode: 0644]
public/javascripts/coquelicot.auth.simplepass.js [new file with mode: 0644]
public/javascripts/coquelicot.js
test_coquelicot.rb
views/auth/simplepass.haml [new file with mode: 0644]
views/index.haml

diff --git a/README b/README
index a97d084..de23023 100644 (file)
--- a/README
+++ b/README
@@ -14,8 +14,14 @@ not so active attackers.
 
 Features
 --------
+ * Support for different authentication methods
 
- * Uploading a file is protected by a common password
+   It is possible to integrate your own authentication mechanisms. Such a 
+   mechanism, needs to implement a single method and provide some JS, as well 
+   as some template partial to render the common fields. For more information
+   have a look at the notes below.
+
+ * Simplepass mechanism: Uploading a file is protected by a common password
 
    In order to prevent random Internet users to eat bandwidth and disk
    space, uploading a file for sharing is protected by a common
@@ -132,11 +138,11 @@ Coquelicot provides a migration script to import Jyraphe 0.5 repositories in
 Future
 ------
 
- * Integrate other authentication systems for uploads
+ * Read settings from a configuration file.
 
-   A common password is a pretty limited authentication scheme.
-   One could like to also configure no password or integrate with
-   webmails or other authentication system.
+   Currently settings, such as the authentication mechanism and its options
+   need to be specified within the application. Coquelicot should load its
+   settings from a configuration file.
 
  * More flexible expiration
 
@@ -201,6 +207,44 @@ is used. It contains a line for each file in the form:
 
     <URL name> <file name>
 
+Authentication Mechanisms
+-------------------------
+
+It is possible to authenticate users against your own common authentication
+mechanism.
+
+Such an authentication mechanism needs to provide the following 3 files:
+    
+* lib/coquelicot/auth/METHOD.rb
+* public/javascripts/coquelicot.auth.METHOD.js
+* views/auth/METHOD.haml
+    
+Their responsibilities are as followed:
+    
+lib/coquelicot/auth/METHOD.rb:
+    
+A module implementing the actual authentication. This module must
+implement one method called `authenticate` which will get all the
+parameters as an argument. To simplify your interaction with the field 
+`upload_token`, that might be serialized as json, we deserialize it prior
+to passing it to the `authenticate` method.
+    
+public/javascripts/coquelicot.auth.METHOD.js:
+    
+We expect 2 javascript methods in that file:
+    
+* authenticationData(): Return a hash of all the necessary data to
+                        authenticate on the app side.
+* authenticationFocus(): Set the focus on the first authentication form
+                         field
+    
+views/auth/METHOD.haml:
+    
+Render the necessary form fields that will be used for authentication.
+    
+The authentication method can be set in the application settings
+including mandatory options for this method.
+
 Authors
 -------
 
index f1c00ae..24e6e44 100644 (file)
@@ -49,8 +49,14 @@ module Coquelicot
   end
 
   class Application < Sinatra::Base
+    def self.authentication_method(method,options={})
+      require "coquelicot/auth/#{method}"
+      set :auth_method, method
+      include (eval "Coquelicot::Auth::#{method.to_s.capitalize}")
+      options.each{|k,v| set k,v }
+    end
+
     set :app_file, __FILE__
-    set :upload_password, '0e5f7d398e6f9cd1f6bac5cc823e363aec636495'
     set :default_expire, 60
     set :maximum_expire, 60 * 24 * 30 # 1 month
     set :gone_period, 10080
@@ -58,10 +64,7 @@ module Coquelicot
     set :random_pass_length, 16
     set :depot_path, Proc.new { File.join(root, 'files') }
 
-    def password_match?(password)
-      return TRUE if settings.upload_password.nil?
-      (not password.nil?) && Digest::SHA1.hexdigest(password) == settings.upload_password
-    end
+    authentication_method :simplepass, :upload_password => 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
 
     GetText::bindtextdomain('coquelicot')
     before do
@@ -101,14 +104,16 @@ module Coquelicot
 
     post '/authenticate' do
       pass unless request.xhr?
-      unless password_match? params[:upload_password] then
+      unless authenticate(params) then
         error 403, "Forbidden"
       end
       'OK'
     end
 
     post '/upload' do
-      unless password_match? params[:upload_password] then
+      # if JS is disabled upload_token might be nil
+      params['upload_token'] = JSON.parse(params['upload_token']) unless params['upload_token'].nil?
+      unless authenticate(params) then
         error 403
       end
       if params[:file] then
@@ -211,6 +216,10 @@ module Coquelicot
         url << request.script_name
         "#{url}/"
       end
+
+      def auth_method
+        Coquelicot.settings.auth_method
+      end
     end
   end
 end
diff --git a/lib/coquelicot/auth/simplepass.rb b/lib/coquelicot/auth/simplepass.rb
new file mode 100644 (file)
index 0000000..1a31284
--- /dev/null
@@ -0,0 +1,12 @@
+module Coquelicot
+  module Auth
+    module Simplepass
+
+      def authenticate(params)
+        return TRUE if settings.upload_password.nil?
+        upload_password = params['upload_token'].is_a?(Hash) ? params['upload_token']['upload_password'] : params['upload_password']
+        (not upload_password.nil?) && Digest::SHA1.hexdigest(upload_password) == settings.upload_password
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/public/javascripts/coquelicot.auth.simplepass.js b/public/javascripts/coquelicot.auth.simplepass.js
new file mode 100644 (file)
index 0000000..27861ea
--- /dev/null
@@ -0,0 +1,9 @@
+function authenticationData(){
+       return {
+               upload_password: $('#upload_password').val()
+       };
+}
+
+function authenticationFocus(){
+       $('#upload_password').focus();
+}
index 606825c..b5d103e 100644 (file)
@@ -51,16 +51,17 @@ function authenticate() {
   var authDiv = $('#upload-authentication').remove();
   var lb = $.lightBoxFu;
   authForm.bind('submit', function() {
-    var uploadPassword = $('#upload_password');
     jQuery.ajax({
       type: 'POST',
       url: 'authenticate',
       dataType: 'text',
-      data: { upload_password: uploadPassword.val() },
+      data: {
+        'upload_token': authenticationData.call()
+      },
       complete: function(res, status) {
         if (status === 'success') {
-          var hiddenField = $('<input type="hidden" name="upload_password" />');
-          hiddenField.val(uploadPassword.val());
+          var hiddenField = $('<input type="hidden" name="upload_token" />');
+          hiddenField.val(JSON.stringify(authenticationData.call()));
           $('#upload').append(hiddenField);
           lb.close();
         } else if (res.responseText == 'Forbidden') {
@@ -68,7 +69,7 @@ function authenticate() {
         } else {
           $('#auth-message').text(i18n.error + alert(status));
         }
-        uploadPassword.val('');
+        authenticationReset();
       }
     });
     return false;
@@ -78,5 +79,9 @@ function authenticate() {
     width: "300px",
     closeOnClick: false
   });
-  $('#upload_password').focus();
+  authenticationFocus();
+}
+
+function authenticationReset(){
+    $('#upload_token').val('');
 }
index 3cf54d6..c08ad3e 100644 (file)
@@ -16,7 +16,7 @@ describe 'Coquelicot' do
 
   def upload(opts={})
     opts = { :file => Rack::Test::UploadedFile.new(__FILE__, 'text/x-script.ruby'),
-             :upload_password => UPLOAD_PASSWORD
+             :upload_token => JSON.dump({ 'upload_password' => UPLOAD_PASSWORD})
            }.merge(opts)
     post '/upload', opts
     return nil unless last_response.redirect?
@@ -29,7 +29,7 @@ describe 'Coquelicot' do
 
   before do
     app.set :environment, :test
-    app.set :upload_password, Digest::SHA1.hexdigest(UPLOAD_PASSWORD)
+    app.authentication_method :simplepass, :upload_password => Digest::SHA1.hexdigest(UPLOAD_PASSWORD)
     app.set :depot_path, Dir.mktmpdir('coquelicot')
   end
 
@@ -114,26 +114,26 @@ describe 'Coquelicot' do
   end
 
   it "should prevent upload without a password" do
-    url = upload :upload_password => ''
+    url = upload :upload_token => JSON.dump({'upload_password' => ''})
     url.should be_nil
     last_response.status.should eql(403)
   end
 
   it "should prevent upload with a wrong password" do
-    url = upload :upload_password => "bad"
+    url = upload :upload_token => JSON.dump({'upload_password' => 'bad'})
     url.should be_nil
     last_response.status.should eql(403)
   end
 
   it "should allow AJAX upload password verification" do
     request "/authenticate", :method => "POST", :xhr => true,
-                             :params => { :upload_password => UPLOAD_PASSWORD }
+                             :params => { :upload_token => { 'upload_password' => UPLOAD_PASSWORD } }
     last_response.should be_ok
     request "/authenticate", :method => "POST", :xhr => true,
-                             :params => { :upload_password => '' }
+                             :params => { :upload_token => '{}' }
     last_response.status.should eql(403)
     request "/authenticate", :method => "POST", :xhr => true,
-                             :params => { :upload_password => 'wrong' }
+                             :params => { :upload_token => JSON.dump({'upload_password' => 'wrong'}) }
     last_response.status.should eql(403)
   end
 
diff --git a/views/auth/simplepass.haml b/views/auth/simplepass.haml
new file mode 100644 (file)
index 0000000..9d24fbe
--- /dev/null
@@ -0,0 +1,3 @@
+.field
+      %label{ :for => 'upload_password' } Upload password:
+      %input.input{ :type => 'password', :id => 'upload_password', :name => 'upload_password' }
\ No newline at end of file
index eba177b..57f9671 100644 (file)
@@ -6,9 +6,8 @@
 %form#upload{ :enctype => 'multipart/form-data',
               :action  => 'upload', :method => 'post' }
   #upload-authentication
-    .field
-      %label{ :for => 'upload_password' } Upload password:
-      %input.input{ :type => 'password', :id => 'upload_password', :name => 'upload_password' }
+    %script{ :type => 'text/javascript', :src => "javascripts/coquelicot.auth.#{auth_method}.js" }
+    = render :haml, :"auth/#{auth_method}", :layout => false
   .field
     %label{ :for => 'file' } File:
     %input.input{ :type => 'file', :id => 'file', :name => 'file' }