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,138 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
# Module that contains the majority of the password reseting functionality.
|
3
|
+
# This module can be included in a Grape::API class that defines a
|
4
|
+
# resource_scope and therefore have all of the functionality with a given
|
5
|
+
# resource (mapping).
|
6
|
+
module PasswordAPICore
|
7
|
+
def self.included(base)
|
8
|
+
base.helpers do
|
9
|
+
def throw_unauthorized(message)
|
10
|
+
throw(:warden, errors: message)
|
11
|
+
end
|
12
|
+
|
13
|
+
def resource_class
|
14
|
+
@rescource_class ||= scope_to_class(resource_scope)
|
15
|
+
end
|
16
|
+
|
17
|
+
def bad_request(messages, code = 422)
|
18
|
+
status(code)
|
19
|
+
{ 'status' => 'error', 'error' => messages.join(',') }
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate_redirect_url!(url)
|
23
|
+
white_list = GrapeTokenAuth.configuration.redirect_whitelist
|
24
|
+
return unless white_list
|
25
|
+
url_valid = white_list.include?(url)
|
26
|
+
error!({ errors: 'redirect url is not in whitelist', status: 'error' }, 403) unless url_valid
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
base.post do
|
31
|
+
email = params[:email]
|
32
|
+
throw_unauthorized('You must provide an email address.') unless email
|
33
|
+
|
34
|
+
redirect_url = params[:redirect_url]
|
35
|
+
validate_redirect_url!(redirect_url)
|
36
|
+
redirect_url ||= GrapeTokenAuth.configuration.default_password_reset_url
|
37
|
+
throw_unauthorized('Missing redirect url.') unless redirect_url
|
38
|
+
resource = ResourceFinder.find(base.resource_scope, params)
|
39
|
+
edit_path = routes[0].route_path.gsub(/\(.*\)/, '') + "/edit"
|
40
|
+
if resource
|
41
|
+
resource.send_reset_password_instructions(
|
42
|
+
provider: 'email',
|
43
|
+
redirect_url: redirect_url,
|
44
|
+
client_config: params[:config_name],
|
45
|
+
edit_path: edit_path
|
46
|
+
)
|
47
|
+
|
48
|
+
if resource.errors.empty?
|
49
|
+
status 200
|
50
|
+
present(success: true,
|
51
|
+
message: "An email has been sent to #{email} containing " +
|
52
|
+
'instructions for resetting your password.'
|
53
|
+
)
|
54
|
+
else
|
55
|
+
return error!({ errors: resource.errors,
|
56
|
+
status: 'error' }, 400)
|
57
|
+
end
|
58
|
+
else
|
59
|
+
error!({ errors: "Unable to find user with email '#{email}'.",
|
60
|
+
status: 'error' }, 404)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
base.get '/edit' do
|
65
|
+
resource_class = GrapeTokenAuth.configuration.scope_to_class(base.resource_scope)
|
66
|
+
resource = resource_class.find_with_reset_token(
|
67
|
+
reset_password_token: params[:reset_password_token]
|
68
|
+
)
|
69
|
+
|
70
|
+
if resource
|
71
|
+
token = Token.new
|
72
|
+
|
73
|
+
resource.tokens[token.client_id] = {
|
74
|
+
token: token.to_password_hash,
|
75
|
+
expiry: token.expiry
|
76
|
+
}
|
77
|
+
|
78
|
+
resource.confirm unless resource.confirmed?
|
79
|
+
|
80
|
+
# TODO: ensure that user is confirmed
|
81
|
+
# @resource.skip_confirmation! if @resource.devise_modules.include?(:confirmable) && !@resource.confirmed_at
|
82
|
+
|
83
|
+
resource.save!
|
84
|
+
|
85
|
+
redirect_url = resource.build_auth_url(
|
86
|
+
params[:redirect_url], token: token.to_s, reset_password: true,
|
87
|
+
client_id: token.client_id,
|
88
|
+
config: params[:config])
|
89
|
+
redirect redirect_url
|
90
|
+
else
|
91
|
+
error!({ success: false }, 404)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
base.put do
|
96
|
+
token_authorizer = TokenAuthorizer.new(AuthorizerData.from_env(env))
|
97
|
+
resource = token_authorizer.find_resource(base.resource_scope)
|
98
|
+
throw(:warden) unless resource
|
99
|
+
unless resource.provider == 'email'
|
100
|
+
error!({ errors: 'Password not required.',
|
101
|
+
status: 'error', success: false }, 422)
|
102
|
+
end
|
103
|
+
# ensure that password params were sent
|
104
|
+
unless params[:password] && params[:password_confirmation]
|
105
|
+
error!({ errors: 'Passwords are missing.',
|
106
|
+
status: 'error', success: false }, 422)
|
107
|
+
end
|
108
|
+
|
109
|
+
# TODO: previous password confirmation
|
110
|
+
if resource.reset_password(params[:password], params[:password_confirmation])
|
111
|
+
return present json: {
|
112
|
+
success: true,
|
113
|
+
data: {
|
114
|
+
user: resource,
|
115
|
+
message: 'Successfully updated'
|
116
|
+
}
|
117
|
+
}
|
118
|
+
else
|
119
|
+
error!({ success: false,
|
120
|
+
errors: resource.errors.to_hash.merge(full_messages: resource.errors.full_messages)
|
121
|
+
}, 422)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# "Empty" Password API where OmniAuthAPICore is mounted, defaults to a :user
|
128
|
+
# resource class
|
129
|
+
class PasswordAPI < Grape::API
|
130
|
+
class << self
|
131
|
+
def resource_scope
|
132
|
+
:user
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
include PasswordAPICore
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
module RegistrationAPICore
|
3
|
+
def self.included(base)
|
4
|
+
base.helpers do
|
5
|
+
def bad_request(messages, code = 422)
|
6
|
+
status(code)
|
7
|
+
{ 'status' => 'error', 'error' => messages.join(',') }
|
8
|
+
end
|
9
|
+
|
10
|
+
def validate_redirect_url!
|
11
|
+
white_list = GrapeTokenAuth.configuration.redirect_whitelist
|
12
|
+
return unless white_list
|
13
|
+
url_valid = white_list.include?(params['confirm_success_url'])
|
14
|
+
errors = ['redirect url is not in whitelist']
|
15
|
+
bad_request(errors, 403) unless url_valid
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate_not_empty!
|
19
|
+
if params.empty?
|
20
|
+
errors = ['email, password, password_confirmation \
|
21
|
+
params are required']
|
22
|
+
bad_request errors, 422
|
23
|
+
else
|
24
|
+
false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def find_resource(env, mapping)
|
29
|
+
token_authorizer = TokenAuthorizer.new(AuthorizerData.from_env(env))
|
30
|
+
token_authorizer.find_resource(mapping)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
base.post '/' do
|
35
|
+
empty_params_error = validate_not_empty!
|
36
|
+
return present(empty_params_error) if empty_params_error
|
37
|
+
redirect_error = validate_redirect_url!
|
38
|
+
return present(redirect_error) if redirect_error
|
39
|
+
mapping = base.resource_scope
|
40
|
+
configuration = GrapeTokenAuth.configuration
|
41
|
+
creator = ResourceCreator.new(params, configuration, mapping)
|
42
|
+
if creator.create!
|
43
|
+
status 200
|
44
|
+
present(data: creator.resource)
|
45
|
+
else
|
46
|
+
present bad_request(creator.errors, 403)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
base.delete do
|
51
|
+
user = find_resource(env, base.resource_scope)
|
52
|
+
return present bad_request(['resource not found.'], 404) unless user
|
53
|
+
user.delete
|
54
|
+
status 200
|
55
|
+
end
|
56
|
+
|
57
|
+
base.put do
|
58
|
+
empty_params_error = validate_not_empty!
|
59
|
+
return present(empty_params_error) if empty_params_error
|
60
|
+
resource = find_resource(env, base.resource_scope)
|
61
|
+
return present bad_request(['resource not found.'], 404) unless resource
|
62
|
+
|
63
|
+
updater = ResourceUpdater.new(resource,
|
64
|
+
params,
|
65
|
+
GrapeTokenAuth.configuration,
|
66
|
+
base.resource_scope)
|
67
|
+
if updater.update!
|
68
|
+
status 200
|
69
|
+
present(data: updater.resource)
|
70
|
+
else
|
71
|
+
present bad_request(updater.errors, 403)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
base.format :json
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class RegistrationAPI < Grape::API
|
80
|
+
class << self
|
81
|
+
def resource_scope
|
82
|
+
:user
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
include RegistrationAPICore
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
module SessionsAPICore
|
3
|
+
def self.included(base)
|
4
|
+
base.helpers do
|
5
|
+
def find_resource(env, mapping)
|
6
|
+
token_authorizer = TokenAuthorizer.new(AuthorizerData.from_env(env))
|
7
|
+
token_authorizer.find_resource(mapping)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
base.post '/sign_in' do
|
12
|
+
start_time = Time.now
|
13
|
+
resource = ResourceFinder.find(base.resource_scope, params)
|
14
|
+
unless resource && resource.valid_password?(params[:password])
|
15
|
+
message = 'Invalid login credentials. Please try again.'
|
16
|
+
throw(:warden, errors: { errors: [message], status: 'error' })
|
17
|
+
end
|
18
|
+
unless resource.confirmed?
|
19
|
+
error_message = 'A confirmation email was sent to your account at ' +
|
20
|
+
"#{resource.email}. You must follow the " +
|
21
|
+
'instructions in the email before your account can be ' +
|
22
|
+
'activated'
|
23
|
+
throw(:warden, errors: { errors: [error_message], status: 'error' })
|
24
|
+
end
|
25
|
+
|
26
|
+
data = AuthorizerData.from_env(env)
|
27
|
+
env['rack.session'] ||= {}
|
28
|
+
data.store_resource(resource, base.resource_scope)
|
29
|
+
auth_header = AuthenticationHeader.new(data, start_time)
|
30
|
+
auth_header.headers.each do |key, value|
|
31
|
+
header key.to_s, value.to_s
|
32
|
+
end
|
33
|
+
status 200
|
34
|
+
present data: resource
|
35
|
+
end
|
36
|
+
|
37
|
+
base.delete '/sign_out' do
|
38
|
+
resource = find_resource(env, base.resource_scope)
|
39
|
+
|
40
|
+
if resource
|
41
|
+
resource.tokens.delete(env[Configuration::CLIENT_KEY])
|
42
|
+
resource.save
|
43
|
+
status 200
|
44
|
+
else
|
45
|
+
status 404
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class SessionsAPI < Grape::API
|
52
|
+
class << self
|
53
|
+
def resource_scope
|
54
|
+
:user
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
include SessionsAPICore
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
# Contains the major functionality of TokenValidation
|
3
|
+
module TokenValidationAPICore
|
4
|
+
def self.included(base)
|
5
|
+
base.get '/validate_token' do
|
6
|
+
token_authorizer = TokenAuthorizer.new(AuthorizerData.from_env(env))
|
7
|
+
resource = token_authorizer.find_resource(base.resource_scope)
|
8
|
+
if resource
|
9
|
+
status 200
|
10
|
+
present data: resource.token_validation_response
|
11
|
+
else
|
12
|
+
throw(:warden, 'errors' => 'Invalid login credentials')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Stub class for TokenValidation where TokenValidationAPICore gets included
|
19
|
+
# which in turn confers the major functionality of the TokenValidationAPI
|
20
|
+
class TokenValidationAPI < Grape::API
|
21
|
+
class << self
|
22
|
+
def resource_scope
|
23
|
+
:user
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
include TokenValidationAPICore
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
class AuthenticationHeader
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
def initialize(data, start_time)
|
6
|
+
@resource = data.first_authenticated_resource
|
7
|
+
@request_start = start_time
|
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 auth_headers_from_resource
|
22
|
+
auth_headers = {}
|
23
|
+
resource.while_record_locked do
|
24
|
+
if !GrapeTokenAuth.change_headers_on_each_request
|
25
|
+
auth_headers = resource.extend_batch_buffer(token, client_id)
|
26
|
+
elsif batch_request?
|
27
|
+
resource.extend_batch_buffer(token, client_id)
|
28
|
+
else
|
29
|
+
auth_headers = resource.create_new_auth_token(client_id)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
coerce_headers_to_strings(auth_headers)
|
33
|
+
end
|
34
|
+
|
35
|
+
def coerce_headers_to_strings(auth_headers)
|
36
|
+
auth_headers.each { |k, v| auth_headers[k] = v.to_s }
|
37
|
+
end
|
38
|
+
|
39
|
+
def batch_request?
|
40
|
+
@batch_request ||= resource.tokens[client_id] &&
|
41
|
+
resource.tokens[client_id]['updated_at'] &&
|
42
|
+
within_batch_request_window?
|
43
|
+
end
|
44
|
+
|
45
|
+
def within_batch_request_window?
|
46
|
+
end_of_window = Time.parse(resource.tokens[client_id]['updated_at']) +
|
47
|
+
GrapeTokenAuth.batch_request_buffer_throttle
|
48
|
+
|
49
|
+
request_start < end_of_window
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
class AuthorizerData
|
3
|
+
attr_reader :uid, :client_id, :token, :expiry, :warden
|
4
|
+
|
5
|
+
def initialize(uid = nil, client_id = nil, token = nil,
|
6
|
+
expiry = nil, warden = nil)
|
7
|
+
@uid = uid
|
8
|
+
@client_id = client_id || 'default'
|
9
|
+
@token = token
|
10
|
+
@expiry = expiry
|
11
|
+
@warden = warden
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.from_env(env)
|
15
|
+
new(
|
16
|
+
*data_from_env(env),
|
17
|
+
env['warden']
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.data_from_env(env)
|
22
|
+
[Configuration::UID_KEY,
|
23
|
+
Configuration::CLIENT_KEY,
|
24
|
+
Configuration::ACCESS_TOKEN_KEY,
|
25
|
+
Configuration::EXPIRY_KEY].map do |key|
|
26
|
+
env[key] || env['HTTP_' + key.gsub('-', '_').upcase]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def exisiting_warden_user(scope)
|
31
|
+
warden_user = warden.user(scope)
|
32
|
+
return unless warden_user && warden_user.tokens[client_id].nil?
|
33
|
+
resource = warden_user
|
34
|
+
resource.create_new_auth_token
|
35
|
+
resource
|
36
|
+
end
|
37
|
+
|
38
|
+
def token_prerequisites_present?
|
39
|
+
!token.nil? && !uid.nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
def fetch_stored_resource(scope)
|
43
|
+
warden.session_serializer.fetch(scope)
|
44
|
+
end
|
45
|
+
|
46
|
+
def store_resource(resource, scope)
|
47
|
+
warden.session_serializer.store(resource, scope)
|
48
|
+
end
|
49
|
+
|
50
|
+
def first_authenticated_resource
|
51
|
+
GrapeTokenAuth.configuration.mappings.each do |scope, _class|
|
52
|
+
resource = fetch_stored_resource(scope)
|
53
|
+
return resource if resource
|
54
|
+
end
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
class Configuration
|
3
|
+
ACCESS_TOKEN_KEY = 'access-token'
|
4
|
+
EXPIRY_KEY = 'expiry'
|
5
|
+
UID_KEY = 'uid'
|
6
|
+
CLIENT_KEY = 'client'
|
7
|
+
EMAIL_VALIDATION = /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+\z/
|
8
|
+
SERIALIZATION_BLACKLIST = %i(encrypted_password
|
9
|
+
reset_password_token
|
10
|
+
reset_password_sent_at
|
11
|
+
remember_created_at
|
12
|
+
sign_in_count
|
13
|
+
current_sign_in_at
|
14
|
+
last_sign_in_at
|
15
|
+
current_sign_in_ip
|
16
|
+
last_sign_in_ip
|
17
|
+
password_salt
|
18
|
+
confirmation_token
|
19
|
+
confirmed_at
|
20
|
+
confirmation_sent_at
|
21
|
+
remember_token
|
22
|
+
unconfirmed_email
|
23
|
+
failed_attempts
|
24
|
+
unlock_token
|
25
|
+
locked_at
|
26
|
+
tokens)
|
27
|
+
|
28
|
+
attr_accessor :token_lifespan,
|
29
|
+
:batch_request_buffer_throttle,
|
30
|
+
:change_headers_on_each_request,
|
31
|
+
:mappings,
|
32
|
+
:redirect_whitelist,
|
33
|
+
:param_white_list,
|
34
|
+
:authentication_keys,
|
35
|
+
:omniauth_prefix,
|
36
|
+
:additional_serialization_blacklist,
|
37
|
+
:ignore_default_serialization_blacklist,
|
38
|
+
:default_password_reset_url,
|
39
|
+
:smtp_configuration,
|
40
|
+
:secret,
|
41
|
+
:digest,
|
42
|
+
:messages,
|
43
|
+
:from_address,
|
44
|
+
:default_url_options,
|
45
|
+
:mailer
|
46
|
+
|
47
|
+
def initialize
|
48
|
+
@token_lifespan = 60 * 60 * 24 * 7 * 2 # 2 weeks
|
49
|
+
@batch_request_buffer_throttle = 5 # seconds
|
50
|
+
@change_headers_on_each_request = true
|
51
|
+
@mappings = {}
|
52
|
+
@authentication_keys = [:email]
|
53
|
+
@omniauth_prefix = '/omniauth'
|
54
|
+
@additional_serialization_blacklist = []
|
55
|
+
@ignore_default_serialization_blacklist = false
|
56
|
+
@default_password_reset_url = nil
|
57
|
+
@smtp_configuration = {}
|
58
|
+
@secret = nil
|
59
|
+
@digest = 'SHA256'
|
60
|
+
@messages = Mail::DEFAULT_MESSAGES
|
61
|
+
@from_address = nil
|
62
|
+
@default_url_options = {}
|
63
|
+
@mailer = GrapeTokenAuth::Mail::SMTPMailer
|
64
|
+
end
|
65
|
+
|
66
|
+
def key_generator
|
67
|
+
fail SecretNotSet unless secret
|
68
|
+
@key_generator ||= CachingKeyGenerator.new(KeyGenerator.new(secret))
|
69
|
+
end
|
70
|
+
|
71
|
+
def serialization_blacklist
|
72
|
+
additional_serialization_blacklist.map(&:to_sym).concat(
|
73
|
+
ignore_default_serialization_blacklist ? [] : SERIALIZATION_BLACKLIST)
|
74
|
+
end
|
75
|
+
|
76
|
+
def scope_to_class(scope = nil)
|
77
|
+
fail MappingsUndefinedError if mappings.empty?
|
78
|
+
mappings[scope]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
# Error when an undefined scope was attempted to be used
|
3
|
+
class ScopeUndefinedError < StandardError
|
4
|
+
def initialize(msg, scope = nil)
|
5
|
+
msg ||= "Trying to use an undefined scope #{scope}. A proper \
|
6
|
+
scope to resource class mapping must be set up in the \
|
7
|
+
GrapeTokenAuth configuration."
|
8
|
+
super(msg)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Error when end-user has not configured any mappings
|
13
|
+
class MappingsUndefinedError < StandardError
|
14
|
+
def message
|
15
|
+
'GrapeTokenAuth mapping are undefined. Define your mappings' +
|
16
|
+
' within the GrapeTokenAuth configuration'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Unauthorized < StandardError
|
21
|
+
end
|
22
|
+
|
23
|
+
class SecretNotSet < StandardError
|
24
|
+
def message
|
25
|
+
'GrapeTokenAuth secret is not set, define your secret with a' +
|
26
|
+
' safe random key in the GrapeTokenAuth configuration'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'thread_safe'
|
2
|
+
require 'openssl'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
# Copied from devise
|
6
|
+
module GrapeTokenAuth
|
7
|
+
# KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2
|
8
|
+
# It can be used to derive a number of keys for various purposes from a given
|
9
|
+
# secret. This lets Rails applications have a single secure secret, but avoid
|
10
|
+
# reusing that key in multiple incompatible contexts.
|
11
|
+
class KeyGenerator
|
12
|
+
def initialize(secret, options = {})
|
13
|
+
@secret = secret
|
14
|
+
# The default iterations are higher than required for our key derivation
|
15
|
+
# uses on the off chance someone uses this for password storage
|
16
|
+
@iterations = options[:iterations] || 2**16
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns a derived key suitable for use. The default key_size is chosen
|
20
|
+
# to be compatible with the default settings
|
21
|
+
# OpenSSL::Digest::SHA1#block_length
|
22
|
+
def generate_key(salt, key_size = 64)
|
23
|
+
OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# CachingKeyGenerator is a wrapper around KeyGenerator which allows users to
|
28
|
+
# avoid re-executing the key generation process when it's called using the
|
29
|
+
# same salt and key_size
|
30
|
+
class CachingKeyGenerator
|
31
|
+
def initialize(key_generator)
|
32
|
+
@key_generator = key_generator
|
33
|
+
@cache_keys = ThreadSafe::Cache.new
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns a derived key suitable for use. The default key_size is chosen
|
37
|
+
# to be compatible with the default settings of
|
38
|
+
# OpenSSL::Digest::SHA1#block_length
|
39
|
+
def generate_key(salt, key_size = 64)
|
40
|
+
key = "#{salt}#{key_size}"
|
41
|
+
@cache_keys[key] ||= @key_generator.generate_key(salt, key_size)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
# Look up tokens are a type of token that allows searching by that token. This
|
3
|
+
# is useful in use cases such as confirmation tokens. These type of tokens are
|
4
|
+
# not appropriate for auth. In auth, look up is done via uid and
|
5
|
+
# verification/persitance with BCrypt. In short, this is a utility class that
|
6
|
+
# should not be used unless you are sure of your need.
|
7
|
+
class LookupToken
|
8
|
+
module ClassMethods
|
9
|
+
# copied from devise, creates a token that is url safe without ambigous
|
10
|
+
# characters
|
11
|
+
def friendly_token(length = 20)
|
12
|
+
rlength = (length * 3) / 4
|
13
|
+
SecureRandom.urlsafe_base64(rlength).tr('lIO0', 'sxyz')
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate(authenticatable_klass, column)
|
17
|
+
loop do
|
18
|
+
raw = friendly_token
|
19
|
+
enc = digest(column, raw)
|
20
|
+
unless authenticatable_klass.exists_in_column?(column, enc)
|
21
|
+
break [raw, enc]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def digest(column, value)
|
27
|
+
return unless value.present?
|
28
|
+
key = key_for(column)
|
29
|
+
OpenSSL::HMAC.hexdigest(open_ssl_digest, key, value)
|
30
|
+
end
|
31
|
+
|
32
|
+
def open_ssl_digest
|
33
|
+
GrapeTokenAuth.configuration.digest
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def key_for(column)
|
39
|
+
GrapeTokenAuth.configuration.key_generator
|
40
|
+
.generate_key("GTA column #{column}")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
extend ClassMethods
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'message_base'
|
2
|
+
require_relative 'messages/password_reset/password_reset_email'
|
3
|
+
require_relative 'messages/confirmation/confirmation_email'
|
4
|
+
|
5
|
+
module GrapeTokenAuth
|
6
|
+
module Mail
|
7
|
+
DEFAULT_MESSAGES = {
|
8
|
+
reset_password_instructions: PasswordResetEmail,
|
9
|
+
confirmation_instructions: ConfirmationEmail
|
10
|
+
}
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def initialize_message(message_type, opts)
|
14
|
+
messages = GrapeTokenAuth.configuration.messages
|
15
|
+
return nil unless messages.key?(message_type)
|
16
|
+
messages[message_type].new(opts)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def valid_email_options?(opts)
|
22
|
+
to_address = opts[:to] || opts['to']
|
23
|
+
return false unless to_address
|
24
|
+
true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module GrapeTokenAuth
|
2
|
+
module Mail
|
3
|
+
class MessageBase
|
4
|
+
attr_accessor :text_body, :html_body, :url_options, :subject, :opts
|
5
|
+
|
6
|
+
def initialize(opts)
|
7
|
+
@opts = opts
|
8
|
+
@to_address = opts[:to]
|
9
|
+
end
|
10
|
+
|
11
|
+
def text_body
|
12
|
+
text_template.result(binding)
|
13
|
+
end
|
14
|
+
|
15
|
+
def html_body
|
16
|
+
html_template.result(binding)
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def url_options
|
22
|
+
@url_options || GrapeTokenAuth.configuration.default_url_options
|
23
|
+
end
|
24
|
+
|
25
|
+
def text_template
|
26
|
+
ERB.new(File.read(self.class::TEXT_TEMPLATE))
|
27
|
+
end
|
28
|
+
|
29
|
+
def html_template
|
30
|
+
ERB.new(File.read(self.class::HTML_TEMPLATE))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|