devise_g5_authenticatable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +25 -0
  6. data/Gemfile +23 -0
  7. data/LICENSE +20 -0
  8. data/README.md +243 -0
  9. data/Rakefile +20 -0
  10. data/app/controllers/devise_g5_authenticatable/registrations_controller.rb +5 -0
  11. data/app/controllers/devise_g5_authenticatable/sessions_controller.rb +58 -0
  12. data/circle.yml +4 -0
  13. data/config/initializers/devise_g5_authenticatable.rb +3 -0
  14. data/config/locales/en.yml +6 -0
  15. data/devise_g5_authenticatable.gemspec +24 -0
  16. data/lib/devise_g5_authenticatable.rb +16 -0
  17. data/lib/devise_g5_authenticatable/controllers/helpers.rb +37 -0
  18. data/lib/devise_g5_authenticatable/controllers/url_helpers.rb +13 -0
  19. data/lib/devise_g5_authenticatable/engine.rb +11 -0
  20. data/lib/devise_g5_authenticatable/g5.rb +4 -0
  21. data/lib/devise_g5_authenticatable/g5/auth_password_validator.rb +30 -0
  22. data/lib/devise_g5_authenticatable/g5/auth_user_creator.rb +48 -0
  23. data/lib/devise_g5_authenticatable/g5/auth_user_updater.rb +43 -0
  24. data/lib/devise_g5_authenticatable/g5/user_exporter.rb +61 -0
  25. data/lib/devise_g5_authenticatable/models/g5_authenticatable.rb +99 -0
  26. data/lib/devise_g5_authenticatable/models/protected_attributes.rb +16 -0
  27. data/lib/devise_g5_authenticatable/omniauth.rb +9 -0
  28. data/lib/devise_g5_authenticatable/routes.rb +58 -0
  29. data/lib/devise_g5_authenticatable/version.rb +3 -0
  30. data/lib/tasks/g5/export_users.rake +13 -0
  31. data/spec/controllers/helpers_spec.rb +295 -0
  32. data/spec/controllers/sessions_controller_spec.rb +256 -0
  33. data/spec/controllers/url_helpers_spec.rb +332 -0
  34. data/spec/dummy/.gitignore +15 -0
  35. data/spec/dummy/README.rdoc +261 -0
  36. data/spec/dummy/Rakefile +7 -0
  37. data/spec/dummy/app/assets/images/rails.png +0 -0
  38. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  39. data/spec/dummy/app/assets/javascripts/custom_sessions.js +2 -0
  40. data/spec/dummy/app/assets/javascripts/home.js +2 -0
  41. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  42. data/spec/dummy/app/assets/stylesheets/custom_sessions.css +4 -0
  43. data/spec/dummy/app/assets/stylesheets/home.css +4 -0
  44. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  45. data/spec/dummy/app/controllers/custom_registrations_controllers.rb +2 -0
  46. data/spec/dummy/app/controllers/custom_sessions_controller.rb +2 -0
  47. data/spec/dummy/app/controllers/home_controller.rb +4 -0
  48. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  49. data/spec/dummy/app/helpers/custom_sessions_helper.rb +2 -0
  50. data/spec/dummy/app/helpers/home_helper.rb +2 -0
  51. data/spec/dummy/app/mailers/.gitkeep +0 -0
  52. data/spec/dummy/app/models/admin.rb +3 -0
  53. data/spec/dummy/app/models/user.rb +10 -0
  54. data/spec/dummy/app/views/anonymous/new.html.erb +0 -0
  55. data/spec/dummy/app/views/home/index.html.erb +1 -0
  56. data/spec/dummy/app/views/layouts/application.html.erb +16 -0
  57. data/spec/dummy/config.ru +4 -0
  58. data/spec/dummy/config/application.rb +64 -0
  59. data/spec/dummy/config/boot.rb +10 -0
  60. data/spec/dummy/config/database.yml.ci +6 -0
  61. data/spec/dummy/config/database.yml.sample +13 -0
  62. data/spec/dummy/config/environment.rb +5 -0
  63. data/spec/dummy/config/environments/development.rb +39 -0
  64. data/spec/dummy/config/environments/production.rb +67 -0
  65. data/spec/dummy/config/environments/test.rb +37 -0
  66. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  67. data/spec/dummy/config/initializers/devise.rb +259 -0
  68. data/spec/dummy/config/initializers/inflections.rb +15 -0
  69. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  70. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  71. data/spec/dummy/config/initializers/session_store.rb +8 -0
  72. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  73. data/spec/dummy/config/locales/devise.en.yml +60 -0
  74. data/spec/dummy/config/locales/en.yml +5 -0
  75. data/spec/dummy/config/routes.rb +70 -0
  76. data/spec/dummy/db/migrate/20131230235849_devise_create_users.rb +42 -0
  77. data/spec/dummy/db/migrate/20140102213131_drop_database_authenticatable.rb +16 -0
  78. data/spec/dummy/db/migrate/20140103032308_drop_recoverable.rb +16 -0
  79. data/spec/dummy/db/migrate/20140103042329_drop_rememberable.rb +13 -0
  80. data/spec/dummy/db/migrate/20140103174810_add_omniauth_columns_to_users.rb +18 -0
  81. data/spec/dummy/db/migrate/20140103191601_add_email_back_to_user.rb +8 -0
  82. data/spec/dummy/db/migrate/20140113202948_devise_create_admins.rb +42 -0
  83. data/spec/dummy/db/migrate/20140113233821_add_provider_and_uid_to_admins.rb +8 -0
  84. data/spec/dummy/db/schema.rb +50 -0
  85. data/spec/dummy/db/seeds.rb +7 -0
  86. data/spec/dummy/lib/assets/.gitkeep +0 -0
  87. data/spec/dummy/lib/tasks/.gitkeep +0 -0
  88. data/spec/dummy/log/.gitkeep +0 -0
  89. data/spec/dummy/public/404.html +26 -0
  90. data/spec/dummy/public/422.html +26 -0
  91. data/spec/dummy/public/500.html +25 -0
  92. data/spec/dummy/public/favicon.ico +0 -0
  93. data/spec/dummy/public/robots.txt +5 -0
  94. data/spec/dummy/script/rails +6 -0
  95. data/spec/dummy/vendor/assets/javascripts/.gitkeep +0 -0
  96. data/spec/dummy/vendor/assets/stylesheets/.gitkeep +0 -0
  97. data/spec/dummy/vendor/plugins/.gitkeep +0 -0
  98. data/spec/factories/admin.rb +10 -0
  99. data/spec/factories/user.rb +10 -0
  100. data/spec/features/edit_registration_spec.rb +109 -0
  101. data/spec/features/registration_spec.rb +99 -0
  102. data/spec/features/sign_in_spec.rb +91 -0
  103. data/spec/features/sign_out_spec.rb +7 -0
  104. data/spec/g5/auth_password_validator_spec.rb +81 -0
  105. data/spec/g5/auth_user_creator_spec.rb +100 -0
  106. data/spec/g5/auth_user_updater_spec.rb +113 -0
  107. data/spec/g5/user_exporter_spec.rb +105 -0
  108. data/spec/models/g5_authenticatable_spec.rb +540 -0
  109. data/spec/models/protected_attributes_spec.rb +17 -0
  110. data/spec/routing/registrations_routing_spec.rb +107 -0
  111. data/spec/routing/sessions_routing_spec.rb +111 -0
  112. data/spec/spec_helper.rb +44 -0
  113. data/spec/support/devise.rb +3 -0
  114. data/spec/support/omniauth.rb +3 -0
  115. data/spec/support/shared_contexts/oauth_error.rb +9 -0
  116. data/spec/support/shared_contexts/rake.rb +21 -0
  117. data/spec/support/shared_examples/registration_error.rb +15 -0
  118. data/spec/support/user_feature_methods.rb +26 -0
  119. data/spec/tasks/export_users_spec.rb +90 -0
  120. metadata +293 -0
@@ -0,0 +1,3 @@
1
+ unless defined?(ActionController::StrongParameters)
2
+ require 'devise_g5_authenticatable/models/protected_attributes'
3
+ end
@@ -0,0 +1,6 @@
1
+ en:
2
+ devise:
3
+ sessions:
4
+ signed_in: 'Signed in successfully.'
5
+ not_found: 'You must sign up before continuing.'
6
+ failure: 'Could not authenticate you from %{kind} because "%{reason}".'
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'devise_g5_authenticatable/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'devise_g5_authenticatable'
8
+ spec.version = DeviseG5Authenticatable::VERSION
9
+ spec.authors = ['Maeve Revels']
10
+ spec.email = ['maeve.revels@getg5.com']
11
+ spec.description = 'Devise extension for the G5 Auth service'
12
+ spec.summary = 'Devise extension for the G5 Auth service'
13
+ spec.homepage = 'https://github.com/G5/devise_g5_authenticatable'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'devise', '~> 3.0'
22
+ spec.add_dependency 'g5_authentication_client', '~> 0.2'
23
+ spec.add_dependency 'omniauth-g5', '~> 0.1'
24
+ end
@@ -0,0 +1,16 @@
1
+ require 'devise_g5_authenticatable/version'
2
+
3
+ require 'devise'
4
+
5
+ require 'devise_g5_authenticatable/omniauth'
6
+ require 'devise_g5_authenticatable/routes'
7
+ require 'devise_g5_authenticatable/controllers/helpers'
8
+ require 'devise_g5_authenticatable/controllers/url_helpers'
9
+
10
+ require 'devise_g5_authenticatable/engine'
11
+
12
+ Devise.add_module(:g5_authenticatable,
13
+ strategy: false,
14
+ route: {session: [nil, :new, :destroy]},
15
+ controller: :sessions,
16
+ model: 'devise_g5_authenticatable/models/g5_authenticatable')
@@ -0,0 +1,37 @@
1
+ module DeviseG5Authenticatable
2
+ module Helpers
3
+ extend ActiveSupport::Concern
4
+
5
+ def clear_blank_passwords
6
+ Devise.mappings.keys.each do |scope|
7
+ if params[scope].present?
8
+ password_params(scope).each { |p| clear_blank_param(scope, p) }
9
+ end
10
+ end
11
+ end
12
+
13
+ def password_params(scope)
14
+ params[scope].keys.select { |k| k =~ /password/ }
15
+ end
16
+
17
+ def clear_blank_param(scope, param_name)
18
+ params[scope].delete(param_name) if params[scope][param_name].blank?
19
+ end
20
+
21
+ def handle_resource_error(error)
22
+ resource.errors[:base] << error.message
23
+ respond_with(resource)
24
+ end
25
+
26
+ module ClassMethods
27
+ def define_helpers(mapping)
28
+ class_eval <<-METHODS, __FILE__, __LINE__ + 1
29
+ def set_updated_by_#{mapping}
30
+ resource_params = params[:#{mapping}] || params
31
+ resource_params[:updated_by] = current_#{mapping}
32
+ end
33
+ METHODS
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,13 @@
1
+ module DeviseG5Authenticatable
2
+ module UrlHelpers
3
+ def g5_authorize_path(resource_or_scope, *args)
4
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
5
+ _devise_route_context.send("#{scope}_g5_authorize_path", *args)
6
+ end
7
+
8
+ def g5_callback_path(resource_or_scope, *args)
9
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
10
+ _devise_route_context.send("#{scope}_g5_callback_path", *args)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module DeviseG5Authenticatable
2
+ class Engine < Rails::Engine
3
+ initializer "devise_g5_authenticatable.helpers" do
4
+ Devise.include_helpers(DeviseG5Authenticatable)
5
+ end
6
+
7
+ rake_tasks do
8
+ load 'tasks/g5/export_users.rake'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ require 'devise_g5_authenticatable/g5/auth_user_creator'
2
+ require 'devise_g5_authenticatable/g5/auth_user_updater'
3
+ require 'devise_g5_authenticatable/g5/auth_password_validator'
4
+ require 'devise_g5_authenticatable/g5/user_exporter'
@@ -0,0 +1,30 @@
1
+ require 'g5_authentication_client'
2
+
3
+ module Devise
4
+ module G5
5
+ class AuthPasswordValidator
6
+ attr_reader :model
7
+
8
+ def initialize(authenticatable_model)
9
+ @model = authenticatable_model
10
+ end
11
+
12
+ def valid_password?(password)
13
+ begin
14
+ auth_user = auth_client(password).me
15
+ rescue OAuth2::Error => error
16
+ raise unless error.code == 'invalid_resource_owner'
17
+ rescue RuntimeError => error
18
+ raise unless error.message =~ /Insufficient credentials/
19
+ end
20
+
21
+ !auth_user.nil?
22
+ end
23
+
24
+ private
25
+ def auth_client(password)
26
+ G5AuthenticationClient::Client.new(username: model.email, password: password)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,48 @@
1
+ require 'g5_authentication_client'
2
+
3
+ module Devise
4
+ module G5
5
+ class AuthUserCreator
6
+ attr_reader :model
7
+
8
+ def initialize(authenticatable_model)
9
+ @model = authenticatable_model
10
+ end
11
+
12
+ def create
13
+ create_auth_user unless auth_user_exists?
14
+ end
15
+
16
+ private
17
+ def create_auth_user
18
+ auth_user = auth_client.create_user(auth_user_args)
19
+ set_auth_attributes(auth_user)
20
+ auth_user
21
+ end
22
+
23
+ def auth_user_exists?
24
+ !model.uid.blank?
25
+ end
26
+
27
+ def auth_client
28
+ G5AuthenticationClient::Client.new(access_token: updated_by.g5_access_token)
29
+ end
30
+
31
+ def updated_by
32
+ model.updated_by || model
33
+ end
34
+
35
+ def auth_user_args
36
+ {email: model.email,
37
+ password: model.password,
38
+ password_confirmation: model.password_confirmation}
39
+ end
40
+
41
+ def set_auth_attributes(auth_user)
42
+ model.provider = 'g5'
43
+ model.uid = auth_user.id
44
+ model.clean_up_passwords
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,43 @@
1
+ require 'g5_authentication_client'
2
+
3
+ module Devise
4
+ module G5
5
+ class AuthUserUpdater
6
+ attr_reader :model
7
+
8
+ def initialize(authenticatable_model)
9
+ @model = authenticatable_model
10
+ end
11
+
12
+ def update
13
+ update_auth_user if credentials_changed?
14
+ end
15
+
16
+ private
17
+ def update_auth_user
18
+ auth_user = auth_client.update_user(auth_user_args)
19
+ model.clean_up_passwords
20
+ auth_user
21
+ end
22
+
23
+ def credentials_changed?
24
+ model.email_changed? || !model.password.blank?
25
+ end
26
+
27
+ def auth_client
28
+ G5AuthenticationClient::Client.new(access_token: updated_by.g5_access_token)
29
+ end
30
+
31
+ def updated_by
32
+ model.updated_by || model
33
+ end
34
+
35
+ def auth_user_args
36
+ {id: model.uid,
37
+ email: model.email,
38
+ password: model.password,
39
+ password_confirmation: model.password_confirmation}
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,61 @@
1
+ require 'g5_authentication_client'
2
+
3
+ module G5
4
+ # Exports all users to the G5 auth server.
5
+ # Assumes presence of User model with uid and
6
+ # provider attributes.
7
+ class UserExporter
8
+ # @param [Hash] options the options to export users with.
9
+ # @option options [String] :client_id the G5 OAuth client ID
10
+ # @option options [String] :client_secret the G5 OAuth client secret
11
+ # @option options [String] :redirect_uri the redirect URI registered with G5
12
+ # @option options [String] :endpoint the endpoint for the G5 Auth server
13
+ # @option options [String] :authorization_code the G5 authorization code to obtain an access token
14
+ def initialize(options={})
15
+ @client_id = options[:client_id]
16
+ @client_secret = options[:client_secret]
17
+ @redirect_uri = options[:redirect_uri]
18
+ @endpoint = options[:endpoint]
19
+ @authorization_code = options[:authorization_code]
20
+ end
21
+
22
+ # Export local users to the G5 Auth server.
23
+ # A record will be created in G5 Auth and associated with each
24
+ # local User. Password data is not automatically
25
+ # exported, but is returned in a dump of SQL update
26
+ # statements suitable for executing on the G5 Auth server.
27
+ #
28
+ # @return [String] SQL dump containing encrypted user passwords
29
+ def export
30
+ update_statements = User.all.collect do |user|
31
+ # The user won't actually be able to log in with their usual password,
32
+ # but at least it won't be set to a guessable value
33
+ auth_user = auth_client.create_user(email: user.email,
34
+ password: user.encrypted_password)
35
+ update_local_user(user, auth_user)
36
+ update_sql(auth_user.id, user.encrypted_password)
37
+ end
38
+
39
+ update_statements.join("\n")
40
+ end
41
+
42
+ private
43
+ def update_local_user(local_user, auth_user)
44
+ local_user.uid = auth_user.id
45
+ local_user.provider = 'g5'
46
+ local_user.save
47
+ end
48
+
49
+ def update_sql(uid, password)
50
+ "update users set encrypted_password='#{password}' where id=#{uid};"
51
+ end
52
+
53
+ def auth_client
54
+ @oauth_client ||= G5AuthenticationClient::Client.new(client_id: @client_id,
55
+ client_secret: @client_secret,
56
+ redirect_uri: @redirect_uri,
57
+ endpoint: @endpoint,
58
+ authorization_code: @authorization_code)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,99 @@
1
+ require 'devise_g5_authenticatable/g5'
2
+
3
+ module Devise
4
+ module Models
5
+ # Authenticatable module, responsible for remote credential management
6
+ # in G5 Auth.
7
+ #
8
+ # The module assumes that the following attributes have already been defined
9
+ # on the model:
10
+ # * `provider`: the value will always be 'g5' for G5 Auth users
11
+ # * `uid`: the unique id for this user in G5 Auth
12
+ # * `g5_access_token`: the current OAuth access token, if one exists
13
+ module G5Authenticatable
14
+ extend ActiveSupport::Concern
15
+
16
+ included do
17
+ attr_accessor :password, :password_confirmation, :current_password,
18
+ :updated_by
19
+
20
+ before_save :auth_user
21
+ end
22
+
23
+ def auth_user
24
+ begin
25
+ if new_record?
26
+ G5::AuthUserCreator.new(self).create
27
+ else
28
+ G5::AuthUserUpdater.new(self).update
29
+ end
30
+ rescue OAuth2::Error => e
31
+ logger.error("Couldn't save user credentials because: #{e}")
32
+ raise ActiveRecord::RecordNotSaved.new(e.code)
33
+ rescue StandardError => e
34
+ logger.error("Couldn't save user credentials because: #{e}")
35
+ raise ActiveRecord::RecordNotSaved.new(e.message)
36
+ end
37
+ end
38
+
39
+ def clean_up_passwords
40
+ self.password = self.password_confirmation = self.current_password = nil
41
+ end
42
+
43
+ def valid_password?(password_to_check)
44
+ validator = Devise::G5::AuthPasswordValidator.new(self)
45
+ validator.valid_password?(password_to_check)
46
+ end
47
+
48
+ def update_with_password(params)
49
+ updated_attributes = params.reject { |k,v| k =~ /password/ && v.blank? }
50
+ current_password = updated_attributes.delete(:current_password)
51
+
52
+ if valid = valid_password?(current_password)
53
+ valid = update_attributes(updated_attributes)
54
+ elsif current_password.blank?
55
+ errors.add(:current_password, :blank)
56
+ else
57
+ errors.add(:current_password, :invalid)
58
+ end
59
+
60
+ valid
61
+ end
62
+
63
+ def update_g5_credentials(oauth_data)
64
+ self.g5_access_token = oauth_data.credentials.token
65
+ end
66
+
67
+ def revoke_g5_credentials!
68
+ self.g5_access_token = nil
69
+ save!
70
+ end
71
+
72
+ module ClassMethods
73
+ def find_for_g5_oauth(oauth_data)
74
+ find_by_provider_and_uid(oauth_data.provider.to_s, oauth_data.uid.to_s)
75
+ end
76
+
77
+ def find_and_update_for_g5_oauth(oauth_data)
78
+ resource = find_for_g5_oauth(oauth_data)
79
+ if resource
80
+ resource.update_g5_credentials(oauth_data)
81
+ resource.save!
82
+ end
83
+ resource
84
+ end
85
+
86
+ def new_with_session(params, session)
87
+ defaults = ActiveSupport::HashWithIndifferentAccess.new
88
+ if auth_data = session && session['omniauth.auth']
89
+ defaults[:email] = auth_data.info.email
90
+ defaults[:provider] = auth_data.provider
91
+ defaults[:uid] = auth_data.uid
92
+ end
93
+
94
+ new(defaults.merge(params))
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,16 @@
1
+ module DeviseG5Authenticatable
2
+ module Models
3
+ module ProtectedAttributes
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ attr_accessible :email, :password, :password_confirmation,
8
+ :current_password, :provider, :uid, :updated_by
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ module Devise::Models::G5Authenticatable
15
+ include DeviseG5Authenticatable::Models::ProtectedAttributes
16
+ end
@@ -0,0 +1,9 @@
1
+ require 'devise/omniauth'
2
+ require 'omniauth-g5'
3
+
4
+ OmniAuth.config.on_failure do |env|
5
+ env['devise.mapping'] = Devise::Mapping.find_by_path!(env['PATH_INFO'], :path)
6
+ controller_name = ActiveSupport::Inflector.camelize(env['devise.mapping'].controllers[:sessions])
7
+ controller_klass = ActiveSupport::Inflector.constantize("#{controller_name}Controller")
8
+ controller_klass.action(:failure).call(env)
9
+ end