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 +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +19 -0
- data/README.md +111 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/grape_devise_auth.gemspec +23 -0
- data/lib/grape_devise_auth/auth_headers.rb +49 -0
- data/lib/grape_devise_auth/auth_helpers.rb +143 -0
- data/lib/grape_devise_auth/authorizer_data.rb +27 -0
- data/lib/grape_devise_auth/concerns/user.rb +195 -0
- data/lib/grape_devise_auth/configuration.rb +37 -0
- data/lib/grape_devise_auth/devise_interface.rb +31 -0
- data/lib/grape_devise_auth/errors/login_failed.rb +4 -0
- data/lib/grape_devise_auth/errors/logout_failed.rb +4 -0
- data/lib/grape_devise_auth/errors/registration_failed.rb +4 -0
- data/lib/grape_devise_auth/errors/unauthorized.rb +4 -0
- data/lib/grape_devise_auth/middleware.rb +65 -0
- data/lib/grape_devise_auth/token_authorizer.rb +54 -0
- data/lib/grape_devise_auth/version.rb +3 -0
- data/lib/grape_devise_auth.rb +43 -0
- metadata +122 -0
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
data/Gemfile
ADDED
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
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,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,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,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: []
|