cognito_rails 0.1.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48fe70f5578d90db88e1a7546f12a16a3b24941bf20a2e8bb4a37b24b3678adc
4
- data.tar.gz: 5463df9f063b8b986ca1ef49d0f9a8efce22862854e7e74cb4aee77a8f101fea
3
+ metadata.gz: 1b69f6c8db91a764df6878e0de53feb9879f7509e79bf0d406e6fe9522f96e61
4
+ data.tar.gz: f44a4f4dfce641e54493730c9f183c9bd2f6a2e73fe4cd0c7a60d084b6d0d09a
5
5
  SHA512:
6
- metadata.gz: '058bb0a9820ee032ca2d56f7c3954c5acb434543d599ddf3aedb029e2c8b6e06b9d93445a9989248c0a9223cc5038a91cd680313d7d1735837c18826a3008999'
7
- data.tar.gz: 5fa75f4e6109b4144a33759340672eee19253d33ba8e9c167b0c4187b85f7b736d8129cfd22221188119c793f8250d550b36ba49b55742d47133ec9fd7d4ec14
6
+ metadata.gz: 17551dd1de906fd5813214f4fdd98bd49f7d4503138cfa5cd9beedc235707dc00b000d356e090a154b7de0c08e692553154b273d50236b31f455903044c97594
7
+ data.tar.gz: ee07b9c1b821bc601ac2c62d4268392783e5f687da1d9b810d6160f64c5b7f3546ffd7e5998f05a5a03fc2326bd162e338e0be2ceda461d2cae1152072e44ec8
@@ -5,26 +5,21 @@ require 'logger'
5
5
  module CognitoRails
6
6
  class Config
7
7
  class << self
8
- # @raise [RuntimeError] if not set
9
8
  # @return [String] AWS access key id
10
- def aws_access_key_id
11
- # @type [String,nil]
12
- @aws_access_key_id || (raise 'Missing config aws_access_key_id')
9
+ def aws_client_credentials
10
+ @aws_client_credentials || {}
13
11
  end
14
12
 
15
- # @!attribute aws_access_key_id [w]
16
- # @return [String]
13
+ # @!attribute aws_client_credentials [w]
14
+ # @return [Hash]
17
15
  # @!attribute aws_region [w]
18
16
  # @return [String]
19
- # @!attribute aws_secret_access_key [w]
20
- # @return [String]
21
17
  # @!attribute aws_user_pool_id [w]
22
18
  # @return [String]
23
19
  # @!attribute default_user_class [w]
24
20
  # @return [String,nil]
25
- attr_writer :aws_access_key_id, :skip_model_hooks, :aws_region,
26
- :aws_secret_access_key, :aws_user_pool_id,
27
- :default_user_class
21
+ attr_writer :aws_client_credentials, :skip_model_hooks, :aws_region,
22
+ :aws_user_pool_id, :default_user_class, :password_generator
28
23
 
29
24
  # @return [Boolean] skip model hooks
30
25
  def skip_model_hooks
@@ -43,12 +38,6 @@ module CognitoRails
43
38
  @aws_region || (raise 'Missing config aws_region')
44
39
  end
45
40
 
46
- # @return [String] AWS secret access key
47
- # @raise [RuntimeError] if not set
48
- def aws_secret_access_key
49
- @aws_secret_access_key || (raise 'Missing config aws_secret_access_key')
50
- end
51
-
52
41
  # @return [String] AWS user pool id
53
42
  # @raise [RuntimeError] if not set
54
43
  def aws_user_pool_id
@@ -60,6 +49,10 @@ module CognitoRails
60
49
  def default_user_class
61
50
  @default_user_class || (raise 'Missing config default_user_class')
62
51
  end
52
+
53
+ def password_generator
54
+ @password_generator || CognitoRails::PasswordGenerator.method(:generate)
55
+ end
63
56
  end
64
57
  end
65
58
  end
@@ -23,6 +23,55 @@ module CognitoRails
23
23
  end
24
24
  end
25
25
 
26
+ # rubocop:disable Metrics/BlockLength
27
+ class_methods do
28
+ # @return [Array<ActiveRecord::Base>] all users
29
+ # @raise [CognitoRails::Error] if failed to fetch users
30
+ # @raise [ActiveRecord::RecordInvalid] if failed to save user
31
+ def sync_from_cognito!
32
+ response = User.all
33
+ response.users.map do |user_data|
34
+ sync_user!(user_data)
35
+ end
36
+ end
37
+
38
+ # @return [Array<ActiveRecord::Base>] all users
39
+ # @raise [CognitoRails::Error] if failed to fetch users
40
+ # @raise [ActiveRecord::RecordInvalid] if failed to save user
41
+ def sync_to_cognito!
42
+ find_each.map do |user|
43
+ user.init_cognito_user
44
+ user.save!
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def sync_user!(user_data)
51
+ external_id = user_data.username
52
+ return if external_id.blank?
53
+
54
+ user = find_or_initialize_by(_cognito_attribute_name => external_id)
55
+ user.email = User.extract_cognito_attribute(user_data.attributes, :email)
56
+ user.phone = User.extract_cognito_attribute(user_data.attributes, :phone_number) if user.respond_to?(:phone)
57
+ _cognito_resolve_custom_attribute(user, user_data)
58
+
59
+ user.save!
60
+ user
61
+ end
62
+
63
+ def _cognito_resolve_custom_attribute(user, user_data)
64
+ _cognito_custom_attributes.each do |attribute|
65
+ next if attribute[:value].is_a?(String)
66
+
67
+ value = User.extract_cognito_attribute(user_data.attributes, attribute[:name])
68
+ next unless value
69
+
70
+ user[attribute[:name].gsub('custom:', '')] = value
71
+ end
72
+ end
73
+ end
74
+
26
75
  # @return [String]
27
76
  def cognito_external_id
28
77
  self[self.class._cognito_attribute_name]
@@ -43,12 +92,17 @@ module CognitoRails
43
92
  def init_cognito_user
44
93
  return if cognito_external_id.present?
45
94
 
95
+ cognito_user = User.new(init_attributes)
96
+ cognito_user.save!
97
+ self.cognito_external_id = cognito_user.id
98
+ end
99
+
100
+ def init_attributes
46
101
  attrs = { email: email, user_class: self.class }
47
102
  attrs[:phone] = phone if respond_to?(:phone)
103
+ attrs[:password] = password if respond_to?(:password)
48
104
  attrs[:custom_attributes] = instance_custom_attributes
49
- cognito_user = User.new(attrs)
50
- cognito_user.save!
51
- self.cognito_external_id = cognito_user.id
105
+ attrs
52
106
  end
53
107
 
54
108
  # @return [Array<Hash>]
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CognitoRails
4
+ class PasswordGenerator
5
+ NUMERIC = (0..9).to_a.freeze
6
+ LOWER_CASE = ('a'..'z').to_a.freeze
7
+ UPPER_CASE = ('A'..'Z').to_a.freeze
8
+ SPECIAL = [
9
+ '^', '$', '*', '.', '[', ']', '{', '}',
10
+ '(', ')', '?', '"', '!', '@', '#', '%',
11
+ '&', '/', '\\', ',', '>', '<', "'", ':',
12
+ ';', '|', '_', '~', '`', '=', '+', '-'
13
+ ].freeze
14
+
15
+ # Generates a random password given a length range
16
+ #
17
+ # @param range [Range]
18
+ # @return [String]
19
+ def self.generate(range = 8..16)
20
+ password_length = rand(range)
21
+ numeric_count = rand(1..(password_length-3))
22
+
23
+ lower_case_count = rand(1..(password_length-(numeric_count+2)))
24
+ upper_case_count = rand(1..(password_length-(numeric_count + lower_case_count + 1)))
25
+ special_count = password_length-(numeric_count + lower_case_count + upper_case_count)
26
+
27
+ numeric_characters = numeric_count.times.map { NUMERIC.sample }
28
+ lower_case_characters = lower_case_count.times.map { LOWER_CASE.sample }
29
+ upper_case_characters = upper_case_count.times.map { UPPER_CASE.sample }
30
+ special_characters = special_count.times.map { SPECIAL.sample }
31
+
32
+ (numeric_characters + lower_case_characters + upper_case_characters + special_characters).shuffle.join
33
+ end
34
+ end
35
+ end
@@ -39,7 +39,7 @@ module CognitoRails
39
39
  def initialize(attributes = {})
40
40
  attributes = attributes.with_indifferent_access
41
41
  self.email = attributes[:email]
42
- self.password = SecureRandom.urlsafe_base64 || attributes[:password]
42
+ self.password = attributes[:password] || Config.password_generator.call
43
43
  self.phone = attributes[:phone]
44
44
  self.user_class = attributes[:user_class] || Config.default_user_class.constantize
45
45
  self.custom_attributes = attributes[:custom_attributes]
@@ -57,11 +57,15 @@ module CognitoRails
57
57
  )
58
58
  user = new(user_class: user_class)
59
59
  user.id = result.username
60
- user.email = result.user_attributes.find { |attribute| attribute[:name] == 'email' }[:value]
61
- user.phone = result.user_attributes.find { |attribute| attribute[:name] == 'phone_number' }&.dig(:value)
60
+ user.email = extract_cognito_attribute(result.user_attributes, :email)
61
+ user.phone = extract_cognito_attribute(result.user_attributes, :phone_number)
62
62
  user
63
63
  end
64
64
 
65
+ def self.all
66
+ cognito_client.list_users(user_pool_id: CognitoRails::Config.aws_user_pool_id)
67
+ end
68
+
65
69
  # @param attributes [Hash]
66
70
  # @option attributes [String] :email
67
71
  # @option attributes [String] :password
@@ -138,6 +142,18 @@ module CognitoRails
138
142
  destroy || (raise ActiveRecord::RecordInvalid, self)
139
143
  end
140
144
 
145
+ # @return [Aws::CognitoIdentityProvider::Client]
146
+ # @raise [RuntimeError]
147
+ def self.cognito_client
148
+ @cognito_client ||= Aws::CognitoIdentityProvider::Client.new(
149
+ { region: CognitoRails::Config.aws_region }.merge(CognitoRails::Config.aws_client_credentials)
150
+ )
151
+ end
152
+
153
+ def self.extract_cognito_attribute(attributes, column)
154
+ attributes.find { |attribute| attribute[:name] == column.to_s }&.dig(:value)
155
+ end
156
+
141
157
  private
142
158
 
143
159
  # @return [Aws::CognitoIdentityProvider::Client]
@@ -155,18 +171,6 @@ module CognitoRails
155
171
  user_class._cognito_verify_phone
156
172
  end
157
173
 
158
- # @return [Aws::CognitoIdentityProvider::Client]
159
- # @raise [RuntimeError]
160
- def self.cognito_client
161
- raise 'Can\'t create user in test mode' if Rails.env.test?
162
-
163
- @cognito_client ||= Aws::CognitoIdentityProvider::Client.new(
164
- access_key_id: CognitoRails::Config.aws_access_key_id,
165
- secret_access_key: CognitoRails::Config.aws_secret_access_key,
166
- region: CognitoRails::Config.aws_region
167
- )
168
- end
169
-
170
174
  # @return [Array<Hash>]
171
175
  def general_user_attributes
172
176
  [
@@ -2,5 +2,5 @@
2
2
 
3
3
  module CognitoRails
4
4
  # @return [String] gem version
5
- VERSION = '0.1.0'
5
+ VERSION = '1.1.0'
6
6
  end
data/lib/cognito_rails.rb CHANGED
@@ -15,6 +15,7 @@ module CognitoRails
15
15
  autoload :Model
16
16
  autoload :User
17
17
  autoload :JWT
18
+ autoload :PasswordGenerator
18
19
 
19
20
  # @private
20
21
  module ModelInitializer
@@ -1,8 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- # rubocop:disable Metrics/BlockLength
4
3
  RSpec.describe CognitoRails::Controller, type: :model do
5
- # rubocop:enable Metrics/BlockLength
6
4
  include CognitoRails::Helpers
7
5
 
8
6
  context 'with an API controller' do
@@ -2,9 +2,7 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- # rubocop:disable Metrics/BlockLength
6
5
  RSpec.describe CognitoRails::JWT, type: :model do
7
- # rubocop:enable Metrics/BlockLength
8
6
  before do
9
7
  allow(URI).to receive(:open).and_return(double(read: jwks))
10
8
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CognitoRails::PasswordGenerator do
6
+ it 'generates a password' do
7
+ expect(described_class.generate).to be_a(String)
8
+ end
9
+
10
+ it 'generates a password with the correct length' do
11
+ 1000.times do
12
+ expect(described_class.generate(8..8).length).to eq(8)
13
+ end
14
+ end
15
+
16
+ it 'contains at least one letter, one number, one upper case letter, one symbol' do
17
+ 1000.times do
18
+ password = described_class.generate
19
+ expect(password).to match(/[a-z]/)
20
+ expect(password).to match(/[A-Z]/)
21
+ expect(password).to match(/[0-9]/)
22
+ include_symbol = CognitoRails::PasswordGenerator::SPECIAL.any? do |symbol|
23
+ password.include?(symbol)
24
+ end
25
+ expect(include_symbol).to be_truthy
26
+ end
27
+ end
28
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- # rubocop:disable Metrics/BlockLength
6
5
  RSpec.describe CognitoRails::User, type: :model do
7
6
  include CognitoRails::Helpers
8
7
 
@@ -83,6 +82,34 @@ RSpec.describe CognitoRails::User, type: :model do
83
82
  user.destroy!
84
83
  end
85
84
 
85
+ it 'uses the password generator defined in config' do
86
+ CognitoRails::Config.password_generator = -> { 'ciao' }
87
+ expect(CognitoRails::User).to receive(:cognito_client).at_least(:once).and_return(fake_cognito_client)
88
+
89
+ expect(fake_cognito_client).to receive(:admin_create_user).with(
90
+ hash_including(
91
+ temporary_password: 'ciao'
92
+ )
93
+ )
94
+ user = User.new(email: sample_cognito_email)
95
+ user.save!
96
+ ensure
97
+ CognitoRails::Config.password_generator = nil
98
+ end
99
+
100
+ it 'uses the custom password passed as parameter' do
101
+ expect(CognitoRails::User).to receive(:cognito_client).at_least(:once).and_return(fake_cognito_client)
102
+
103
+ expect(fake_cognito_client).to receive(:admin_create_user).with(
104
+ hash_including(
105
+ temporary_password: '12345678'
106
+ )
107
+ )
108
+ user = User.new(email: sample_cognito_email)
109
+ user.password = '12345678'
110
+ user.save!
111
+ end
112
+
86
113
  it 'saves custom attributes in cognito' do
87
114
  expect(CognitoRails::User).to receive(:cognito_client).at_least(:once).and_return(fake_cognito_client)
88
115
 
@@ -111,6 +138,42 @@ RSpec.describe CognitoRails::User, type: :model do
111
138
  end
112
139
  end
113
140
 
141
+ context 'class methods' do
142
+ before do
143
+ expect(CognitoRails::User).to receive(:cognito_client).at_least(:once).and_return(fake_cognito_client)
144
+ end
145
+
146
+ it '#sync_from_cognito!' do
147
+ expect(fake_cognito_client).to receive(:list_users).and_return(
148
+ OpenStruct.new(
149
+ users: [
150
+ build_cognito_user_data('some@example.com'),
151
+ build_cognito_user_data('some2@example.com')
152
+ ],
153
+ pagination_token: nil
154
+ )
155
+ )
156
+
157
+ expect do
158
+ users = User.sync_from_cognito!
159
+
160
+ expect(users).to be_a(Array)
161
+ expect(users.size).to eq(2)
162
+ expect(users.first).to be_a(User)
163
+ end.to change { User.count }.by(2)
164
+
165
+ expect(User.pluck(:email)).to match_array(['some@example.com', 'some2@example.com'])
166
+ expect(User.pluck(:name)).to match_array(['Giovanni', 'Giovanni'])
167
+ end
168
+
169
+ it '#sync_to_cognito!' do
170
+ User.create!(email: sample_cognito_email)
171
+
172
+ expect_any_instance_of(User).to receive(:init_cognito_user).exactly(1).times
173
+ User.sync_to_cognito!
174
+ end
175
+ end
176
+
114
177
  context 'admin' do
115
178
  before do
116
179
  expect(CognitoRails::User).to receive(:cognito_client).at_least(:once).and_return(fake_cognito_client)
data/spec/spec_helper.rb CHANGED
@@ -11,16 +11,17 @@ require 'factories/user'
11
11
  I18n.enforce_available_locales = false
12
12
  RSpec::Expectations.configuration.warn_about_potential_false_positives = false
13
13
 
14
- Dir[File.expand_path('../support/*.rb', __FILE__)].each { |f| require f }
14
+ Dir[File.expand_path('../support/*.rb', __FILE__)].sort.each { |f| require f }
15
15
 
16
- CognitoRails::Config.aws_access_key_id = 'access_key_id'
16
+ CognitoRails::Config.aws_client_credentials = {
17
+ access_key_id: 'access_key_id',
18
+ secret_access_key: 'secret_access_key'
19
+ }
17
20
  CognitoRails::Config.aws_region = 'region'
18
- CognitoRails::Config.aws_secret_access_key = 'secret_access_key'
19
21
  CognitoRails::Config.aws_user_pool_id = 'user_pool_id'
20
22
  CognitoRails::Config.default_user_class = 'User'
21
23
 
22
24
  RSpec.configure do |config|
23
-
24
25
  config.include FactoryBot::Syntax::Methods
25
26
 
26
27
  config.before(:suite) do
@@ -40,4 +40,24 @@ module CognitoRails::Helpers
40
40
  client
41
41
  end
42
42
  end
43
+
44
+ def build_cognito_user_data(email)
45
+ OpenStruct.new(
46
+ username: SecureRandom.uuid,
47
+ user_status: 'CONFIRMED',
48
+ enabled: true,
49
+ user_last_modified_date: Time.now,
50
+ attributes: [
51
+ OpenStruct.new(
52
+ name: 'email',
53
+ value: email
54
+ ),
55
+ OpenStruct.new(
56
+ name: 'custom:name',
57
+ value: 'Giovanni'
58
+ )
59
+ ],
60
+ mfa_options: []
61
+ )
62
+ end
43
63
  end
@@ -13,6 +13,8 @@ class User < ActiveRecord::Base
13
13
  cognito_verify_email
14
14
  define_cognito_attribute 'role', 'user'
15
15
  define_cognito_attribute 'name', :name
16
+
17
+ attr_accessor :password
16
18
  end
17
19
 
18
20
  class Admin < ActiveRecord::Base
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cognito_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mònade
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-27 00:00:00.000000000 Z
11
+ date: 2023-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -97,10 +97,12 @@ files:
97
97
  - lib/cognito_rails/controller.rb
98
98
  - lib/cognito_rails/jwt.rb
99
99
  - lib/cognito_rails/model.rb
100
+ - lib/cognito_rails/password_generator.rb
100
101
  - lib/cognito_rails/user.rb
101
102
  - lib/cognito_rails/version.rb
102
103
  - spec/cognito_rails/controller_spec.rb
103
104
  - spec/cognito_rails/jwt_spec.rb
105
+ - spec/cognito_rails/password_generator_spec.rb
104
106
  - spec/cognito_rails/user_spec.rb
105
107
  - spec/factories/user.rb
106
108
  - spec/spec_helper.rb
@@ -133,6 +135,7 @@ summary: Add Cognito authentication to your Rails API
133
135
  test_files:
134
136
  - spec/cognito_rails/controller_spec.rb
135
137
  - spec/cognito_rails/jwt_spec.rb
138
+ - spec/cognito_rails/password_generator_spec.rb
136
139
  - spec/cognito_rails/user_spec.rb
137
140
  - spec/factories/user.rb
138
141
  - spec/spec_helper.rb