cognito_rails 1.4.0 → 1.5.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: 8d013a998e0deb672975845d617ee6e7bd98985e9776005574edddc60efea303
4
- data.tar.gz: ddf9787f8bb69aff3ffcff50464fda787b9097875d9d8da165ef9074138ac4b6
3
+ metadata.gz: 0e26720020fe66850fb2c51baf995cbb22e1bb0713fa84714b18c7c69ba7eeca
4
+ data.tar.gz: f0fdfbdf0375fb7a8dcd1d3c4d44393947f785a58e33579d4907d97010ea2b40
5
5
  SHA512:
6
- metadata.gz: b943c7b79fc60f473b99bd93b52319a4f7abe01f78266ed587f94ca9daf0c74d13ed0c8d72cb44d77c14813a7509b805adb96e929d43d7f22c5ba2b127a3a55b
7
- data.tar.gz: 7a65fadd47485c198d6a70696c9e79d52da01130b35d2fd9479e86de64784f15786b4956d4329a873335bec7db794673f86f5971c938e3a234f7f72ad06d7969
6
+ metadata.gz: ca4c17f11c1e0c9b2619385ae7019632d83505838ef957760a349ccb21dc55e27be8c61c797af9b45db0a93fa17bf0ee6569dd86f8e5275269cab299d991ec96
7
+ data.tar.gz: b66d984895beb150302232cb7efa59528fd46cd9c61ea3193458fea3a93fc772db27f83af2020879e0c1ca9bb11b861e944570433fcee3222dbc217beaeef4e6
@@ -50,9 +50,42 @@ module CognitoRails
50
50
  @default_user_class || (raise 'Missing config default_user_class')
51
51
  end
52
52
 
53
+ # @param user_class [String,Symbol,Class,nil]
54
+ # @param options [Hash]
55
+ # @return [void]
56
+ def register_user_scope(user_class, options = {})
57
+ user_class = CognitoRails::Utils.resolve_model_class(user_class)
58
+ return if user_class.nil?
59
+
60
+ options = options.with_indifferent_access
61
+
62
+ user_scopes[user_class.name] = {
63
+ user_pool_id: options[:user_pool_id],
64
+ aws_region: options[:aws_region],
65
+ access_key_id: options[:access_key_id],
66
+ secret_access_key: options[:secret_access_key]
67
+ }.compact.with_indifferent_access
68
+ end
69
+
70
+ # @param user_class [String,Symbol,Class,nil]
71
+ # @return [Hash]
72
+ def user_scope_for(user_class)
73
+ user_class = CognitoRails::Utils.resolve_model_class(user_class)
74
+ return {} if user_class.nil?
75
+
76
+ (user_scopes[user_class.name] || {}).dup.with_indifferent_access
77
+ end
78
+
53
79
  def password_generator
54
80
  @password_generator || CognitoRails::PasswordGenerator.method(:generate)
55
81
  end
82
+
83
+ private
84
+
85
+ # @return [Hash{String => Hash}]
86
+ def user_scopes
87
+ @user_scopes ||= {}
88
+ end
56
89
  end
57
90
  end
58
91
  end
@@ -5,33 +5,76 @@ module CognitoRails
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  # @scope class
8
- # @!attribute _cognito_user_class [rw]
9
- # @return [String,nil] class name of user model
8
+ # @!attribute _cognito_user_classes [rw]
9
+ # @return [Hash{Symbol => String,Class,nil}] user class by attribute
10
10
 
11
11
  included do
12
- class_attribute :_cognito_user_class
12
+ class_attribute :_cognito_user_classes
13
+ self._cognito_user_classes = {}
13
14
  end
14
15
 
15
16
  # @return [ActiveRecord::Base,nil]
16
17
  def current_user
17
- @current_user ||= cognito_user_klass.find_by_cognito(external_cognito_id) if external_cognito_id
18
+ cognito_user_for(:current_user)
19
+ end
20
+
21
+ # @param attribute [Symbol]
22
+ # @return [ActiveRecord::Base,nil]
23
+ def cognito_user_for(attribute)
24
+ attribute = attribute.to_sym
25
+ external_id = external_cognito_id(attribute)
26
+ return unless external_id
27
+
28
+ user_klass = cognito_user_klass(attribute)
29
+ return unless user_klass
30
+
31
+ ivar = "@#{attribute}"
32
+ var = instance_variable_get(ivar)
33
+ return var if var
34
+
35
+ user = user_klass.find_by_cognito(external_id)
36
+ instance_variable_set(ivar, user)
18
37
  end
19
38
 
20
39
  private
21
40
 
41
+ # Get the useer from the specified attribute, or from the default :current_user if not found
42
+ module ClassMethods
43
+ # @param attribute [Symbol]
44
+ # @return [void]
45
+ def _cognito_define_user_reader(attribute)
46
+ return if method_defined?(attribute)
47
+
48
+ define_method(attribute) do
49
+ cognito_user_for(attribute)
50
+ end
51
+ end
52
+ end
53
+
22
54
  # @return [#find_by_cognito]
23
- def cognito_user_klass
24
- @cognito_user_klass ||= (self.class._cognito_user_class || CognitoRails::Config.default_user_class)&.constantize
55
+ def cognito_user_klass(attribute = :current_user)
56
+ attribute = attribute.to_sym
57
+ @cognito_user_klasses ||= {}
58
+ @cognito_user_klasses[attribute] ||= begin
59
+ user_class = self.class._cognito_user_classes[attribute]
60
+ user_class ||= CognitoRails::Config.default_user_class
61
+ CognitoRails::Utils.resolve_model_class(user_class)
62
+ end
25
63
  end
26
64
 
27
65
  # @return [String,nil] cognito user id
28
- def external_cognito_id
66
+ def external_cognito_id(attribute = :current_user)
29
67
  # @type [String,nil]
30
68
  token = request.headers['Authorization']&.split(' ')&.last
31
69
 
32
70
  return unless token
33
71
 
34
- CognitoRails::JWT.decode(token)&.dig(0, 'sub')
72
+ user_class = cognito_user_klass(attribute)
73
+ scope = CognitoRails::User.with_credentials(user_class)
74
+ user_pool_id = scope.user_pool_id
75
+ aws_region = scope.aws_region
76
+ jwt_payload = CognitoRails::JWT.decode(token, user_pool_id: user_pool_id, aws_region: aws_region)
77
+ jwt_payload&.dig(0, 'sub')
35
78
  end
36
79
  end
37
80
  end
@@ -8,9 +8,12 @@ module CognitoRails
8
8
  class JWT
9
9
  class << self
10
10
  # @param token [String] JWT token
11
+ # @param user_pool_id [String,nil] AWS Cognito User Pool ID
12
+ # @param aws_region [String,nil] AWS region
11
13
  # @return [Array<Hash>,nil]
12
- def decode(token)
13
- aws_idp = with_cache { URI.open(jwks_url).read }
14
+ def decode(token, user_pool_id: nil, aws_region: nil)
15
+ url = jwks_url(user_pool_id: user_pool_id, aws_region: aws_region)
16
+ aws_idp = with_cache(url) { URI.open(url).read }
14
17
  jwt_config = JSON.parse(aws_idp, symbolize_names: true)
15
18
 
16
19
  ::JWT.decode(token, nil, true, { jwks: jwt_config, algorithms: ['RS256'] })
@@ -21,17 +24,20 @@ module CognitoRails
21
24
 
22
25
  private
23
26
 
24
- def jwks_url
25
- "https://cognito-idp.#{Config.aws_region}.amazonaws.com/#{Config.aws_user_pool_id}/.well-known/jwks.json"
27
+ def jwks_url(user_pool_id: nil, aws_region: nil)
28
+ user_pool_id ||= Config.aws_user_pool_id
29
+ aws_region ||= Config.aws_region
30
+ "https://cognito-idp.#{aws_region}.amazonaws.com/#{user_pool_id}/.well-known/jwks.json"
26
31
  end
27
32
 
33
+ # @param cache_key [String]
28
34
  # @param block [Proc]
29
35
  # @yield [String] to be cached
30
36
  # @return [String] cached block
31
- def with_cache(&block)
37
+ def with_cache(cache_key, &block)
32
38
  return yield unless Config.cache_adapter.respond_to?(:fetch)
33
39
 
34
- Config.cache_adapter.fetch('aws_idp', expires_in: 4.hours, &block)
40
+ Config.cache_adapter.fetch("aws_idp:#{cache_key}", expires_in: 4.hours, &block)
35
41
  end
36
42
  end
37
43
  end
@@ -13,6 +13,8 @@ module CognitoRails
13
13
  class_attribute :_cognito_custom_attributes
14
14
  class_attribute :_cognito_attribute_name
15
15
  class_attribute :_cognito_password_policy
16
+ class_attribute :_cognito_aws_user_pool_id
17
+ class_attribute :_cognito_aws_client_credentials
16
18
  self._cognito_custom_attributes = []
17
19
 
18
20
  before_create do
@@ -24,14 +26,13 @@ module CognitoRails
24
26
  end
25
27
  end
26
28
 
27
- # rubocop:disable Metrics/BlockLength
28
29
  class_methods do
29
30
  # @return [Array<ActiveRecord::Base>] all users
30
31
  # @raise [CognitoRails::Error] if failed to fetch users
31
32
  # @raise [ActiveRecord::RecordInvalid] if failed to save user
32
33
  # @yield [user, user_data] yields user and user_data just before saving
33
34
  def sync_from_cognito!
34
- response = User.all
35
+ response = User.with_credentials(self).all
35
36
  response.users.map do |user_data|
36
37
  sync_user!(user_data) do |user|
37
38
  yield user, user_data if block_given?
@@ -90,7 +91,7 @@ module CognitoRails
90
91
  end
91
92
 
92
93
  def cognito_user
93
- @cognito_user ||= User.find(cognito_external_id, user_class: self.class)
94
+ @cognito_user ||= cognito_scope.find(cognito_external_id)
94
95
  end
95
96
 
96
97
  protected
@@ -98,16 +99,19 @@ module CognitoRails
98
99
  def init_cognito_user
99
100
  return if cognito_external_id.present?
100
101
 
101
- cognito_user = User.new(init_attributes)
102
+ cognito_user = cognito_scope.new(init_attributes)
102
103
  cognito_user.save!
103
104
  self.cognito_external_id = cognito_user.id
104
105
  end
105
106
 
106
107
  def init_attributes
107
- attrs = { email: email, user_class: self.class }
108
+ attrs = { email: email }
108
109
  attrs[:phone] = phone if respond_to?(:phone)
109
110
  attrs[:password] = password if respond_to?(:password)
110
111
  attrs[:custom_attributes] = instance_custom_attributes
112
+ attrs[:verify_email] = self.class._cognito_verify_email
113
+ attrs[:verify_phone] = self.class._cognito_verify_phone
114
+ attrs[:password_policy] = self.class._cognito_password_policy
111
115
  attrs
112
116
  end
113
117
 
@@ -128,6 +132,11 @@ module CognitoRails
128
132
  cognito_user&.destroy!
129
133
  end
130
134
 
135
+ def cognito_scope
136
+ @cognito_scope ||= User.with_credentials(self.class)
137
+
138
+ end
139
+
131
140
  class_methods do
132
141
  # @param name [String] attribute name
133
142
  # @return [ActiveRecord::Base] model class
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'active_record'
4
4
  require 'active_model/validations'
5
- require 'securerandom'
6
5
  require 'aws-sdk-cognitoidentityprovider'
7
6
 
8
7
  module CognitoRails
@@ -20,9 +19,13 @@ module CognitoRails
20
19
  # @return [Array<Hash>,nil]
21
20
  # @!attribute user_class [rw]
22
21
  # @return [Class,nil]
23
- # rubocop:disable Metrics/ClassLength
24
22
  class User
25
- # rubocop:enable Metrics/ClassLength
23
+ SCOPE_KEYS = %i[
24
+ user_pool_id
25
+ aws_region
26
+ access_key_id
27
+ secret_access_key
28
+ ].freeze
26
29
 
27
30
  include ActiveModel::Validations
28
31
 
@@ -30,48 +33,143 @@ module CognitoRails
30
33
 
31
34
  validates :email, presence: true
32
35
 
36
+ # Helper bound to a specific Cognito configuration.
37
+ class CredentialsScope
38
+ def initialize(scope = {})
39
+ @scope = User.scope_from(scope)
40
+ end
41
+
42
+ # @param id [String]
43
+ # @return [OpenStruct]
44
+ def find_raw(id)
45
+ cognito_client.admin_get_user(user_pool_id: user_pool_id, username: id)
46
+ end
47
+
48
+ # @param id [String]
49
+ # @return [CognitoRails::User]
50
+ def find(id)
51
+ response = find_raw(id)
52
+
53
+ new.tap do |user|
54
+ user.id = response.username
55
+ user.email = User.extract_cognito_attribute(response.user_attributes, :email)
56
+ user.phone = User.extract_cognito_attribute(response.user_attributes, :phone_number)
57
+ end
58
+ end
59
+
60
+ # @return [OpenStruct]
61
+ def all
62
+ cognito_client.list_users(user_pool_id: user_pool_id)
63
+ end
64
+
65
+ # @param attributes [Hash]
66
+ # @return [CognitoRails::User]
67
+ def create!(attributes = {})
68
+ user = new(attributes)
69
+ user.save!
70
+ user
71
+ end
72
+
73
+ # @param attributes [Hash]
74
+ # @return [CognitoRails::User]
75
+ def create(attributes = {})
76
+ user = new(attributes)
77
+ user.save
78
+ user
79
+ end
80
+
81
+ # @param attributes [Hash]
82
+ # @return [CognitoRails::User]
83
+ def new(attributes = {})
84
+ attrs = attributes.with_indifferent_access
85
+ User.new(attrs.merge(scope: @scope))
86
+ end
87
+
88
+ # @return [String]
89
+ def user_pool_id
90
+ @scope[:user_pool_id] || Config.aws_user_pool_id
91
+ end
92
+
93
+ # @return [String]
94
+ def aws_region
95
+ @scope[:aws_region] || config_credentials[:region] || Config.aws_region
96
+ end
97
+
98
+ # @return [Aws::CognitoIdentityProvider::Client]
99
+ def cognito_client
100
+ return User.cognito_client if use_default_client?
101
+
102
+ credentials = {
103
+ access_key_id: @scope[:access_key_id],
104
+ secret_access_key: @scope[:secret_access_key]
105
+ }.compact
106
+
107
+ User.cognito_client_for_credentials(credentials, aws_region: aws_region)
108
+ end
109
+
110
+ private
111
+
112
+ def use_default_client?
113
+ @scope[:aws_region].nil? && @scope[:access_key_id].nil? && @scope[:secret_access_key].nil?
114
+ end
115
+
116
+ def config_credentials
117
+ Config.aws_client_credentials.with_indifferent_access
118
+ end
119
+ end
120
+
33
121
  # @param attributes [Hash]
34
122
  # @option attributes [String] :email
35
123
  # @option attributes [String, nil] :password
36
124
  # @option attributes [String, nil] :phone
37
125
  # @option attributes [Array<Hash>, nil] :custom_attributes
38
- # @option attributes [Class, nil] :user_class
126
+ # @option attributes [String, nil] :user_pool_id
127
+ # @option attributes [String, nil] :aws_region
128
+ # @option attributes [String, nil] :access_key_id
129
+ # @option attributes [String, nil] :secret_access_key
130
+ # @option attributes [Boolean, nil] :verify_email
131
+ # @option attributes [Boolean, nil] :verify_phone
132
+ # @option attributes [Symbol, nil] :password_policy
133
+ # @option attributes [String,Symbol,Class,nil] :user_class
39
134
  def initialize(attributes = {})
40
135
  attributes = attributes.with_indifferent_access
136
+
137
+ @scope = self.class.extract_scope(attributes)
138
+ @verify_email = attributes[:verify_email]
139
+ @verify_phone = attributes[:verify_phone]
140
+ @password_policy = attributes[:password_policy]
141
+
142
+ self.user_class = resolve_instance_user_class(attributes)
41
143
  self.email = attributes[:email]
42
144
  self.password = attributes[:password] || Config.password_generator.call
43
145
  self.phone = attributes[:phone]
44
- self.user_class = attributes[:user_class] || Config.default_user_class.constantize
45
146
  self.custom_attributes = attributes[:custom_attributes]
46
147
  end
47
148
 
149
+ # @param credentials [Hash]
150
+ # @return [CredentialsScope]
151
+ def self.with_credentials(credentials = {})
152
+ CredentialsScope.new(credentials)
153
+ end
154
+
155
+ # @param id [String]
156
+ # @return [OpenStruct]
157
+ def self.find_raw(id)
158
+ resolve_scope(nil).find_raw(id)
159
+ end
160
+
48
161
  # @param id [String]
49
- # @param user_class [nil,Object]
50
162
  # @return [CognitoRails::User]
51
- def self.find(id, user_class = nil)
52
- result = cognito_client.admin_get_user(
53
- {
54
- user_pool_id: CognitoRails::Config.aws_user_pool_id, # required
55
- username: id # required
56
- }
57
- )
58
- user = new(user_class: user_class)
59
- user.id = result.username
60
- user.email = extract_cognito_attribute(result.user_attributes, :email)
61
- user.phone = extract_cognito_attribute(result.user_attributes, :phone_number)
62
- user
163
+ def self.find(id)
164
+ resolve_scope(nil).find(id)
63
165
  end
64
166
 
167
+ # @return [OpenStruct]
65
168
  def self.all
66
- cognito_client.list_users(user_pool_id: CognitoRails::Config.aws_user_pool_id)
169
+ resolve_scope(nil).all
67
170
  end
68
171
 
69
172
  # @param attributes [Hash]
70
- # @option attributes [String] :email
71
- # @option attributes [String] :password
72
- # @option attributes [String, nil] :phone
73
- # @option attributes [Array<Hash>, nil] :custom_attributes
74
- # @option attributes [Class, nil] :user_class
75
173
  # @return [CognitoRails::User]
76
174
  def self.create!(attributes = {})
77
175
  user = new(attributes)
@@ -80,11 +178,6 @@ module CognitoRails
80
178
  end
81
179
 
82
180
  # @param attributes [Hash]
83
- # @option attributes [String] :email
84
- # @option attributes [String] :password
85
- # @option attributes [String, nil] :phone
86
- # @option attributes [Array<Hash>, nil] :custom_attributes
87
- # @option attributes [Class, nil] :user_class
88
181
  # @return [CognitoRails::User]
89
182
  def self.create(attributes = {})
90
183
  user = new(attributes)
@@ -92,6 +185,59 @@ module CognitoRails
92
185
  user
93
186
  end
94
187
 
188
+ # @return [Aws::CognitoIdentityProvider::Client]
189
+ def self.cognito_client
190
+ cognito_client_for_credentials(Config.aws_client_credentials)
191
+ end
192
+
193
+ # @param credentials [Hash]
194
+ # @param aws_region [String,nil]
195
+ # @return [Aws::CognitoIdentityProvider::Client]
196
+ def self.cognito_client_for_credentials(credentials, aws_region: nil)
197
+ client_options = cognito_client_options(credentials, aws_region: aws_region)
198
+ cache_key = client_options.sort_by { |key, _| key.to_s }
199
+
200
+ @cognito_clients ||= {}
201
+ @cognito_clients[cache_key] ||= Aws::CognitoIdentityProvider::Client.new(client_options)
202
+ end
203
+
204
+ # @param attributes [Array<Hash,OpenStruct>]
205
+ # @param column [String,Symbol]
206
+ # @return [String,nil]
207
+ def self.extract_cognito_attribute(attributes, column)
208
+ attribute = attributes.find { |entry| read_attribute_name(entry) == column.to_s }
209
+ return unless attribute
210
+
211
+ read_attribute_value(attribute)
212
+ end
213
+
214
+ # @param credentials [Hash]
215
+ # @param aws_region [String,nil]
216
+ # @return [Hash]
217
+ def self.cognito_client_options(credentials, aws_region: nil)
218
+ credentials = (credentials || {}).with_indifferent_access
219
+ region = aws_region || credentials[:region] || Config.aws_region
220
+
221
+ {
222
+ region: region,
223
+ access_key_id: credentials[:access_key_id],
224
+ secret_access_key: credentials[:secret_access_key]
225
+ }.compact.with_indifferent_access
226
+ end
227
+
228
+ def self.scope_from(scope)
229
+ case scope
230
+ when nil
231
+ {}.with_indifferent_access
232
+ when Hash
233
+ normalize_scope_hash(scope)
234
+ when Class
235
+ CognitoRails::Config.user_scope_for(scope)
236
+ else
237
+ raise ArgumentError, 'scope must be a Hash'
238
+ end
239
+ end
240
+
95
241
  # @return [Boolean]
96
242
  def new_record?
97
243
  !persisted?
@@ -127,7 +273,7 @@ module CognitoRails
127
273
 
128
274
  cognito_client.admin_delete_user(
129
275
  {
130
- user_pool_id: CognitoRails::Config.aws_user_pool_id,
276
+ user_pool_id: user_pool_id,
131
277
  username: id
132
278
  }
133
279
  )
@@ -142,38 +288,76 @@ module CognitoRails
142
288
  destroy || (raise ActiveRecord::RecordInvalid, self)
143
289
  end
144
290
 
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
- )
291
+ private
292
+
293
+ def self.resolve_scope(scope = nil)
294
+ with_credentials(scope_from(scope))
151
295
  end
152
296
 
153
- def self.extract_cognito_attribute(attributes, column)
154
- attributes.find { |attribute| attribute[:name] == column.to_s }&.dig(:value)
297
+ def self.extract_scope(attributes)
298
+ inline_scope = attributes.slice(*SCOPE_KEYS).with_indifferent_access
299
+ raw_scope = attributes.delete(:scope)
300
+ raw_scope = raw_scope.nil? ? {} : raw_scope
301
+
302
+ merged_scope = raw_scope.with_indifferent_access.merge(inline_scope)
303
+
304
+ scope_from(merged_scope)
155
305
  end
156
306
 
157
- private
307
+ def self.normalize_scope_hash(scope)
308
+ scope = scope.with_indifferent_access
309
+
310
+ {
311
+ user_pool_id: scope[:user_pool_id],
312
+ aws_region: scope[:aws_region],
313
+ access_key_id: scope[:access_key_id],
314
+ secret_access_key: scope[:secret_access_key]
315
+ }.compact.with_indifferent_access
316
+ end
317
+
318
+ def self.read_attribute_name(attribute)
319
+ attribute.respond_to?(:name) ? attribute.name.to_s : attribute[:name].to_s
320
+ end
321
+
322
+ def self.read_attribute_value(attribute)
323
+ attribute.respond_to?(:value) ? attribute.value : attribute[:value]
324
+ end
325
+
326
+ def resolve_instance_user_class(attributes)
327
+ resolved = CognitoRails::Utils.resolve_model_class(attributes[:user_class])
328
+ resolved || Config.default_user_class.constantize
329
+ end
330
+
331
+ def user_pool_id
332
+ credentials_scope.user_pool_id
333
+ end
334
+
335
+ def aws_region
336
+ credentials_scope.aws_region
337
+ end
158
338
 
159
339
  # @return [Aws::CognitoIdentityProvider::Client]
160
340
  def cognito_client
161
- self.class.cognito_client
341
+ credentials_scope.cognito_client
162
342
  end
163
343
 
164
344
  # @return [Boolean]
165
345
  def verify_email?
346
+ return @verify_email unless @verify_email.nil?
347
+
166
348
  user_class._cognito_verify_email
167
349
  end
168
350
 
169
351
  # @return [Boolean]
170
352
  def verify_phone?
353
+ return @verify_phone unless @verify_phone.nil?
354
+
171
355
  user_class._cognito_verify_phone
172
356
  end
173
357
 
174
358
  # @return [Symbol] :temporary | :user_provided
175
359
  def cognito_password_policy
176
- user_class._cognito_password_policy || :temporary
360
+ @password_policy || user_class._cognito_password_policy || :temporary
177
361
  end
178
362
 
179
363
  # @return [Array<Hash>]
@@ -181,7 +365,7 @@ module CognitoRails
181
365
  [
182
366
  *([{ name: 'email', value: email }] if email),
183
367
  *([{ name: 'phone_number', value: phone }] if phone),
184
- *custom_attributes
368
+ *Array(custom_attributes)
185
369
  ]
186
370
  end
187
371
 
@@ -193,7 +377,7 @@ module CognitoRails
193
377
  ]
194
378
  end
195
379
 
196
- # @return [Array<Hash>]
380
+ # @return [Hash]
197
381
  def password_attributes
198
382
  if cognito_password_policy == :user_provided
199
383
  { message_action: 'SUPPRESS' }
@@ -204,43 +388,37 @@ module CognitoRails
204
388
 
205
389
  def set_user_provided_password
206
390
  cognito_client.admin_set_user_password(
207
- {
208
- user_pool_id: CognitoRails::Config.aws_user_pool_id,
209
- username: email,
210
- password: password,
211
- permanent: true
212
- }
391
+ user_pool_id: user_pool_id,
392
+ username: email,
393
+ password: password,
394
+ permanent: true
213
395
  )
214
396
  end
215
397
 
216
398
  def save_for_create
217
- resp = cognito_client.admin_create_user(
399
+ response = cognito_client.admin_create_user(
218
400
  {
219
- user_pool_id: CognitoRails::Config.aws_user_pool_id,
401
+ user_pool_id: user_pool_id,
220
402
  username: email,
221
- user_attributes: [
222
- *general_user_attributes,
223
- *verify_user_attributes
224
- ],
403
+ user_attributes: [*general_user_attributes, *verify_user_attributes],
225
404
  **password_attributes
226
405
  }
227
406
  )
228
407
 
229
408
  set_user_provided_password if cognito_password_policy == :user_provided
230
-
231
- self.id = resp.user.attributes.find { |a| a[:name] == 'sub' }[:value]
409
+ self.id = self.class.extract_cognito_attribute(response.user.attributes, :sub)
232
410
  end
233
411
 
234
412
  def save_for_update
235
413
  cognito_client.admin_update_user_attributes(
236
- {
237
- user_pool_id: CognitoRails::Config.aws_user_pool_id,
238
- username: id,
239
- user_attributes: [
240
- *general_user_attributes
241
- ]
242
- }
414
+ user_pool_id: user_pool_id,
415
+ username: id,
416
+ user_attributes: [*general_user_attributes]
243
417
  )
244
418
  end
419
+
420
+ def credentials_scope
421
+ @credentials_scope ||= self.class.with_credentials(@scope)
422
+ end
245
423
  end
246
424
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CognitoRails
4
+ module Utils
5
+ class << self
6
+ # @param model_class [String,Symbol,Class,nil]
7
+ # @return [Class,nil]
8
+ def resolve_model_class(model_class)
9
+ case model_class
10
+ when nil
11
+ nil
12
+ when String, Symbol
13
+ model_class.to_s.constantize
14
+ else
15
+ model_class
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module CognitoRails
4
4
  # @return [String] gem version
5
- VERSION = '1.4.0'
5
+ VERSION = '1.5.0'
6
6
  end
data/lib/cognito_rails.rb CHANGED
@@ -17,29 +17,48 @@ module CognitoRails
17
17
  autoload :User
18
18
  autoload :JWT
19
19
  autoload :PasswordGenerator
20
+ autoload :Utils
20
21
 
21
22
  # @private
22
23
  module ModelInitializer
23
24
  # @param attribute_name [String]
25
+ # @param user_pool_id [String]
26
+ # @param aws_credentials [Hash,nil]
24
27
  # @return [void]
25
- def as_cognito_user(attribute_name: 'external_id')
28
+ def as_cognito_user(attribute_name: 'external_id', user_pool_id: nil, aws_credentials: nil)
26
29
  send :include, CognitoRails::Model
27
30
  self._cognito_attribute_name = attribute_name
31
+ self._cognito_aws_user_pool_id = user_pool_id
32
+ self._cognito_aws_client_credentials = aws_credentials
33
+
34
+ credentials = (aws_credentials || {}).with_indifferent_access
35
+
36
+ CognitoRails::Config.register_user_scope(
37
+ self,
38
+ {
39
+ user_pool_id: user_pool_id,
40
+ aws_region: credentials[:region],
41
+ access_key_id: credentials[:access_key_id],
42
+ secret_access_key: credentials[:secret_access_key]
43
+ }
44
+ )
28
45
  end
29
46
  end
30
47
 
31
48
  # @private
32
49
  module ControllerInitializer
33
50
  # @param user_class [Class,nil]
51
+ # @param attribute_name [String,Symbol]
34
52
  # @return [void]
35
- def cognito_authentication(user_class: nil)
53
+ def cognito_authentication(user_class: nil, attribute_name: :current_user)
36
54
  send :include, CognitoRails::Controller
37
- self._cognito_user_class = user_class
55
+
56
+ attribute = attribute_name.to_sym
57
+ self._cognito_user_classes = _cognito_user_classes.merge(attribute => user_class)
58
+ _cognito_define_user_reader(attribute)
38
59
  end
39
60
  end
40
61
  end
41
62
 
42
- # rubocop:disable Lint/SendWithMixinArgument
43
- ActiveRecord::Base.send(:extend, CognitoRails::ModelInitializer)
44
- ActionController::Metal.send(:extend, CognitoRails::ControllerInitializer)
45
- # rubocop:enable Lint/SendWithMixinArgument
63
+ ActiveRecord::Base.extend CognitoRails::ModelInitializer
64
+ ActionController::Metal.extend CognitoRails::ControllerInitializer
@@ -48,4 +48,52 @@ RSpec.describe CognitoRails::Controller, type: :model do
48
48
  expect(controller.current_user).to eq(user)
49
49
  end
50
50
  end
51
+
52
+ context 'with a custom controller attribute' do
53
+ class AdminController < ActionController::Base
54
+ cognito_authentication user_class: 'Admin', attribute_name: :admin_user
55
+
56
+ def request
57
+ @request ||= OpenStruct.new({ headers: { 'Authorization' => 'Bearer aaaaa' } })
58
+ end
59
+ end
60
+
61
+ let(:controller) { AdminController.new }
62
+
63
+ it 'returns a user through the configured attribute' do
64
+ user = Admin.create!(email: sample_cognito_email, phone: sample_cognito_phone, cognito_id: '123123123')
65
+
66
+ expect(CognitoRails::JWT).to receive(:decode).at_least(:once).and_return([{ 'sub' => '123123123' }])
67
+ expect(controller.admin_user).to eq(user)
68
+ end
69
+
70
+ it 'keeps current_user retrocompatible with the default class' do
71
+ user = User.create!(email: sample_cognito_email, external_id: '123123123')
72
+
73
+ expect(CognitoRails::JWT).to receive(:decode).at_least(:once).and_return([{ 'sub' => '123123123' }])
74
+ expect(controller.current_user).to eq(user)
75
+ end
76
+ end
77
+
78
+ context 'with multiple cognito_authentication declarations' do
79
+ class DualAuthController < ActionController::Base
80
+ cognito_authentication
81
+ cognito_authentication user_class: 'Admin', attribute_name: :admin_user
82
+
83
+ def request
84
+ @request ||= OpenStruct.new({ headers: { 'Authorization' => 'Bearer aaaaa' } })
85
+ end
86
+ end
87
+
88
+ let(:controller) { DualAuthController.new }
89
+
90
+ it 'resolves both configured user readers' do
91
+ user = User.create!(email: sample_cognito_email, external_id: '123123123')
92
+ admin = Admin.create!(email: sample_cognito_email, phone: sample_cognito_phone, cognito_id: '123123123')
93
+
94
+ expect(CognitoRails::JWT).to receive(:decode).at_least(:once).and_return([{ 'sub' => '123123123' }])
95
+ expect(controller.current_user).to eq(user)
96
+ expect(controller.admin_user).to eq(admin)
97
+ end
98
+ end
51
99
  end
@@ -25,11 +25,13 @@ RSpec.describe CognitoRails::User, type: :model do
25
25
  expect(record.user_class).to eq(User)
26
26
  end
27
27
 
28
- it 'finds a user with admin class' do
29
- expect(described_class).to receive(:cognito_client).and_return(fake_cognito_client)
28
+ it 'finds a user with explicit credentials scope' do
29
+ allow(Aws::CognitoIdentityProvider::Client).to receive(:new).and_return(fake_cognito_client)
30
+ described_class.instance_variable_set(:@cognito_clients, nil)
31
+
32
+ record = described_class.with_credentials(Admin).find(sample_cognito_id)
30
33
 
31
- record = described_class.find(sample_cognito_id, Admin)
32
- expect(record.user_class).to eq(Admin)
34
+ expect(record.id).to eq(sample_cognito_id)
33
35
  end
34
36
 
35
37
  it 'finds a user with default class' do
@@ -208,7 +210,27 @@ RSpec.describe CognitoRails::User, type: :model do
208
210
 
209
211
  context 'admin' do
210
212
  before do
211
- expect(CognitoRails::User).to receive(:cognito_client).at_least(:once).and_return(fake_cognito_client)
213
+ allow(CognitoRails::User).to receive(:cognito_client).and_return(fake_cognito_client)
214
+ allow(Aws::CognitoIdentityProvider::Client).to receive(:new).and_return(fake_cognito_client)
215
+ CognitoRails::User.instance_variable_set(:@cognito_clients, nil)
216
+ end
217
+
218
+ it 'uses model aws_credentials if present' do
219
+ Admin.create!(email: sample_cognito_email, phone: '12345678')
220
+
221
+ expect(Aws::CognitoIdentityProvider::Client).to have_received(:new).with(
222
+ hash_including(
223
+ region: 'admin-region',
224
+ access_key_id: 'admin_access_key_id',
225
+ secret_access_key: 'admin_secret_access_key'
226
+ )
227
+ )
228
+ end
229
+
230
+ it 'caches clients by region and credentials' do
231
+ expect(Aws::CognitoIdentityProvider::Client).to receive(:new).once.and_return(fake_cognito_client)
232
+
233
+ 2.times { Admin.create!(email: "#{SecureRandom.uuid}@mail.com", phone: SecureRandom.hex(5)) }
212
234
  end
213
235
 
214
236
  it '#find_by_cognito' do
@@ -36,7 +36,14 @@ class Admin < ActiveRecord::Base
36
36
  validates :phone, presence: true
37
37
  validates :phone, uniqueness: true
38
38
 
39
- as_cognito_user attribute_name: 'cognito_id'
39
+ as_cognito_user(
40
+ attribute_name: 'cognito_id',
41
+ aws_credentials: {
42
+ region: 'admin-region',
43
+ access_key_id: 'admin_access_key_id',
44
+ secret_access_key: 'admin_secret_access_key'
45
+ }
46
+ )
40
47
  cognito_verify_email
41
48
  cognito_verify_phone
42
49
  define_cognito_attribute 'role', 'admin'
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: 1.4.0
4
+ version: 1.5.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: 2025-01-20 00:00:00.000000000 Z
11
+ date: 2026-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -113,6 +113,7 @@ files:
113
113
  - lib/cognito_rails/model.rb
114
114
  - lib/cognito_rails/password_generator.rb
115
115
  - lib/cognito_rails/user.rb
116
+ - lib/cognito_rails/utils.rb
116
117
  - lib/cognito_rails/version.rb
117
118
  - spec/cognito_rails/controller_spec.rb
118
119
  - spec/cognito_rails/jwt_spec.rb