From 20fef10787507031469ce0aea33ead2e9ca6b12f Mon Sep 17 00:00:00 2001
From: Ken Dreyer <ktdreyer@ktdreyer.com>
Date: Sat, 21 Apr 2012 22:43:33 -0600
Subject: [PATCH 2/2] implement Kerberos authentication

Add a new Gitorious::Authentication provider, KerberosAuthentication.
Instead of supporting username+password authentication, this module
checks the request.env HTTP request object from Rails.

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 (eg mod_auth_kerb, or mod_ssl, etc).

Add a new "Kerberos" option to the login form.

Add a 401 HTTP error page. The sample Apache configuration shows how to
configure this error page with mod_auth_kerb.

Add some basic tests for the KerberosAuthentication plugin.
---
 app/controllers/sessions_controller.rb             |   23 +++++
 app/views/sessions/new.html.erb                    |    7 ++
 config/authentication.sample.yml                   |   30 ++++++
 .../authentication/kerberos_authentication.rb      |   98 ++++++++++++++++++++
 public/401.html                                    |   51 ++++++++++
 .../authentication/kerberos_authentication_test.rb |   97 +++++++++++++++++++
 6 files changed, 306 insertions(+), 0 deletions(-)
 create mode 100644 lib/gitorious/authentication/kerberos_authentication.rb
 create mode 100644 public/401.html
 create mode 100644 test/unit/lib/gitorious/authentication/kerberos_authentication_test.rb

diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 1a9118d..06df555 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
@@ -104,6 +111,22 @@ class SessionsController < ApplicationController
     end
   end
 
+  # Single sign-on, via a web server's authentication mechanism.
+  def http_authentication
+    credentials = Gitorious::Authentication::Credentials.new
+    credentials.env = request.env
+    self.current_user = Gitorious::Authentication.authenticate(credentials)
+    if logged_in?
+      successful_login
+    else
+      # If the web server is protecting this sessions URL, you might end up
+      # with an HTTP 401 error, in which case you won't even get this far.
+      # Still, it could happen, if you do some further application-level checks
+      # within your Gitorious::Authentication module.
+      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..f4f93c8 100644
--- a/config/authentication.sample.yml
+++ b/config/authentication.sample.yml
@@ -75,5 +75,35 @@ 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
+      #    ErrorDocument 401 /401.html
+      #  </Location>
+    
+    # End Kerberos configuration example
+    ############################################################################
 
 # production:
diff --git a/lib/gitorious/authentication/kerberos_authentication.rb b/lib/gitorious/authentication/kerberos_authentication.rb
new file mode 100644
index 0000000..d67f53b
--- /dev/null
+++ b/lib/gitorious/authentication/kerberos_authentication.rb
@@ -0,0 +1,98 @@
+# 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(env)
+        # We could also check find_username_from_kerberos
+        # to ensure the user isn't using an admin principal.
+        return (env['HTTP_AUTHORIZATION'] =~ /Negotiate /)
+      end
+
+      # The HTTP authentication callback
+      def authenticate(credentials)
+        return false unless credentials.env && valid_kerberos_login(credentials.env)
+        username = find_username_from_kerberos(credentials.env)
+        Rails.logger.debug("Kerberos: REMOTE_USER '#{credentials.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.env HTTP object. See the above note about multi-realm and
+      # Gitorious username restrictions.
+      def find_username_from_kerberos(env)
+        # strip off the realm.
+        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 = username + '@' + @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
diff --git a/public/401.html b/public/401.html
new file mode 100644
index 0000000..5529f76
--- /dev/null
+++ b/public/401.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="chrome=1">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>401 Unauthorized - Gitorious</title>
+    <link href="/stylesheets/gts-common.css" media="screen" rel="stylesheet" type="text/css">
+    <script src="/javascripts/lib/all.js" type="text/javascript"></script>
+    <!--[if IE 8]><link rel="stylesheet" href="/stylesheets/ie8.css" type="text/css"><![endif]-->
+    <!--[if IE 7]><link rel="stylesheet" href="/stylesheets/ie7.css" type="text/css"><![endif]-->
+  </head>
+  <body>
+    <div id="wrapper">
+      <div id="header">
+        <h1 id="logo"><a href="/"><img alt="Logo" src="/img/logo.png?1299242754" /></a></h1>
+      </div>
+      <div id="top-bar"></div>
+      <div id="container">
+        <div id="content">
+          <h1>Browser authentication failed</h1>
+          <p>
+            Gitorious could not verify your browser credentials.
+          </p>
+          <p>
+            If you are using Kerberos authentication, please make sure that you have valid Kerberos tickets, and that you have configured your browser correctly.
+          </p>
+        </div>
+      </div>
+      <div id="footer">
+        <div class="powered-by">
+  	  <a href="http://gitorious.org">
+            <img alt="Powered by Gitorious" src="/img/poweredby.png?1299242754"></a>
+        </div>
+	<div id="footer-links">
+	  <h3>Gitorious</h3>
+	  <ul>
+	    <li><a href="/">Home</a></li>
+	    <li><a href="/about">About Gitorious</a></li>
+	    <li><a href="/about/faq">FAQ</a></li>
+	    <li><a href="/contact">Contact</a></li>
+	  </ul>
+	  <ul>
+	    <li><a href="http://groups.google.com/group/gitorious">Discussion group</a></li>
+	    <li><a href="http://blog.gitorious.org">Blog</a></li>
+	  </ul>
+	</div>
+        <div class="clear"></div>
+      </div>
+    </div>
+  </body>
+</html>
diff --git a/test/unit/lib/gitorious/authentication/kerberos_authentication_test.rb b/test/unit/lib/gitorious/authentication/kerberos_authentication_test.rb
new file mode 100644
index 0000000..17dc444
--- /dev/null
+++ b/test/unit/lib/gitorious/authentication/kerberos_authentication_test.rb
@@ -0,0 +1,97 @@
+# 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/>.
+#++
+
+require "test_helper"
+class Gitorious::Authentication::KerberosAuthenticationTest < ActiveSupport::TestCase
+
+  # Accepts a Kerberos principal string, and returns a 
+  # Gitorious::Authentication::Credentials object
+  def valid_client_credentials(principal)
+    # construct a simple Rails request.env hash
+    env = Hash.new
+    env['HTTP_AUTHORIZATION'] = 'Negotiate ABCDEF123456'
+    env['REMOTE_USER'] = principal
+    # Wrap this in the G::A::Credentials object.
+    credentials = Gitorious::Authentication::Credentials.new
+    credentials.env = env
+    credentials
+  end
+
+
+  context "Configuration" do
+    setup do
+      @kerberos = Gitorious::Authentication::KerberosAuthentication.new({
+          "realm" => "EXAMPLE.COM",
+        })
+    end
+
+    should "require a realm" do
+      assert_raises Gitorious::Authentication::ConfigurationError do
+        kerberos = Gitorious::Authentication::KerberosAuthentication.new({})
+      end
+    end
+
+    should "use a default email domain" do
+      assert_equal "example.com", @kerberos.email_domain
+    end
+  end
+
+  context "Authentication" do
+    setup do
+      @kerberos = Gitorious::Authentication::KerberosAuthentication.new({
+          "realm" => "EXAMPLE.COM",
+        })
+    end
+
+    should "not accept invalid credentials" do
+      # Pass in an empty hash, to simulate the missing environment variables.
+      assert !@kerberos.valid_kerberos_login({})
+    end
+
+    should "accept valid credentials" do
+      env = Hash['HTTP_AUTHORIZATION' => 'Negotiate ABCDEF123456']
+      assert @kerberos.valid_kerberos_login(env)
+    end
+
+    should "return the actual user" do
+      assert_equal(users(:moe), @kerberos.authenticate(valid_client_credentials("moe@EXAMPLE.COM")))
+    end
+  end
+
+  context "Auto-registration" do
+    setup do
+      @kerberos = Gitorious::Authentication::KerberosAuthentication.new({
+          "realm" => "EXAMPLE.COM",
+        })
+    end
+
+    should "create a new user with attributes mapped from Kerberos" do
+      user = @kerberos.authenticate(valid_client_credentials("moe.szyslak@EXAMPLE.COM"))
+      assert_equal "moe.szyslak@example.com", user.email
+      assert_equal "moe-szyslak", user.login
+
+      assert user.valid?
+    end
+
+    should "transform user's login to not contain dots" do
+      user = @kerberos.authenticate(valid_client_credentials("mr.moe.szyslak@EXAMPLE.COM"))
+
+      assert_equal "mr-moe-szyslak", user.login
+    end
+  end
+end
-- 
1.7.1

