tardate / authlogic_rpx

Authlogic plugin to provide RPX support NB: Rails 3.2+ support is becoming an issue and I am not actively trying to keep this project updated (in favour of devise-based authentication). If anyone wishes to rescuscitate this project for Rails 3.2 and beyond please let me know and perhaps we could discuss transferring project ownership
MIT License
74 stars 11 forks source link

= Authlogic_RPX

== Purpose

Authlogic_RPX is an Authlogic extension library that provides support for authentication using the Janrain Engage (formerly RPX) multi-authentication service offered by JanRain. To use RPX, you must first register your application at {Janrain Engage}[http://rpxnow.com/]. A free "Basic" account is available, in addition to paid enhanced versions. All work with Authlogic_RPX.

Key features and capabilities:

== Authlogic_RPX References

The demonstration Rails application is where you can see Authlogic_RPX in action:

== Authlogic and RPX References

== Installing Authlogic_RPX gem

Three gems are required: authlogic, rpx_now, and authlogic_rpx. Install these as appropriate to your environment and preferences.

Currently tested versions:

=== 1. Direct gem installation

installs authlogic and rpx_now gem dependencies

sudo gem install authlogic_rpx

=== 2. Using Rails 2.3.x config.gems

Include in config/environment.rb:

config.gem 'authlogic', :version => '= 2.1.6' config.gem 'rpx_now', :version => '= 0.6.23' config.gem 'authlogic_rpx', :version => '>= 1.2.0'

Then to install, run from the command line:

sudo rake gems:install

=== 3. Using .gems file (e.g for heroku.com deployments)

Include in RAILS_ROOT/.gems:

authlogic_rpx --version '>= 1.2.0'

=== 4. Using Bundler for Rails 3

Include in RAILS_ROOT/Gemfile

gem 'authlogic', '= 2.1.6' gem 'rpx_now', '= 0.6.23' gem 'authlogic_rpx', '= 1.2.0'

== About Authlogic_RPX

Using Authlogic_RPX is very similar to using standard authlogic, with the addition of just a few configuration options. So if you already have a project setup with authlogic, adding RPX support will be trivial.

An important capability to be aware of is "auto registration". This means that when a user has logged in with RPX, if an account does not already exist in your application, it will be automatically created. That is, there is no separate/special "register" step for users to go through before just signing in. You can disable this if you need, but for most sites that use RPX as a primary authentication mechanism, this is probably what you want to happen.

One of the main limitations of Authlogic_RPX versions up to 1.0.4 was that it did not include any specific support for identity mapping. This means that if a user signs in with twitter one day, and facebook the next, then your application would see these as tow distinct users (NB: RPX provides some protection for this by trying to remember the last authentication method your users used. It's not always perfect however).

From Authlogic_RPX version 1.1.0 we have added built-in identity mapping and merging support. This is what we call 'internal' mapping. The legacy approach from 1.0.4 and earlier is still supported as an option. This mapping mode is called 'none'.

The JanRain RPXnow service has its own identity mapping implementation, but only available for paid accounts. This is still not supported directly by Authlogic_RPX, but is something we'd like to get into a future version.

=== Chosing Your Mapping Mode

When you configure Authlogic_RPX, you will need to decide which mapping mode to use.

The options are:

==== Upgrading from Authlogic_RPX 1.0.4 or earlier

In Authlogic_RPX v1.0.4 and earlier, the rpx_identifier was stored in the user model, and identity mapping was not supported.

If you are upgrading to 1.1.0 or later and wish to start using internal mapping:

e.g.

class User < ActiveRecord::Base acts_as_authentic do |c| c.account_mapping_mode :internal end end

If you are upgrading to 1.1.0 or later and wish to continue using the legacy/1.0.4 approach (i.e. no mapping):

e.g.

class User < ActiveRecord::Base acts_as_authentic do |c| c.account_mapping_mode :none end end

== The Step-by-Step Guide to Using Authlogic_RPX

Note: in what follows, the user model is called User and the session controller takes the name UserSession (the authlogic convention). You are not restricted to these names - could be Member and MemberSession for example - but for simplicity, this documentation will stick to using the "User" convention.

The main steps for enabling Authlogic_RPX:

=== 1. Enable RPX for your user model

The user model will have a has_many relationship with a new model, called RPXIdentifier. A generator is provider to create the necessary migration:

ruby script/generate add_authlogic_rpx_migration [mapping:mapping_mode] [user_model:model_name]

The generator takes two optional parameters: mapping and user_model.

The mapping_mode parameter indicates which style of Authlogic_RPX-supported identity mapping should be used. The default mapping_mode is 'internal' Allowed values for mapping_mode are:

The user_model parameter specifies the name of the user/member model in your application. The default model_name is 'User'. e.g. to generate the RPX migration where the user model is called 'Member' and you do not want to support identity mapping:

ruby script/generate add_authlogic_rpx_migration mapping:none user_model:member

You may need to customise the migration file to remove database constraints on other fields if they will be unused in the RPX case (e.g. crypted_password and password_salt to make password authentication optional).

If you are using auto-registration, you must also remove any database constraints for fields that will be automatically mapped (see notes in "3. Add custom user profile mapping during auto-registration")

==== Sample Migration Generated Script (using internal mapping)

The following command will generate a migration for the case where you want to use authlogic_rpx internal mapping and your user model is called 'User':

ruby script/generate add_authlogic_rpx_migration mapping:internal user_model:user

The migration script will appear like this:

class AddAuthlogicRpxMigration < ActiveRecord::Migration def self.up create_table :rpx_identifiers do |t| t.string :identifier, :null => false t.string :provider_name t.integer :user_id, :null => false t.timestamps end add_index :rpx_identifiers, :identifier, :unique => true, :null => false add_index :rpx_identifiers, :user_id, :unique => false, :null => false

  # == Customisation may be required here ==
  # You may need to remove database constraints on other fields if they will be unused in the RPX case
  # (e.g. crypted_password and password_salt to make password authentication optional). 
  # If you are using auto-registration, you must also remove any database constraints for fields that will be automatically mapped
  # e.g.:
  #change_column :users, :crypted_password, :string, :default => nil, :null => true
  #change_column :users, :password_salt, :string, :default => nil, :null => true

end

def self.down
  drop_table :rpx_identifiers

  # == Customisation may be required here ==
  # Restore user model database constraints as appropriate
  # e.g.:
  #[: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

{See the source for the sample 20091227051253_add_authlogic_rpx_migration.rb}[http://github.com/tardate/rails-authlogic-rpx-sample/blob/master/db/migrate/20091227051253_add_authlogic_rpx_migration.rb].

==== Sample Migration Generated Script (using no mapping)

The following command will generate a migration for the case where you don't want to use authlogic_rpx mapping and your user model is called 'Member':

ruby script/generate add_authlogic_rpx_migration mapping:none user_model:member

The migration script will appear like this:

class AddAuthlogicRpxMigration < ActiveRecord::Migration

def self.up
  add_column :members, :rpx_identifier, :string
  add_index :members, :rpx_identifier

  # == Customisation may be required here ==
  # You may need to remove database constraints on other fields if they will be unused in the RPX case
  # (e.g. crypted_password and password_salt to make password authentication optional).
  # If you are using auto-registration, you must also remove any database constraints for fields that will be automatically mapped

  # e.g.:
  #change_column :members, :crypted_password, :string, :default => nil, :null => true
  #change_column :members, :password_salt, :string, :default => nil, :null => true

end

def self.down
  remove_column :members, :rpx_identifier

  # == Customisation may be required here ==
  # Restore user model database constraints as appropriate
  # e.g.:
  #[:crypted_password, :password_salt].each do |field|
  #  Member.all(:conditions => "#{field} is NULL").each { |user| user.update_attribute(field, "") if user.send(field).nil? }
  #  change_column :members, field, :string, :default => "", :null => false
  #end

end

end

==== Configuring the User model

The user model then needs to be tagged with "acts_as_authentic". This is the minimal configuration:

class User < ActiveRecord::Base acts_as_authentic end

Two RPX-specific user configuration options are available.

The account_mapping_mode options are defined as follows:

For example, the following shows how to set standard Authlogic configurations (validations_scope), enables RPX account merging, and specifies :internal account mapping:

class User < ActiveRecord::Base acts_as_authentic do |c| c.validations_scope = :company_id # for available Authlogic options see documentation in the various Config modules of Authlogic::ActsAsAuthentic

  # enable Authlogic_RPX account merging (false by default, if this statement is not present)
  c.account_merge_enabled true

  # set Authlogic_RPX account mapping mode
  c.account_mapping_mode :internal

end # block optional

end

{See the source for the sample user.rb}[http://github.com/tardate/rails-authlogic-rpx-sample/blob/master/app/models/user.rb].

NB: The RPXIdentifier model is included in the authlogic_rpx gem and does not need to be added to your project.

=== 2. Add RPX configuration for the Authlogic session model

Authlogic provides a helper to create the session model:

script/generate session user_session

The minimum configuration required is to add your RPX_API_KEY:

class UserSession < Authlogic::Session::Base rpx_key RPX_API_KEY end

Get an API key by registering your application at {RPX}[http://rpxnow.com/]. A free "Basic" account is available, in addition to paid enhanced versions. All work with Authlogic_RPX.

You probably don't want to put your API key in directly. A recommended approach is to set the key as an environment variable, and then set it as a constant in config/environment.rb:

RPX_API_KEY = ENV['RPX_API_KEY']

Two additional RPX-specific session configuration options are available.

For example, to disable auto-registration and enable extended info:

class UserSession < Authlogic::Session::Base rpx_key RPX_API_KEY auto_register false rpx_extended_info end

{See the source for the sample user_session.rb}[http://github.com/tardate/rails-authlogic-rpx-sample/blob/master/app/models/user_session.rb].

=== 3. Add custom user profile mapping (optional)

Authlogic_rpx provides three hooks for mapping information from the RPX profile into your application's user model:

See https://rpxnow.com/docs#profile_data for the definition of available attributes in the RPX profile.

==== 3a. map_rpx_data: user profile mapping during auto-registration

When users auto-register, profile data from RPX is available to be inserted in the user's record on your site. By default, authlogic_rpx will map the username and email fields.

If you have other fields you want to map, you can provide your own implementation of the map_rpx_data method in the UserSession model. In that method, you will be updating the "self.attempted_record" object, with information from the "@rpx_data" object. See the {RPX documentation}[https://rpxnow.com/docs#profile_data] to find out about the set of information that is available.

class UserSession < Authlogic::Session::Base rpx_key RPX_API_KEY rpx_extended_info

private

# map_rpx_data maps additional fields from the RPX response into the user object
# override this in your session controller to change the field mapping
# see https://rpxnow.com/docs#profile_data for the definition of available attributes
#
def map_rpx_data
  # map core profile data using authlogic indirect column names
  self.attempted_record.send("#{klass.login_field}=", @rpx_data['profile']['preferredUsername'] ) if attempted_record.send(klass.login_field).blank?
  self.attempted_record.send("#{klass.email_field}=", @rpx_data['profile']['email'] ) if attempted_record.send(klass.email_field).blank?

  # map some other columns explicitly
  self.attempted_record.fullname = @rpx_data['profile']['displayName'] if attempted_record.fullname.blank?

    if rpx_extended_info?
    # map some extended attributes
    end
end

end

WARNING: if you are using auto-registration, any fields you map should NOT have constraints enforced at the database level. Authlogic_rpx will optimistically attempt to save the user record during registration, and violating a database constraint will cause the authentication/registration to fail.

You can/should enforce any required validations at the model level e.g.

validates_uniqueness_of :username, :case_sensitive => false

This will allow the auto-registration to proceed, and the user can be given a chance to rectify the validation errors on your user profile page.

If it is not acceptable in your application to have user records created with potential validation errors in auto-populated fields, you will need to override map_rpx_data and implement whatever special handling makes sense in your case. For example:

{See the source for the sample user_session.rb}[http://github.com/tardate/rails-authlogic-rpx-sample/blob/master/app/models/user_session.rb].

==== 3b. map_rpx_data_each_login: user profile mapping during login

map_rpx_data_each_login provides a hook to allow you to map RPX profile information every time the user logs in.

By default, nothing is mapped. If you have other fields you want to map, you can provide your own implementation of the map_rpx_data_each_login method in the UserSession model.

This would mainly be used to update relatively volatile information that you are maintaining in the user model (such as profile image url)

In the map_rpx_data_each_login procedure, you will be writing to fields of the "self.attempted_record" object, pulling data from the @rpx_data object. For example:

def map_rpx_data_each_login

we'll always update photo_url

self.attempted_record.photo_url = @rpx_data['profile']['photo']

end

{See the source for the sample user_session.rb}[http://github.com/tardate/rails-authlogic-rpx-sample/blob/master/app/models/user_session.rb].

==== 3c. map_added_rpx_data: user profile mapping when adding RPX to an existing account

map_added_rpx_data maps additional fields from the RPX response into the user object during the "add RPX to existing account" process.

Override this in your user model to perform field mapping as may be desired. Provide your own implementation of the map_added_rpx_data method in the User model (NOT UserSession, unlike for map_rpx_data and map_rpx_data_each_login).

In the map_added_rpx_data procedure, you will be writing to fields of the "self" object, pulling data from the rpx_data parameter. For example:

def map_added_rpx_data( rpx_data )
    # map some additional fields, e.g. photo_url
    self.photo_url = rpx_data['profile']['photo'] if photo_url.blank?
end

{See the source for the sample user.rb}[http://github.com/tardate/rails-authlogic-rpx-sample/blob/master/app/models/user.rb].

=== 4. Add application controller helpers: current_user, current_user_session

We'll add current_user and current_user_session helpers. These can then be used in controllers and views to get a handle on the "current" logged in user.

class ApplicationController < ActionController::Base helper :all # include all helpers, all the time protect_from_forgery # See ActionController::RequestForgeryProtection for details

# Scrub sensitive parameters from your log
filter_parameter_logging :password, :password_confirmation

helper_method :current_user, :current_user_session

private

def current_user_session
    return @current_user_session if defined?(@current_user_session)
    @current_user_session = UserSession.find
end

def current_user
    return @current_user if defined?(@current_user)
    @current_user = current_user_session && current_user_session.record
end

end

{See the source for the sample user_session_controller.rb}[http://github.com/tardate/rails-authlogic-rpx-sample/blob/master/app/controllers/application_controller.rb].

=== 5. Setup the Authlogic session controller

If you don't already have a user session controller, create one. There are four actions of significance for authlogic_rpx:

$ script/generate controller user_sessions index new create destroy

{See the source for the sample user_session_controller.rb}[http://github.com/tardate/rails-authlogic-rpx-sample/blob/master/app/controllers/user_sessions_controller.rb].

In config/routes.rb we can define the standard routes for this controller and two named routes for the main login/out (or singin/out if you prefer that terminology):

map.signin "signin", :controller => "user_sessions", :action => "new" map.signout "signout", :controller => "user_sessions", :action => "destroy" map.resources :user_sessions

==== index This is where RPX will return to if the user cancelled the login process, so it needs to be handled. You probably just want to redirect the user to an appropriate alternative:

def index
    redirect_to current_user ? root_url : new_user_session_url
end

==== new Typically used to render a login form

def new
    @user_session = UserSession.new
end

==== create This is where the magic happens for authentication. Authlogic hides all the underlying wiring, and you just need to "save" the session!

Authlogic_rpx provides two additional methods that you might want to use to tailor you application behaviour:

==== destroy The logout action..

def destroy
    @user_session = current_user_session
    @user_session.destroy if @user_session
    flash[:notice] = "Successfully signed out."
    redirect_to articles_path
end

=== 6. Setup the Authlogic user controller

The users controller handles the actual user creation and editing actions. In it's standard form, it looks like any other controller with an underlying ActiveRecord model.

There are five basic actions to consider. If you don't already have a controller, create it:

$ script/generate controller users new create edit show update

{See the source for the sample users_controller.rb}[http://github.com/tardate/rails-authlogic-rpx-sample/blob/master/app/controllers/users_controller.rb].

The users controller just needs standard routes defined in config/routes.rb:

map.resources :users

==== new Stock standard form for a user to register on the site. Only required if you will allow users to register without using RPX auto-registration (using standard password authentication).

def new
    @user = User.new
end

==== create As for new, stock standard and only required if you will allow users to register without using RPX auto-registration.

def create
    @user = User.new(params[:user])
    if @user.save
        flash[:notice] = "Successfully registered user."
        redirect_to articles_path
    else
        render :action => 'new'
    end
end

==== show Display's the user's profile. Uses the current_user helper that we'll include in the application controller.

def show
    @user = current_user
end

==== edit Allows the user to edit their profile. Calling valid? will ensure any validation errors are highlighted. This can be relevant with RPX since auto-registration may not include all the profile data you want to make "mandatory" for normal users.

def edit
    @user = current_user
    @user.valid?
end

==== update Handles the submission of the edit form. Again, uses the current_user helper that we'll include in the application controller.

def update
    @user = current_user
    @user.attributes = params[:user]
    if @user.save
        flash[:notice] = "Successfully updated user."
        redirect_back_or_default articles_path
    else
        render :action => 'edit'
    end
end

=== 7. Use view helpers to provide login links

So how to put a "login" link on your page? Two helper methods are provided:

Each takes an options hash:

For example, to insert a login link in a navigation bar is as simple as this:

<%= link_to "Home", root_path %> | <% if current_user %> <%= link_to "Profile", user_path(:current) %> | <%= link_to "Sign out", signout_path %> <% else %> <%= rpx_popup( :link_text => "Register/Sign in with RPX..", :app_name => RPX_APP_NAME, :return_url => user_sessions_url, :unobtrusive => false ) %>> <% end %>

NOTE: One of the most common problems people encounter in testing out authlogic_rpx is to not set the correct :app_name.

NOTE2: Make sure the application name is entered all in lowercase. If you do not, it can cause SSL certificate errors to be displayed when logging in with certain browsers (notably Android 2.1 webkit).

=== 8. Allow users to "Add RPX" to existing accounts (optional)

If you got this far and have a working application, you are ready to go, especially if you only plan to support RPX authentication.

However, if you support other authentication methods (e.g. by password), you probably want the ability to let user's add RPX to an existing account. This is not possible by default, however adding it is simply a matter of providing another method on your user controller.

The route may be called anything you like. Let's use "addrpxauth" for example.

This action has the special purpose of receiving an update of the RPX identity information

for current user - to add RPX authentication to an existing non-RPX account.

RPX only supports :post, so this cannot simply go to update method (:put)

def addrpxauth @user = current_user if @user.save flash[:notice] = "Successfully added RPX authentication for this account." render :action => 'show' else render :action => 'edit' end end

{This is demonstrated in the sample users_controller.rb}[http://github.com/tardate/rails-authlogic-rpx-sample/blob/master/app/controllers/users_controller.rb].

You'll note this is almost identical to the "update". The main difference is that it needs to be enabled for :post by RPX. In config/routes.rb:

map.addrpxauth "addrpxauth", :controller => "users", :action => "addrpxauth", :method => :post

To make an "Add RPX authentication for this account.." link, use rpx_popup as for normal RPX login, but set the return_url to the "addrpxauth" callback you have provided, and set the option :add_rpx to tru:

<%= rpx_popup( :link_text => "Add RPX authentication for this account..", :app_name => RPX_APP_NAME, :return_url => addrpxauth_url, :add_rpx => true, :unobtrusive => false ) %>

=== 9. Customise Account Merge Behaviour (optional)

Account merging is disabled by default. It is enabled by setting account_merge_enabled to true in the User model:

class User < ActiveRecord::Base acts_as_authentic do |c| c.account_merge_enabled true end end

Account merging is applicable if you have allowed users to add RPX to an existing accounts (see 8. Allow users to "Add RPX" to existing accounts). When merging is enabled, Authlogic_RPX will migrate the RPX login identifier(s) from other users who had previously claimed the identifiers now being used.

For example, take the following scenario:

Authlogic_rpx provides two hooks for customising the account merge behaviour to handle things like migration of application objects and cleaning up old accounts:

The Authlogic_RPX sample application provides an example of migrating application objects and cleaning up obsolete accounts. From the user model:

before_merge_rpx_data provides a hook for application developers to perform data migration prior to the merging of user accounts.

This method is called just before authlogic_rpx merges the user registration for 'from_user' into 'to_user'

Authlogic_RPX is responsible for merging registration data.

#

By default, it does not merge any other details (e.g. application data ownership)

# def before_merge_rpx_data( from_user, to_user ) to_user.articles << from_user.articles to_user.comments << from_user.comments end

after_merge_rpx_data provides a hook for application developers to perform account clean-up after authlogic_rpx has

migrated registration details.

#

By default, does nothing. It could, for example, be used to delete or disable the 'from_user' account

# def after_merge_rpx_data( from_user, to_user ) from_user.destroy end

{See the sample user.rb}[http://github.com/tardate/rails-authlogic-rpx-sample/blob/master/app/models/user.rb].

=== Ready to try it?

That's all there is. To see Authlogic_RPX in action, check out the demonstration Rails application:

== Improving Authlogic_RPX: next steps; how to help

Authlogic_RPX is open source and hosted on {github}[http://github.com/tardate/authlogic_rpx]. Developer's are welcome to fork and play - if you have improvements or bug fixes, just send a request to pull from your fork.

If you have issues or feedback, please log them in the {issues list on github}[http://github.com/tardate/authlogic_rpx/issues]

Some of the improvements currently on the radar:

== Note on programmatically grabbing an authenticated session

If you need to programmatically perform proxy authentication as a specific user (e.g. to run a batch process on behalf of the user), authlogic provides the necessary capability and this can be used with RPX-authenticated users too:

app.get "/" # force Authlogic::Session::Base.controller activation user = User.find(:first) session = UserSession.create(user, true) # skip authentication and log the user in directly, the true means "remember me" session.valid? => true

== Internals

Some design principles:

==== building the gem

Build and distribute (the gemcutter way):

update gemspec

$ rake gemspec

build the gem

$ rake build

push the gem to gemcutter (e.g. for version 1.0.3)

gem push authlogic_rpx-1.0.3.gem

== Kudos and Kopywrite

Thanks to {binarylogic}[http://github.com/binarylogic] for cleaning up authentication in rails by creating Authlogic in the first place and offering it to the community.

The idea of adding RPX support to authlogic is not new. Some early ideas were found in the following projects, although it was decided not to base this implementation on a fork of these, since the approaches varied considerably:

authlogic_rpx was created by Paul Gallagher (tardate.com) and released under the MIT license. Big thanks for contributions from {Joris}[http://github.com/trooster], {John}[http://gitub.com/jjb], {Damir}[http://gitub.com/sidonath], {Ben}[http://github.com/Empact]