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 +4 -4
- data/lib/cognito_rails/config.rb +33 -0
- data/lib/cognito_rails/controller.rb +51 -8
- data/lib/cognito_rails/jwt.rb +12 -6
- data/lib/cognito_rails/model.rb +14 -5
- data/lib/cognito_rails/user.rb +242 -64
- data/lib/cognito_rails/utils.rb +20 -0
- data/lib/cognito_rails/version.rb +1 -1
- data/lib/cognito_rails.rb +26 -7
- data/spec/cognito_rails/controller_spec.rb +48 -0
- data/spec/cognito_rails/user_spec.rb +27 -5
- data/spec/support/schema.rb +8 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e26720020fe66850fb2c51baf995cbb22e1bb0713fa84714b18c7c69ba7eeca
|
|
4
|
+
data.tar.gz: f0fdfbdf0375fb7a8dcd1d3c4d44393947f785a58e33579d4907d97010ea2b40
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca4c17f11c1e0c9b2619385ae7019632d83505838ef957760a349ccb21dc55e27be8c61c797af9b45db0a93fa17bf0ee6569dd86f8e5275269cab299d991ec96
|
|
7
|
+
data.tar.gz: b66d984895beb150302232cb7efa59528fd46cd9c61ea3193458fea3a93fc772db27f83af2020879e0c1ca9bb11b861e944570433fcee3222dbc217beaeef4e6
|
data/lib/cognito_rails/config.rb
CHANGED
|
@@ -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
|
|
9
|
-
# @return [String,nil] class
|
|
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 :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/cognito_rails/jwt.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
40
|
+
Config.cache_adapter.fetch("aws_idp:#{cache_key}", expires_in: 4.hours, &block)
|
|
35
41
|
end
|
|
36
42
|
end
|
|
37
43
|
end
|
data/lib/cognito_rails/model.rb
CHANGED
|
@@ -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 ||=
|
|
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 =
|
|
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
|
|
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
|
data/lib/cognito_rails/user.rb
CHANGED
|
@@ -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
|
-
|
|
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 [
|
|
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
|
|
52
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
def self.
|
|
148
|
-
|
|
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.
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
399
|
+
response = cognito_client.admin_create_user(
|
|
218
400
|
{
|
|
219
|
-
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
|
-
|
|
238
|
-
|
|
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
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
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
|
data/spec/support/schema.rb
CHANGED
|
@@ -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
|
|
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
|
+
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:
|
|
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
|