Giter VIP home page Giter VIP logo

louisstamour / authlogic_openid Goto Github PK

View Code? Open in Web Editor NEW

This project forked from binarylogic/authlogic_openid

3.0 3.0 1.0 132 KB

A really long README detailing how to use this slightly modified code to add multiple OpenID support, Windows Live ID support and Facebook Connect support. Profile merging and UI still needs to be thought about, and a good refactoring might be in order also. Not sure still how appropriate Authlogic is for this, and I would suggest renaming the User model to Login.

Home Page: http://authlogic-oid.rubyforge.org

License: MIT License

Ruby 100.00%

authlogic_openid's Introduction

Latest update: Facebook Connect logins

(These were posted in reverse order. If you came here from Railscast 160 and Railscast 170, start at the bottom of this README and work your way up.)

First we’ll need the Facebooker plugin:

script/plugin install git://github.com/mmangino/facebooker.git

Two things the readme says you can add to your application controller:

ensure_application_is_installed_by_facebook_user
# optional - ensures users have added the application in facebook to ensure access all of facebooker‘s features.

filter_parameter_logging :fb_sig_friends
# recommended - prevents a violation of Facebook Terms of Service while reducing log bloat

If you get a Facebooker::AdapterBase::UnableToLoadAdapter error like I did after installing Facebooker, it’s because you haven’t configured config/facebooker.yml yet. So open it up and add the relevant info from your Facebook app signup – see wiki.developers.facebook.com/index.php/Connect/Setting_Up_Your_Site

An example facebooker.yml file: (Obviously you MUST replace the api_key and secret_key.)

development:
  api_key: a123b4567890b1cde23f456ghi7jk89l
  secret_key: a1234567bc8d9ef0g1h23i456789j01k
  canvas_page_name:
  callback_url: http://localhost:3000/
  pretty_errors: true
  set_asset_host_to_callback_url: false
  api: new
  tunnel:
    public_host_username: 
    public_host: 
    public_port: 4007
    local_port: 3000
    server_alive_interval: 0

Okay, back to our terminal work:

script/generate xd_receiver
script/generate migration AddFacebookFieldsToUser fb_user_id:integer email_hash:string

The first command creates cross-domain receiver files for Facebook callbacks. The second creates a migration, which we need to edit to say:

class AddFacebookFieldsToUser < ActiveRecord::Migration
  def self.up
    add_column :users, :fb_user_id, :integer, :limit => 8
    add_column :users, :email_hash, :string
  end

  def self.down
    remove_column :users, :email_hash
    remove_column :users, :fb_user_id
  end
end

The :limit => 8 makes the :integer act as a bigint where possible.

rake db:migrate

Now we modify application.html.erb to add the Facebook Connect JavaScript and so on.

Just before the </body> (to speed up HTML loading), add:

<%= javascript_include_tag :defaults%>
<%= fb_connect_javascript_tag %>
<%= init_fb_connect %>

And where you want the Facebook Connect sign in / sign out to be, you can put code like the following:

<%- if current_user && current_user.fb_user_id -%>
<fb:profile-pic uid="<%= current_user.fb_user_id %>" facebook-logo="true" size="thumb" ></fb:profile-pic>
<p><a href="#" onclick='FB.Connect.logoutAndRedirect("/logout")'>Logout of Facebook Connect</a></p>
<%- else -%>
<%= fb_login_button('window.location = "/login/Facebook";')%>
<%- end -%>

This assumes routes (and related methods) like:

map.login "login", :controller => "user_sessions", :action => "new"
map.login_with "login/:service", :controller => "user_sessions", :action => "create"
map.logout "logout", :controller => "user_sessions", :action => "destroy"

In my user_sessions controller, I check if params is equal to “Facebook”, in the same way that I earlier looked for “windows” to process Windows Live ID. The Facebook code is as follows: (Could be improved)

elsif params[:service].downcase == "facebook"
  if facebook_session
    @user_session = UserSession.new
    @user_session.fb_user_id = facebook_session.user.id # can't be passed via initializer, haven't researched this.
    if current_user
      @user = User.new
      @user.fb_user_id = facebook_session.user.id
      @user.profile = current_user.profile
      @user.save do |result|
        if result
          flash[:notice] = "Login/Registration successful."
        else
          flash[:notice] = "Login/Registration failed."
        end
        redirect_to root_url
        return
      end
    end
  end
end

But wait, you ask, what’s facebook_session? Well, since Facebooker is tightly integrated with Rails, and since Facebook has lots of potential features, I didn’t try to force it to use Authlogic’s sessions, and instead used the built-in Facebooker ones in addition to the current_user provided by Authlogic. In my Application controller, I added:

filter_parameter_logging :fb_sig_friends
before_filter :set_facebook_session
helper_method :facebook_session

Overall, this is perhaps not the smartest way to process Facebook logins, but it does seem to get the job done. I think I’ll try refactoring next however, or rethinking this whole use of authlogic. Plus, profile merging is still bugging me.

Here’s some code I used for profiles - on the show profile page:

<p>
  <strong>Sign-in IDs:</strong>
</p>
<ul>
<%- @profile.users.each do |u| -%>
  <li><%=h "OpenID: #{u.openid_identifier}" if u.openid_identifier %>
  <%=h "Windows Live (Internal ID: #{u.wll_id})" if u.wll_id %>
  <%=h "Facebook Connect (Internal ID: #{u.fb_user_id})" if u.fb_user_id %></li>
<%- end -%>
</ul>
<p>
  <%= link_to "Add another Sign-in ID", new_user_path %>
</p>

And in the ProfilesController:

def destroy
  @profile = current_user.profile
  @profile.users.each do |u|
    u.destroy
  end
  @profile.destroy
  flash[:notice] = "Successfully destroyed profile."
  redirect_to profiles_url
end

Now I have to figure out also how to explain “multiple login IDs” to my users. I think I’ll read up on Google’s OpenID usability studies. Certainly, if I didn’t know better, faced with logging in through 3 different options, I might randomly pick each time and get it wrong 3 times in a row, not knowing I should have associated all three login types the first time I logged in. Maybe I need a wizard of some kind. Or make it clearer which account they’ve logged in as, for when they return to the site later and have to pick again.

Introduction to the Windows Live ID commit from earlier

You’ll need to git clone this version of the code, as after spending hours trying to override methods, I realized the simplest way to support other types of logins would be to add other fields to the User model, such as wll_id.

LICENSING WARNING: windowslivelogin.rb included in this fork might be protected by the Windows Live SDK License Agreement at msdn.microsoft.com/en-us/library/bb226715.aspx which you must accept to use the SDK. It restricts code, at least sample code, from being redistributed under a license that requires open source distribution. I believe then that an MIT license is appropriate, however given that the MIT license is easily the less restrictive of the two, I may be required to license this code not as MIT but as Windows Live SDK protected – or at least that one file and these instructions.

See below for my way of doing multiple logins, this builds on that in some ways – such as by assuming that all User model fields are optional within my User.find_or_create_by_wll_id within the UserSession validation. I also added a has_profile method to ensure that all created Users have profiles, even if blank. (I updated the example User model code below to reflect that.)

The only problems I’m having now with the code is what to do about multiple profiles / merging them, and trying to de-couple the idea of registering with the idea of signing in. When someone signs in with an OpenID or Windows Live, the User should be automatically created if it doesn’t exist and associated with the current profile. If the ID or User already exists, but you’re signed in under a different profile, then it has to merge the profiles, likely out of scope for this module. Even registering then, could be as simple as clicking a Register option on the login form rather than Login. My User controller seems less and less useful, except in creating additional logins without signing in. Another concern I just realised is “adding” Windows Live IDs to an existing profile. If you’re signed in already, and you sign in as Windows Live, the Windows Live session must replace your current session, since sign-out must then be handled by Windows Live. To be more precise, the user and expiry must change … I’ll have to look into this further. (Now I see the value of tests :p)

An implementation of a Windows Live ID controller / settings.

Before we begin, you will want to read up on what all this means at msdn.microsoft.com/en-us/library/bb676633.aspx It’s easy enough to follow, though the docs are a little scattered and not Ruby-specific.

Okay, lets start by adding :wll_id to the User model:

script/generate migration AddWllIdToUser wll_id:string
rake db:migrate

We’ll also need to create an Application-Key.yml file and load its values to WLL_CONFIG … or something. ;-) I put the yml file in the Rails root directory, where you have the Rakefile and README for your application, etc. Then I added the following line to a configuration file, like environment.rb:

WLL_CONFIG = YAML.load_file(Rails.root.join("Application-Key.yml"))

Here’s what Application-Key.yml looks like, it’s based off of the sample Application-Key.xml file. You will need to sign up for a Windows Live ID application key at live.azure.com/ – click Live Services, then New Service, then Existing APIs. Your returning URL will receive both get and post requests, you may want it to be a separate UserSessionController method, but we’ll get to that in a second. (You can change this URL at any time; localhost is not a valid domain, for local testing use 127.0.0.1 example.org in your /etc/hosts and tell Microsoft it’s example.org)

--- 
:appid: 000000001A01B984
:secret: abC1D2ef3g4hiJk5lmnOPQR6StUV7wXY
:securityalgorithm: wsignin1.0

There’s a blank line below :securityalgorithm: wsignin1.0. DON’T COPY AND PASTE THE ABOVE SAMPLE! It won’t work, you have to replace the appid and secret with your own registered appid and secret. Again, save the file to your Root folder as Application-Key.yml, or put it in a safe spot. The idea is that you decrypt the response from Microsoft with your secret key, this way Microsoft knows your app is the only one that can understand the unique ID embedded in the returned token, and in return, you know the ID came from Microsoft and not through a third-party man-in-the-middle attack. This is only as secure as your secret key, however, so keep it safe and away from prying eyes.

We are now nearly done, all we have to do is add the login/logout iframe to layouts/application.html.erb and modify our UserSession controller to handle redirects from Windows Live after sign-in or sign-out. Unlike OpenID, the iframe loading on each page means we do not handle the “Sign in” or “Sign out” clicks and we only know the button is pressed after the sign-in has occurred or during the sign out, when the browser loads an image from us to tell us to clear cookies. (Yes, when someone signs out of Windows Live, they are supposed to sign out of all Windows Live properties, including us and Hotmail. This also means we’ll want to hide our sign out button when we are signed in through Windows Live.)

So below is my sample code from application.html.erb – all the relevant bits say WLL_CONFIG near them:

<%- if current_user -%>
  Hello <%= current_user.profile.first_name || "Anonymous" %>! You can 
  <%= link_to "view", profile_path(current_user.profile) %> or 
  <%= link_to "edit your profile", edit_profile_path(:current) %><% unless WLL_CONFIG && current_user.wll_id %>; or 
  <%= link_to "logout", logout_path %>.<% end %>
<%- else -%>
  Hello! Please 
  <%= link_to "register", new_user_path %> or 
  <%= link_to "login", login_path %> now, thanks! New: Login with <%= link_to "Google", login_with_path('Google') %>! <%= link_to image_tag('y-signin.png', :border =>0), login_with_path('Yahoo!') %>
<%- end -%>
<%- if WLL_CONFIG -%>
<iframe 
  id="WebAuthControl" 
  name="WebAuthControl" 
  src="http://login.live.com/controls/WebAuthButton.htm?appid=<%= WLL_CONFIG[:appid]%>&amp;style=<%= CGI.escape 'font-size: 10pt; font-family: verdana; background: white;'%>"
  width="80px"
  height="20px"
  marginwidth="0"
  marginheight="0"
  align="middle"
  frameborder="0"
  scrolling="no">
</iframe>
<%- end -%>

You’ll note the iframe src points to WebAuthButton.htm which is a self-contained badge that says “Sign in” or “Sign out” – for all options, see msdn.microsoft.com/en-us/library/bb676638.aspx (The other two are somewhat ugly looking bits of text more appropriate for cases where Windows Live is the only authentication mechanism used.)

On to user_sessions_controller.rb where in the create method or something based on it, before the usual @user_session.save, I have:

wll = WindowsLiveLogin.new(WLL_CONFIG[:appid], WLL_CONFIG[:secret], WLL_CONFIG[:securityalgorithm])
if request.request_parameters[:action] == "login" # The user has completed a successful sign-in to your site.
                                                # This value appears in the HTTP POST response only.

  # Extract the user's authentication token from the POST response and store it in a session

  token = params[:stoken]
  context = CGI.unescape(params[:appctx]) if params[:appctx] # A string we pass in the iframe that is returned to us
                                                             # Could be used to redirect user back to where they were.
  wll_user = wll.processToken(token, context)
  if wll_user
    # ... if we're already signed in, lets add this profile to the current account.
    if current_user
      @user = User.new({ :wll_id => wll_user.id })
      @user.profile = current_user.profile
      @user.save do |result|
        if result
          flash[:notice] = "Login/Registration successful."
        else
          flash[:notice] = "Login/Registration failed."
        end
        redirect_to root_url
        return
      end
    end

    @user_session = UserSession.new({ :remember_me => wll_user.usePersistentCookie?,
                                      :remember_me_for => 10.years }) # Not sure if :remember_me_for works as specified here.

    @user_session.wll_id = wll_user.id # this is the unique id for the user

    # Do not use persistent cookies if the value of the user's UsePersistentCookie property is false.
    # Persistent cookies allow the user to remained signed in across multiple browser sessions.

    # ... from here we skip to @user_session.save... below. ;-)

  else            
    flash[:notice] = "Sorry, there was an error processing the Windows Live ID login."
    redirect_to root_url
    return
  end

elsif request.query_parameters[:action] == "clearcookie" # The Windows Live ID sign-out page is calling your site to clear user cookies.
                                                         # This value appears in the HTTP GET response.

# Clear the session cookie you created at sign-in and return a GIF image
# to the service to indicate that the user has been signed out.

@user_session = UserSession.find
@user_session.destroy unless @user_session.nil?
type, response = wll.getClearCookieResponse()
render :text => response, :content_type => type
return

elsif request.query_parameters[:action] == "logout" # The user has completed a successful sign out from Windows Live.
                                                    # This value appears in the HTTP GET response.

# Clear the session cookie and redirect the signed-out user
# to a page on your site that is appropriate for unauthenticated users.

@user_session = UserSession.find
@user_session.destroy unless @user_session.nil?
flash[:notice] = "Successfully logged out."
redirect_to root_url
return

end

Note the use of request.request_parameters[:action] or request.query_parameters[:action] == "logout" – this is because params is of course overridden to say create or whatever the Rails action is. Instead, we need to look at the original request_parameters (POST) or query_parameters (GET) to see what Windows Live tells us to do: “login” (redirects like normal), “clearcookie” (returns GIF), or “logout” (redirects like normal).

Okay, now you’ve a working Windows Live ID setup. See, wasn’t that easy? For people confused about what to do with these files, just put them in vendor/plugins, e.g. vendor/plugins/authlogic_openid/lib/windowslivelogin.rb … it doesn’t need to be a gem to work. (In fact, that’s the only way I’ve tested it so far, I’m clueless at gem packaging)

Some tips for using Authlogic OpenID (original instructions follow this)

What’s neat about authlogic_openid, which Railscast 170 doesn’t cover, is that it can support multiple OpenIDs out of the box. Here’s how:

Rather than my earlier attempt at turning User into a profile and using multiple Login objects to store OpenIDs, we’ll do the sensible thing and instead have multiple USERS – objects that act_as_authentic – and one Profile model that has_many USERs.

To start with, we’ll generate the Profile object:

script/generate model Profile first_name:string last_name:string email:string

Next let’s modify our User and Profile models adding belongs_to :profile and has_many :users. Two more changes: We’ll have to remove the username validation if we want to make one-click OpenID registration, and we’ll want to update our map_openid_registration function to create a profile and/or update it. (I’m not sure how well the map_openid_registration code works, though it has created a profile each time so far…)

class Profile < ActiveRecord::Base
  has_many :users
end

class User < ActiveRecord::Base
  belongs_to :profile

  validate :has_profile

  acts_as_authentic do |c|
    c.openid_required_fields = [:nickname, :fullname, "http://axschema.org/namePerson/first", 
      "http://axschema.org/namePerson/last", :email, 'http://schema.openid.net/contact/email']
  end

  private

  def has_profile
    unless self.profile
      self.profile = Profile.new
    end
  end

  def map_openid_registration(registration)
    unless self.profile
      self.profile = Profile.new
    end
    self.profile.email = registration["email"] || registration["http://schema.openid.net/contact/email"].to_s if self.profile.email.blank?
    self.profile.first_name = (registration[:fullname].split(" ").first if registration[:fullname]) || (registration["http://axschema.org/namePerson/first"][0] if registration["http://axschema.org/namePerson/first"]) || registration["nickname"] || self.profile.email.to_s[/[^@]*/] if self.profile.first_name.blank?
    self.profile.last_name = (registration[:fullname].split(" ").last if registration[:fullname]) || registration["http://axschema.org/namePerson/last"].to_s if self.profile.last_name.blank?
  end
end

Then open the generated create_profiles.rb migration and change it to something like:

class CreateProfiles < ActiveRecord::Migration
  def self.up
    create_table :profiles do |t|
      t.string :first_name
      t.string :last_name
      t.string :email
      t.timestamps
    end
    add_column :users, :profile_id, :integer
    User.find(:all).each do |u|
      u.profile = Profile.create(
        :first_name => u.first_name,
        :last_name => u.last_name,
        :email => u.email
      )
    end
    remove_column :users, :first_name
    remove_column :users, :last_name
    remove_column :users, :email
  end

  def self.down
    remove_column :users, :profile_id
    add_column :users, :first_name, :string
    add_column :users, :last_name, :string
    add_column :users, :email, :string
    drop_table :profiles
  end
end

The above migration should port over all existing profile data, though I haven’t tested that function yet myself.

Your UsersController might then have this method:

def create
  @user = User.new(params[:user])
  if(!current_user && params[:user] && params[:user][:openid_identifier] && User.find_by_openid_identifier(params[:user][:openid_identifier]))
    # This is so that if the OpenID exists as a User, we authenticate rather than register.
    redirect_to "/login/other?openid_identifier=#{params[:user][:openid_identifier]}"
    # The above redirect must be a GET request which is why it looks odd.
  else
    if(current_user)
      @user.profile = current_user.profile
    end
    @user.save do |result|
      if result
        flash[:notice] = "Registration successful."
        redirect_to root_url
      else
        render :action => 'new'
      end
    end
  end
end

This way you can easily add other OpenIDs to your profile when signed-in. I also added a bit of code to redirect if an ID already exists as a user. This way the Registration page can be somewhat used as a sign-in page. (I can’t replace it with the sign-in page, as you would still want to say “username taken” rather than “password incorrect”)

Authlogic OpenID

Authlogic OpenID is an extension of the Authlogic library to add OpenID support. Authlogic v2.0 introduced an enhanced API that makes “plugging in” alternate authentication methods as easy as installing a gem.

* Documentation: authlogic-oid.rubyforge.org * Authlogic: github.com/binarylogic/authlogic * Live example: authlogicexample.binarylogic.com

Install and use

1. Make some simple changes to your database:

class AddUsersOpenidField < ActiveRecord::Migration
  def self.up
    add_column :users, :openid_identifier, :string
    add_index :users, :openid_identifier

    change_column :users, :login, :string, :default => nil, :null => true
    change_column :users, :crypted_password, :string, :default => nil, :null => true
    change_column :users, :password_salt, :string, :default => nil, :null => true
  end

  def self.down
    remove_column :users, :openid_identifier

    [:login, :crypted_password, :password_salt].each do |field|
      User.all(:conditions => "#{field} is NULL").each { |user| user.update_attribute(field, "") if user.send(field).nil? }
      change_column :users, field, :string, :default => "", :null => false
    end
  end
end

2. Install the openid_authentication plugin

$ script/plugin install git://github.com/rails/open_id_authentication.git

For more information on how to configure the plugin, checkout it’s README: github.com/rails/open_id_authentication/tree/master

3. Install the Authlogic Openid gem

$ sudo gem install authlogic-oid

Now add the gem dependency in your config:

config.gem "authlogic-oid", :lib => "authlogic_openid"

Or for older version of rails, install it as a plugin:

$ script/plugin install git://github.com/binarylogic/authlogic_openid.git

4. Make sure you save your objects properly

You only need to save your objects this way if you want the user to authenticate with their OpenID provider.

That being said, you probably want to do this in your controllers. You should do this for BOTH your User objects and UserSession objects (assuming you are authenticating users). It should look something like this:

@user_session.save do |result|
  if result
    flash[:notice] = "Login successful!"
    redirect_back_or_default account_url
  else
    render :action => :new
  end
end

You should save your @user objects this way as well, because you also want the user to verify that they own the OpenID identifier that they supplied.

Notice we are saving with a block. Why? Because we need to redirect the user to their OpenID provider so that they can authenticate. When we do this, we don’t want to execute that block of code, because if we do, we will get a DoubleRender error. This lets us skip that entire block and send the user along their way without any problems.

That’s it! The rest is taken care of for you.

Redirecting from the models?

If you are interested, I explain myself below. Regardless, if you don’t feel comfortable with the organization of the logic,you can easily do this using the traditional method. As you saw in the setup instructions, this library leverages the open_id_authentication rails plugin. After the user has been authenticated just do this:

UserSession.create(@user)

It’s that simple. For more information there is a great OpenID tutorial at: railscasts.com/episodes/68-openid-authentication

Now, here are my thoughts on the subject:

You are probably thinking: “Ben, you can’t handle controller responsibilities in models”. I agree with you on that comment, but my personal opinion is that these are not controller responsibilities. The fact that OpenID authentication requires a redirect should not effect the location of the logic / code. It’s all part of the authentication process, which is the entire purpose of this library. This library is not one big module of code, its a collection of modules that all deal with OpenID authentication. These modules get included wherever it makes sense. That’s the whole idea behind modules. To group common logic.

Let’s take a step back and look at the traditional method of OpenID authentication in rails. What if you wanted to authenticate with OpenID in multiple controllers in your application (Ex: registration and loggin in)? You would probably pull out the common code into a module and include it in the respective controllers. Even better, you might create a class that elegantly handles this process and then place it in your lib directory. Then, if you really wanted to be slick, you might take it another step further and have your models trigger this class during certain actions. Then what do we have? This exact library, that’s exactly what this is.

The last thing I will leave you with, to get you thinking, is… where do sweepers lie in the MVC pattern? Without this, things like caching would be extremely difficult. There is a big difference between misplacing code / logic, and organizing logic into a separate module and hooking it in using the API provided by your models. Especially when the logic needs to be triggered by actions invoked on models.

Regardless, if I still haven’t convinced you, I hope this library is of some benefit to you. At the very least an example of how to extend Authlogic.

Copyright © 2009 Ben Johnson of [Binary Logic](www.binarylogic.com), released under the MIT license

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.