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 git@github.com: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: scinix@gmail.com
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: scinix@gmail.com
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 => 'admin@example.net'
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!