First you'll need to register your application so that you can obtain your 37Signals client credentials. I recommend setting the redirect URI to http://127.0.0.1/users/auth/37signals/callback
so that you can test it locally if you need to. If you're using pow.cx you can specify that symlink as the default and easily test your OAuth integration. Even better if you're using the powder gem you can just run "powder default" from the app root and it'll link that up as the default app which will then respond at 127.0.0.1.
Next you'll need the omniauth-37signals gem. I'm using Devise with Omniauth support so I'm going to assume you're doing something similar. Your Gemfile should have the following in regards to devise/omniauth, upgrade versions as needed:
gem "devise", ">= 2.2.3"
gem "omniauth-37signals", "~> 1.0.5"
Run bundle install to install the new gems and then run "rails generate devise:install" to create the default devise.rb
initializer file. Then configure the omniauth provider for 37Signals:
Devise.setup do |config|
config.omniauth '37signals', 'id', 'secret'
end
You'll probably want to setup a model to interact with devise, go ahead and run that next "rails generate devise User email name". This will generate the User model as well as bunch of other relevant files and methods for interacting with your user. Typically you'll want to authenticate with multiple OAuth providers, the easiest way to do this is to create a Authorization model which will store the relevant OAuth provider's details:
rails generate model Authorization provider:string uid:string user:references oauth_token:text oauth_refresh_token:text oath_expires_at:datetime
Make sure you set the oauthtoken
and oauthrefreshtoken
as text columns, they use pretty long keys and they usually get truncated on accident if you use a varchar. OAuth tokens from 37Signals also expire after a given amount of time, they provide this date in the returned authentication details so we'd like to store that for future reference. The oauthrefreshtoken is used to obtain a new oauthtoken without needing to prompt the user to go through the OAuth process again, if we wanted to we could just run a background job to refresh tokens that are about to expire.
After you've done that make sure to migrate and then setup your models with the following settings:
class User < ActiveRecord::Base
attr_accessible :name,
:email,
:password,
:password_confirmation,
:remember_me
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:omniauthable
has_many :authorizations
end
class Authorization < ActiveRecord::Base
belongs_to :user
attr_accessible :provider,
:uid,
:oauth_token,
:oauth_refresh_token,
:oauth_expires_at
validates :provider, :uid, :presence => true
end
Next you'll need to make sure you have the proper routes setup.
devise_scope :user do
# Normally the provider is mapped back to an action such as :facebook mapping back to the "facebook" action within
# the controller. Since we can't name an action "37signals" we have to map it to a method name that is allowed.
get '/users/auth/37signals/callback' => 'users/omniauth_callbacks#thirty_seven_signals'
end
devise_for :users, :controllers => { :registrations => 'registrations', :omniauth_callbacks => "users/omniauth_callbacks" }
Now you'll need to create your omniauth callback controller:
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def thirty_seven_signals
oauthorize
end
def passthru
render :file => "#{Rails.root}/public/404.html", :status => 404, :layout => false
end
private
def oauthorize
@user = find_for_ouath(env["omniauth.auth"], current_user)
if @user
flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => kind
session["devise.#{kind.downcase}_data"] = env["omniauth.auth"]
sign_in_and_redirect @user, :event => :authentication
end
end
def find_for_ouath(access_token, resource=nil)
if resource
# Means our user is signed in. Add the authorization to the user
resource.add_provider(access_token)
else
auth = Authorization.find_or_create(access_token)
auth.user
end
end
end
Now we'll need to add in those new methods that we just used in User and Authorization.
def self.find_or_create(auth_hash)
unless auth = find_by_provider_and_uid(auth_hash["provider"], auth_hash["uid"])
user = User.find_or_create_by_email(auth_hash["info"]["email"])
user.update_attribute(:name, auth_hash["info"]["name"])
auth = user.authorizations.create(
:provider => auth_hash["provider"],
:uid => auth_hash["uid"],
:oauth_token => auth_hash["credentials"]["token"],
:oauth_refresh_token => auth_hash["credentials"]["refresh_token"],
:oauth_expires_at => auth_hash["extra"]["raw_info"]["expires_at"]
)
end
auth
end
def add_provider(auth_hash)
# Check if the provider already exists, so we don't add it twice
unless authorizations.find_by_provider_and_uid(auth_hash["provider"], auth_hash["uid"])
authorizations.create(
:provider => auth_hash["provider"],
:uid => auth_hash["uid"],
:oauth_token => auth_hash["credentials"]["token"],
:oauth_refresh_token => auth_hash["credentials"]["refresh_token"],
:oauth_expires_at => auth_hash["extra"]["raw_info"]["expires_at"]
)
end
self
end
This is what env["omniauth.auth"] looks like when a user comes back from connecting your application, just for reference:
{
"provider": "37signals",
"uid": 555,
"info": {
"email": "user@example.com",
"first_name": "John",
"last_name": "Smith",
"name": "John Smith"
},
"credentials": {
"token": "...",
"refresh_token": "...",
"expires_at": 1362252189,
"expires": true
},
"extra": {
"accounts": [{
"name": "Basecamp Project 1",
"href": "https://project1.basecamphq.com",
"id": 123,
"product": "basecamp"
}, {
"name": "Campfire account",
"href": "https://campfire1.campfirenow.com",
"id": 456,
"product": "campfire"
}]
},
"raw_info": {
"accounts": [{
"name": "Basecamp Project 1",
"href": "https://project1.basecamphq.com",
"id": 123,
"product": "basecamp"
}, {
"name": "Campfire account",
"href": "https://campfire1.campfirenow.com",
"id": 456,
"product": "campfire"
}]
},
"expires_at": "2013-03-02T19:23:09Z",
"identity": {
"id": 555,
"last_name": "Smith",
"email_address": "user@example.com",
"first_name": "John"
}
}
Once you've setup all of that the last thing to do is just provide a link to get them connected to the OAuth dialog box by using "useromniauthauthorize_path('37signals')".
If you want to test sign in via OAuth and the 37Signals provider then you can use the add_mock method. If you place this in features/support/omniauth.rb it'll load and take over whenever you hit the OAuth URLs to properly return the specified hash.
OmniAuth.config.test_mode = true
# mock_auth expects a symbol so you have to provide it in this manner for it to properly reference it when mocking the OAuth calls. I've only added the items that I needed for my application, there's more that's returned as you'll notice in the example hash above.
OmniAuth.config.mock_auth[:"37signals"] = {
"provider"=> "37signals",
"uid"=> "556595",
"info"=> {
"email"=> "test@xxxx.com",
"name"=> "Test User"
},
"credentials" => {
"token" => "secret-token",
"refresh_token" => "refresh-token",
"expires_at" => 1362252189
},
"extra" => {
"accounts" => [{
"name" => "Campfire account",
"href" => "https://example.basecamphq.com",
"id" => 1399584,
"product" => "campfire"
}],
"raw_info" => {"expires_at" => 20.days.from_now}
}
}