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 +4 -4
- data/lib/cognito_rails/config.rb +10 -17
- data/lib/cognito_rails/model.rb +57 -3
- data/lib/cognito_rails/password_generator.rb +35 -0
- data/lib/cognito_rails/user.rb +19 -15
- data/lib/cognito_rails/version.rb +1 -1
- data/lib/cognito_rails.rb +1 -0
- data/spec/cognito_rails/controller_spec.rb +0 -2
- data/spec/cognito_rails/jwt_spec.rb +0 -2
- data/spec/cognito_rails/password_generator_spec.rb +28 -0
- data/spec/cognito_rails/user_spec.rb +64 -1
- data/spec/spec_helper.rb +5 -4
- data/spec/support/cognito_helpers.rb +20 -0
- data/spec/support/schema.rb +2 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1b69f6c8db91a764df6878e0de53feb9879f7509e79bf0d406e6fe9522f96e61
|
4
|
+
data.tar.gz: f44a4f4dfce641e54493730c9f183c9bd2f6a2e73fe4cd0c7a60d084b6d0d09a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 17551dd1de906fd5813214f4fdd98bd49f7d4503138cfa5cd9beedc235707dc00b000d356e090a154b7de0c08e692553154b273d50236b31f455903044c97594
|
7
|
+
data.tar.gz: ee07b9c1b821bc601ac2c62d4268392783e5f687da1d9b810d6160f64c5b7f3546ffd7e5998f05a5a03fc2326bd162e338e0be2ceda461d2cae1152072e44ec8
|
data/lib/cognito_rails/config.rb
CHANGED
@@ -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
|
11
|
-
|
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
|
16
|
-
# @return [
|
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 :
|
26
|
-
:
|
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
|
data/lib/cognito_rails/model.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/cognito_rails/user.rb
CHANGED
@@ -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 =
|
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
|
61
|
-
user.phone = result.user_attributes
|
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
|
[
|
data/lib/cognito_rails.rb
CHANGED
@@ -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.
|
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
|
data/spec/support/schema.rb
CHANGED
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:
|
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-
|
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
|