grape_token_auth 0.0.0 → 0.1.0.rc1
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/.rspec +1 -1
- data/.ruby-version +1 -0
- data/CONTRIBUTING.md +46 -0
- data/README.md +222 -12
- data/Rakefile +11 -1
- data/bin/rspec +16 -0
- data/circle.yml +6 -0
- data/config/database.yml +8 -0
- data/grape_token_auth.gemspec +15 -0
- data/lib/grape_token_auth/api_helpers.rb +30 -0
- data/lib/grape_token_auth/apis/confirmation_api.rb +49 -0
- data/lib/grape_token_auth/apis/omniauth_api.rb +149 -0
- data/lib/grape_token_auth/apis/password_api.rb +138 -0
- data/lib/grape_token_auth/apis/registration_api.rb +88 -0
- data/lib/grape_token_auth/apis/session_api.rb +60 -0
- data/lib/grape_token_auth/apis/token_validation_api.rb +29 -0
- data/lib/grape_token_auth/authentication_header.rb +52 -0
- data/lib/grape_token_auth/authorizer_data.rb +58 -0
- data/lib/grape_token_auth/configuration.rb +81 -0
- data/lib/grape_token_auth/exceptions.rb +29 -0
- data/lib/grape_token_auth/key_generator.rb +44 -0
- data/lib/grape_token_auth/lookup_token.rb +46 -0
- data/lib/grape_token_auth/mail/mail.rb +28 -0
- data/lib/grape_token_auth/mail/message_base.rb +34 -0
- data/lib/grape_token_auth/mail/messages/confirmation/confirmation.html.erb +16 -0
- data/lib/grape_token_auth/mail/messages/confirmation/confirmation.text.erb +8 -0
- data/lib/grape_token_auth/mail/messages/confirmation/confirmation_email.rb +27 -0
- data/lib/grape_token_auth/mail/messages/password_reset/password_reset.html.erb +18 -0
- data/lib/grape_token_auth/mail/messages/password_reset/password_reset.text.erb +9 -0
- data/lib/grape_token_auth/mail/messages/password_reset/password_reset_email.rb +27 -0
- data/lib/grape_token_auth/mail/smtp_mailer.rb +50 -0
- data/lib/grape_token_auth/middleware.rb +42 -0
- data/lib/grape_token_auth/mount_helpers.rb +80 -0
- data/lib/grape_token_auth/omniauth/omniauth_failure_html.rb +26 -0
- data/lib/grape_token_auth/omniauth/omniauth_html_base.rb +23 -0
- data/lib/grape_token_auth/omniauth/omniauth_resource.rb +109 -0
- data/lib/grape_token_auth/omniauth/omniauth_success_html.rb +61 -0
- data/lib/grape_token_auth/omniauth/response_template.html.erb +38 -0
- data/lib/grape_token_auth/orm_integrations/active_record_token_auth.rb +310 -0
- data/lib/grape_token_auth/resource/resource_creator.rb +48 -0
- data/lib/grape_token_auth/resource/resource_crud_base.rb +43 -0
- data/lib/grape_token_auth/resource/resource_finder.rb +53 -0
- data/lib/grape_token_auth/resource/resource_updater.rb +40 -0
- data/lib/grape_token_auth/token.rb +23 -0
- data/lib/grape_token_auth/token_authentication.rb +8 -0
- data/lib/grape_token_auth/token_authorizer.rb +60 -0
- data/lib/grape_token_auth/unauthorized_middleware.rb +20 -0
- data/lib/grape_token_auth/version.rb +1 -1
- data/lib/grape_token_auth.rb +65 -2
- metadata +266 -13
@@ -0,0 +1,310 @@
|
|
1
|
+
require 'bcrypt'
|
2
|
+
|
3
|
+
module GrapeTokenAuth
|
4
|
+
module ActiveRecord
|
5
|
+
module TokenAuth
|
6
|
+
attr_accessor :password, :password_confirmation
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.serialize :tokens, JSON
|
10
|
+
base.after_initialize { self.tokens ||= {} }
|
11
|
+
base.validates :password, presence: true, on: :create
|
12
|
+
base.validate :password_confirmation_matches,
|
13
|
+
if: :encrypted_password_changed?
|
14
|
+
base.validates :email, uniqueness: { scope: :provider },
|
15
|
+
format: { with: Configuration::EMAIL_VALIDATION,
|
16
|
+
message: 'invalid email' }, allow_blank: true
|
17
|
+
base.before_update :synchronize_email_and_uid
|
18
|
+
|
19
|
+
class << base
|
20
|
+
def exists_in_column?(column, value)
|
21
|
+
where(column => value).count > 0
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_with_reset_token(attributes)
|
25
|
+
original_token = attributes[:reset_password_token]
|
26
|
+
reset_password_token = LookupToken.digest(:reset_password_token,
|
27
|
+
original_token)
|
28
|
+
|
29
|
+
recoverable = find_or_initialize_by(reset_password_token:
|
30
|
+
reset_password_token)
|
31
|
+
|
32
|
+
return nil unless recoverable.persisted?
|
33
|
+
|
34
|
+
recoverable.reset_password_token = original_token
|
35
|
+
recoverable
|
36
|
+
end
|
37
|
+
|
38
|
+
def reset_token_lifespan
|
39
|
+
@reset_token_lifespan || 60 * 60 * 6 # 6 hours
|
40
|
+
end
|
41
|
+
|
42
|
+
def confirmation_token_lifespan
|
43
|
+
@confirmat_token_lifespan || 60 * 60 * 24 * 3 # 3 days
|
44
|
+
end
|
45
|
+
|
46
|
+
def confirm_by_token(token)
|
47
|
+
confirmation_digest = LookupToken.digest(:confirmation_token,
|
48
|
+
token)
|
49
|
+
confirmable = find_or_initialize_by(confirmation_token:
|
50
|
+
confirmation_digest)
|
51
|
+
|
52
|
+
return nil unless confirmable.persisted?
|
53
|
+
|
54
|
+
confirmable.confirm
|
55
|
+
confirmable
|
56
|
+
end
|
57
|
+
|
58
|
+
def serialize_into_session(record)
|
59
|
+
[record.to_key, record.authenticatable_salt]
|
60
|
+
end
|
61
|
+
|
62
|
+
def serialize_from_session(key, salt)
|
63
|
+
record = get(key)
|
64
|
+
record if record && record.authenticatable_salt == salt
|
65
|
+
end
|
66
|
+
|
67
|
+
def get(key)
|
68
|
+
find(key).first
|
69
|
+
end
|
70
|
+
|
71
|
+
attr_writer :reset_token_lifespan
|
72
|
+
attr_writer :confirmation_token_lifespan
|
73
|
+
attr_accessor :case_insensitive_keys
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def authenticatable_salt
|
78
|
+
end
|
79
|
+
|
80
|
+
def reset_password_period_valid?
|
81
|
+
return false unless reset_password_sent_at
|
82
|
+
expiry = reset_password_sent_at.utc + self.class.reset_token_lifespan
|
83
|
+
Time.now.utc <= expiry
|
84
|
+
end
|
85
|
+
|
86
|
+
def reset_password(password, password_confirmation)
|
87
|
+
self.password = password
|
88
|
+
self.password_confirmation = password_confirmation
|
89
|
+
save
|
90
|
+
end
|
91
|
+
|
92
|
+
def password_confirmation_matches
|
93
|
+
return if password.present? && password_confirmation.present? &&
|
94
|
+
password == password_confirmation
|
95
|
+
errors.add(:password_confirmation,
|
96
|
+
'password confirmation does not match')
|
97
|
+
end
|
98
|
+
|
99
|
+
def create_new_auth_token(client_id = nil)
|
100
|
+
self.tokens = {} if tokens.nil?
|
101
|
+
token = Token.new(client_id)
|
102
|
+
last_token = tokens.fetch(client_id, {})['token']
|
103
|
+
tokens[token.client_id] = token.to_h.merge(last_token: last_token)
|
104
|
+
self.save!
|
105
|
+
|
106
|
+
build_auth_header(token)
|
107
|
+
end
|
108
|
+
|
109
|
+
def valid_token?(token, client_id)
|
110
|
+
return false unless tokens && tokens[client_id]
|
111
|
+
return true if token_is_current?(token, client_id)
|
112
|
+
return true if token_can_be_reused?(token, client_id)
|
113
|
+
|
114
|
+
false
|
115
|
+
end
|
116
|
+
|
117
|
+
def password=(new_password)
|
118
|
+
@password = new_password
|
119
|
+
self.encrypted_password = BCrypt::Password.create(new_password)
|
120
|
+
end
|
121
|
+
|
122
|
+
def valid_password?(password)
|
123
|
+
BCrypt::Password.new(encrypted_password) == password
|
124
|
+
end
|
125
|
+
|
126
|
+
def while_record_locked(&block)
|
127
|
+
with_lock(&block)
|
128
|
+
end
|
129
|
+
|
130
|
+
def extend_batch_buffer(token, client_id)
|
131
|
+
token_hash = tokens[client_id]
|
132
|
+
token_hash[:updated_at] = Time.now
|
133
|
+
expiry = token_hash[:expiry] || token_hash['expiry']
|
134
|
+
save!
|
135
|
+
build_auth_header(Token.new(client_id, token, expiry))
|
136
|
+
end
|
137
|
+
|
138
|
+
# Copied out of Devise. Excludes the serialization blacklist.
|
139
|
+
def serializable_hash(options = nil)
|
140
|
+
options ||= {}
|
141
|
+
options[:except] = Array(options[:except])
|
142
|
+
|
143
|
+
if options[:force_except]
|
144
|
+
options[:except].concat Array(options[:force_except])
|
145
|
+
else
|
146
|
+
blacklist = GrapeTokenAuth.configuration.serialization_blacklist
|
147
|
+
options[:except].concat blacklist
|
148
|
+
end
|
149
|
+
|
150
|
+
super(options)
|
151
|
+
end
|
152
|
+
|
153
|
+
def send_reset_password_instructions(opts)
|
154
|
+
token = set_reset_password_token
|
155
|
+
|
156
|
+
opts ||= {}
|
157
|
+
opts[:client_config] ||= 'default'
|
158
|
+
opts[:token] = token
|
159
|
+
opts[:to] = email
|
160
|
+
|
161
|
+
GrapeTokenAuth.send_notification(:reset_password_instructions, opts)
|
162
|
+
|
163
|
+
token
|
164
|
+
end
|
165
|
+
|
166
|
+
def send_confirmation_instructions(opts)
|
167
|
+
opts ||= {}
|
168
|
+
token = generate_confirmation_token!
|
169
|
+
opts[:token] = token
|
170
|
+
|
171
|
+
# fall back to "default" config name
|
172
|
+
opts[:client_config] ||= 'default'
|
173
|
+
opts[:to] = pending_reconfirmation? ? unconfirmed_email : email
|
174
|
+
|
175
|
+
GrapeTokenAuth.send_notification(:confirmation_instructions, opts)
|
176
|
+
token
|
177
|
+
end
|
178
|
+
|
179
|
+
# devise method
|
180
|
+
def confirm(args = {})
|
181
|
+
pending_any_confirmation do
|
182
|
+
if confirmation_period_expired?
|
183
|
+
errors.add(:email, :confirmation_period_expired)
|
184
|
+
return false
|
185
|
+
end
|
186
|
+
|
187
|
+
self.confirmed_at = Time.now.utc
|
188
|
+
|
189
|
+
#if self.class.reconfirmable && unconfirmed_email.present?
|
190
|
+
# self.email = unconfirmed_email
|
191
|
+
# self.unconfirmed_email = nil
|
192
|
+
|
193
|
+
# # We need to validate in such cases to enforce e-mail uniqueness
|
194
|
+
# saved = save(validate: true)
|
195
|
+
#else
|
196
|
+
saved = save(validate: args[:ensure_valid] == true)
|
197
|
+
|
198
|
+
after_confirmation if saved
|
199
|
+
saved
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def build_auth_url(url, params)
|
204
|
+
url = URI(url)
|
205
|
+
expiry = tokens[params[:client_id]][:expiry]
|
206
|
+
url.query = params.merge(uid: uid, expiry: expiry).to_query
|
207
|
+
url.to_s
|
208
|
+
end
|
209
|
+
|
210
|
+
def pending_reconfirmation?
|
211
|
+
unconfirmed_email.present?
|
212
|
+
end
|
213
|
+
|
214
|
+
def confirmed?
|
215
|
+
!!confirmed_at
|
216
|
+
end
|
217
|
+
|
218
|
+
def skip_confirmation!
|
219
|
+
self.confirmed_at = Time.now.utc
|
220
|
+
end
|
221
|
+
|
222
|
+
def confirmation_period_expired?
|
223
|
+
confirmation_sent_at && (Time.now > confirmation_sent_at + self.class.confirmation_token_lifespan)
|
224
|
+
end
|
225
|
+
|
226
|
+
def after_confirmation
|
227
|
+
end
|
228
|
+
|
229
|
+
def token_validation_response
|
230
|
+
as_json(except: [:tokens, :created_at, :updated_at])
|
231
|
+
end
|
232
|
+
|
233
|
+
private
|
234
|
+
|
235
|
+
# devise method
|
236
|
+
def pending_any_confirmation
|
237
|
+
if (!confirmed? || pending_reconfirmation?)
|
238
|
+
yield
|
239
|
+
else
|
240
|
+
self.errors.add(:email, :already_confirmed)
|
241
|
+
false
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def generate_confirmation_token!
|
246
|
+
token, enc = GrapeTokenAuth::LookupToken.generate(self.class,
|
247
|
+
:confirmation_token)
|
248
|
+
self.confirmation_token = enc
|
249
|
+
self.confirmation_sent_at = Time.now.utc
|
250
|
+
save(validate: false)
|
251
|
+
token
|
252
|
+
end
|
253
|
+
|
254
|
+
def set_reset_password_token
|
255
|
+
token, enc = GrapeTokenAuth::LookupToken.generate(self.class,
|
256
|
+
:reset_password_token)
|
257
|
+
|
258
|
+
self.reset_password_token = enc
|
259
|
+
self.reset_password_sent_at = Time.now.utc
|
260
|
+
save(validate: false)
|
261
|
+
token
|
262
|
+
end
|
263
|
+
|
264
|
+
def synchronize_email_and_uid
|
265
|
+
self.uid = email if provider == 'email'
|
266
|
+
end
|
267
|
+
|
268
|
+
def token_is_current?(token, client_id)
|
269
|
+
client_id_info = tokens[client_id]
|
270
|
+
expiry = client_id_info['expiry'] || client_id_info[:expiry]
|
271
|
+
token_hash = client_id_info['token'] || client_id_info[:token]
|
272
|
+
return false unless expiry && token
|
273
|
+
return false unless DateTime.strptime(expiry.to_s, '%s') > Time.now
|
274
|
+
return false unless tokens_match?(token_hash, token)
|
275
|
+
true
|
276
|
+
end
|
277
|
+
|
278
|
+
def fetch_with_indifference(hash, key)
|
279
|
+
hash[key.to_sym] || hash[key.to_s]
|
280
|
+
end
|
281
|
+
|
282
|
+
def token_can_be_reused?(token, client_id)
|
283
|
+
updated_at = fetch_with_indifference(tokens[client_id], :updated_at)
|
284
|
+
last_token = fetch_with_indifference(tokens[client_id], :last_token)
|
285
|
+
return false unless updated_at && last_token
|
286
|
+
return false unless within_batch_window?(Time.parse(updated_at))
|
287
|
+
return false unless tokens_match?(last_token, token)
|
288
|
+
true
|
289
|
+
end
|
290
|
+
|
291
|
+
def tokens_match?(token_hash, token)
|
292
|
+
BCrypt::Password.new(token_hash) == token
|
293
|
+
end
|
294
|
+
|
295
|
+
def within_batch_window?(time)
|
296
|
+
time > Time.now - GrapeTokenAuth.batch_request_buffer_throttle
|
297
|
+
end
|
298
|
+
|
299
|
+
def build_auth_header(token)
|
300
|
+
{
|
301
|
+
'access-token' => token.to_s,
|
302
|
+
'expiry' => token.expiry.to_s,
|
303
|
+
'client' => token.client_id.to_s,
|
304
|
+
'token-type' => 'Bearer',
|
305
|
+
'uid' => uid.to_s
|
306
|
+
}
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative 'resource_crud_base'
|
2
|
+
|
3
|
+
module GrapeTokenAuth
|
4
|
+
class ResourceCreator < ResourceCrudBase
|
5
|
+
def create!
|
6
|
+
validate_scope!
|
7
|
+
validate_params!
|
8
|
+
return false unless errors.empty?
|
9
|
+
create_resource!
|
10
|
+
return false unless errors.empty?
|
11
|
+
resource
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def create_resource!
|
17
|
+
@resource = resource_class.create(permitted_params.merge(provider: 'email'))
|
18
|
+
return if @resource.valid?
|
19
|
+
pull_validation_messages
|
20
|
+
end
|
21
|
+
|
22
|
+
def permitted_attributes
|
23
|
+
white_list = GrapeTokenAuth.configuration.param_white_list || {}
|
24
|
+
other_attributes = white_list[scope] || []
|
25
|
+
[:email, :password, :password_confirmation] + other_attributes
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate_params!
|
29
|
+
unpack_params.each do |label, value|
|
30
|
+
errors << validation_message(label, value)
|
31
|
+
end
|
32
|
+
errors.compact!
|
33
|
+
end
|
34
|
+
|
35
|
+
def unpack_params
|
36
|
+
[:email, :password_confirmation, :password]
|
37
|
+
.each_with_object({}) do |key, unpacked|
|
38
|
+
unpacked[key] = find_with_indifference(params, key)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def validation_message(label, value)
|
43
|
+
return "#{label} is required" unless value
|
44
|
+
return "#{label} must be a string" unless value.is_a? String
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
class ResourceCrudBase
|
3
|
+
attr_reader :resource, :errors, :scope
|
4
|
+
|
5
|
+
def initialize(params, configuration, scope = :user)
|
6
|
+
@configuration = configuration
|
7
|
+
@params = params
|
8
|
+
@errors = []
|
9
|
+
@scope = scope
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
attr_reader :configuration, :params, :resource_class
|
15
|
+
|
16
|
+
def validate_scope!
|
17
|
+
@resource_class = configuration.scope_to_class(scope)
|
18
|
+
fail ScopeUndefinedError.new(nil, scope) unless resource_class
|
19
|
+
end
|
20
|
+
|
21
|
+
def pull_validation_messages
|
22
|
+
@resource.errors.messages.map do |k, v|
|
23
|
+
v.each { |e| errors << "#{k} #{e}" }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def permitted_params
|
28
|
+
permitted_attributes.each_with_object({}) do |key, permitted|
|
29
|
+
value = find_with_indifference(params, key)
|
30
|
+
permitted[key] = value if value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_with_indifference(hash, key)
|
35
|
+
if hash.key?(key.to_sym)
|
36
|
+
return hash[key.to_sym]
|
37
|
+
elsif hash.key?(key.to_s)
|
38
|
+
return hash[key.to_s]
|
39
|
+
end
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
class ResourceFinder
|
3
|
+
def initialize(scope, params)
|
4
|
+
@scope = scope
|
5
|
+
@params = params
|
6
|
+
set_resource_class
|
7
|
+
set_finder_key
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.find(scope, params)
|
11
|
+
new(scope, params).find_resource
|
12
|
+
end
|
13
|
+
|
14
|
+
def find_resource
|
15
|
+
return unless finder_key
|
16
|
+
find_resource_by_key
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :scope, :params, :resource_class, :finder_key
|
22
|
+
|
23
|
+
def set_finder_key
|
24
|
+
auth_keys = configuration.authentication_keys
|
25
|
+
@finder_key = (params.keys.map(&:to_sym) & auth_keys).first
|
26
|
+
end
|
27
|
+
|
28
|
+
def find_resource_by_key
|
29
|
+
query_value = params[finder_key] || params[finder_key.to_s]
|
30
|
+
query = "#{finder_key} = ? AND provider='email'"
|
31
|
+
|
32
|
+
insensitive_keys = resource_class.case_insensitive_keys
|
33
|
+
if insensitive_keys && insensitive_keys.include?(finder_key)
|
34
|
+
query_value.downcase!
|
35
|
+
end
|
36
|
+
|
37
|
+
resource_class.where(query, query_value).first
|
38
|
+
|
39
|
+
# if ActiveRecord::Base.connection.adapter_name.downcase.starts_with? 'mysql'
|
40
|
+
# q = "BINARY " + q
|
41
|
+
# end
|
42
|
+
end
|
43
|
+
|
44
|
+
def configuration
|
45
|
+
GrapeTokenAuth.configuration
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_resource_class
|
49
|
+
@resource_class = configuration.scope_to_class(scope)
|
50
|
+
fail(ScopeUndefinedError.new(scope)) unless resource_class
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
class ResourceUpdater < ResourceCrudBase
|
3
|
+
def initialize(resource, params, configuration, scope = :user)
|
4
|
+
@resource = resource
|
5
|
+
super(params, configuration, scope)
|
6
|
+
end
|
7
|
+
|
8
|
+
def update!
|
9
|
+
validate_scope!
|
10
|
+
return false unless errors.empty?
|
11
|
+
update_resource!
|
12
|
+
return false unless errors.empty?
|
13
|
+
resource
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def case_fix_params
|
19
|
+
insensitive_keys = resource_class.case_insensitive_keys || []
|
20
|
+
params = permitted_params
|
21
|
+
insensitive_keys.each do |k|
|
22
|
+
value = params[k]
|
23
|
+
params[k] = value.downcase if value
|
24
|
+
end
|
25
|
+
params
|
26
|
+
end
|
27
|
+
|
28
|
+
def update_resource!
|
29
|
+
resource.update(case_fix_params)
|
30
|
+
return if resource.valid?
|
31
|
+
pull_validation_messages
|
32
|
+
end
|
33
|
+
|
34
|
+
def permitted_attributes
|
35
|
+
white_list = GrapeTokenAuth.configuration.param_white_list || {}
|
36
|
+
other_attributes = white_list[scope] || []
|
37
|
+
[:email] + other_attributes
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
class Token
|
3
|
+
attr_reader :token, :client_id, :expiry
|
4
|
+
|
5
|
+
def initialize(client_id = nil, token = nil, expiry = nil)
|
6
|
+
@client_id = client_id || SecureRandom.urlsafe_base64(nil, false)
|
7
|
+
@token = token || SecureRandom.urlsafe_base64(nil, false)
|
8
|
+
@expiry = expiry || (Time.now + GrapeTokenAuth.token_lifespan).to_i
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
@token
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_h
|
16
|
+
{ expiry: expiry, token: to_password_hash, updated_at: Time.now }
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_password_hash
|
20
|
+
@password_hash ||= BCrypt::Password.create(@token)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
class TokenAuthorizer
|
3
|
+
attr_reader :data
|
4
|
+
|
5
|
+
def initialize(authorizer_data)
|
6
|
+
@data = authorizer_data
|
7
|
+
end
|
8
|
+
|
9
|
+
def authenticate_from_token(scope)
|
10
|
+
initialize_resource_class(scope)
|
11
|
+
return nil unless resource_class
|
12
|
+
|
13
|
+
resource_from_existing_warden_user(scope)
|
14
|
+
return resource if correct_resource_type_logged_in?
|
15
|
+
|
16
|
+
find_resource(scope)
|
17
|
+
end
|
18
|
+
|
19
|
+
def find_resource(scope)
|
20
|
+
initialize_resource_class(scope)
|
21
|
+
return nil unless resource_class
|
22
|
+
|
23
|
+
return nil unless data.token_prerequisites_present?
|
24
|
+
|
25
|
+
load_user_from_uid
|
26
|
+
return nil unless user_authenticated?
|
27
|
+
|
28
|
+
user
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :resource_class, :user, :resource
|
34
|
+
|
35
|
+
def initialize_resource_class(scope)
|
36
|
+
@resource_class = GrapeTokenAuth.configuration.scope_to_class(scope)
|
37
|
+
end
|
38
|
+
|
39
|
+
def load_user_from_uid
|
40
|
+
@user = resource_class.find_by_uid(data.uid)
|
41
|
+
# TODO: hacky solution to the fact that this statement can fail sporadically
|
42
|
+
# with multiple requests. Nil returned from the statement. but
|
43
|
+
# re-executing the request causes it to pass?
|
44
|
+
rescue ::ActiveRecord::StatementInvalid
|
45
|
+
@user = resource_class.find_by_uid(data.uid)
|
46
|
+
end
|
47
|
+
|
48
|
+
def resource_from_existing_warden_user(scope)
|
49
|
+
@resource = data.exisiting_warden_user(scope)
|
50
|
+
end
|
51
|
+
|
52
|
+
def correct_resource_type_logged_in?
|
53
|
+
resource && resource.class == resource_class
|
54
|
+
end
|
55
|
+
|
56
|
+
def user_authenticated?
|
57
|
+
user && user.valid_token?(data.token, data.client_id)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
class UnauthorizedMiddleware
|
3
|
+
def self.call(env)
|
4
|
+
warden_opts = env.fetch('warden.options', {})
|
5
|
+
errors = warden_opts['errors'] || warden_opts[:errors]
|
6
|
+
[401,
|
7
|
+
{ 'Content-Type' => 'application/json'
|
8
|
+
},
|
9
|
+
prepare_errors(errors)
|
10
|
+
]
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.prepare_errors(errors)
|
14
|
+
return [] unless errors
|
15
|
+
return [errors.to_json] if errors.class == Hash
|
16
|
+
return [{ 'errors' => errors }.to_json] if errors.class == String
|
17
|
+
[]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/grape_token_auth.rb
CHANGED
@@ -1,5 +1,68 @@
|
|
1
|
-
require
|
1
|
+
require 'grape_token_auth/version'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'grape'
|
4
|
+
Dir.glob(File.expand_path('../**/*.rb', __FILE__)).each { |path| require path }
|
2
5
|
|
3
6
|
module GrapeTokenAuth
|
4
|
-
|
7
|
+
class << self
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
attr_writer :configuration
|
11
|
+
|
12
|
+
def configure
|
13
|
+
yield configuration if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
def configuration
|
17
|
+
@configuration ||= Configuration.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def_delegators :configuration, :token_lifespan,
|
21
|
+
:batch_request_buffer_throttle,
|
22
|
+
:change_headers_on_each_request
|
23
|
+
|
24
|
+
def setup!(&block)
|
25
|
+
add_auth_strategy
|
26
|
+
configure(&block) if block_given?
|
27
|
+
end
|
28
|
+
|
29
|
+
def configure_warden(warden_manager)
|
30
|
+
configuration.mappings.each do |scope, klass|
|
31
|
+
warden_manager.serialize_into_session(scope) do |record|
|
32
|
+
klass.serialize_into_session(record)
|
33
|
+
end
|
34
|
+
|
35
|
+
warden_manager.serialize_from_session(scope) do |key|
|
36
|
+
klass.serialize_from_session(*key)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def setup_warden!(builder)
|
42
|
+
builder.use Warden::Manager do |manager|
|
43
|
+
manager.failure_app = GrapeTokenAuth::UnauthorizedMiddleware
|
44
|
+
manager.default_scope = :user
|
45
|
+
GrapeTokenAuth.configure_warden(manager)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_omniauth_path_prefix!
|
50
|
+
::OmniAuth.config.path_prefix = configuration.omniauth_prefix
|
51
|
+
end
|
52
|
+
|
53
|
+
def send_notification(notification_type, opts)
|
54
|
+
message = GrapeTokenAuth::Mail.initialize_message(notification_type, opts)
|
55
|
+
configuration.mailer.send!(message, opts)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def add_auth_strategy
|
61
|
+
Grape::Middleware::Auth::Strategies.add(
|
62
|
+
:grape_devise_token_auth,
|
63
|
+
GrapeTokenAuth::Middleware,
|
64
|
+
->(options) { [options] }
|
65
|
+
)
|
66
|
+
end
|
67
|
+
end
|
5
68
|
end
|