devise_meteor 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +4 -0
  4. data/CODE_OF_CONDUCT.md +13 -0
  5. data/Gemfile +16 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +78 -0
  8. data/Rakefile +7 -0
  9. data/app/models/devise_meteor/meteor_profile.rb +12 -0
  10. data/app/models/devise_meteor/meteor_service.rb +16 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +7 -0
  13. data/devise_meteor.gemspec +37 -0
  14. data/lib/devise/strategies/meteor.rb +92 -0
  15. data/lib/devise_meteor/concerns/meteor_user_model.rb +148 -0
  16. data/lib/devise_meteor/engine.rb +10 -0
  17. data/lib/devise_meteor/railtie.rb +7 -0
  18. data/lib/devise_meteor/strategies/encrypter.rb +15 -0
  19. data/lib/devise_meteor/strategies/hasher.rb +91 -0
  20. data/lib/devise_meteor/strategies/strategy.rb +92 -0
  21. data/lib/devise_meteor/version.rb +3 -0
  22. data/lib/devise_meteor.rb +37 -0
  23. data/lib/generators/devise_meteor/install_generator.rb +16 -0
  24. data/lib/generators/templates/meteor_initializer.rb +12 -0
  25. data/spec/factories/users.rb +19 -0
  26. data/spec/models/meteor_authentication_spec.rb +261 -0
  27. data/spec/rails_helper.rb +72 -0
  28. data/spec/spec_helper.rb +92 -0
  29. data/spec/support/api_macros.rb +30 -0
  30. data/spec/support/controller_macros.rb +18 -0
  31. data/spec/support/devise_macros.rb +58 -0
  32. data/spec/support/omniauth_macros.rb +44 -0
  33. data/spec/support/request_macros.rb +13 -0
  34. data/spec/test_app/README.rdoc +3 -0
  35. data/spec/test_app/Rakefile +6 -0
  36. data/spec/test_app/app/assets/javascripts/application.js +13 -0
  37. data/spec/test_app/app/assets/stylesheets/application.css +15 -0
  38. data/spec/test_app/app/controllers/application_controller.rb +5 -0
  39. data/spec/test_app/app/controllers/products_controller.rb +89 -0
  40. data/spec/test_app/app/helpers/application_helper.rb +2 -0
  41. data/spec/test_app/app/models/user.rb +43 -0
  42. data/spec/test_app/app/views/layouts/application.html.erb +14 -0
  43. data/spec/test_app/app/views/products/_form.html.erb +29 -0
  44. data/spec/test_app/app/views/products/edit.html.erb +6 -0
  45. data/spec/test_app/app/views/products/index.html.erb +33 -0
  46. data/spec/test_app/app/views/products/new.html.erb +5 -0
  47. data/spec/test_app/app/views/products/show.html.erb +25 -0
  48. data/spec/test_app/bin/bundle +3 -0
  49. data/spec/test_app/bin/rails +4 -0
  50. data/spec/test_app/bin/rake +4 -0
  51. data/spec/test_app/bin/setup +29 -0
  52. data/spec/test_app/config/application.rb +28 -0
  53. data/spec/test_app/config/boot.rb +5 -0
  54. data/spec/test_app/config/environment.rb +5 -0
  55. data/spec/test_app/config/environments/development.rb +38 -0
  56. data/spec/test_app/config/environments/production.rb +77 -0
  57. data/spec/test_app/config/environments/test.rb +45 -0
  58. data/spec/test_app/config/initializers/assets.rb +11 -0
  59. data/spec/test_app/config/initializers/backtrace_silencers.rb +7 -0
  60. data/spec/test_app/config/initializers/cookies_serializer.rb +3 -0
  61. data/spec/test_app/config/initializers/devise.rb +268 -0
  62. data/spec/test_app/config/initializers/filter_parameter_logging.rb +4 -0
  63. data/spec/test_app/config/initializers/inflections.rb +16 -0
  64. data/spec/test_app/config/initializers/mime_types.rb +4 -0
  65. data/spec/test_app/config/initializers/secret_token.rb +3 -0
  66. data/spec/test_app/config/initializers/session_store.rb +3 -0
  67. data/spec/test_app/config/initializers/wrap_parameters.rb +9 -0
  68. data/spec/test_app/config/locales/en.yml +23 -0
  69. data/spec/test_app/config/mongoid.yml +80 -0
  70. data/spec/test_app/config/routes.rb +5 -0
  71. data/spec/test_app/config/secrets.yml +22 -0
  72. data/spec/test_app/config.ru +4 -0
  73. data/spec/test_app/lib/tasks/cucumber.rake +65 -0
  74. data/spec/test_app/log/test.log +23511 -0
  75. data/spec/test_app/public/404.html +67 -0
  76. data/spec/test_app/public/422.html +67 -0
  77. data/spec/test_app/public/500.html +66 -0
  78. data/spec/test_app/public/favicon.ico +0 -0
  79. metadata +350 -0
@@ -0,0 +1,10 @@
1
+ module DeviseMeteor
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace DeviseMeteor
4
+
5
+ config.generators do |g|
6
+ g.test_framework :rspec
7
+ g.fixture_replacement :factory_girl, :dir => 'spec/factories'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ require 'rails'
2
+
3
+ module DeviseMeteor
4
+ class Railtie < Rails::Railtie
5
+
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ module DeviseMeteor
2
+ module Encrypter
3
+
4
+ def self.digest(password)
5
+ sha256_hasher = DeviseMeteor::BCryptSHA256Hasher.new
6
+ sha256_hasher.encode(password, sha256_hasher.salt)
7
+ end
8
+
9
+
10
+ def self.compare(password, encrypted_password)
11
+ sha256_hasher = DeviseMeteor::BCryptSHA256Hasher.new
12
+ sha256_hasher.verify(password, encrypted_password)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,91 @@
1
+ require "base64"
2
+ require "bcrypt"
3
+ require "openssl"
4
+
5
+ module DeviseMeteor
6
+ class Hasher
7
+ attr_reader :algorithm
8
+
9
+ # Returns salt value to be used for hashing.
10
+ #
11
+ # @return [String] random salt value.
12
+ def salt
13
+ SecureRandom.hex(9)
14
+ end
15
+
16
+ # Returns if the given password match the encoded password.
17
+ #
18
+ # @param [String] password in plain text
19
+ # @param [String] encoded password to be matched
20
+ # @return [Boolean] if password match encoded password.
21
+ def verify(password, encoded)
22
+ raise NotImplementedError
23
+ end
24
+
25
+ # Returns given password encoded with the given salt.
26
+ #
27
+ # @param [String] password in plain text
28
+ # @param [String] salt to be used during hashing
29
+ # @return [String] given password hashed using the given salt
30
+ def encode(password, salt)
31
+ raise NotImplementedError
32
+ end
33
+
34
+ # Returns if given encoded password needs to be updated.
35
+ #
36
+ # @param [String] encoded password
37
+ # @return [Boolean] if encoded password needs to be updated
38
+ def must_update(encoded)
39
+ false
40
+ end
41
+
42
+ private
43
+
44
+ def constant_time_compare(a, b)
45
+ check = a.bytesize ^ b.bytesize
46
+ a.bytes.zip(b.bytes) { |x, y| check |= x ^ y }
47
+ check == 0
48
+ end
49
+ end
50
+
51
+ # BCryptSHA256Hasher implements a BCrypt password hasher using SHA256.
52
+ class BCryptSHA256Hasher < Hasher
53
+ def initialize
54
+ @algorithm = :bcrypt_sha256
55
+ @cost = 10
56
+ @digest = OpenSSL::Digest::SHA256.new
57
+ end
58
+
59
+ def salt
60
+ BCrypt::Engine.generate_salt(@cost)
61
+ end
62
+
63
+ def get_password_string(password)
64
+ @digest.digest(password) unless @digest.nil?
65
+ end
66
+
67
+ def encode(password, salt)
68
+ password = get_password_string(password)
69
+ hash = BCrypt::Engine.hash_secret(password, salt)
70
+ return hash
71
+ end
72
+
73
+ def verify(password, encoded)
74
+ password_digest = get_password_string(password)
75
+ hash = BCrypt::Engine.hash_secret(password_digest, encoded)
76
+ Devise.secure_compare(encoded, hash)
77
+
78
+ constant_time_compare(encoded, hash)
79
+ end
80
+
81
+ end
82
+
83
+ # BCryptHasher implements a BCrypt password hasher.
84
+ class BCryptHasher < BCryptSHA256Hasher
85
+ def initialize
86
+ @algorithm = :bcrypt
87
+ @cost = 10
88
+ @digest = nil
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,92 @@
1
+ require 'devise/strategies/authenticatable'
2
+
3
+ module Devise
4
+ module Strategies
5
+ class Meteor < Authenticatable
6
+
7
+ def valid?
8
+ valid_for_params_auth? || valid_for_http_auth?
9
+ end
10
+
11
+ def authenticate!
12
+ self.password = params_auth_hash[:password]
13
+
14
+ # search for user email
15
+ devise_resource = valid_for_params_auth? && mapping.to.find_for_database_authentication(authentication_hash)
16
+ meteor_resource = User.where(:emails.elem_match => {address: params_auth_hash[:email]}).first unless devise_resource
17
+ resource = devise_resource || meteor_resource
18
+
19
+ return fail!(:not_found_in_database) unless resource
20
+
21
+ # before continuing authentication process
22
+ # check existing passwords and synchronize them
23
+ if meteor_auth_missing?(resource)
24
+ # when already devise credentials stored
25
+ # assign them to devise_meteor fields
26
+ new_hashed_password = User.new(:password => password).encrypted_password
27
+ resource.services.set(password: {bcrypt: new_hashed_password})
28
+
29
+ elsif devise_auth_missing?(resource)
30
+ # when user registered through devise_meteor
31
+ # and credentials for devise not present
32
+ email = params_auth_hash[:email]
33
+ crypt = resource.services.password[:bcrypt]
34
+
35
+ unless resource.update_attributes(encrypted_password: crypt, email: email)
36
+ fail(resource.unauthenticated_message)
37
+ end
38
+ resource.reload
39
+
40
+ elsif both_auth_present?(resource)
41
+ #when both passwords already set
42
+ else
43
+ return pass
44
+ end
45
+
46
+ # now do validation
47
+ # this code is copied from Devise::Strategies::DatabaseAuthenticable
48
+ encrypted = false
49
+ if validate(resource) { encrypted = true; resource.valid_password?(password) }
50
+ remember_me(resource)
51
+ resource.after_database_authentication
52
+ success!(resource)
53
+ end
54
+
55
+ mapping.to.new.password = password if !encrypted && Devise.paranoid
56
+ fail(:not_found_in_database) unless resource
57
+ end
58
+
59
+ private
60
+
61
+ def both_auth_present?(resource)
62
+ has_a_meteor_password?(resource) && has_a_devise_password?(resource)
63
+ end
64
+
65
+ def devise_auth_missing?(resource)
66
+ has_a_meteor_password?(resource) && !has_a_devise_password?(resource)
67
+ end
68
+
69
+ def meteor_auth_missing?(resource)
70
+ has_a_devise_password?(resource) && !has_a_meteor_password?(resource)
71
+ end
72
+
73
+ def has_a_devise_password?(resource)
74
+ resource.encrypted_password.present?
75
+ end
76
+
77
+ def has_a_meteor_password?(resource)
78
+ resource.services.password.present?
79
+ end
80
+
81
+ # if you migrate from some other strategies than bcrypt
82
+ def legacy_authenticates?(resource, password)
83
+ # resource.encrypted_password == encrypt(resource, password)
84
+ end
85
+
86
+ def encrypt(resource, password)
87
+ # some old strategies code
88
+ end
89
+
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,3 @@
1
+ module DeviseMeteor
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,37 @@
1
+ require 'devise_meteor/engine'
2
+
3
+
4
+ require "devise_meteor/version"
5
+
6
+ require 'rails'
7
+
8
+ require 'devise_meteor/railtie' if defined?(Rails)
9
+ require 'devise_meteor/railtie'
10
+
11
+ require 'devise'
12
+ require 'devise_meteor/strategies/encrypter'
13
+ require 'devise_meteor/strategies/hasher'
14
+ require 'devise/strategies/meteor'
15
+
16
+ require 'devise_meteor/concerns/meteor_user_model'
17
+
18
+ module DeviseMeteor
19
+
20
+ # enable configuration
21
+ class << self
22
+ attr_accessor :configuration
23
+ end
24
+
25
+ def self.configure
26
+ self.configuration ||= Configuration.new
27
+ yield(configuration)
28
+ end
29
+
30
+ class Configuration
31
+ attr_accessor :option
32
+
33
+ def initialize
34
+ @option = 'default_option'
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,16 @@
1
+ module DeviseMeteor
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+
5
+ source_root File.expand_path("../../templates", __FILE__)
6
+
7
+ desc "Creates initializer for devise_meteor specific config"
8
+
9
+ def copy_initializer
10
+ template "meteor_initializer.rb", "config/initializers/devise_meteor.rb"
11
+
12
+ puts "Added initializer to your app."
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ DeviseMeteor.configure do |config|
2
+
3
+
4
+ end
5
+
6
+ # you can uncomment this lines to match up "strings" ObjectIds from accounts-* packages
7
+
8
+ #class BSON::ObjectId
9
+ # def self.mongoize(k)
10
+ # k.to_s
11
+ # end
12
+ #end
@@ -0,0 +1,19 @@
1
+ FactoryGirl.define do
2
+
3
+ factory :user do
4
+ #after(:build) { |u| u.password_confirmation = u.password = mypassword }
5
+ email { Faker::Internet.email }
6
+ password "password"
7
+ password_confirmation "password"
8
+
9
+ factory :confirmed_user do
10
+ confirmed_at Date.today
11
+ end
12
+ end
13
+
14
+ factory :invalid_user, parent: :user do
15
+ email { Faker::Internet.email }
16
+ password "short"
17
+ password_confirmation "short"
18
+ end
19
+ end
@@ -0,0 +1,261 @@
1
+ require 'rails_helper'
2
+
3
+ describe DeviseMeteor::MeteorUserModel do
4
+
5
+ let(:meteor_user_attributes) {
6
+
7
+ {
8
+ '_id' => "633DYLkePqv2xMbDi",
9
+ "createdAt" => Time.now,
10
+ "emails" => [
11
+ {"address" => Faker::Internet.email,
12
+ "verified" => true}
13
+ ],
14
+ "profile" => {"firstName" => "DemoFirstName",
15
+ "lastName" => "DemoLastName"
16
+ },
17
+ "services" => {
18
+ "password" => {
19
+ "bcrypt" => "$2a$10$4my6JyO3FxMDr4xm24k2Kev7Jqu0E.8KecXEv8fHhZQMSCHrQCUM."
20
+ },
21
+ "resume" => {
22
+ "loginTokens" => [
23
+ {
24
+ "when" => Time.now,
25
+ "hashedToken" => "fPtV4ud7WzT9nKwgfRb56MwI8GORe3kGc4cO6FfM8xY="
26
+ }
27
+ ]
28
+ }
29
+ },
30
+ "username" => "demouser"
31
+ }
32
+
33
+ }
34
+
35
+ describe "Meteor User attribute mapping" do
36
+
37
+ it "calls meteor_map_attributes before saving" do
38
+ user = User.new
39
+
40
+ expect(user).to receive(:meteor_map_attributes)
41
+ user.save
42
+
43
+ end
44
+
45
+ before :each do
46
+ @user = create :user
47
+ end
48
+
49
+ context 'emails' do
50
+
51
+ it 'should be an array' do
52
+ expect(@user.emails).to be_an Array
53
+ end
54
+
55
+ describe 'when changing the email' do
56
+
57
+ it 'does not verify on new unconfirmed' do
58
+ @user.confirm
59
+ @user.email = "test@test.com"
60
+ result2 = {address: @user.email, verified: false}
61
+
62
+ @user.save!
63
+ expect(@user.emails).to include result2
64
+ end
65
+
66
+ it 'does verify on confirm' do
67
+ @user.confirm
68
+ @user.email = "test@test.com"
69
+ @user.save!
70
+ result2 = {address: @user.email, verified: true}
71
+
72
+ @user.confirm
73
+
74
+ expect(@user.emails).to include result2
75
+ end
76
+
77
+ it 'does not overwrite old email' do
78
+ result1 = {address: @user.email, verified: true}
79
+
80
+ @user.confirm
81
+ @user.email = "test@test.com"
82
+ result2 = {address: @user.email, verified: false}
83
+
84
+ @user.save!
85
+ expect(@user.emails).to include result1
86
+ expect(@user.emails).to include result2
87
+ end
88
+
89
+ it 'does not have duplicated emails with different verified status' do
90
+ confirmed = {address: @user.email, verified: true}
91
+ @user.confirm
92
+
93
+ @user.email = "another@email.com"
94
+ unconfirmed = {address: @user.email, verified: false}
95
+ @user.save!
96
+ expect(@user.emails).to include confirmed
97
+ expect(@user.emails).to include unconfirmed
98
+ expect(@user.emails.size).to eql 2
99
+ end
100
+
101
+ end
102
+
103
+ describe 'verified' do
104
+
105
+ specify 'on new email should be false' do
106
+ result = {address: @user.email, verified: false}
107
+
108
+ expect(@user.emails).to include(result)
109
+ end
110
+
111
+ it 'on confirmed email should be true' do
112
+ result = {address: @user.email, verified: true}
113
+
114
+ @user.confirm
115
+
116
+ expect(@user.emails).to include(result)
117
+ end
118
+
119
+ end
120
+
121
+ end
122
+
123
+ describe 'createdAt' do
124
+ it 'should match created_at' do
125
+ expect(@user.created_at).to eql @user.createdAt
126
+ end
127
+ end
128
+
129
+ describe 'profile' do
130
+
131
+ it 'has a relation to Profile' do
132
+ expect(@user.profile).to be_a DeviseMeteor::MeteorProfile
133
+ end
134
+
135
+ describe 'name' do
136
+
137
+ it 'writes into name field' do
138
+ name = "John Doe"
139
+ @user.update_attribute(:name, name)
140
+ expect(User.last.profile.name).to eql name
141
+ end
142
+
143
+ it 'reads from name field' do
144
+ name = "John Doe"
145
+ @user.update_attribute(:name, name)
146
+ expect(@user.name).to eql @user.profile.name
147
+ end
148
+ end
149
+
150
+ describe 'username' do
151
+
152
+ it 'writes into name field' do
153
+ username = "John Doe"
154
+ @user.update_attribute(:username, username)
155
+ expect(User.last.profile.username).to eql username
156
+ end
157
+
158
+ it 'reads from name field' do
159
+ username = "John Doe"
160
+ @user.update_attribute(:username, username)
161
+ expect(@user.username).to eql @user.profile.username
162
+ end
163
+ end
164
+
165
+ end
166
+
167
+ describe 'services' do
168
+
169
+ # this example is copy pasted from the devise_meteor documentation
170
+ # http://docs.meteor.com/#/full/meteor_users
171
+ # bcrypt: password decrypted is "asdfasdf"
172
+ let(:valid_services_hash) {
173
+ {"password" => {
174
+ "bcrypt" => "$2a$10$4my6JyO3FxMDr4xm24k2Kev7Jqu0E.8KecXEv8fHhZQMSCHrQCUM."
175
+ },
176
+ "facebook" => {
177
+ "id" => "709050",
178
+ "accessToken" => "S0M3_R34LLY-CMPLX-T0K3N"
179
+ },
180
+ "resume" => {
181
+ "loginTokens" => [
182
+ {"token" => "4no7eR-c0mpl3x-c0d3-with-numb3r5",
183
+ "when" => 1234567890}
184
+ ]
185
+ }}
186
+ }
187
+
188
+ let(:decrypted_password) {
189
+ 'asdfasdf'
190
+ }
191
+
192
+ it 'must have a service relation' do
193
+ expect(@user.services).to be_a DeviseMeteor::MeteorService
194
+ end
195
+
196
+ context 'password' do
197
+ before :each do
198
+ @user.confirm
199
+ end
200
+
201
+ let(:valid_password_hash) {
202
+ valid_services_hash[:password]
203
+ }
204
+
205
+ context 'changed by devise' do
206
+ it 'writes password on creation' do
207
+ expect(User.last.services.password[:bcrypt]).to eql @user.services.password[:bcrypt]
208
+ end
209
+
210
+ it 'writes new password also to session.password' do
211
+ new_password = 'N3W_P455W0RD'
212
+
213
+ @user.password = new_password
214
+ @user.password_confirmation = new_password
215
+ @user.save
216
+
217
+ expect(User.last.services.password[:bcrypt]).to eql @user.services.password[:bcrypt]
218
+ end
219
+
220
+ it 'tries to use the meteors password' do
221
+ password = @user.services.password[:bcrypt]
222
+ true
223
+ end
224
+
225
+ end
226
+
227
+ end
228
+
229
+ context 'facebook' do
230
+
231
+ it 'allows multiple provider assignment' do
232
+ @user.services.update_attributes(facebook: valid_services_hash['facebook'])
233
+
234
+ expect(User.last.services.facebook[:id]).to eq valid_services_hash['facebook']['id']
235
+ expect(User.last.services.facebook[:id]).to eq valid_services_hash['facebook']['id']
236
+ end
237
+
238
+ it 'allows single provider assignment' do
239
+ @user.services.update_attribute(:facebook, valid_services_hash['facebook'])
240
+
241
+ expect(User.last.services.facebook[:id]).to eq valid_services_hash['facebook']['id']
242
+ expect(User.last.services.facebook[:accessToken]).to eq valid_services_hash['facebook']['accessToken']
243
+ end
244
+
245
+ it 'allows to change provider data' do
246
+ new_facebook_hash = {id: '12345678', accessToken: 'New_Access_token'}
247
+
248
+ @user.services.update_attribute(:facebook, valid_services_hash['facebook'])
249
+ expect(User.last.services.facebook[:id]).to eq valid_services_hash['facebook']['id']
250
+ expect(User.last.services.facebook[:accessToken]).to eq valid_services_hash['facebook']['accessToken']
251
+
252
+ @user.services.update_attribute(:facebook, new_facebook_hash)
253
+
254
+ expect(User.last.services.facebook[:id]).to eq new_facebook_hash[:id]
255
+ expect(User.last.services.facebook[:accessToken]).to eq new_facebook_hash[:accessToken]
256
+ end
257
+
258
+ end
259
+ end
260
+ end
261
+ end