Skip to content
This repository has been archived by the owner on Aug 10, 2021. It is now read-only.

Latest commit

 

History

History
489 lines (368 loc) · 14.9 KB

devnote.rdoc

File metadata and controls

489 lines (368 loc) · 14.9 KB

Development Note

Start Point

Main reference of this project is below:

Create new rails app and register to github

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.

Setup Group/User/Service

$ 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.

Implement Authentication 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.

Implement OAuth Provider

Install and Setup Doorkeeper

$ 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)

Implement API

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

Troubleshoot! Redirect to origin

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!

Troubleshoot! Redirect to origin - Rewritten

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.

Add OmniAuth-LDAP

Install and Config

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!

Add New Attribute to User

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!

Some more Improvement

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

Other references

Authentication Part 1

Authentication Advanced

OAuth Single Sign On

OAuth Implements

stand-alone app!

Other Topics