Implements iChain authentication

This commit is contained in:
chrisbr 2014-11-04 17:42:47 +01:00
parent 15c6ed127f
commit 49d565def2
21 changed files with 269 additions and 126 deletions

View file

@ -14,6 +14,8 @@ gem 'paper_trail'
# Use devise as authentification framework # Use devise as authentification framework
gem 'devise' gem 'devise'
gem 'devise_ichain_authenticatable'
# Use omniauth to support openID authentication # Use omniauth to support openID authentication
gem 'omniauth' gem 'omniauth'
gem 'omniauth-facebook' gem 'omniauth-facebook'

View file

@ -118,6 +118,8 @@ GEM
railties (>= 3.2.6, < 5) railties (>= 3.2.6, < 5)
thread_safe (~> 0.1) thread_safe (~> 0.1)
warden (~> 1.2.3) warden (~> 1.2.3)
devise_ichain_authenticatable (0.3.1)
devise (>= 2.2)
diff-lcs (1.2.5) diff-lcs (1.2.5)
docile (1.1.3) docile (1.1.3)
erubis (2.7.0) erubis (2.7.0)
@ -432,6 +434,7 @@ DEPENDENCIES
database_cleaner database_cleaner
delayed_job_active_record delayed_job_active_record
devise devise
devise_ichain_authenticatable
factory_girl_rails factory_girl_rails
font-awesome-rails font-awesome-rails
formtastic (~> 2.3.0.rc3) formtastic (~> 2.3.0.rc3)

View file

@ -1,4 +1,9 @@
$(function () { $(function () {
$("#user_biography").bind('keyup', function() {
word_count(this, 'bio_length', 150);
} );
/** /**
* Displays a modal with the questions of the registration. * Displays a modal with the questions of the registration.
*/ */

View file

@ -8,24 +8,29 @@ class ApplicationController < ActionController::Base
check_authorization unless: :devise_controller? check_authorization unless: :devise_controller?
def store_location def store_location
session[:return_to] = request.fullpath if request.get? && controller_name != 'user_sessions' && controller_name != 'sessions' # store last url - this is needed for post-login redirect to whatever the user last visited.
return unless request.get?
if (request.path != '/accounts/sign_in' &&
request.path != '/accounts/sign_up' &&
request.path != '/accounts/password/new' &&
request.path != '/accounts/password/edit' &&
request.path != '/accounts/confirmation' &&
request.path != '/accounts/sign_out' &&
request.path != '/users/ichain_registration/ichain_sign_up' &&
!request.path.starts_with?(Devise.ichain_base_url) &&
!request.xhr?) # don't store ajax calls
session[:return_to] = request.fullpath
end
end end
def after_sign_in_path_for(resource) def after_sign_in_path_for(_resource)
if (can? :view, Conference) && if (can? :view, Conference) &&
(!session[:return_to] || (!session[:return_to] ||
session[:return_to] && session[:return_to] &&
session[:return_to] == root_path) session[:return_to] == root_path)
admin_conference_index_path admin_conference_index_path
else else
if session[:return_to] && session[:return_to] || root_path
!session[:return_to].start_with?(user_registration_path)
logger.debug "Returning to #{session[:return_to]}"
session[:return_to]
else
logger.debug "Not returning to #{session[:return_to]} because it would loop"
super
end
end end
end end
@ -53,6 +58,13 @@ class ApplicationController < ActionController::Base
redirect_to root_path, alert: exception.message redirect_to root_path, alert: exception.message
end end
rescue_from IChainRecordNotFound do
Rails.logger.debug('IChain Record was not Unique!')
sign_out(current_user)
flash[:error] = 'Your E-Mail adress is already registered at OSEM. Please contact the admin if you want to attach your openSUSE Account to OSEM!'
redirect_to root_path
end
def not_found def not_found
raise ActionController::RoutingError.new('Not Found') raise ActionController::RoutingError.new('Not Found')
end end

View file

@ -3,55 +3,12 @@ class RegistrationsController < Devise::RegistrationsController
def edit def edit
@openids = Openid.where(user_id: current_user.id).order(:provider) @openids = Openid.where(user_id: current_user.id).order(:provider)
super
end end
def update def update
@openids = Openid.where(user_id: current_user.id).order(:provider) @openids = Openid.where(user_id: current_user.id).order(:provider)
@user = User.find(current_user.id) super
email_changed = false
if !params[:user][:email].nil?
if @user.email != params[:user][:email]
email_changed = true
else
params[:user].delete :email
end
end
password_changed = false
if !params[:user][:password].nil?
if !params[:user][:password].empty?
password_changed = true
else
params[:user].delete :password
params[:user].delete :password_confirmation
end
end
if email_changed || password_changed
successfully_updated = @user.update_with_password(account_update_params)
else
params[:user].delete :current_password
successfully_updated = @user.update_without_password(account_update_params)
end
if successfully_updated
if email_changed
unless @user.nil?
@user.update_attribute('email', params[:user][:email])
end
set_flash_message :notice, :update_needs_confirmation
else
set_flash_message :notice, :updated
end
# Sign in the user bypassing validation in case his password changed
sign_in @user, bypass: true
redirect_to after_update_path_for(@user)
else
flash[:alert] = 'Updating account failed. ' \
"#{@user.errors.full_messages.join('. ')}."
render 'edit'
end
end end
protected protected
@ -67,8 +24,7 @@ class RegistrationsController < Devise::RegistrationsController
def configure_permitted_parameters def configure_permitted_parameters
devise_parameter_sanitizer.for(:account_update) do |u| devise_parameter_sanitizer.for(:account_update) do |u|
u. u.
permit(:email, :password, :password_confirmation, :current_password, :name, :biography, permit(:email, :password, :password_confirmation, :current_password, :username)
:nickname, :affiliation, :username)
end end
devise_parameter_sanitizer.for(:sign_up) do |u| devise_parameter_sanitizer.for(:sign_up) do |u|
u. u.

View file

@ -11,11 +11,13 @@ module Users
def handle(provider) def handle(provider)
auth_hash = request.env['omniauth.auth'] auth_hash = request.env['omniauth.auth']
uid = auth_hash[:uid]
openid = Openid.find_for_oauth(auth_hash) # Get or create openid 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, # 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 # even if the email of the associated user and the email of the provided openid are different
unless (user = openid.user) unless (user = openid.user)
user = User.find_for_auth(auth_hash, current_user) # Get or create users user = User.find_for_auth(auth_hash, current_user) # Get or create users
user.username = "#{uid}@#{provider}" if user.username.blank?
end end
begin begin
@ -28,7 +30,8 @@ module Users
sign_in user sign_in user
redirect_to root_path, notice: user.email + " signed in successfully with #{provider}" redirect_to root_path, notice: user.email + " signed in successfully with #{provider}"
rescue => e rescue => e
redirect_back_or_to new_user_registration_path, alert: 'Failed' + e.message flash[:error] = e.message
redirect_back_or_to new_user_registration_path
end end
end end
end end

View file

@ -0,0 +1,30 @@
class UsersController < ApplicationController
before_filter :verify_user
load_and_authorize_resource
# GET /users/1
def show
@events = @user.events.where(state: :confirmed)
end
# GET /users/1/edit
def edit
end
# PATCH/PUT /users/1
def update
if @user.update(user_params)
redirect_to @user, notice: 'User was successfully updated.'
else
flash[:error] = "A error prohibited your Profile from being saved: #{@user.errors.full_messages.join('. ')}."
render :edit
end
end
private
# Only allow a trusted parameter "white list" through.
def user_params
params.require(:user).permit(:user_id, :name, :biography, :nickname, :affiliation)
end
end

View file

@ -123,6 +123,7 @@ class Ability
end end
can :index, :schedule # show? can :index, :schedule # show?
can :show, User
end end
def signed_in(user) def signed_in(user)
@ -131,6 +132,9 @@ class Ability
# Conference Registration # Conference Registration
can :manage, Registration, user_id: user.id can :manage, Registration, user_id: user.id
can :manage, User, id: user.id
can :show, User
## Proposals ## Proposals
# Users can manage their own proposals # Users can manage their own proposals
can :manage, Event, id: user.events.pluck(:id) can :manage, Event, id: user.events.pluck(:id)

View file

@ -1,3 +1,6 @@
class IChainRecordNotFound < StandardError
end
class User < ActiveRecord::Base class User < ActiveRecord::Base
rolify rolify
include Gravtastic include Gravtastic
@ -8,9 +11,17 @@ class User < ActiveRecord::Base
# Include default devise modules. Others available are: # Include default devise modules. Others available are:
# :token_authenticatable, :confirmable, # :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable # :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable, devise_modules = []
:recoverable, :rememberable, :trackable, :validatable, :confirmable,
:omniauthable, omniauth_providers: [:suse, :google, :facebook, :github] if CONFIG['authentication']['ichain']['enabled']
devise_modules += [ :ichain_authenticatable, :ichain_registerable ]
else
devise_modules += [:database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable,
:omniauthable, omniauth_providers: [:suse, :google, :facebook, :github] ]
end
devise(*devise_modules)
has_and_belongs_to_many :roles has_and_belongs_to_many :roles
has_many :openids has_many :openids
@ -31,12 +42,13 @@ class User < ActiveRecord::Base
has_many :subscriptions, dependent: :destroy has_many :subscriptions, dependent: :destroy
accepts_nested_attributes_for :roles accepts_nested_attributes_for :roles
validates :name, presence: true validates :email, presence: true
validates :username, validates :username,
:uniqueness => { uniqueness: {
:case_sensitive => false case_sensitive: false
} },
presence: true
# Returns the ticket purchased ticket # Returns the ticket purchased ticket
# ====Returns # ====Returns
@ -45,10 +57,25 @@ class User < ActiveRecord::Base
ticket_purchases.where(ticket_id: id).first ticket_purchases.where(ticket_id: id).first
end end
def self.for_ichain_username(username, attributes)
user = find_by(username: username)
if user
user.update_attributes(email: attributes[:email])
else
begin
user = create(username: username, email: attributes[:email])
rescue ActiveRecord::RecordNotUnique
raise IChainRecordNotFound
end
end
user
end
def self.find_for_database_authentication(warden_conditions) def self.find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup conditions = warden_conditions.dup
if login = conditions.delete(:login) login = conditions.delete(:login)
where(conditions).where(["lower(username) = :value OR lower(email) = :value", { :value => login.downcase }]).first if login
where(conditions).where(['lower(username) = :value OR lower(email) = :value', { value: login.downcase }]).first
else else
where(conditions).first where(conditions).first
end end

View file

@ -1,36 +1,25 @@
%h1 Edit your Account
.row .row
.col-md-12 .col-md-12
= semantic_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| = semantic_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f|
= f.inputs name: 'Profile' do = f.inputs name: 'Account' do
= f.input :name, as: :string = f.input :username, required: false, input_html: { autocomplete: 'off' }
= f.input :nickname, as: :string = f.input :email, required: false, input_html: { autocomplete: 'off' }
= f.input :affiliation, as: :string, = f.input :password, hint: "(Leave blank if you don't want to change it)", input_html: { autocomplete: 'off' }
hint: 'This could be a company, a user group, or nothing at all.' = f.input :password_confirmation, input_html: { autocomplete: 'off' }
= f.input :biography, input_html: {rows: 5,
'onkeyup' => "word_count(this, 'biography-count', 150)"}
You have used
%span#biography-count #{current_user.biography_word_count}
words. Biographies are limited to 150 words.
%br
%br
= f.inputs name: 'OpenID' do = f.inputs name: 'OpenID' do
%h4 %h4
Currently the following openIDs are associated with your account Currently the following openIDs are associated with your account
- @openids.each do |openid| - @openids.each do |openid|
%li= "#{openid.provider}:#{openid.email}" %li= "#{openid.provider}:#{openid.email}"
%br %br
%h4
To add an openID with a different email address to your account, sign in with your
openID while logged in to OSEM
- if User.omniauth_providers.present? - if User.omniauth_providers.present?
%h4
To add an openID with a different email address to your account, sign in with your
openID while logged in to OSEM
#openidlinks #openidlinks
= render 'devise/shared/openid' = render 'devise/shared/openid'
= f.inputs name: 'Confirmation' do
= f.inputs name: 'Account' do
= f.input :username, required: false, input_html: {autocomplete: "off"}
= f.input :email, required: false, input_html: {autocomplete: "off"}
= f.input :password, hint: "(Leave blank if you don't want to change it)", input_html: {autocomplete: 'off'}
= f.input :password_confirmation, input_html: {autocomplete: 'off'}
= f.input :current_password, input_html: {autocomplete: 'off'}, = f.input :current_password, input_html: {autocomplete: 'off'},
hint: '(we need your current password to confirm password or email changes)' hint: '(we need your current password to confirm password, email or username changes)'
= f.action :submit, as: :button, label: 'Update', button_html: {class: 'btn btn-primary'} = f.action :submit, as: :button, label: 'Update Account', button_html: {class: 'btn btn-primary'}

View file

@ -16,7 +16,7 @@
%ul.nav.navbar-nav.navbar-right %ul.nav.navbar-nav.navbar-right
%li.dropdown %li.dropdown
%a.dropdown-toggle{"data-toggle" => "dropdown", :href => "#", id: "current-user-detail"} %a.dropdown-toggle{"data-toggle" => "dropdown", :href => "#", id: "current-user-detail"}
- if not current_user.name.empty? - if not current_user.name.blank?
#{current_user.name} #{current_user.name}
-else -else
#{current_user.email} #{current_user.email}
@ -26,13 +26,13 @@
= render 'layouts/user_menu' = render 'layouts/user_menu'
- else - else
%ul.nav.navbar-nav.navbar-right %ul.nav.navbar-nav.navbar-right
- if current_page?(new_registration_path('user')) - if CONFIG['authentication']['ichain']['enabled']
%li.active %li{:class=> "#{active_nav_li(new_ichain_registration_path('user'))}"}
= link_to(new_registration_path('user')) do = link_to(new_ichain_registration_path('user')) do
%span.fa.fa-heart %span.fa.fa-heart
Sign Up Sign Up
- else - else
%li %li{:class=> "#{active_nav_li(new_registration_path('user'))}"}
= link_to(new_registration_path('user')) do = link_to(new_registration_path('user')) do
%span.fa.fa-heart %span.fa.fa-heart
Sign Up Sign Up
@ -42,23 +42,29 @@
Sign In Sign In
%span.caret %span.caret
.dropdown-menu{:style => "padding: 17px;"} .dropdown-menu{:style => "padding: 17px;"}
= form_tag user_session_path do - if CONFIG['authentication']['ichain']['enabled']
= text_field_tag 'user[email]', nil, id: 'user_email_dd' = form_tag new_user_ichain_session_path do
= password_field_tag 'user[password]', nil, id: 'user_password_dd' = text_field_tag 'username', nil, id: 'user_ichain_email_dd'
%p.text-right = password_field_tag 'password', nil, id: 'user_ichain_password_dd'
%small %button.btn.btn-success.btn-block Sign in
Remember me - else
= check_box_tag 'user[remember_me]' = form_tag new_user_session_path do
%button.btn.btn-success.btn-block Sign in = text_field_tag 'user[login]', nil, id: 'user_login_dd'
- unless omniauth_configured.empty? = password_field_tag 'user[password]', nil, id: 'user_password_dd'
.divider %p.text-right
%h6.text-center %small
or Remember me
= render 'devise/shared/openid' = check_box_tag 'user[remember_me]'
%p.text-right %button.btn.btn-success.btn-block Sign in
%a.small{"data-toggle" => "collapse", "data-target" => "#navbar-devise-help"} - unless omniauth_configured.empty?
Need Help? .divider
#navbar-devise-help.collapse %h6.text-center
= link_to "Forgot your password?", new_password_path(User.new) or
%li.hidden-lg = render 'devise/shared/openid'
= link_to('Sign In', new_user_session_path) %p.text-right
%a.small{"data-toggle" => "collapse", "data-target" => "#navbar-devise-help"}
Need Help?
#navbar-devise-help.collapse
= link_to "Forgot your password?", new_password_path(User.new)
%li.hidden-lg
= link_to('Sign In', new_user_session_path)

View file

@ -1,16 +1,26 @@
- unless CONFIG['authentication']['ichain']['enabled']
%li
= link_to(edit_user_registration_path) do
%span.fa.fa-wrench
Edit Account
%li %li
= link_to(edit_user_registration_path) do = link_to(edit_user_path(current_user.id)) do
%span.fa.fa-wrench %span.fa.fa-user
Edit Account Edit Profil
-if @conference and @conference.id -if @conference and @conference.id
%li %li
= link_to(conference_proposal_index_path(@conference.short_title)) do = link_to(conference_proposal_index_path(@conference.short_title)) do
%span.fa.fa-comment %span.fa.fa-comment
My Submissions My Submissions
%li %li
= link_to(destroy_user_session_path, :method=>'delete') do - if CONFIG['authentication']['ichain']['enabled']
%span.fa.fa-minus = link_to(destroy_user_ichain_session_path, :method=>'delete') do
Sign out %span.fa.fa-minus
Sign out
- else
= link_to(destroy_user_session_path, :method=>'delete') do
%span.fa.fa-minus
Sign out
- if can? :index, Conference - if can? :index, Conference
%li.divider %li.divider
%li %li

View file

@ -0,0 +1,17 @@
%h1 Edit your profile
.row
.col-md-12
= semantic_form_for(@user, url: user_path(@user.id)) do |f|
= f.inputs name: 'Profile' do
= f.input :name, as: :string
= f.input :nickname, as: :string
= f.input :affiliation, as: :string,
hint: 'This could be a company, a user group, or nothing at all.'
= f.input :biography, input_html: { rows: 5, 'onkeyup' => "word_count(this, 'biography-count', 150)" }
You have used
%span#bio_length
0
words. Biographies are limited to 150 words.
%br
%br
= f.action :submit, as: :button, label: 'Update', button_html: {class: 'btn btn-primary'}

View file

@ -0,0 +1,16 @@
%h1
= @user.name
%h4
= @user.biography
- if @user.events.confirmed.any?
%h3
= "#{@user.name} presents #{pluralize(@user.events.confirmed.count, 'Event')}:"
%ul.list-unstyled
- @user.events.confirmed.each do |event|
%li
%h4
= link_to event.title, conference_proposal_path(event.conference.short_title, event.id)
%strong
at
= event.conference.title

View file

@ -16,6 +16,11 @@ defaults: &defaults
speakerdeck: 'Speakerdeck', speakerdeck: 'Speakerdeck',
instagram: 'Instagram' } instagram: 'Instagram' }
# If you want to use iChain to handle registration and authentication enable the next lines
authentication:
ichain:
enabled: false
development: development:
<<: *defaults <<: *defaults

View file

@ -244,4 +244,35 @@ Devise.setup do |config|
# When using omniauth, Devise cannot automatically set Omniauth path, # When using omniauth, Devise cannot automatically set Omniauth path,
# so you need to do it manually. For the users scope, it would be: # so you need to do it manually. For the users scope, it would be:
# config.omniauth_path_prefix = "/my_engine/users/auth" # config.omniauth_path_prefix = "/my_engine/users/auth"
# You will always need to set this parameter.
config.ichain_base_url = "https://my.application.org"
# Paths (relative to ichain_base_url) used by your proxy
# config.ichain_login_path = "ICSLogin/"
# config.ichain_registration_path = "ICSLogin/auth-up/"
# config.ichain_logout_path = "cmd/ICSLogout/"
# The header used by your iChain proxy to pass the username.
# config.ichain_username_header = "HTTP_X_USERNAME"
# Additional parameters, beyond the username, provided by the iChain proxy.
# HTTP_X_EMAIL is expected by default. Set to {} if no additional attributes
# are configured in the proxy.
# config.ichain_attributes_header = {:email => "HTTP_X_EMAIL"}
# Configuration options for requests sent to the iChain proxy
# config.ichain_context = "default"
# config.ichain_proxypath = "reverse"
# Activate the test mode, useful when no real iChain is present, like in
# testing and development environments
# config.ichain_test_mode = true
# In test mode, you can skip asking for the user information by always
# forcing the following username. The user will be permanently signed in.
# config.ichain_force_test_username = "testuser"
# In test mode, force the following additional attributes
# config.ichain_force_test_attributes = {:email => "testuser@example.com"}
end end

View file

@ -17,7 +17,7 @@ en:
unauthenticated: 'You need to sign in or sign up before continuing.' unauthenticated: 'You need to sign in or sign up before continuing.'
unconfirmed: 'You have to confirm your account before continuing.' unconfirmed: 'You have to confirm your account before continuing.'
locked: 'Your account is locked.' locked: 'Your account is locked.'
invalid: 'Invalid email or password.' invalid: 'Invalid login or password.'
invalid_token: 'Invalid authentication token.' invalid_token: 'Invalid authentication token.'
timeout: 'Your session expired, please sign in again to continue.' timeout: 'Your session expired, please sign in again to continue.'
inactive: 'Your account was not activated yet.' inactive: 'Your account was not activated yet.'

View file

@ -1,8 +1,16 @@
Osem::Application.routes.draw do Osem::Application.routes.draw do
devise_for :users, controllers: { registrations: :registrations, if CONFIG['authentication']['ichain']['enabled']
omniauth_callbacks: 'users/omniauth_callbacks' }, devise_for :users, controllers: { registrations: :registrations }
path: 'accounts' else
devise_for :users,
controllers: {
registrations: :registrations,
omniauth_callbacks: 'users/omniauth_callbacks' },
path: 'accounts'
end
resources :users, except: [:new, :index, :create, :destroy]
namespace :admin do namespace :admin do
resources :users resources :users

View file

@ -0,0 +1,18 @@
class GenerateUsername < ActiveRecord::Migration
class TempUser < ActiveRecord::Base
self.table_name = 'users'
attr_accessible :username, :email
end
def change
TempUser.all.each do |user|
if user.username.blank?
username = user.email.split('@')[0]
if TempUser.find_by(username: username)
username = username + user.id.to_s
end
user.update_attributes(username: username)
end
end
end
end

View file

@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20141103132913) do ActiveRecord::Schema.define(version: 20141104131625) do
create_table "ahoy_events", force: true do |t| create_table "ahoy_events", force: true do |t|
t.uuid "visit_id" t.uuid "visit_id"

View file

@ -3,6 +3,7 @@ FactoryGirl.define do
factory :user do factory :user do
sequence(:email) { |n| "example#{n}@example.com" } sequence(:email) { |n| "example#{n}@example.com" }
sequence(:name) { |n| "name#{n}" } sequence(:name) { |n| "name#{n}" }
sequence(:username) { |n| "username#{n}" }
password 'changeme' password 'changeme'
password_confirmation 'changeme' password_confirmation 'changeme'
confirmed_at { Time.now } confirmed_at { Time.now }