devise_g5_authenticatable 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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