From 7b32a40a5ae6a8425e4dcd274e832a0a9a8c7dbd Mon Sep 17 00:00:00 2001
From: Ken Dreyer <ktdreyer@ktdreyer.com>
Date: Thu, 12 Apr 2012 16:10:58 -0600
Subject: [PATCH] implement Kerberos authentication

Add a new Gitorious::Authentication provider, KerberosAuthentication.
Instead of supporting username+password authentication, this module
supports a new Gitorious::Authentication interface:
"authenticate_http(request)". This allows us to pass in the HTTP request
object from Rails. I intend this interface to be robust enough to
support other HTTP authentication mechanisms beyond Kerberos (for
example, SSL client certs with mod_ssl).

Add a "authenticate_http" method to the main Gitorious::Authentication
module, to support authentication modules that do not use a
username+password combination.

mod_auth_kerb does not have a "pass-through" mechanism. If you fail
Kerberos auth, then mod_auth_kerb will display a 401 error. This means
that we must use mod_auth_kerb on a URL that will not interfere with
other password-based authentication mechanisms. Add a new "http" action
to the sessions controller to satisfy this requirement. I intend this to
be a general interface for any web server-integrated authentication
mechanism.

Add a new "Kerberos" option to the login form.
---
 app/controllers/sessions_controller.rb             |   17 +++
 app/views/sessions/new.html.erb                    |    7 ++
 config/authentication.sample.yml                   |   29 ++++++
 lib/gitorious/authentication.rb                    |   17 +++
 .../authentication/kerberos_authentication.rb      |  104 ++++++++++++++++++++
 5 files changed, 174 insertions(+), 0 deletions(-)
 create mode 100644 lib/gitorious/authentication/kerberos_authentication.rb

diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index e8abe58..7dafb16 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -43,6 +43,13 @@ class SessionsController < ApplicationController
     end
   end
 
+  # This controller action should be protected behind a web server
+  # single sign-on authentication mechanism, like Apache's mod_auth_kerb
+  # or mod_ssl.
+  def http
+    http_authentication
+  end
+
   def destroy
     self.current_user.forget_me if logged_in?
     cookies.delete :auth_token
@@ -101,6 +108,16 @@ class SessionsController < ApplicationController
     end
   end
 
+  # Single sign-on, via a web server's authentication mechanism.
+  def http_authentication
+    self.current_user = Gitorious::Authentication.authenticate_http(request)
+    if logged_in?
+      successful_login
+    else
+      failed_login "Gitorious could not verify your browser's credentials.", 'http'
+    end
+  end
+
   def failed_login(message = "Authentication failed.",method="")
     if method==""
       flash.now[:error] = message
diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb
index 7f4adde..ff031de 100644
--- a/app/views/sessions/new.html.erb
+++ b/app/views/sessions/new.html.erb
@@ -30,6 +30,13 @@
         <h1>Log in to <%= GitoriousConfig["site_name"] %></h1>
         <p class="openid-switch">Or log in with <a href="#" class="foo1">OpenID</a>.</p>
         <p class="regular-switch" style="display: none">Or log in with your <a href="#" class="foo2">regular account</a>.</p>
+        <% kerberos = Gitorious::Authentication::Configuration.authentication_methods.detect do |m|
+             Gitorious::Authentication::KerberosAuthentication === m
+           end
+          if kerberos
+        %>
+        <p>Or log in with <%= link_to 'Kerberos', :controller => "sessions", :action=> "http" %> (single sign-on).</p>
+        <% end %>
         <div class="horisontal-shadow"></div>
         <% if flash[:error] %>
           <div class="error">
diff --git a/config/authentication.sample.yml b/config/authentication.sample.yml
index aa6b807..8383294 100644
--- a/config/authentication.sample.yml
+++ b/config/authentication.sample.yml
@@ -75,5 +75,34 @@ development:
 
     # End Crowd configuration example
     ############################################################################
+    
+    ############################################################################
+    # Example of configuring Kerberos authentication
+    #- adapter: Gitorious::Authentication::KerberosAuthentication
+    
+      # Set the Kerberos realm (should be uppercase)
+      #realm: EXAMPLE.COM
+      
+      # The default email domain for users in this realm. If you do not
+      # specify any email_domain, the default is to use the lowercase
+      # realm value.
+      #email_domain: example.com
+      
+      # Note that you must also set up Apache's mod_auth_kerb within
+      # httpd.conf. For example:
+      #  # Enable SSO authentication via Kerberos
+      #  <Location /sessions/http>
+      #    AuthType Kerberos
+      #    AuthName "Gitorious Web UI"
+      #    KrbMethodNegotiate on
+      #    KrbMethodK5Passwd off
+      #    KrbServiceName HTTP
+      #    KrbAuthRealm EXAMPLE.COM
+      #    Krb5Keytab /etc/httpd/http.keytab
+      #    Require valid-user
+      #  </Location>
+    
+    # End Kerberos configuration example
+    ############################################################################
 
 # production:
diff --git a/lib/gitorious/authentication.rb b/lib/gitorious/authentication.rb
index 23207d2..42acb04 100644
--- a/lib/gitorious/authentication.rb
+++ b/lib/gitorious/authentication.rb
@@ -28,5 +28,22 @@ module Gitorious
       end
       return nil
     end
+
+    # Use an alternate type of Gitorious::Authentication that depends on
+    # underlying auth mechanisms in HTTP, such as SSL or Kerberos.
+    # Instead of passing in a username and password, we will pass in the
+    # http request object.
+    # Like authenticate() above, return a User object.
+    def self.authenticate_http(request)
+      Configuration.authentication_methods.each do |authenticator|
+	if authenticator.respond_to? "authenticate_http"
+	  if result = authenticator.authenticate_http(request)
+	    return result
+	  end
+	end
+      end
+      return nil
+    end
+
   end
 end
diff --git a/lib/gitorious/authentication/kerberos_authentication.rb b/lib/gitorious/authentication/kerberos_authentication.rb
new file mode 100644
index 0000000..0419956
--- /dev/null
+++ b/lib/gitorious/authentication/kerberos_authentication.rb
@@ -0,0 +1,104 @@
+# encoding: utf-8
+#--
+#   Copyright (C) 2011 Gitorious AS
+#
+#   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/>.
+#++
+module Gitorious
+  module Authentication
+    class KerberosAuthentication
+      attr_reader(:realm, :email_domain)
+
+      def initialize(options)
+        validate_requirements(options)
+        setup_attributes(options)
+      end
+
+      def validate_requirements(options)
+        # Multi-realm auth is not possible, because we could have username
+        # collisions in the user database. It will be possible when Gitorious
+        # supports "@" signs in usernames. For now you can only authenticate
+        # users from a single Kerberos relam.
+        raise ConfigurationError, "Kerberos Realm required" unless options.key?("realm")
+      end
+
+      def setup_attributes(options)
+        @realm = options["realm"]
+        @email_domain = options["email_domain"] || options["realm"].downcase
+      end
+
+      # Check if this HTTP user logged in with Kerberos, or not.
+      # Apache's mod_auth_kerb will set this environment variable.
+      # If the login was unsuccesful, we'll never get this far because
+      # mod_auth_kerb will return a 401 error to the browser.
+      def valid_kerberos_login(request)
+        # We could also check find_username_from_kerberos
+        # to ensure the user isn't using an admin principal.
+        return (request.env['HTTP_AUTHORIZATION'] =~ /Negotiate /)
+      end
+
+      # The username/password authentication callback
+      # This is a no-op with Kerberos. Use authenticate_http instead.
+      def authenticate(username, password)
+        return nil 
+      end
+
+      # The HTTP authentication callback
+      def authenticate_http(request)
+        return false unless valid_kerberos_login(request)
+        username = find_username_from_kerberos(request)
+        Rails.logger.debug("Kerberos: REMOTE_USER '#{request.env['REMOTE_USER']}'.")
+        Rails.logger.debug("Kerberos: found username '#{username}'.")
+        if existing_user = User.find_by_login(transform_username(username))
+          user = existing_user
+        else
+          user = auto_register(username)
+        end
+        user
+      end
+      
+      # Find the Gitorious username from a Kerberos principal in the request
+      # HTTP object. See the above note about multi-realm and Gitorious
+      # username restrictions.
+      def find_username_from_kerberos(request)
+        # strip off the realm.
+        request.env['REMOTE_USER'].gsub("@#{@realm}", '')
+      end
+
+      # Transform a Kerberos username into something that passes Gitorious'
+      # username validations (like the LDAPAuthentication module does).
+      def transform_username(username)
+        username.gsub(".", "-")
+      end
+
+      def auto_register(username)
+        user = User.new
+        user.login = transform_username(username)
+        user.email = user.login + '@' + @email_domain
+        Rails.logger.debug("Kerberos: username after transform_username: '#{user.login}'.")
+        Rails.logger.debug("Kerberos: email '#{user.email}'.")
+
+        # Again, similar to LDAPAuthentication's implementation
+        user.password = "left_blank"
+        user.password_confirmation = "left_blank"
+        user.terms_of_use = '1'
+        user.aasm_state = "terms_accepted"
+        user.activated_at = Time.now.utc
+        user.save!
+        user
+      end
+
+    end
+  end
+end
-- 
1.7.1

