Merge pull request #253 from differentreality/final_openid

Authenticate via openid
This commit is contained in:
Stella Rouzi 2014-06-19 15:53:56 +03:00
commit 0c698bac10
24 changed files with 412 additions and 15 deletions

View file

@ -14,6 +14,11 @@ gem 'paper_trail'
# Use devise as authentification framework
gem 'devise'
# Use omniauth to support openID authentication
gem 'omniauth'
gem 'omniauth-facebook'
gem 'omniauth-openid'
gem 'omniauth-google-oauth2'
# Use cancan as authorization framework
gem 'cancan'

View file

@ -103,6 +103,8 @@ GEM
factory_girl_rails (4.4.1)
factory_girl (~> 4.4.0)
railties (>= 3.0.0)
faraday (0.9.0)
multipart-post (>= 1.2, < 3)
ffi (1.9.3)
formatador (0.2.4)
formtastic (2.3.0.rc3)
@ -126,6 +128,7 @@ GEM
activesupport (>= 4.0.1)
haml (>= 3.1, < 5.0)
railties (>= 4.0.1)
hashie (2.1.1)
hike (1.2.3)
hoptoad_notifier (2.4.11)
activesupport
@ -143,6 +146,7 @@ GEM
jquery-ui-rails (4.2.1)
railties (>= 3.2.16)
json (1.8.1)
jwt (1.0.0)
launchy (2.4.2)
addressable (~> 2.3)
letter_opener (1.2.0)
@ -162,10 +166,34 @@ GEM
mini_portile (0.6.0)
minitest (5.3.3)
multi_json (1.9.2)
multi_xml (0.5.5)
multipart-post (2.0.0)
mysql2 (0.3.16)
nio4r (1.0.0)
nokogiri (1.6.2.1)
mini_portile (= 0.6.0)
oauth2 (0.9.4)
faraday (>= 0.8, < 0.10)
jwt (~> 1.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (~> 1.2)
omniauth (1.2.1)
hashie (>= 1.2, < 3)
rack (~> 1.0)
omniauth-facebook (1.6.0)
omniauth-oauth2 (~> 1.1)
omniauth-google-oauth2 (0.2.4)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-oauth2 (1.1.2)
faraday (>= 0.8, < 0.10)
multi_json (~> 1.3)
oauth2 (~> 0.9.3)
omniauth (~> 1.2)
omniauth-openid (1.0.1)
omniauth (~> 1.0)
rack-openid (~> 1.3.1)
orm_adapter (0.5.0)
paper_trail (3.0.1)
activerecord (>= 3.0, < 5.0)
@ -196,6 +224,9 @@ GEM
quiet_assets (1.0.2)
railties (>= 3.1, < 5.0)
rack (1.5.2)
rack-openid (1.3.1)
rack (>= 1.1.0)
ruby-openid (>= 2.1.8)
rack-test (0.6.2)
rack (>= 1.0)
rails (4.1.0)
@ -259,6 +290,7 @@ GEM
powerpack (~> 0.0.6)
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.4)
ruby-openid (2.5.0)
ruby-progressbar (1.5.1)
rubyzip (1.0.0)
sass (3.2.19)
@ -346,6 +378,10 @@ DEPENDENCIES
jquery-ui-rails
letter_opener
mysql2
omniauth
omniauth-facebook
omniauth-google-oauth2
omniauth-openid
paper_trail
paperclip
prawn_rails

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

View file

@ -1,6 +1,10 @@
class RegistrationsController < Devise::RegistrationsController
before_action :configure_permitted_parameters, if: :devise_controller?
def edit
@openids = Openid.where(user_id: current_user.id).order(:provider)
end
def update
@user = User.find(current_user.id)
email_changed = false

View file

@ -0,0 +1,34 @@
module Users
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_filter :verify_authenticity_token
[:novell, :google, :facebook, :twitter].each do |provider|
define_method(provider) { handle(provider) }
end
private
def handle(provider)
auth_hash = request.env['omniauth.auth']
openid = Openid.find_for_oauth(auth_hash) # Get or create openid
# If openid exists and is associated with a user, sign in with associated user,
# even if the email of the associated user and the email of the provided openid are different
unless user = openid.user
user = User.find_for_auth(auth_hash, current_user) # Get or create users
end
begin
user.save!
if openid.user != user
openid.user = user
end
openid.save!
sign_in user
redirect_to root_path, notice: user.email + " signed in successfully with #{provider}"
rescue Exception => e
redirect_back_or_to new_user_registration_path, alert: 'Failed' + e.message
end
end
end
end

View file

@ -37,4 +37,4 @@ class DatatableSupporters < Datatable
def columns
["name", "email", "name", "name", "name"]
end
end
end

20
app/models/openid.rb Normal file
View file

@ -0,0 +1,20 @@
class Openid < ActiveRecord::Base
belongs_to :user
validates :provider, :uid, presence: true
# Searches for openid based on provider and uid.
# Returns found openid or a new openid.
# ====Returns
# * Openid::ActiveRecord_Relation -> openid
def self.find_for_oauth(auth)
openid = Openid.where(provider: auth.provider, uid: auth.uid).first_or_initialize
if openid.new_record?
openid.email = auth.info.email
if existing_openid = Openid.where(email: openid.email).first
openid.user_id = existing_openid.user_id
end
end
openid
end
end

View file

@ -4,13 +4,15 @@ class User < ActiveRecord::Base
# :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:confirmable
:recoverable, :rememberable, :trackable, :validatable, :confirmable,
:omniauthable, omniauth_providers: [:novell, :google, :facebook]
has_and_belongs_to_many :roles
has_one :person, :inverse_of => :user
has_many :openids
attr_accessible :email, :password, :password_confirmation, :remember_me, :role_id, :role_ids, :person_attributes
attr_accessible :email, :password, :password_confirmation, :remember_me, :role_id, :role_ids,
:person_attributes
accepts_nested_attributes_for :person
accepts_nested_attributes_for :roles
@ -19,6 +21,25 @@ class User < ActiveRecord::Base
delegate :last_name, :first_name, :public_name, to: :person
# Searches for user based on email. Returns found user or new user.
# ====Returns
# * +User::ActiveRecord_Relation+ -> user
def self.find_for_auth(auth, current_user = nil)
user = current_user
if user.nil? # No current user available, user is not already logged in
user = User.where(email: auth.info.email).first_or_initialize
end
if user.new_record?
user.email = auth.info.email
user.password = Devise.friendly_token[0, 20]
user.skip_confirmation!
end
user
end
def role?(role)
Rails.logger.debug("Checking role in user")
!!roles.find_by_name(role.to_s.downcase.camelize)

View file

@ -15,6 +15,18 @@
%br
%br
= render :partial => 'devise/registrations/volunteerperson', :locals => {:p => p}
= f.inputs :name => 'OpenID' do
%h4
Currently using the following openIDs:
- @openids.each do |openid|
= openid.provider
= openid.email
%br
%h4
To add an openID to your account using a different email address, sign in with your
openID while logged in to OSEM
= render 'devise/shared/openid'
= f.inputs :name => "Account" do
= f.input :email, :required => false
= f.input :password, :hint => "(Leave blank if you don't want to change it)", :input_html => {:autocomplete => "off"}

View file

@ -3,13 +3,17 @@
%h1.text-center Sign Up
.col-md-5.col-md-offset-1
.well
= semantic_form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f|
= semantic_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
= f.input :email
= f.input :password
= f.input :password_confirmation
= f.action :submit, :as => :button, :label => "Sign Up", :button_html => {:class => "btn btn-primary"}
.col-md-3.col-md-offset-2
= f.action :submit, as: :button, label: 'Sign Up', button_html: {class: 'btn btn-primary'}
%br
%br
%h4 Already have an account?
= render 'devise/shared/links'
.col-md-4
.well
%h4
Already have an account?
= render "devise/shared/links"
%h4 Or use your openID
= render 'devise/shared/openid'

View file

@ -0,0 +1,21 @@
.row
.page-header
%h2.text-center Sign In
.col-md-5.col-md-offset-1
.well
= semantic_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
= f.input :email
= f.input :password
- if devise_mapping.rememberable?
= f.check_box :remember_me
= f.label 'Remember me'
%br
= f.action :submit, as: :button, label: 'Sign In', button_html: {class: 'btn btn-primary'}
%br
%br
= render "devise/shared/links"
.col-md-4
.well
%h4 Or use your openID
= render 'devise/shared/openid'

View file

@ -0,0 +1,19 @@
- if controller_name != 'sessions'
= link_to "Sign in", new_user_session_path
%br
- if devise_mapping.registerable? && controller_name != 'registrations'
= link_to "Sign up", new_registration_path(resource_name)
%br
- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations'
= link_to "Forgot your password?", new_password_path(resource_name)
%br
- if devise_mapping.confirmable? && controller_name != 'confirmations'
= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name)
%br
- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks'
= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name)
%br

View file

@ -0,0 +1,4 @@
- if devise_mapping.omniauthable?
- resource_class.omniauth_providers.each do |provider|
= link_to image_tag("#{provider}.png", size: '32x32') + provider , omniauth_authorize_path(resource_name, provider)
%br

View file

@ -51,5 +51,6 @@
%button.btn.btn-block Sign in
%br
= link_to "Forgot your password?", new_password_path('user')
= link_to 'Sign in with openID', new_user_session_path
%li.hidden-lg
= link_to('Sign In', new_user_session_path)

View file

@ -1,9 +1,21 @@
# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
Devise.setup do |config|
# ==> Secret key, generate one with `rake secret`
config.secret_key = Rails.application.secrets.devise_secret_key
# ==> openIDs configuration
# Define the available openID providers that can be used to log in
# Pass each provider to User model in :omniauth_providers (for open_id providers use their name)
# Include provider in users/omniauth_callbacks_controller.rb
config.omniauth :open_id, name: 'novell', identifier: 'http://www.opensuse.org/openid/user'
config.omniauth :google_oauth2, Rails.application.secrets.google_key, Rails.application.secrets.google_secret, {
name: 'google',
scope: 'email' }
config.omniauth :facebook, Rails.application.secrets.facebook_key, Rails.application.secrets.facebook_secret
# ==> Mailer Configuration
# Configure the e-mail address which will be shown in Devise::Mailer,
# note that it will be overwritten if you use your own mailer class with default "from" parameter.

View file

@ -2,7 +2,9 @@ Osem::Application.routes.draw do
get 'conference/show'
devise_for :users, controllers: { registrations: :registrations }, path: 'accounts'
devise_for :users, controllers: { registrations: :registrations,
omniauth_callbacks: 'users/omniauth_callbacks' },
path: 'accounts'
namespace :admin do
resources :users

View file

@ -0,0 +1,12 @@
class CreateOpenids < ActiveRecord::Migration
def change
create_table :openids do |t|
t.string :provider
t.string :email
t.string :uid
t.integer :user_id
t.timestamps
end
end
end

View file

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20140617145048) do
ActiveRecord::Schema.define(version: 20140618062623) do
create_table "answers", force: true do |t|
t.string "title"
@ -82,11 +82,11 @@ ActiveRecord::Schema.define(version: 20140617145048) do
t.text "description"
t.text "registration_description"
t.text "ticket_description"
t.text "sponsor_description"
t.string "sponsor_email"
t.string "twitter_url"
t.string "facebook_url"
t.string "google_url"
t.text "sponsor_description"
t.string "sponsor_email"
t.text "lodging_description"
t.boolean "include_registrations_in_splash", default: false
t.boolean "include_sponsors_in_splash", default: false
@ -213,6 +213,15 @@ ActiveRecord::Schema.define(version: 20140617145048) do
t.string "website_link"
end
create_table "openids", force: true do |t|
t.string "provider"
t.string "email"
t.string "uid"
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "people", force: true do |t|
t.string "guid", null: false
t.string "first_name", default: ""

View file

@ -0,0 +1,108 @@
require 'spec_helper'
feature Openid do
let!(:participant_role) { create(:participant_role) }
let!(:admin_role) { create(:admin_role) }
describe 'sign in with openid' do
it 'has option to log in with Google account' do
visit '/accounts/sign_in'
expect(page.has_content?('Or use your openID')).to be true
expect(page.has_content?('google')).to be true
end
it 'signs in *new* user with Google account' do
expected_count_openid = Openid.count + 1
expected_count_user = User.count + 1
visit '/accounts/sign_in'
mock_auth_new_user
click_link 'google'
expect(flash).to eq('test-1@gmail.com signed in successfully with google')
expect(Openid.count).to eq(expected_count_openid)
expect(User.count).to eq(expected_count_user)
end
it 'signs in an existing user' do
create(:participant, email: 'test-participant-1@google.com')
expected_count_openid = Openid.count + 1
expected_count_user = User.count
visit '/accounts/sign_in'
mock_auth_existing_user_participant
click_link 'google'
expect(flash).to eq('test-participant-1@google.com signed in successfully with google')
expect(Openid.count).to eq(expected_count_openid)
expect(User.count).to eq(expected_count_user)
end
it 'can handle authentication error' do
OmniAuth.config.mock_auth[:google] = :invalid_credentials
visit '/accounts/sign_in'
expect(page.has_content?('Or use your openID')).to be true
click_link 'google'
expect(flash).to eq("Could not authenticate you from Google because \"Invalid credentials\".")
end
it 'adds openid to existing user' do
# Sign in user
user = create(:participant, email: 'test-participant-1@google.com')
sign_in user
# Add openID to current user
expected_count_openid = Openid.count + 1
expected_count_user = User.count
visit '/accounts/edit'
mock_auth_new_user
click_link 'google'
expect(flash).to eq('test-participant-1@google.com signed in successfully with google')
expect(Openid.count).to eq(expected_count_openid)
expect(User.count).to eq(expected_count_user)
expect(Openid.where(email: 'test-1@gmail.com').first.nil?).to eq(false)
end
it 'signs in with openID using the same email as another associated openid' do
# Sign in user
create(:participant, email: 'test-participant-1@google.com')
expected_count_openid = Openid.count + 1
expected_count_user = User.count
visit '/accounts/sign_in'
mock_auth_existing_user_participant
click_link 'google'
expect(flash).to eq('test-participant-1@google.com signed in successfully with google')
expect(Openid.count).to eq(expected_count_openid)
expect(User.count).to eq(expected_count_user)
# Add openID to current user with email test-1@gmail.com
expected_count_openid = Openid.count + 1
expected_count_user = User.count
visit '/accounts/edit'
mock_auth_new_user
click_link 'google'
expect(flash).to eq('test-participant-1@google.com signed in successfully with google')
expect(Openid.count).to eq(expected_count_openid)
expect(User.count).to eq(expected_count_user)
expect(Openid.where(email: 'test-participant-1@google.com').first.nil?).to eq(false)
expect(Openid.where(email: 'test-1@gmail.com').first.nil?).to eq(false)
# Sign in with different openID using same email (test-1@gmail.com)
sign_out
expected_count_openid = Openid.count + 1
expected_count_user = User.count
visit '/accounts/sign_in'
mock_auth_new_user_fb
click_link 'facebook'
expect(flash).to eq('test-participant-1@google.com signed in successfully with facebook')
expect(Openid.count).to eq(expected_count_openid)
expect(User.count).to eq(expected_count_user)
last_openid = Openid.last
expect(last_openid.uid).to eq('facebook-test-uid-1')
expect(last_openid.email).to eq('test-1@gmail.com')
end
end
end

View file

@ -53,6 +53,9 @@ RSpec.configure do |config|
# Includes support/login_macros for feature tests
config.include LoginMacros, type: :feature
# Includes omniauth macro
config.include(OmniauthMacros)
# Includes support/flash for feature tests
config.include Flash, type: :feature
@ -63,3 +66,5 @@ RSpec.configure do |config|
c.syntax = :expect
end
end
OmniAuth.config.test_mode = true

View file

@ -4,7 +4,7 @@ module LoginMacros
fill_in 'user_email', with: user.email
fill_in 'user_password', with: user.password
find(:xpath, "//div[@id='content']//input[@name='commit']").click
find(:xpath, "//div[@id='content']//button[@type='submit']").click
expect(page.has_content?('Signed in successfully')).to be true
end

View file

@ -0,0 +1,68 @@
module OmniauthMacros
# The mock_auth configuration allows you to set per-provider (or default)
# authentication hashes to return during integration testing.
def mock_auth_new_user
OmniAuth.config.mock_auth[:google] =
OmniAuth::AuthHash.new(
provider: 'google',
uid: 'google-test-uid-1',
info: {
email: 'test-1@gmail.com'
},
credentials: {
token: 'mock_token',
secret: 'mock_secret'
}
)
end
def mock_auth_new_user_fb
OmniAuth.config.mock_auth[:facebook] =
OmniAuth::AuthHash.new(
provider: 'google',
uid: 'facebook-test-uid-1',
info: {
email: 'test-1@gmail.com'
},
credentials: {
token: 'mock_token',
secret: 'mock_secret'
}
)
end
def mock_auth_existing_user_participant
# The mock_auth configuration allows you to set per-provider (or default)
# authentication hashes to return during integration testing.
OmniAuth.config.mock_auth[:google] =
OmniAuth::AuthHash.new(
provider: 'google',
uid: 'google-test-uid-participant-1',
info: {
email: 'test-participant-1@google.com'
},
credentials: {
token: 'mock_token',
secret: 'mock_secret'
}
)
end
def mock_auth_existing_user_admin
# The mock_auth configuration allows you to set per-provider (or default)
# authentication hashes to return during integration testing.
OmniAuth.config.mock_auth[:google] =
OmniAuth::AuthHash.new(
provider: 'google',
uid: 'google-test-uid-admin-1',
info: {
email: 'test-admin-1@google.com'
},
credentials: {
token: 'mock_token',
secret: 'mock_secret'
}
)
end
end