This is the initial implementation of login via OpenID
authorKai Krueger <kakrueger@gmail.com>
Sun, 10 Jan 2010 17:41:32 +0000 (17:41 +0000)
committerKai Krueger <kakrueger@gmail.com>
Sun, 10 Jan 2010 17:41:32 +0000 (17:41 +0000)
This is the patch presented in trac ticket #2500

With this commit, it is possible to assosciate an openID
to an existing OSM account. Once associated, it is possible
to either login via OpenID, or as always via username and password.

Other aspects, such as account creation and the need for a valid email
are unchanged.

app/controllers/user_controller.rb
app/models/user.rb
app/views/user/account.html.erb
app/views/user/login.html.erb
app/views/user/new.html.erb
config/environment.rb
config/locales/en.yml
db/migrate/049_add_open_id_authentication_tables.rb [new file with mode: 0644]

index ca84d770199b347dc88a9f7555d30b25c4f6cce3..e661aa1e253777901f21c53927f9470b636be1f7 100644 (file)
@@ -44,6 +44,14 @@ class UserController < ApplicationController
     @title = t 'user.account.title'
     @tokens = @user.oauth_tokens.find :all, :conditions => 'oauth_tokens.invalidated_at is null and oauth_tokens.authorized_at is not null'
 
+    #The redirect from the OpenID provider reenters here again
+    #and we need to pass the parameters through to the 
+    #open_id_authentication function
+    if params[:open_id_complete]
+      openid_verify('')
+      return
+    end
+
     if params[:user] and params[:user][:display_name] and params[:user][:description]
       if params[:user][:email] != @user.email
         @user.new_email = params[:user][:email]
@@ -55,6 +63,10 @@ class UserController < ApplicationController
         @user.pass_crypt = params[:user][:pass_crypt]
         @user.pass_crypt_confirmation = params[:user][:pass_crypt_confirmation]
       end
+      if (params[:user][:openid_url].length == 0)
+        #Clearing doesn't need OpenID validation, so we can set it here.
+        @user.openid_url = nil
+      end
 
       @user.description = params[:user][:description]
       @user.languages = params[:user][:languages].split(",")
@@ -71,9 +83,64 @@ class UserController < ApplicationController
           flash.now[:notice] = t 'user.account.flash update success'
         end
       end
+
+      if (params[:user][:openid_url].length > 0)
+       begin
+         @norm_openid_url = OpenIdAuthentication.normalize_identifier(params[:user][:openid_url])
+         if (@norm_openid_url != @user.openid_url)
+           #If the OpenID has changed, we want to check that it is a valid OpenID and one
+           #the user has control over before saving the openID as a password equivalent for
+           #the user.
+           openid_verify(@norm_openid_url)
+         end
+       rescue
+         flash.now[:error] = t 'user.login.openid invalid'
+       end
+      end
+    end
+  end
+
+  def openid_specialcase_mapping(openid_url)
+    #Special case gmail.com, as it is pontentially a popular OpenID provider and unlike
+    #yahoo.com, where it works automatically, Google have hidden their OpenID endpoint
+    #somewhere obscure making it less userfriendly.
+    if (openid_url.match(/(.*)gmail.com(\/?)$/) or openid_url.match(/(.*)googlemail.com(\/?)$/) )
+      return 'https://www.google.com/accounts/o8/id'
+    end
+
+    return nil
+  end  
+
+  def openid_verify(openid_url)
+    authenticate_with_open_id(openid_url) do |result, identity_url|
+      if result.successful?
+        #We need to use the openid url passed back from the OpenID provider
+        #rather than the one supplied by the user, as these can be different.
+        #e.g. one can simply enter yahoo.com in the login box, i.e. no user specific url
+        #only once it comes back from the OpenID provider do we know the unique address for
+        #the user.
+        @user.openid_url = identity_url
+        if @user.save
+          flash.now[:notice] = t 'user.account.flash update success'
+        end
+      else if result.missing?
+             mapped_id = openid_specialcase_mapping(openid_url)
+             if mapped_id
+               openid_verify(mapped_id)
+             else
+               flash.now[:error] = t 'user.login.openid missing provider'
+             end
+           else if result.invalid?
+                  flash.now[:error] = t 'user.login.openid invalid'
+                else
+                  flash.now[:error] = t 'user.login.auth failure'
+                end
+           end
+      end
     end
   end
 
+
   def set_home
     if params[:user][:home_lat] and params[:user][:home_lon]
       @user.home_lat = params[:user][:home_lat].to_f
@@ -142,22 +209,39 @@ class UserController < ApplicationController
     # The user is logged in already, so don't show them the signup page, instead
     # send them to the home page
     redirect_to :controller => 'site', :action => 'index' if session[:user]
+
+    @nickname = params['nickname']
+    @email = params['email']
   end
 
   def login
+
+    #The redirect from the OpenID provider reenters here again
+    #and we need to pass the parameters through to the 
+    # open_id_authentication function
+    if params[:open_id_complete]
+      open_id_authentication('')
+    end
+
+    
     if params[:user] and session[:user].nil?
-      email_or_display_name = params[:user][:email]
-      pass = params[:user][:password]
-      user = User.authenticate(:username => email_or_display_name, :password => pass)
-      if user
-        session[:user] = user.id
-      elsif User.authenticate(:username => email_or_display_name, :password => pass, :inactive => true)
-        flash.now[:error] = t 'user.login.account not active'
+
+      if !params[:user][:openid_url].empty?
+        open_id_authentication(params[:user][:openid_url])
       else
-        flash.now[:error] = t 'user.login.auth failure'
+        email_or_display_name = params[:user][:email]
+        pass = params[:user][:password]
+        user = User.authenticate(:username => email_or_display_name, :password => pass)
+        if user
+          session[:user] = user.id
+        elsif User.authenticate(:username => email_or_display_name, :password => pass, :inactive => true)
+          flash.now[:error] = t 'user.login.account not active'
+        else
+          flash.now[:error] = t 'user.login.auth failure'
+        end
       end
     end
-
+  
     if session[:user]
       # The user is logged in, if the referer param exists, redirect them to that
       # unless they've also got a block on them, in which case redirect them to
@@ -177,6 +261,51 @@ class UserController < ApplicationController
     @title = t 'user.login.title'
   end
 
+  def open_id_authentication(openid_url)
+    #TODO: only ask for nickname and email, if we don't already have a user for that openID, in which case
+    #email and nickname are already filled out. I don't know how to do that with ruby syntax though, as we
+    #don't want to duplicate the do block
+    #On the other hand it also doesn't matter too much if we ask every time, as the OpenID provider should
+    #remember these results, and shouldn't repromt the user for these data each time.
+    authenticate_with_open_id(openid_url, :return_to => request.protocol + request.host_with_port + '/login?referer=' + params[:referer], :optional => [:nickname, :email]) do |result, identity_url, registration|
+      if result.successful?
+        #We need to use the openid url passed back from the OpenID provider
+        #rather than the one supplied by the user, as these can be different.
+        #e.g. one can simply enter yahoo.com in the login box, i.e. no user specific url
+        #only once it comes back from the OpenID provider do we know the unique address for
+        #the user.
+        user = User.find_by_openid_url(identity_url)
+        if user
+          if user.visible? and user.active?
+            session[:user] = user.id
+          else
+            user = nil
+            flash.now[:error] = t 'user.login.account not active'
+          end
+        else
+          #We don't have a user registered to this OpenID. Redirect to the create account page
+          #with username and email filled in if they have been given by the OpenID provider through
+          #the simple registration protocol
+          redirect_to :controller => 'user', :action => 'new', :nickname => registration['nickname'], :email => registration['email']
+        end
+      else if result.missing?
+             #Try and apply some heuristics to make common cases more userfriendly
+             mapped_id = openid_specialcase_mapping(openid_url)
+             if mapped_id
+               open_id_authentication(mapped_id)
+             else
+               flash.now[:error] = t 'user.login.openid missing provider'
+             end
+           else if result.invalid?
+                  flash.now[:error] = t 'user.login.openid invalid'
+                else
+                  flash.now[:error] = t 'user.login.auth failure'
+                end
+           end
+      end
+    end
+  end
+
   def logout
     if session[:token]
       token = UserToken.find_by_token(session[:token])
index 54b3fa371c4acc3674a0a303dea79d93a9ffa02b..eb69dc03148107bc4bd8092382db1b5634957135 100644 (file)
@@ -22,6 +22,7 @@ class User < ActiveRecord::Base
   validates_confirmation_of :pass_crypt#, :message => ' must match the confirmation password'
   validates_uniqueness_of :display_name, :allow_nil => true
   validates_uniqueness_of :email
+  validates_uniqueness_of :openid_url, :allow_nil => true
   validates_length_of :pass_crypt, :within => 8..255
   validates_length_of :display_name, :within => 3..255, :allow_nil => true
   validates_length_of :email, :within => 6..255
index ae00f7b34c8cb2a61d002b5255a8be89536fef9a..99d0f216e9fe1a675b414fcda1ed584ac2c184f4 100644 (file)
@@ -5,7 +5,8 @@
   <tr><td class="fieldName"><%= t 'user.new.display name' %></td><td><%= f.text_field :display_name %></td></tr>
   <tr><td class="fieldName"><%= t 'user.new.email address' %></td><td><%= f.text_field :email, {:size => 50, :maxlength => 255} %> <span class="minorNote"><%= t 'user.account.email never displayed publicly' %></span></td></tr>
   <tr><td class="fieldName" style="padding-bottom:0px;"><%= t 'user.new.password' %></td><td style="padding-bottom:0px;"><%= f.password_field :pass_crypt, {:value => '', :size => 30, :maxlength => 255} %></td></tr>
-  <tr><td class="fieldName"><%= t 'user.new.confirm password' %></td><td><%= f.password_field :pass_crypt_confirmation, {:value => '', :size => 30, :maxlength => 255} %></td></tr>
+  <tr><td class="fieldName" style="padding-bottom:0px;"><%= t 'user.new.confirm password' %></td><td><%= f.password_field :pass_crypt_confirmation, {:value => '', :size => 30, :maxlength => 255} %></td></tr>
+  <tr><td class="fieldName" ><%= t 'user.account.openid.openid' %></td><td style="padding-bottom:0px;"><%= f.text_field :openid_url %> (<a href="<%= t 'user.account.openid.link' %>" target="_new"><%= t 'user.account.openid.link text' %></a>) </td></tr>
 
   <tr>
   <td class="fieldName" valign="top"><%= t 'user.account.public editing.heading' %></td>
index 19e1f21e143333ce75e30920efbe98dffe95092a..16d820b69c48144421900d37806b0143f35371cf 100644 (file)
@@ -1,4 +1,4 @@
-<h1><%= t 'user.login.heading' %></h1>
+       <h1><%= t 'user.login.heading' %></h1>
 
 <p><%= t 'user.login.please login', :create_user_link => link_to(t('user.login.create_account'), :controller => 'user', :action => 'new', :referer => params[:referer]) %></p>
 
@@ -7,7 +7,15 @@
 <table id="loginForm">
   <tr><td class="fieldName"><%= t 'user.login.email or username' %></td><td><%= text_field('user', 'email',{:size => 28, :maxlength => 255, :tabindex => 1}) %></td></tr>
   <tr><td class="fieldName"><%= t 'user.login.password' %></td><td><%= password_field('user', 'password',{:size => 28, :maxlength => 255, :tabindex => 2}) %> <span class="minorNote">(<%= link_to t('user.login.lost password link'), :controller => 'user', :action => 'lost_password' %>)</span></td></tr>
-  <tr><td colspan="2">&nbsp;<!--vertical spacer--></td></tr>
-  <tr><td></td><td align="right"><%= submit_tag t('user.login.login_button'), :tabindex => 3 %></td></tr>
+<tr><td colspan="2">&nbsp;<!--vertical spacer--></td></tr>
+<tr><td></td><td align="right"><%= submit_tag t('user.login.login_button'), :tabindex => 3 %></td></tr>
+<tr><td colspan = "3"><h4><%= t 'user.login.alternatively' %></h4></td></tr>
+
+
+<tr><td colspan="2"><%= t 'user.login.openid description' %> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(<a href="<%= t 'user.account.openid.link' %>" target="_new"><%= t 'user.account.openid.link text' %></a>)</td></tr>
+
+<tr><td class="fieldName"><%= t 'user.login.openid' %></td><td><%= text_field('user', 'openid_url',{:size => 28, :maxlength => 255, :tabindex => 3}) %></td></tr>
+<tr><td colspan="2">&nbsp;<!--vertical spacer--></td></tr>
+<tr><td></td><td align="right"><%= submit_tag t('user.login.login_button'), :tabindex => 3 %></td></tr>
 </table>
 <% end %>
index 88a1d7bf9e3508fb1e6c5b3ee3b887a3ea0d64a9..b029e101ef57ea4187c7b09aafaeed4ec67594ba 100644 (file)
 <% form_tag :action => 'save' do %>
 <%= hidden_field_tag('referer', h(params[:referer])) unless params[:referer].nil? %>
 <table id="signupForm">
-  <tr><td class="fieldName"><%= t 'user.new.email address' %></td><td><%= text_field('user', 'email',{:size => 50, :maxlength => 255, :tabindex => 1}) %></td></tr>
+  <tr><td class="fieldName"><%= t 'user.new.email address' %></td><td><%= text_field('user', 'email',{:size => 50, :maxlength => 255, :tabindex => 1, :value => @email}) %></td></tr>
   <tr><td class="fieldName"><%= t 'user.new.confirm email address' %></td><td><%= text_field('user', 'email_confirmation',{:size => 50, :maxlength => 255, :tabindex => 2}) %></td></tr>
   <tr><td></td><td><span class="minorNote"><%= t 'user.new.not displayed publicly' %></span></td></tr>
   <tr><td colspan="2">&nbsp;<!--vertical spacer--></td></tr>
-  <tr><td class="fieldName"><%= t 'user.new.display name' %></td><td><%= text_field('user', 'display_name',{:size => 30, :maxlength => 255, :tabindex => 3}) %></td></tr>
+  <tr><td class="fieldName"><%= t 'user.new.display name' %></td><td><%= text_field('user', 'display_name',{:size => 30, :maxlength => 255, :tabindex => 3, :value => @nickname}) %></td></tr>
   <tr><td></td><td><span class="minorNote"><%= t 'user.new.display name description' %></span></td></tr>
   <tr><td colspan="2">&nbsp;<!--vertical spacer--></td></tr>
   <tr><td class="fieldName"><%= t 'user.new.password' %></td><td><%= password_field('user', 'pass_crypt',{:size => 30, :maxlength => 255, :tabindex => 4}) %></td></tr>
index 4493d07a43359b5c37d2f1a8269aad00b2260796..da71cc57230d20fbefef4f323f5a077ac0ea1a95 100644 (file)
@@ -52,6 +52,7 @@ Rails::Initializer.run do |config|
   config.gem 'rmagick', :lib => 'RMagick'
   config.gem 'oauth', :version => '>= 0.3.6'
   config.gem 'httpclient'
+  config.gem 'ruby-openid', :lib => 'openid', :version => '>=2.0.4'
 
   # Only load the plugins named here, in the order given. By default, all plugins 
   # in vendor/plugins are loaded in alphabetical order.
index 6231f0dbe971d846c8c4ba27396748a002892484..66fbfbe95bfd03311ad94e75c08a9cae35efe814 100644 (file)
@@ -1347,10 +1347,15 @@ en:
       create_account: "create an account"
       email or username: "Email Address or Username:"
       password: "Password:"
+      openid: "OpenID:"
+      openid description: "Use your OpenID to login"
+      alternatively: "Alternatively"
       lost password link: "Lost your password?"
       login_button: "Login"
       account not active: "Sorry, your account is not active yet.<br />Please click on the link in the account confirmation email to activate your account."
       auth failure: "Sorry, could not log in with those details."
+      openid missing provider: "Sorry, could not contact your OpenID provider"
+      openid invalid: "Sorry, your OpenID seems misformed"
     lost_password:
       title: "Lost password"
       heading: "Forgotten Password?"
@@ -1447,6 +1452,10 @@ en:
       title: "Edit account"
       my settings: My settings
       email never displayed publicly: "(never displayed publicly)"
+      openid:
+        openid: "OpenID:"
+        link: "http://wiki.openstreetmap.org/wiki/OpenID"
+        link text: "what is this?"
       public editing:
         heading: "Public editing:"
         enabled: "Enabled. Not anonymous and can edit data."
diff --git a/db/migrate/049_add_open_id_authentication_tables.rb b/db/migrate/049_add_open_id_authentication_tables.rb
new file mode 100644 (file)
index 0000000..7dfff20
--- /dev/null
@@ -0,0 +1,30 @@
+class AddOpenIdAuthenticationTables < ActiveRecord::Migration
+  def self.up
+    create_table :open_id_authentication_associations, :force => true do |t|
+      t.integer :issued, :lifetime
+      t.string :handle, :assoc_type
+      t.binary :server_url, :secret
+    end
+
+    create_table :open_id_authentication_nonces, :force => true do |t|
+      t.integer :timestamp, :null => false
+      t.string :server_url, :null => true
+      t.string :salt, :null => false
+    end
+    
+    add_column :users, :openid_url, :string 
+
+    add_index :users, [:openid_url], :name => "user_openid_unique_idx", :unique => true
+    add_index :open_id_authentication_associations, [:server_url], :name => "open_id_associations_server_url_idx"
+    add_index :open_id_authentication_nonces, [:timestamp], :name => "open_id_nonces_timestamp_idx"
+  end
+
+  def self.down
+    remove_index :users, :name => "user_openid_unique_idx"
+    remove_index :open_id_authentication_associations, :name => "open_id_associations_server_url_idx"
+    remove_index :open_id_authentication_nonces, :name => "open_id_nonces_timestamp_idx"
+    remove_column :users, :openid_url
+    drop_table :open_id_authentication_associations
+    drop_table :open_id_authentication_nonces
+  end
+end