cognito_rails 0.1.0 → 1.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.
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