Tutorial: Using OpenID with AuthlogicNovember 21st, 2008I know some of you have been waiting for OpenID support in Authlogic. I came up with a pretty slick solution this (check out a live example here), I think you will like it. That being said, let me show you: My dreamMy dream is that the following code, in your sessions controller, would be capable of handling any type of authentication method you throw at it: # app/controllers/user_sessions_controller.rb def create @user_session = UserSession.new(params[:user_session]) @user_session.save do |result| if result flash[:notice] = "Login successful!" redirect_back_or_default account_url else render :action => :new end end end That could handle regular authentication, OpenID authentication, Facebook connection authentication, LDAP authentication, or whatever you want, the sky is the limit. There is no OpenID cruft in your controller, and it won't get cluttered as you add more methods. Your controller stays clean and focused. Guess what? You're in luck, because the above code can do that. Here's how... My approachAs I'm sure you know, OpenID is not the only single sign on method. Microsoft has their own, Facebook has their own, corporations have their own private method, a lot of universities use LDAP, etc. So it would be silly of me to build specific OpenID support directly into Authlogic. I want to keep Authlogic focused on the core authentication methods. I want to keep it clean, small, and focused. By writing an OpenID solution specific for Authlogic I am just recreating the wheel, bloating Authlogic, and adding more responsibility to the library. All of the current OpenID solutions for ruby are very good, there is very little room for improvement. Why not use them? So I took a different approach. Instead of writing OpenID specific code, I am giving you all of the tools you need to implement any type of single sign on method easily and quickly. Now when you get that client that has to have the Facebook single sign on method, it will fit into your application nicely, and you don't have to wait for Authlogic to add support for it. Let me walk you through implementing OpenID, and you can judge for yourself how easy this is: What I'm assuming you know
Some helpful links for reference
1. Install the ruby-openid gem and open_id_authentication pluginInstall the openid gem: $ gem install ruby-openid Now install the open_id_authentication rails plugin: $ script/plugin install git://github.com/rails/open_id_authentication.git 2. Add the proper database fields$ script/generate migration add_users_openid_field Your migration should look like: 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 We are changing the login, crypted_password, and password_salt field to be optional now that your users have the OpenID option. The down method is setting all nil fields to "" otherwise you will get a database error. By default the open_id_authentication plugin uses database store to store all of the OpenID associations and nonces. It comes with a handy generator for those migrations: $ script/generate open_id_authentication_tables create_openid_tables Now let's migrate everything: $ rake db:migrate 3. Update User validationNow let's make some changes to our User model to accommodate for this: # app/models/user.rb acts_as_authentic :login_field_validation_options => {:if => :openid_identifier_blank?}, :password_field_validation_options => {:if => :openid_identifier_blank?} validate :normalize_openid_identifier validates_uniqueness_of :openid_identifier, :allow_blank => true # For acts_as_authentic configuration def openid_identifier_blank? openid_identifier.blank? end private def normalize_openid_identifier begin self.openid_identifier = OpenIdAuthentication.normalize_url(openid_identifier) if !openid_identifier.blank? rescue OpenIdAuthentication::InvalidOpenId => e errors.add(:openid_identifier, e.message) end end The above code just tells Authlogic to allow blank values for the login and password, it makes sure a login is supplied if no openid is given, it makes sure the openid is unique, and it also normalizes the openid that the user passes. Simple enough. 4. Update your UserSessionsControllerHere is my favorite part, and why Authlogic is so nice for integrating with methods like this. Change your create method to look like this: # app/controllers/user_sessions_controller.rb def create @user_session = UserSession.new(params[:user_session]) @user_session.save do |result| if result flash[:notice] = "Login successful!" redirect_back_or_default account_url else render :action => :new end end end Notice the small 1 line change? We are now passing a block to save. I think that's pretty slick because that block will do everything for you. It will redirect the user to their proper OpenID provider, thus keeping your UserSessionsController clean and focused and not full of OpenID clutter. But wait, Authlogic is not magical, we need to tell our UserSession how to integrate in with the open_id_authentication plugin. No problem... 5. Update your UserSessionLet's just override the save method and implement our OpenID specific code: # app/models/user_session.rb class UserSession < Authlogic::Session::Base attr_accessor :openid_identifier def authenticating_with_openid? !openid_identifier.blank? || controller.params[:open_id_complete] end def save(&block) if authenticating_with_openid? raise ArgumentError.new("You must supply a block to authenticate with OpenID") unless block_given? controller.send(:authenticate_with_open_id, openid_identifier) do |result, openid_identifier| if !result.successful? errors.add_to_base(result.message) yield false return end record = klass.find_by_openid_identifier(openid_identifier) if !record errors.add(:openid_identifier, "did not match any users in our database, have you set up your account to use OpenID?") yield false return end self.unauthorized_record = record super end else super end end end Let me explain what is going on here:
Final thoughtsAs you can see, you can take the above method and do pretty much anything you want. There are no restrictions and there is no authentication method that can't be implemented this way. Authlogic doesn't choose how you implement the method either. You can use any plugin you want, or write your own code, the sky is the limit. Let me know what you think, if you like it, hate it, etc. I am always looking to improve my tutorials / libraries. 12 Responses to “Tutorial: Using OpenID with Authlogic”Leave a Reply |
SearchCategories |
Interesting approach. I don’t use authlogic yet, but I’ll probably try it, along with the other new options (clearance and I forgot the other names), for some new projects.
However, for the OpenID support, there are two things that I haven’t seen people supporting:
1. If one signs up with openid, but provides only some of the required attributes (login, email, etc) they should be redirected to the signin page, with the fields they provided already filled in. Once they submit the rest of the field. The knowledge that they actually signed in via OpenId should remain. The other solutions I’ve seen just make the user go back to password auth.
I implemented this once based on the open_id_authentication plugin. The only problem was that one could fake the form after authenticating to change the openid they authenticated with. This, combined with autologin if OpenId, would allow users to login once as whatever OpenID they wanted :/.
2. IMO OpenID signin should be done in the same place as login, and just behave find_or_create style, instead of having two separate steps. Also, if one tries to signin with an already existing account, they should just be logged in.
Come to think of it, these things should be in the ontroller so I’m probably a bit offtopic. I get the feeling the underlying auth logic (heh) should be able to accomodate some of these things (getting optional parameters, remembering that the user came through some SSI, etc)
I’ll repeat Cristi in saying that’s an interesting approach! However, I’m not a fan of forcing all authentication logic into a model. It’s taking the concept of skinny controller, fat model a bit too far.
For example, here’s a controller I wrote which handles both normal authentication and OpenID authentication. It also handles cases where the user is not registered with OpenID, attempts to register there, or redirects them when not valid. http://github.com/ryanb/myideadrawer/tree/master/app/controllers/sessions_controller.rb
Yep, there’s a lot of “cruft”, but it’s the right kind of cruft. Take a look at the code. It’s all flash messages and redirects. That kind of thing belongs in a controller. It would be interesting to re-implement that same logic using your UserSession model approach. In my mind it would require auth logic to be spread across both the controller and UserSession model, but I may be wrong about that. Anyway, maybe an idea for the next blog post. :)
In your tutorial (and also your live example), error occurs when a user signs up with login and password. The code in ‘user’ model, “validates_presence_of :password …” results in error because ‘acts_as_authentic’ make password field unaccessible. I think that some code like ‘attr_reader :password’ must be added to the user model for preventing error.
Cristi and Ryan, I think you guys make some excellent points, and they are precisely why I didn’t integrate OpenID directly into Authlogic. The approach above is one of many, there are many different flavors to OpenID authentication. Crisiti, I’m going to play around with your idea and try to implement it, just to make sure Authlogic doesn’t get in the way.
Ryan, I agree there is a point where fat object skinny controller can go too far, but in this instance I don’t believe it is. The whole process of authenticating with OpenID is business logic and business logic belongs in the UserSession model. Rendering, setting up flashes, and things that deal with the interface belong in the controller. After all, the whole point of a controller is to pick which view to display and prepare everything for it. In my opinion checking the OpenID nonces and all of that madness really is more business logic. Keep in mind, I am not doing any of the redirecting either, the open\_id\_authentication plugin is doing that. A plugin written by the core rails team. My last point is that the controller you showed me is fine, but if you started adding other authentication methods, like the alternatives I listed in the tutorial, I think you would feel that controller becoming way too cluttered and confusing. Regardless, that is the beauty of rails, either approach will work just fine. In fact, integrating Authlogic into your controller would be extremely simple.
Thanks for your comments, I love the feedback.
I built a small test application using OpenID and an earlier version of authlogic, but trying to follow the lead of another project, embark, in allowing users to authenticate with mulitple OpenIDs.
I find the whole OpenID business pretty much under development and change; not sure if there is a “best practice” out there yet. In embark’s case the developer used the services at idselector.com that attempt to make it easier for users to login with common providers’ OpenIDs. I guess if you are going to allow logins using multiple OpenIDs it really makes sense for each of those logins to be a separate identity, all tied to a single user. To complicate matters, identities can pull their own nicknames and emails from the OpenID by default. At which point you have to worry about which email to use for notifications, nickname/username/email uniqueness problems, etc.
And then there’s the question of whether your site wants to allow anyone to “sign up”, or only permits approved users to access your resources. So either an administrator pre-builds user accounts (with username and password authentication) and then allows users to associate OpenIDs to them (after which point they can “forget” their passwords), or you need a state-machine-type user that goes through an administrator approval process before his account is activated.
And there’s also the use of a captcha to try to make sure that there is a real human on the client side. Is there a state or flag that needs to be set at the first login to make sure that the user has correctly answered a captcha challenge? Again I’m not enough of an expert to know where to implement this—my main experience is managing a Google Apps domain. Accounts can be provisioned, but the first time the user logs in, he/she has to respond to a captcha as well as accept the terms of use before he can go on. Otherwise the account is “ready” but not “active”.
I notice that core authlogic does pay some attention to the user state, by looking for active? approved? and confirmed? methods on the user object. I guess the captcha/terms of service acceptance could represent the transition from :approved to :confirmed. Or :confirmed could represent the fact that the user responded to an activation email. Maybe you could clarify in you README or a tutorial what you think approved vs. confirmed vs. active means.
I guess these are more subjects for tutorials and/or examples that don’t concern authlogic itself, but might be part of an authlogic-more gem. Sorry for the rambling, and I know you probably lots of reasons to just keep to authlogic proper; just thought I’d bring these up in case they had any impact on your wonderful design.
Hey Peter,
Excellent post. The above approach is just my personal approach, you can integrate OpenID support into your app however you please, just use my method above to get you started. It all comes down to setting unauthorized_record to an object, so do whatever you want before that, if that makes sense.
Also, yes Authlogic responds to the active?, approved?, and confirmed? methods as a convenience. You can define those methods any way you want in your model, Authlogic doesn’t do anything in terms of defining those. If you have more, just set up a before_validation callback or overwrite the validate method and do your own checking. The errors work just like ActiveRecord.
Let me know if this helps.
Great tutorial! However, shouldn’t the user creation and update methods validate the OpenId passed in as well? If the user mistypes their OpenId, they have no way of getting back into their account — not to mention the possibilities for spammers with one-time fake OpenIds.
Or have you left that as an exercise for the reader? ;)
Hi Matt,
The user model does validate the url by making sure it is actually an OpenID url, but you are correct, it does not redirect them to their OpenID service and make them authenticate. If you want to do this go for it. This tutorial was meant to be a starting point and more or less focus on integrating OpenID with authlogic. There are a million ways you can integrate OpenID, choose whatever you are most comfortable with. Nothing is hidden behind the scenes here, so if you wanted to integrate this feature it would be as simple as making the authenticate_with_open_id call in your UsersController. In fact, I will more than likely play around with that and add it into this tutorial. Thanks for your comment.
Hi Ben,
I’ve been playing around with it for a bit and think I have most of a solution. If you like, I could fork the tutorial project on GitHub and post my changes there?
Thanks, Matt
I’m exploring Authlogic and really like the choices you’re making code-wise—thanks for releasing this to the public.
One thing I ran into right away is that as a user I don’t necessarily “know” my OpenID url.
If I’m using a service like myopenid.com, then the URL they give me is very obviously OpenID url. (Typically something like http://myusername.myopenid.com/.) However, if I use a service like Yahoo! then I might think my OpenID url is actually http://www.yahoo.com because that’s exactly what Yahoo! tells me it is. (See http://openid.yahoo.com/, there’s magic in the background that returns your actual OpenID url.)
In other words, I don’t think it’s a safe assumption that the OpenID url people enter in your sample registration box is actually their OpenID url. Some verification needs to take place to protect users from themselves.
I’m looking into some way to handle this appropriately, and if I come up with something elegant I’ll certainly pass it along. Just an FYI.
Thanks again.
Hi Nick,
Matt already forked the authlogic example and implemented what you are talking about. I have been meaning to merge his changes and update my tutorial. You might want to take a look at his repository of authlogic_example.
Thanks Ben, I reread Matt’s suggestion after posting mine (wrong order to do that in) and realized we were saying the same thing. Sorry for the repeat, I will definitely check out his repository. Thanks again.