grape_devise_auth 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a31a5bc78acdb9b71c8fadb7e40c6749b528b351
4
+ data.tar.gz: 65f84ad5104121b80f71f5d95c5d0a4e9a45eac6
5
+ SHA512:
6
+ metadata.gz: c2ce492cb137cabd59586407b26de00d5f7c399f3f18598d5d0bf55fa2c9f8caf74df123b1f09716147f3e7472d39ed49116914f9001c03c1650af601fdcbe71
7
+ data.tar.gz: de83764552a6cb31da9df696288a2f685c7f0bec2a8331a0d681e71668450791bbaf10580446d5eded79be8d18b37c59c08573f218eb71d0c756af71b713109f
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in grape_devise_auth.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ The MIT License (MIT)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # GrapeDeviseAuth
2
+
3
+ GrapeDeviseAuth allows to use [devise][3] based registration/authorization inside [grape][2]. This gem is based on [grape_devise_auth_token][1] so all credit goes to its authors.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'grape_devise_auth'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install grape_devise_auth
20
+
21
+ ## Usage
22
+
23
+ Place this line in an initializer in your rails app or at least somewhere before
24
+ the grape API will get loaded:
25
+
26
+ ```ruby
27
+ GrapeDeviseAuth.setup!
28
+ ```
29
+
30
+ Available config parameters and default values:
31
+
32
+ ```
33
+ batch_request_buffer_throttle = 2.weeks
34
+ change_headers_on_each_request = true
35
+ authenticate_all = false
36
+ default_provider = 'email'
37
+ token_lifespan = 2.weeks
38
+ max_number_of_devices = 10
39
+ headers_names = {:'access-token' => 'access-token',
40
+ :'client' => 'client',
41
+ :'expiry' => 'expiry',
42
+ :'uid' => 'uid',
43
+ :'token-type' => 'token-type' }
44
+ remove_tokens_after_password_reset = false
45
+ ```
46
+
47
+ Within the Grape API:
48
+
49
+ ```
50
+ class Posts < Grape::API
51
+ auth :grape_devise_auth, resource_class: :user
52
+
53
+ helpers GrapeDeviseAuth::AuthHelpers
54
+
55
+ # ...
56
+ end
57
+ ```
58
+
59
+ Inside your User model:
60
+
61
+ ```
62
+ include GrapeDeviseAuth::Concerns::User
63
+
64
+ # ...
65
+ ```
66
+
67
+ Endpoints can be called by `method_name_YOUR_MAPPING_HERE!` (e.g. `authenticate_user!`).
68
+
69
+ For Example:
70
+
71
+ ```
72
+ get '/' do
73
+ authenticate_user!
74
+ login_user!
75
+ logout_user!
76
+ register_user!
77
+ end
78
+ ```
79
+
80
+ Get current auth headers:
81
+
82
+ ```
83
+ user_auth_headers
84
+ ```
85
+
86
+
87
+ Devise routes must be present:
88
+
89
+ ```
90
+ Rails.application.routes.draw do
91
+ devise_for :users
92
+ end
93
+ ```
94
+
95
+ Every endpoind has a version that doesn't fail or returns 401. For example authenticate_user(notice that it lacks of exclamation mark)
96
+
97
+
98
+ Necessary parameters for endpoints:
99
+
100
+ login_user! - uid and password (inside request body)
101
+
102
+ register_user! - uid and any field to have validation for (inside request body)
103
+
104
+ authenticate_user! - uid, client, access-token (inside request headers)
105
+
106
+
107
+
108
+ [1]: https://github.com/mcordell/grape_devise_token_auth
109
+ [2]: https://github.com/intridea/grape
110
+ [3]: https://github.com/plataformatec/devise
111
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "grape_devise_auth"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'grape_devise_auth/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "grape_devise_auth"
8
+ spec.version = GrapeDeviseAuth::VERSION
9
+ spec.authors = ["Anton Sokolskyi"]
10
+ spec.email = ["antonsokolskyi@gmail.com"]
11
+
12
+ spec.summary = %q{Allows to use Devise-based registration/authorization inside Grape API}
13
+ spec.homepage = "https://github.com/antonsokolskyy/GrapeDeviseAuth"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_development_dependency "bundler", "~> 1.8"
20
+ spec.add_development_dependency "rake", "~> 10.0"
21
+ spec.add_dependency 'grape', '>= 0.15.0'
22
+ spec.add_dependency 'devise', '>= 4.2'
23
+ end
@@ -0,0 +1,49 @@
1
+ module GrapeDeviseAuth
2
+ class AuthHeaders
3
+ extend Forwardable
4
+
5
+ def initialize(warden, mapping, request_start, data)
6
+ @resource = warden.user(:user)
7
+ @request_start = request_start
8
+ @data = data
9
+ end
10
+
11
+ def headers
12
+ return {} unless resource && resource.valid? && client_id
13
+ auth_headers_from_resource
14
+ end
15
+
16
+ private
17
+
18
+ def_delegators :@data, :token, :client_id
19
+ attr_reader :request_start, :resource
20
+
21
+ def batch_request?
22
+ @batch_request ||= resource.tokens[client_id] &&
23
+ resource.tokens[client_id]['updated_at'] &&
24
+ within_batch_request_window?
25
+ end
26
+
27
+ def within_batch_request_window?
28
+ end_of_window = Time.parse(resource.tokens[client_id]['updated_at']) +
29
+ GrapeDeviseAuth.batch_request_buffer_throttle
30
+
31
+ request_start < end_of_window
32
+ end
33
+
34
+ def auth_headers_from_resource
35
+ auth_headers = {}
36
+ resource.with_lock do
37
+ if !GrapeDeviseAuth.change_headers_on_each_request
38
+ auth_headers = resource.extend_batch_buffer(token, client_id)
39
+ elsif batch_request?
40
+ resource.extend_batch_buffer(token, client_id)
41
+ # don't set any headers in a batch request
42
+ else
43
+ auth_headers = resource.create_new_auth_token(client_id)
44
+ end
45
+ end
46
+ auth_headers
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,143 @@
1
+ module GrapeDeviseAuth
2
+ module AuthHelpers
3
+ def self.included(_base)
4
+ Devise.mappings.keys.each do |mapping|
5
+ define_method("current_#{mapping}") do
6
+ warden.user(mapping)
7
+ end
8
+
9
+ define_method("authenticate_#{mapping}") do
10
+ load_auth_headers_data(mapping)
11
+ authorizer_data = AuthorizerData.from_env(env)
12
+ devise_interface = DeviseInterface.new(authorizer_data)
13
+ token_authorizer = TokenAuthorizer.new(authorizer_data,
14
+ devise_interface)
15
+
16
+ resource = token_authorizer.authenticate_from_token(mapping)
17
+ if resource
18
+ devise_interface.set_user_in_warden(mapping, resource)
19
+ update_expiry_for_client_token(authorizer_data.client_id)
20
+ true
21
+ end
22
+ end
23
+
24
+ define_method("authenticate_#{mapping}!") do
25
+ authentication = send("authenticate_#{mapping}")
26
+ raise Unauthorized unless authentication
27
+ authentication
28
+ end
29
+
30
+ define_method("login_#{mapping}") do
31
+ field = authentication_field(mapping)
32
+ uid = find_uid(field)
33
+ resource = resource_class(mapping).find_by_uid(uid)
34
+
35
+ if resource && valid_params?(field, uid) && resource.valid_password?(params[:password]) && (!resource.respond_to?(:active_for_authentication?) || resource.active_for_authentication?)
36
+ update_env_with_auth_data(resource.create_new_auth_token)
37
+ warden.set_user(resource, scope: mapping, store: false)
38
+ end
39
+ end
40
+
41
+ define_method("login_#{mapping}!") do
42
+ login = send("login_#{mapping}")
43
+ raise LoginFailed unless login
44
+ login
45
+ end
46
+
47
+ define_method("logout_#{mapping}") do
48
+ resource = warden.user(mapping)
49
+ client_id = env[Configuration::CLIENT_KEY]
50
+ warden.logout
51
+ if resource && client_id && resource.tokens[client_id]
52
+ resource.tokens.delete(client_id)
53
+ resource.save!
54
+ else
55
+ nil
56
+ end
57
+ end
58
+
59
+ define_method("logout_#{mapping}!") do
60
+ logout = send("logout_#{mapping}")
61
+ raise LogoutFailed unless logout
62
+ logout
63
+ end
64
+
65
+ define_method("#{mapping}_auth_headers") do
66
+ env[Configuration::CURRENT_AUTH_HEADERS]
67
+ end
68
+
69
+ define_method("register_#{mapping}") do
70
+ resource = resource_class(mapping).new(declared(params))
71
+ resource.provider = GrapeDeviseAuth.default_provider
72
+
73
+ if resource_class(mapping).case_insensitive_keys.include?(:email)
74
+ resource.email = declared(params)['email'].try :downcase
75
+ end
76
+
77
+ if resource.save
78
+ update_env_with_auth_data(resource.create_new_auth_token)
79
+ else
80
+ nil
81
+ end
82
+ end
83
+
84
+ define_method("register_#{mapping}!") do
85
+ register = send("register_#{mapping}")
86
+ raise RegistrationFailed unless register
87
+ register
88
+ end
89
+ end
90
+ end
91
+
92
+ def warden
93
+ @warden ||= env['warden']
94
+ end
95
+
96
+ def authenticated?(scope = :user)
97
+ user_type = "current_#{scope}"
98
+ return false unless respond_to?(user_type)
99
+ !!send(user_type)
100
+ end
101
+
102
+ private
103
+
104
+ def valid_params?(key, val)
105
+ params[:password] && key && val
106
+ end
107
+
108
+ def resource_class(m = nil)
109
+ mapping = if m
110
+ Devise.mappings[m]
111
+ else
112
+ Devise.mappings[resource_name] || Devise.mappings.values.first
113
+ end
114
+ mapping.to
115
+ end
116
+
117
+ def authentication_field(mapping)
118
+ field = (params.keys.map(&:to_sym) && resource_class(mapping).authentication_keys).first
119
+ end
120
+
121
+ def find_uid(field)
122
+ request.headers[field.to_s.capitalize] || params[field] || request.headers['Uid'] || params['uid']
123
+ end
124
+
125
+ def load_auth_headers_data(mapping)
126
+ env[Configuration::UID_KEY] = find_uid(authentication_field(mapping))
127
+ env[Configuration::CLIENT_KEY] = request.headers['Client'] || params['client']
128
+ env[Configuration::ACCESS_TOKEN_KEY] = request.headers['Access-Token'] || params['access-token']
129
+ end
130
+
131
+ def update_expiry_for_client_token(client_id)
132
+ if @user
133
+ @client_id = client_id
134
+ @user.tokens[@client_id]['expiry'] = (Time.now + GrapeDeviseAuth.token_lifespan).to_i
135
+ @user.save
136
+ end
137
+ end
138
+
139
+ def update_env_with_auth_data(auth_data)
140
+ env[Configuration::CURRENT_AUTH_HEADERS] = auth_data
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,27 @@
1
+ module GrapeDeviseAuth
2
+ class AuthorizerData
3
+ attr_reader :uid, :client_id, :token, :expiry, :warden
4
+
5
+ def initialize(uid, client_id, token, expiry, warden)
6
+ @uid = uid
7
+ @client_id = client_id
8
+ @token = token
9
+ @expiry = expiry
10
+ @warden = warden
11
+ end
12
+
13
+ def self.from_env(env)
14
+ new(
15
+ env[Configuration::UID_KEY],
16
+ env[Configuration::CLIENT_KEY] || 'default',
17
+ env[Configuration::ACCESS_TOKEN_KEY],
18
+ env[Configuration::EXPIRY_KEY],
19
+ env['warden']
20
+ )
21
+ end
22
+
23
+ def token_prerequisites_present?
24
+ token && uid
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,195 @@
1
+ require 'bcrypt'
2
+
3
+ module GrapeDeviseAuth
4
+ module Concerns
5
+ module User
6
+ extend ActiveSupport::Concern
7
+
8
+ def self.tokens_match?(token_hash, token)
9
+ @token_equality_cache ||= {}
10
+
11
+ key = "#{token_hash}/#{token}"
12
+ result = @token_equality_cache[key] ||= (::BCrypt::Password.new(token_hash) == token)
13
+ if @token_equality_cache.size > 10000
14
+ @token_equality_cache = {}
15
+ end
16
+ result
17
+ end
18
+
19
+ included do
20
+ # Hack to check if devise is already enabled
21
+ unless self.method_defined?(:devise_modules)
22
+ devise :database_authenticatable, :registerable,
23
+ :recoverable, :trackable, :validatable, :confirmable
24
+ else
25
+ self.devise_modules.delete(:omniauthable)
26
+ end
27
+
28
+ unless tokens_has_json_column_type?
29
+ serialize :tokens, JSON
30
+ end
31
+
32
+ # can't set default on text fields in mysql, simulate here instead.
33
+ after_save :set_empty_token_hash
34
+ after_initialize :set_empty_token_hash
35
+
36
+ # get rid of dead tokens
37
+ before_save :destroy_expired_tokens
38
+
39
+ # remove old tokens if password has changed
40
+ before_save :remove_tokens_after_password_reset
41
+
42
+ # allows user to change password without current_password
43
+ attr_writer :allow_password_change
44
+ def allow_password_change
45
+ @allow_password_change || false
46
+ end
47
+
48
+ # don't use default devise email validation
49
+ def email_required?
50
+ false
51
+ end
52
+
53
+ def email_changed?
54
+ false
55
+ end
56
+ end
57
+
58
+
59
+ module ClassMethods
60
+ protected
61
+
62
+ def tokens_has_json_column_type?
63
+ table_exists? && self.columns_hash['tokens'] && self.columns_hash['tokens'].type.in?([:json, :jsonb])
64
+ end
65
+ end
66
+
67
+
68
+ def build_auth_header(token, client_id='default')
69
+ client_id ||= 'default'
70
+
71
+ # client may use expiry to prevent validation request if expired
72
+ # must be cast as string or headers will break
73
+ expiry = self.tokens[client_id]['expiry'] || self.tokens[client_id][:expiry]
74
+
75
+ max_clients = GrapeDeviseAuth.max_number_of_devices
76
+ while self.tokens.keys.length > 0 and max_clients < self.tokens.keys.length
77
+ oldest_token = self.tokens.min_by { |cid, v| v[:expiry] || v["expiry"] }
78
+ self.tokens.delete(oldest_token.first)
79
+ end
80
+
81
+ self.save!
82
+
83
+ return {
84
+ GrapeDeviseAuth.headers_names[:"access-token"] => token,
85
+ GrapeDeviseAuth.headers_names[:"token-type"] => "Bearer",
86
+ GrapeDeviseAuth.headers_names[:"client"] => client_id,
87
+ GrapeDeviseAuth.headers_names[:"expiry"] => expiry.to_s,
88
+ GrapeDeviseAuth.headers_names[:"uid"] => self.uid
89
+ }
90
+ end
91
+
92
+ def extend_batch_buffer(token, client_id)
93
+ self.tokens[client_id]['updated_at'] = Time.now
94
+
95
+ return build_auth_header(token, client_id)
96
+ end
97
+
98
+ def valid_token?(token, client_id='default')
99
+ client_id ||= 'default'
100
+
101
+ return false unless self.tokens[client_id]
102
+
103
+ return true if token_is_current?(token, client_id)
104
+ return true if token_can_be_reused?(token, client_id)
105
+
106
+ # return false if none of the above conditions are met
107
+ return false
108
+ end
109
+
110
+ def token_is_current?(token, client_id)
111
+ # ghetto HashWithIndifferentAccess
112
+ expiry = self.tokens[client_id]['expiry'] || self.tokens[client_id][:expiry]
113
+ token_hash = self.tokens[client_id]['token'] || self.tokens[client_id][:token]
114
+
115
+ return true if (
116
+ # ensure that expiry and token are set
117
+ expiry and token and
118
+
119
+ # ensure that the token has not yet expired
120
+ DateTime.strptime(expiry.to_s, '%s') > Time.now and
121
+
122
+ # ensure that the token is valid
123
+ GrapeDeviseAuth::Concerns::User.tokens_match?(token_hash, token)
124
+ )
125
+ end
126
+
127
+ # allow batch requests to use the previous token
128
+ def token_can_be_reused?(token, client_id)
129
+ # ghetto HashWithIndifferentAccess
130
+ updated_at = self.tokens[client_id]['updated_at'] || self.tokens[client_id][:updated_at]
131
+ last_token = self.tokens[client_id]['last_token'] || self.tokens[client_id][:last_token]
132
+
133
+
134
+ return true if (
135
+ # ensure that the last token and its creation time exist
136
+ updated_at and last_token and
137
+
138
+ # ensure that previous token falls within the batch buffer throttle time of the last request
139
+ Time.parse(updated_at) > Time.now - GrapeDeviseAuth.batch_request_buffer_throttle and
140
+
141
+ # ensure that the token is valid
142
+ ::BCrypt::Password.new(last_token) == token
143
+ )
144
+ end
145
+
146
+ # update user's auth token (should happen on each request)
147
+ def create_new_auth_token(client_id=nil)
148
+ client_id ||= SecureRandom.urlsafe_base64(nil, false)
149
+ last_token ||= nil
150
+ token = SecureRandom.urlsafe_base64(nil, false)
151
+ token_hash = ::BCrypt::Password.create(token)
152
+ expiry = (Time.now + GrapeDeviseAuth.token_lifespan).to_i
153
+
154
+ if self.tokens[client_id] and self.tokens[client_id]['token']
155
+ last_token = self.tokens[client_id]['token']
156
+ end
157
+
158
+ self.tokens[client_id] = {
159
+ token: token_hash,
160
+ expiry: expiry,
161
+ last_token: last_token,
162
+ updated_at: Time.now
163
+ }
164
+
165
+ return build_auth_header(token, client_id)
166
+ end
167
+
168
+ protected
169
+
170
+ def set_empty_token_hash
171
+ self.tokens ||= {} if has_attribute?(:tokens)
172
+ end
173
+
174
+ def destroy_expired_tokens
175
+ if self.tokens
176
+ self.tokens.delete_if do |cid, v|
177
+ expiry = v[:expiry] || v["expiry"]
178
+ DateTime.strptime(expiry.to_s, '%s') < Time.now
179
+ end
180
+ end
181
+ end
182
+
183
+ def remove_tokens_after_password_reset
184
+ there_is_more_than_one_token = self.tokens && self.tokens.keys.length > 1
185
+ should_remove_old_tokens = GrapeDeviseAuth.remove_tokens_after_password_reset &&
186
+ encrypted_password_changed? && there_is_more_than_one_token
187
+
188
+ if should_remove_old_tokens
189
+ latest_token = self.tokens.max_by { |cid, v| v[:expiry] || v["expiry"] }
190
+ self.tokens = { latest_token.first => latest_token.last }
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,37 @@
1
+ module GrapeDeviseAuth
2
+ class Configuration
3
+ attr_accessor :batch_request_buffer_throttle,
4
+ :change_headers_on_each_request,
5
+ :authenticate_all,
6
+ :default_provider,
7
+ :token_lifespan,
8
+ :max_number_of_devices,
9
+ :headers_names,
10
+ :remove_tokens_after_password_reset
11
+
12
+ ACCESS_TOKEN_KEY = 'HTTP_ACCESS_TOKEN'
13
+ EXPIRY_KEY = 'HTTP_EXPIRY'
14
+ UID_KEY = 'HTTP_UID'
15
+ CLIENT_KEY = 'HTTP_CLIENT'
16
+ CURRENT_AUTH_HEADERS = 'CURRENT_AUTH_HEADERS'
17
+
18
+ def initialize
19
+ @batch_request_buffer_throttle = 2.weeks
20
+ @change_headers_on_each_request = true
21
+ @authenticate_all = false
22
+ @default_provider = 'email'
23
+ @token_lifespan = 2.weeks
24
+ @max_number_of_devices = 10
25
+ @headers_names = {:'access-token' => 'access-token',
26
+ :'client' => 'client',
27
+ :'expiry' => 'expiry',
28
+ :'uid' => 'uid',
29
+ :'token-type' => 'token-type' }
30
+ @remove_tokens_after_password_reset = false
31
+ end
32
+
33
+ def auth_all?
34
+ @authenticate_all
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,31 @@
1
+ module GrapeDeviseAuth
2
+ class DeviseInterface
3
+ def initialize(data)
4
+ @warden = data.warden
5
+ @client_id = data.client_id
6
+ end
7
+
8
+ # extracted and simplified from Devise
9
+ def set_user_in_warden(scope, resource)
10
+ scope = Devise::Mapping.find_scope!(scope)
11
+ warden.set_user(resource, scope: scope, store: false)
12
+ end
13
+
14
+ def mapping_to_class(m)
15
+ mapping = m ? Devise.mappings[m] : Devise.mappings.values.first
16
+ @resource_class = mapping.to
17
+ end
18
+
19
+ def exisiting_warden_user(resource_class)
20
+ warden_user = warden.user(resource_class.to_s.underscore.to_sym)
21
+ return unless warden_user && warden_user.tokens[@client_id].nil?
22
+ resource = warden_user
23
+ resource.create_new_auth_token
24
+ resource
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :warden
30
+ end
31
+ end
@@ -0,0 +1,4 @@
1
+ module GrapeDeviseAuth
2
+ class LoginFailed < StandardError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module GrapeDeviseAuth
2
+ class LogoutFailed < StandardError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module GrapeDeviseAuth
2
+ class RegistrationFailed < StandardError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module GrapeDeviseAuth
2
+ class Unauthorized < StandardError
3
+ end
4
+ end
@@ -0,0 +1,65 @@
1
+ module GrapeDeviseAuth
2
+ class Middleware
3
+ extend Forwardable
4
+
5
+ def initialize(app, resource_name)
6
+ @app = app
7
+ @resource_name = resource_name
8
+ end
9
+
10
+ def call(env)
11
+ setup(env)
12
+ begin
13
+ auth_all
14
+ responses_with_auth_headers(*@app.call(env))
15
+ rescue Unauthorized => _e
16
+ return unauthorized
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :authorizer_data, :token_authorizer, :resource, :request_start
23
+ def_delegators :@authorizer_data, :warden, :token, :client_id
24
+
25
+ def auth_all
26
+ return if skip_auth_all?
27
+ user = token_authorizer.authenticate_from_token(@resource_name)
28
+ fail Unauthorized unless user
29
+ sign_in_user(user)
30
+ end
31
+
32
+ def skip_auth_all?
33
+ !GrapeDeviseAuth.configuration.auth_all?
34
+ end
35
+
36
+ def setup(env)
37
+ @request_start = Time.now
38
+ @authorizer_data = AuthorizerData.from_env(env)
39
+ @devise_interface = DeviseInterface.new(@authorizer_data)
40
+ @token_authorizer = TokenAuthorizer.new(@authorizer_data,
41
+ @devise_interface)
42
+ end
43
+
44
+ def sign_in_user(user)
45
+ @devise_interface.set_user_in_warden(@resource_name, user)
46
+ end
47
+
48
+ def responses_with_auth_headers(status, headers, response)
49
+ auth_headers = AuthHeaders.new(warden, @resource_name, request_start, authorizer_data)
50
+ [
51
+ status,
52
+ headers.merge(auth_headers.headers),
53
+ response
54
+ ]
55
+ end
56
+
57
+ def unauthorized
58
+ [401,
59
+ { 'Content-Type' => 'application/json'
60
+ },
61
+ []
62
+ ]
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,54 @@
1
+ module GrapeDeviseAuth
2
+ class TokenAuthorizer
3
+ extend Forwardable
4
+
5
+ def initialize(data, devise_interface)
6
+ @data = data
7
+ @devise_interface = devise_interface
8
+ end
9
+
10
+ def authenticate_from_token(mapping = nil)
11
+ @resource_class = devise_interface.mapping_to_class(mapping)
12
+ return nil unless resource_class
13
+
14
+ # client id is not required
15
+ client_id = data.client_id || 'default'
16
+
17
+ resource_from_existing_devise_user
18
+ return resource if correct_resource_type_logged_in? &&
19
+ resource_does_not_have_client_token?(client_id)
20
+
21
+ return nil unless data.token_prerequisites_present?
22
+ load_user_from_uid
23
+ return nil unless user_authenticated?
24
+
25
+ user
26
+ end
27
+
28
+ private
29
+
30
+ attr_accessor :resource_class
31
+ attr_reader :data, :resource, :user, :devise_interface
32
+ def_delegators :@data, :warden, :uid, :token, :client_id
33
+
34
+ def user_authenticated?
35
+ user && user.valid_token?(token, client_id)
36
+ end
37
+
38
+ def load_user_from_uid
39
+ @user = resource_class.find_by_uid(uid)
40
+ end
41
+
42
+ def resource_from_existing_devise_user
43
+ @resource = @devise_interface.exisiting_warden_user(resource_class)
44
+ end
45
+
46
+ def correct_resource_type_logged_in?
47
+ resource && resource.class == resource_class
48
+ end
49
+
50
+ def resource_does_not_have_client_token?(client_id)
51
+ resource.tokens[client_id].nil?
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,3 @@
1
+ module GrapeDeviseAuth
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,43 @@
1
+ %w(version middleware auth_helpers authorizer_data errors/unauthorized
2
+ token_authorizer configuration auth_headers devise_interface concerns/user
3
+ errors/login_failed errors/logout_failed errors/registration_failed).each do |file|
4
+ require "grape_devise_auth/#{file}"
5
+ end
6
+
7
+ require 'grape'
8
+
9
+ module GrapeDeviseAuth
10
+ class << self
11
+ extend Forwardable
12
+
13
+ def_delegators :configuration,
14
+ :batch_request_buffer_throttle,
15
+ :change_headers_on_each_request,
16
+ :default_provider,
17
+ :token_lifespan,
18
+ :max_number_of_devices,
19
+ :headers_names,
20
+ :remove_tokens_after_password_reset
21
+
22
+ def configuration
23
+ @configuration ||= Configuration.new
24
+ end
25
+
26
+ def config
27
+ yield(configuration)
28
+ end
29
+
30
+ def setup!(middleware = false)
31
+ yield(configuration) if block_given?
32
+ add_auth_strategy
33
+ end
34
+
35
+ def add_auth_strategy
36
+ Grape::Middleware::Auth::Strategies.add(
37
+ :grape_devise_auth,
38
+ GrapeDeviseAuth::Middleware,
39
+ ->(options) { [options[:resource_class]] }
40
+ )
41
+ end
42
+ end
43
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: grape_devise_auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Anton Sokolskyi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-11-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: grape
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 0.15.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 0.15.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: devise
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '4.2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '4.2'
69
+ description:
70
+ email:
71
+ - antonsokolskyi@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - Gemfile
78
+ - LICENSE.txt
79
+ - README.md
80
+ - Rakefile
81
+ - bin/console
82
+ - bin/setup
83
+ - grape_devise_auth.gemspec
84
+ - lib/grape_devise_auth.rb
85
+ - lib/grape_devise_auth/auth_headers.rb
86
+ - lib/grape_devise_auth/auth_helpers.rb
87
+ - lib/grape_devise_auth/authorizer_data.rb
88
+ - lib/grape_devise_auth/concerns/user.rb
89
+ - lib/grape_devise_auth/configuration.rb
90
+ - lib/grape_devise_auth/devise_interface.rb
91
+ - lib/grape_devise_auth/errors/login_failed.rb
92
+ - lib/grape_devise_auth/errors/logout_failed.rb
93
+ - lib/grape_devise_auth/errors/registration_failed.rb
94
+ - lib/grape_devise_auth/errors/unauthorized.rb
95
+ - lib/grape_devise_auth/middleware.rb
96
+ - lib/grape_devise_auth/token_authorizer.rb
97
+ - lib/grape_devise_auth/version.rb
98
+ homepage: https://github.com/antonsokolskyy/GrapeDeviseAuth
99
+ licenses:
100
+ - MIT
101
+ metadata: {}
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubyforge_project:
118
+ rubygems_version: 2.4.5.1
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Allows to use Devise-based registration/authorization inside Grape API
122
+ test_files: []