Main reference of this project is below:
Start new rails app on github.
$ rails new siso --skip-bundle; cd siso <...> $ git init; git add .; git commit -m "new rails app 'siso'." $ git remote add origin [email protected]:hyeoncheon/siso.git $ git push -u origin master
and install gem for OmniAuth. (at the start, omniauth-openid only)
$ cat >>Gemfile<<EOF ### local gem 'omniauth-openid' EOF $ bundle install --path=$HOME/.gem Fetching source index for https://rubygems.org/ <...>
setup OmniAuth-OpenID.
$ cat >>config/initializers/omniauth.rb<<EOF Rails.application.config.middleware.use OmniAuth::Builder do require 'openid/store/filesystem' provider :openid, :store => OpenID::Store::Filesystem.new('/tmp') end EOF
generate new controller for session management. it handle callback from OA.
$ rails g controller sessions create app/controllers/sessions_controller.rb invoke erb create app/views/sessions invoke test_unit create test/functional/sessions_controller_test.rb invoke helper create app/helpers/sessions_helper.rb invoke test_unit create test/unit/helpers/sessions_helper_test.rb invoke assets invoke coffee create app/assets/javascripts/sessions.js.coffee invoke scss create app/assets/stylesheets/sessions.css.scss $
Finally, setup route and write callback method for test.
add to config/route.rb
match '/auth/:provider/callback', :to => 'sessions#create'
add to app/controllers/sessions_controller.rb
def create render :xml => request.env['omniauth.auth'].to_xml end
OK, then start rails server and open localhost:3000/auth/open_id this is step #1.
$ rails g model group name:string active:boolean invoke active_record create db/migrate/20130103081505_create_groups.rb create app/models/group.rb invoke test_unit create test/unit/group_test.rb create test/fixtures/groups.yml $ rails g model user name:string mail:string active:boolean group:references invoke active_record create db/migrate/20130103081720_create_users.rb create app/models/user.rb invoke test_unit create test/unit/user_test.rb create test/fixtures/users.yml $ rails g model service provider:string uid:string sname:string smail:string user:references invoke active_record create db/migrate/20130103081856_create_services.rb create app/models/service.rb invoke test_unit create test/unit/service_test.rb create test/fixtures/services.yml $ rails g controller groups create app/controllers/groups_controller.rb invoke erb create app/views/groups invoke test_unit create test/functional/groups_controller_test.rb invoke helper create app/helpers/groups_helper.rb invoke test_unit create test/unit/helpers/groups_helper_test.rb invoke assets invoke coffee create app/assets/javascripts/groups.js.coffee invoke scss create app/assets/stylesheets/groups.css.scss $ rails g controller users create app/controllers/users_controller.rb invoke erb create app/views/users invoke test_unit create test/functional/users_controller_test.rb invoke helper create app/helpers/users_helper.rb invoke test_unit create test/unit/helpers/users_helper_test.rb invoke assets invoke coffee create app/assets/javascripts/users.js.coffee invoke scss create app/assets/stylesheets/users.css.scss $ rails g controller services create app/controllers/services_controller.rb invoke erb create app/views/services invoke test_unit create test/functional/services_controller_test.rb invoke helper create app/helpers/services_helper.rb invoke test_unit create test/unit/helpers/services_helper_test.rb invoke assets invoke coffee create app/assets/javascripts/services.js.coffee invoke scss create app/assets/stylesheets/services.css.scss $ sed -i 's/:name$/:name, :null => false/' db/migrate/*groups.rb $ sed -i 's/:active$/:active, :default => true/' db/migrate/*groups.rb $ sed -i 's/:mail$/:mail, :null => false/' db/migrate/*users.rb $ sed -i 's/:active$/:active, :default => false/' db/migrate/*users.rb $ sed -i 's/:provider$/:provider, :null => false/' db/migrate/*services.rb $ sed -i 's/:uid$/:uid, :null => false/' db/migrate/*services.rb $ rake db:migrate
some default data
$ cat >>db/seeds.db <<EOF Group.create([{:name => 'admin'}, {:name => 'users'}, {:name => 'guest'}]) EOF $ rake db:seed
ok, then implement service callback.
main structure of create callback is:
def create omniauth = request.env['omniauth.auth'] ## build auth info(ai) from provider specific data structure. unless @auth = Service.find_by_provider_and_uid(ai[:provider], ai[:uid]) unless @user = User.find_by_mail(ai[:mail]) ## service and user(has same mail) not found, so register new user. user = Group.find_by_name('guest').users.create(:mail => ai[:mail]) @auth = user.services.create(:uid => ai[:uid],...) else ## new auth but user exist with same mail ## if current_user and current_user has same mail then add auth. ## or abort with error message. end else ## existing auth. so process login process. end ## build session information redirect_to services_path end
and finally generate/implement related helper and views. this is step #2.
$ echo "gem 'doorkeeper'" >> Gemfile $ bundle install --path=$HOME/.gem
then setup.
$ rails generate doorkeeper:install create config/initializers/doorkeeper.rb create config/locales/doorkeeper.en.yml route use_doorkeeper <...> $ rails g doorkeeper:migration create db/migrate/20130105165513_create_doorkeeper_tables.rb $ rake db:migrate
now implement ‘resource_owner_authenticator’ but need to improve.
User.find_by_id(session[:user]) || redirect_to(services_path)
we need to implement api for user information exchange.
add route to config/routes.rb
namespace :api do namespace :v1 do get '/me' => "credentials#me" end end
now implement simple api for user information
$ mkdir -p app/controllers/api/v1 $ cat app/controllers/api/v1/api_controller.rb module Api::V1 class ApiController < ::ApplicationController def current_resource_owner User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token end end end $ cat app/controllers/api/v1/credentials_controller.rb module Api::V1 class CredentialsController < ApiController doorkeeper_for :all respond_to :json def me respond_with current_resource_owner end end end
so now client can get user information like:
--- !map:OmniAuth::AuthHash info: !map:OmniAuth::AuthHash::InfoHash mail: [email protected] uid: 1 provider: :siso credentials: !map:Hashie::Mash expires: true expires_at: 1357487865 token: 2d8dc426827eb573aeb84e0e4608782be5f262dd84861a443e62682e45aa298b extra: !map:Hashie::Mash raw_info: !map:Hashie::Mash name: mail: [email protected] group_id: 3 id: 1 active: false created_at: "2013-01-05T17:22:12Z" updated_at: "2013-01-05T17:22:12Z"
sample provider app can be found at github.com/applicake/doorkeeper-provider-app
It works fine for me but there is some bad flow. when user not logged in on SiSO and this is first time to login, the flow like this:
client page -> server's oauth endpoint(doorkeeper) -> server's login page -> server's login handler(omniauth strategy/callback) -> server's default page.
with out login(means case of already logged in), the flow is:
client page -> server's oauth endpoint -> client page(origin)
so we need to redirect from login handler to oauth endpoint that can handle redirect to origin properly. for this, login handler should know URL of oauth endpoint.
so now:
client page -> server's oauth endpoint -> server's login page -> server's login handler -> server's oauth endpoint again -> client
for this, pass origin to omniauth strategy by add some query string. changes on app/views/layouts/application.html.erb is following:
- <%= link_to "OpenID", "/auth/open_id" %> + <%= link_to "OpenID", "/auth/open_id?#{@origin}" %>
this @origin is set by services controller:
# callback url for omniauth strategy. it works for open_id. @origin = {"origin" => params["origin"]}.to_query if params["origin"]
this params is originally passed by resource_owner_authenticator of doorkeeper:
resource_owner_authenticator do signin_path = services_path + "?" + {"origin" => request.fullpath}.to_query User.find_by_id(session[:user]) || redirect_to(signin_path) end
now it works we expected!
but above method it not pretty. I decided to using session but something is wrong. so just add dedicated cookie named siso_oauth_origin.
doorkeeper’s resource_owner_authenticator set cookie.
unless user = User.find_by_id(session[:user]) cookies[:siso_oauth_origin] = { :value => request.fullpath } redirect_to(services_path) end user
login front page does nothing.
in authentication callback, (services_controller.rb)
next_path = cookies[:siso_oauth_origin] || services_path cookies.delete :siso_oauth_origin redirect_to next_path
it’s more simple and works fine.
simply, do:
$ echo "gem 'omniauth-ldap'" >> Gemfile $ bundle install --path=$HOME/.gem
then add following to config/initializers/omniauth.rb
provider :ldap, :title => 'EXAMPLE.NET', :host => 'ldap.example.com', :port => 389, :method => :plain, :base => 'ou=Humans,dc=example,dc=net', :uid => 'mail', :password => 'bind_dn_s_password_here', :try_sasl => false, :bind_dn => '[email protected]'
finally add next to app/controllers/services_controller.rb
elsif ai[:provider].to_s == 'ldap' ai[:uid] = omniauth['extra']['raw_info']['employeenumber'] ai[:name] = omniauth['extra']['raw_info']['extensionattribute10'] ai[:mail] = omniauth['info']['email'] ai[:image] = omniauth['extra']['raw_info']['thumbnailphoto'] ai[:phone] = omniauth['info']['phone'] ai[:mobile] = omniauth['info']['mobile']
:image, :phone, :mobile are new to ldap and not currently used.
then test it, show it works!
ok, ldap has more informations. so migrate db as follow:
$ rails g migration AddAttrsToUsers mobile:string phone:string image:binary $ rake db:migrate
then add codes to controller. (with some fixes)
elsif ai[:provider].to_s == 'ldap' ai[:uid] = omniauth['extra']['raw_info']['employeenumber'].first ai[:name] = omniauth['extra']['raw_info']['extensionattribute10'].first ai[:mail] = omniauth['info']['email'] ai[:image] = omniauth['extra']['raw_info']['thumbnailphoto'].first ai[:phone] = omniauth['info']['phone'] ai[:mobile] = omniauth['info']['mobile'] user.update_attributes(:name => ai[:name], :image => ai[:image], :phone => ai[:phone], :mobile => ai[:mobile]) @auth.update_attributes(:sname => user.name)
additionally, set attr_accessible to user model.
attr_accessible :image, :mobile, :phone
we have image binary on database so write method for it:
class UsersController < ApplicationController def show_image @user = User.find(params[:id]) send_data(@user.image, :filename => @user.id.to_s + ".jpg", :type => "Image/Jpeg", :disposition => "inline") end end
finally, change views,
<img src="<%= url_for(:controller => "users", :action => "show_image", :id => session[:user]) %>" />
and route to above method:
match '/users/:id.jpg', :to => 'users#show_image'
ok, it works!
when we add binary image to model, credentials_controller does not work. (and in fact, it is not fully implemented.) so change something.
user = current_resource_owner user.image = request.protocol + request.env['HTTP_HOST'] + photo_user_path(user.id) respond_with user
as seen, method name is changed from show_iamge to photo. and some more. so routes.rb also changed.
resources :users do member do get 'photo' end end
stand-alone app!