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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -1
  3. data/.ruby-version +1 -0
  4. data/CONTRIBUTING.md +46 -0
  5. data/README.md +222 -12
  6. data/Rakefile +11 -1
  7. data/bin/rspec +16 -0
  8. data/circle.yml +6 -0
  9. data/config/database.yml +8 -0
  10. data/grape_token_auth.gemspec +15 -0
  11. data/lib/grape_token_auth/api_helpers.rb +30 -0
  12. data/lib/grape_token_auth/apis/confirmation_api.rb +49 -0
  13. data/lib/grape_token_auth/apis/omniauth_api.rb +149 -0
  14. data/lib/grape_token_auth/apis/password_api.rb +138 -0
  15. data/lib/grape_token_auth/apis/registration_api.rb +88 -0
  16. data/lib/grape_token_auth/apis/session_api.rb +60 -0
  17. data/lib/grape_token_auth/apis/token_validation_api.rb +29 -0
  18. data/lib/grape_token_auth/authentication_header.rb +52 -0
  19. data/lib/grape_token_auth/authorizer_data.rb +58 -0
  20. data/lib/grape_token_auth/configuration.rb +81 -0
  21. data/lib/grape_token_auth/exceptions.rb +29 -0
  22. data/lib/grape_token_auth/key_generator.rb +44 -0
  23. data/lib/grape_token_auth/lookup_token.rb +46 -0
  24. data/lib/grape_token_auth/mail/mail.rb +28 -0
  25. data/lib/grape_token_auth/mail/message_base.rb +34 -0
  26. data/lib/grape_token_auth/mail/messages/confirmation/confirmation.html.erb +16 -0
  27. data/lib/grape_token_auth/mail/messages/confirmation/confirmation.text.erb +8 -0
  28. data/lib/grape_token_auth/mail/messages/confirmation/confirmation_email.rb +27 -0
  29. data/lib/grape_token_auth/mail/messages/password_reset/password_reset.html.erb +18 -0
  30. data/lib/grape_token_auth/mail/messages/password_reset/password_reset.text.erb +9 -0
  31. data/lib/grape_token_auth/mail/messages/password_reset/password_reset_email.rb +27 -0
  32. data/lib/grape_token_auth/mail/smtp_mailer.rb +50 -0
  33. data/lib/grape_token_auth/middleware.rb +42 -0
  34. data/lib/grape_token_auth/mount_helpers.rb +80 -0
  35. data/lib/grape_token_auth/omniauth/omniauth_failure_html.rb +26 -0
  36. data/lib/grape_token_auth/omniauth/omniauth_html_base.rb +23 -0
  37. data/lib/grape_token_auth/omniauth/omniauth_resource.rb +109 -0
  38. data/lib/grape_token_auth/omniauth/omniauth_success_html.rb +61 -0
  39. data/lib/grape_token_auth/omniauth/response_template.html.erb +38 -0
  40. data/lib/grape_token_auth/orm_integrations/active_record_token_auth.rb +310 -0
  41. data/lib/grape_token_auth/resource/resource_creator.rb +48 -0
  42. data/lib/grape_token_auth/resource/resource_crud_base.rb +43 -0
  43. data/lib/grape_token_auth/resource/resource_finder.rb +53 -0
  44. data/lib/grape_token_auth/resource/resource_updater.rb +40 -0
  45. data/lib/grape_token_auth/token.rb +23 -0
  46. data/lib/grape_token_auth/token_authentication.rb +8 -0
  47. data/lib/grape_token_auth/token_authorizer.rb +60 -0
  48. data/lib/grape_token_auth/unauthorized_middleware.rb +20 -0
  49. data/lib/grape_token_auth/version.rb +1 -1
  50. data/lib/grape_token_auth.rb +65 -2
  51. 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