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,16 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
5
+ </head>
6
+ <body>
7
+ <h1>Email Confirmation</h1>
8
+ <p>Hello,</p>
9
+
10
+ <p>To activate your account please confirm your email by clicking the link below:</p>
11
+
12
+ <a href="<%= confirmation_link %>"><%= confirmation_link %></a>
13
+
14
+ <p>If you did not submit this request, please ignore this email</p>
15
+ </body>
16
+ </html>
@@ -0,0 +1,8 @@
1
+ Hello,
2
+
3
+ To activate your account please confirm your email by clicking the link below:
4
+
5
+ <%= confirmation_link %>
6
+
7
+ If you did not submit this request, please ignore this email.
8
+
@@ -0,0 +1,27 @@
1
+ module GrapeTokenAuth
2
+ module Mail
3
+ class ConfirmationEmail < MessageBase
4
+ TEXT_TEMPLATE = File.expand_path('../confirmation.text.erb', __FILE__)
5
+ HTML_TEMPLATE = File.expand_path('../confirmation.html.erb', __FILE__)
6
+
7
+ def initialize(opts)
8
+ @subject = opts[:subject] || 'Confirm your email'
9
+ super(opts)
10
+ end
11
+
12
+ def confirmation_link
13
+ protocol = url_options[:ssl] ? URI::HTTPS : URI::HTTP
14
+ options = url_options.merge(query: confirmation_params.to_query)
15
+ protocol.build(options).to_s
16
+ end
17
+
18
+ def confirmation_params
19
+ {
20
+ redirect_url: opts[:redirect_url],
21
+ config: opts[:client_config],
22
+ confirmation_token: opts[:token]
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
5
+ </head>
6
+ <body>
7
+ <h1>Password Reset</h1>
8
+ <p>Hello,</p>
9
+
10
+ <p> A password request has been submitted for your account, in order to
11
+ reset your password, please click the link below or copy it into your
12
+ browser:</p>
13
+
14
+ <a href="<%= reset_link %>"><%= reset_link %></a>
15
+
16
+ <p>If you did not submit this request, please ignore this email</p>
17
+ </body>
18
+ </html>
@@ -0,0 +1,9 @@
1
+ Hello,
2
+
3
+ A password request has been submitted for your account, in order to
4
+ reset your password, please follow the link below:
5
+
6
+ <%= reset_link %>
7
+
8
+ If you did not submit this request, please ignore this email.
9
+
@@ -0,0 +1,27 @@
1
+ module GrapeTokenAuth
2
+ module Mail
3
+ class PasswordResetEmail < MessageBase
4
+ TEXT_TEMPLATE = File.expand_path('../password_reset.text.erb', __FILE__)
5
+ HTML_TEMPLATE = File.expand_path('../password_reset.html.erb', __FILE__)
6
+
7
+ def initialize(opts)
8
+ @subject = opts[:subject] || 'Password Reset'
9
+ super(opts)
10
+ end
11
+
12
+ def reset_link
13
+ protocol = url_options[:ssl] ? URI::HTTPS : URI::HTTP
14
+ options = url_options.merge(query: reset_params.to_query, path: opts[:edit_path])
15
+ protocol.build(options).to_s
16
+ end
17
+
18
+ def reset_params
19
+ {
20
+ redirect_url: opts[:redirect_url],
21
+ config: opts[:client_config],
22
+ reset_password_token: opts[:token]
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,50 @@
1
+ module GrapeTokenAuth
2
+ module Mail
3
+ class SMTPMailer
4
+ def initialize(message, opts)
5
+ @message = message
6
+ @opts = opts
7
+ @to_address = opts[:to] || opts['to']
8
+ end
9
+
10
+ def send_mail
11
+ email.deliver
12
+ end
13
+
14
+ def prepare_email!
15
+ @email = ::Mail.new
16
+ @email.to = to_address
17
+ @email.subject = message.subject
18
+ @email.from = GrapeTokenAuth.configuration.from_address
19
+ @email.text_part = prepare_text
20
+ @email.html_part = prepare_html
21
+ self
22
+ end
23
+
24
+ def self.send!(message, options)
25
+ new(message, options).prepare_email!.send_mail
26
+ end
27
+
28
+ def valid_options?
29
+ return false unless to_address
30
+ true
31
+ end
32
+
33
+ protected
34
+
35
+ def prepare_html
36
+ part = ::Mail::Part.new
37
+ part.body = message.html_body
38
+ part
39
+ end
40
+
41
+ def prepare_text
42
+ part = ::Mail::Part.new
43
+ part.body = message.text_body
44
+ part
45
+ end
46
+
47
+ attr_reader :message, :email, :opts, :to_address
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,42 @@
1
+ module GrapeTokenAuth
2
+ class Middleware
3
+ def initialize(app, _options)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ setup(env)
9
+ begin
10
+ responses_with_auth_headers(*@app.call(env))
11
+ rescue Unauthorized
12
+ return unauthorized
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :app, :request_start, :authorizer_data, :scope
19
+
20
+ def unauthorized
21
+ [401,
22
+ { 'Content-Type' => 'application/json'
23
+ },
24
+ []
25
+ ]
26
+ end
27
+
28
+ def setup(env)
29
+ @request_start = Time.now
30
+ @authorizer_data = AuthorizerData.from_env(env)
31
+ end
32
+
33
+ def responses_with_auth_headers(status, headers, response)
34
+ auth_headers = AuthenticationHeader.new(authorizer_data, request_start)
35
+ [
36
+ status,
37
+ headers.merge(auth_headers.headers),
38
+ response
39
+ ]
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,80 @@
1
+ module GrapeTokenAuth
2
+ module MountHelpers
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def mount_registration(opts = {})
9
+ mount_api('RegistrationAPI', opts)
10
+ end
11
+
12
+ def mount_sessions(opts = {})
13
+ mount_api('SessionsAPI', opts)
14
+ end
15
+
16
+ def mount_confirmation(opts = {})
17
+ opts[:to] = opts[:to].to_s.chomp('/') + '/confirmation'
18
+ mount_api('ConfirmationAPI', opts)
19
+ end
20
+
21
+ def mount_token_validation(opts = {})
22
+ mount_api('TokenValidationAPI', opts)
23
+ end
24
+
25
+ def mount_password_reset(opts = {})
26
+ opts[:to] = opts[:to].to_s.chomp('/') + '/password'
27
+ mount_api('PasswordAPI', opts)
28
+ end
29
+
30
+ def mount_omniauth(opts = {})
31
+ path = opts[:to] || '/'
32
+
33
+ if mapping = opts[:for]
34
+ api = create_api_subclass('OmniAuthAPI', mapping)
35
+ else
36
+ api = GrapeTokenAuth::OmniAuthAPI
37
+ end
38
+
39
+ mount api => path
40
+ end
41
+
42
+ def mount_omniauth_callbacks(opts = {})
43
+ fail 'Oauth callback API is not scope specific. Only mount it once and do not pass a "for" option' if opts[:for]
44
+ fail 'Oauth callback API path is specificed in the configuration. Do not pass a "to" option' if opts[:to]
45
+ prefix = GrapeTokenAuth.set_omniauth_path_prefix!
46
+ mount GrapeTokenAuth::OmniAuthCallBackRouterAPI => prefix
47
+ end
48
+
49
+ private
50
+
51
+ def mount_api(api_class_name, opts)
52
+ path = opts[:to] || '/'
53
+
54
+ if opts[:for]
55
+ api = create_api_subclass(api_class_name, opts[:for])
56
+ else
57
+ api = GrapeTokenAuth.const_get(api_class_name)
58
+ end
59
+
60
+ mount api => path
61
+ end
62
+
63
+ def create_api_subclass(class_name, mapping)
64
+ resource_class = GrapeTokenAuth.configuration.scope_to_class(mapping)
65
+ fail ScopeUndefinedError.new(nil, mapping) unless resource_class
66
+ scope_name = mapping.to_s.split('_').collect(&:capitalize).join
67
+ klass = Class.new(Grape::API) do
68
+ class << self
69
+ def resource_scope
70
+ @resource_scope
71
+ end
72
+ end
73
+ end
74
+ klass.instance_variable_set(:@resource_scope, mapping)
75
+ klass.include(GrapeTokenAuth.const_get("#{class_name}Core"))
76
+ GrapeTokenAuth.const_set("#{scope_name}#{class_name}", klass)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,26 @@
1
+ require_relative './omniauth_html_base.rb'
2
+
3
+ module GrapeTokenAuth
4
+ class OmniAuthFailureHTML < OmniAuthHTMLBase
5
+ FAILURE_MESSAGE = 'authFailure'
6
+
7
+ def initialize(error_message)
8
+ @error_message = error_message
9
+ end
10
+
11
+ def auth_origin_url
12
+ "/#?error=#{error_message}"
13
+ end
14
+
15
+ def json_post_data
16
+ {
17
+ 'message' => FAILURE_MESSAGE,
18
+ 'error' => error_message
19
+ }.to_json
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :error_message
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ require 'erb'
2
+ require 'json'
3
+
4
+ module GrapeTokenAuth
5
+ class OmniAuthHTMLBase
6
+ def render_html
7
+ unless respond_to?(:json_post_data) && respond_to?(:auth_origin_url)
8
+ fail 'Invalid OmniAuthHTMLBase class'
9
+ end
10
+ template.result(binding)
11
+ end
12
+
13
+ private
14
+
15
+ def template_path
16
+ File.expand_path('../response_template.html.erb', __FILE__)
17
+ end
18
+
19
+ def template
20
+ ERB.new(File.read(template_path))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,109 @@
1
+ module GrapeTokenAuth
2
+ class OmniAuthResource
3
+ extend Forwardable
4
+
5
+ attr_reader :resource
6
+
7
+ def_delegators :token, :expiry, :client_id
8
+ def_delegator :resource, :uid
9
+
10
+ def initialize(resource, auth_hash, omniauth_params)
11
+ @resource = resource
12
+ @auth_hash = auth_hash
13
+ @omniauth_params = omniauth_params
14
+ end
15
+
16
+ def persist_oauth_attributes!
17
+ set_crazy_password
18
+ sync_token_to_resource
19
+ sync_attributes_to_resource
20
+ # skip_confirmable_email
21
+ resource.save!
22
+ end
23
+
24
+ def self.fetch_or_create(resource_class, auth_hash, oauth_params)
25
+ resource = resource_class.where(
26
+ uid: auth_hash['uid'],
27
+ provider: auth_hash['provider']).first_or_initialize
28
+ new(resource, auth_hash, oauth_params)
29
+ end
30
+
31
+ def token
32
+ @token ||= Token.new
33
+ end
34
+
35
+ def token_value
36
+ token.to_s
37
+ end
38
+
39
+ def attributes
40
+ { 'auth_token' => token_value,
41
+ 'client_id' => token.client_id,
42
+ 'expiry' => token.expiry }.merge(resource.serializable_hash)
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :auth_hash, :omniauth_params
48
+
49
+ def set_crazy_password
50
+ # set crazy password for new oauth users. this is only used to prevent
51
+ # access via email sign-in.
52
+ return if resource.id
53
+ p = SecureRandom.urlsafe_base64(nil, false)
54
+ resource.password = p
55
+ resource.password_confirmation = p
56
+ end
57
+
58
+ def sync_attributes_to_resource
59
+ # sync user info with provider, update/generate auth token
60
+ assign_provider_attrs
61
+
62
+ # assign any additional (whitelisted) attributes
63
+ assign_extra_attributes
64
+ end
65
+
66
+ def assign_provider_attrs
67
+ info_hash = auth_hash['info']
68
+ attrs = %i(nickname name image email).each_with_object({}) do |k, hsh|
69
+ hsh[k] = info_hash.fetch(k, '')
70
+ end
71
+ resource.assign_attributes(attrs)
72
+ end
73
+
74
+ def assign_extra_attributes
75
+ extra_params = whitelisted_params
76
+ resource.assign_attributes(extra_params) if extra_params
77
+ end
78
+
79
+ def whitelisted_params
80
+ whitelist = GrapeTokenAuth.configuration.param_white_list
81
+ return unless whitelist
82
+ scoped_list = whitelist[scope] || whitelist[scope.to_s]
83
+ return unless scoped_list
84
+ scoped_list.each_with_object({}) do |key, permitted|
85
+ value = find_with_indifference(omniauth_params, key)
86
+ permitted[key] = value if value
87
+ end
88
+ end
89
+
90
+ def scope
91
+ klass = resource.class
92
+ @scope ||= GrapeTokenAuth.configuration.mappings
93
+ .find { |k,v| v == klass }.try(:[], 0)
94
+ end
95
+
96
+ def find_with_indifference(hash, key)
97
+ if hash.key?(key.to_sym)
98
+ return hash[key.to_sym]
99
+ elsif hash.key?(key.to_s)
100
+ return hash[key.to_s]
101
+ end
102
+ nil
103
+ end
104
+
105
+ def sync_token_to_resource
106
+ resource.tokens[token.client_id] = token.to_h
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,61 @@
1
+ require_relative './omniauth_html_base.rb'
2
+
3
+ module GrapeTokenAuth
4
+ class OmniAuthSuccessHTML < OmniAuthHTMLBase
5
+ extend Forwardable
6
+
7
+ SUCCESS_MESSAGE = 'deliverCredentials'
8
+
9
+ def_delegators :oauth_resource, :resource, :persist_oauth_attributes!
10
+
11
+ def initialize(oauth_resource, auth_hash, omniauth_params)
12
+ @oauth_resource = oauth_resource
13
+ @auth_hash = auth_hash
14
+ @omniauth_params = omniauth_params
15
+ end
16
+
17
+ def self.build(resource_class, auth_hash, omniauth_params)
18
+ oauth_resource = OmniAuthResource.fetch_or_create(resource_class,
19
+ auth_hash,
20
+ omniauth_params)
21
+ new(oauth_resource, auth_hash, omniauth_params)
22
+ end
23
+
24
+ def auth_origin_url
25
+ @omniauth_params['auth_origin_url'] || @omniauth_params[:auth_origin_url]
26
+ end
27
+
28
+ def window_type
29
+ @omniauth_params['omniauth_window_type'] ||
30
+ @omniauth_params[:omniauth_window_type]
31
+ end
32
+
33
+ def full_redirect_url
34
+ "#{auth_origin_url}?#{auth_origin_query_params.to_query}"
35
+ end
36
+
37
+ def json_post_data
38
+ success_attributes = { 'message' => SUCCESS_MESSAGE,
39
+ 'config' => omniauth_params['config'] }
40
+ oauth_resource.attributes.merge(success_attributes).to_json
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :oauth_resource, :omniauth_params
46
+
47
+ def config
48
+ omniauth_params['config_name']
49
+ end
50
+
51
+ def auth_origin_query_params
52
+ {
53
+ auth_token: oauth_resource.token,
54
+ client_id: oauth_resource.client_id,
55
+ uid: oauth_resource.uid,
56
+ expiry: oauth_resource.expiry,
57
+ config: config
58
+ }
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,38 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <script>
5
+ /*
6
+ The data is accessible in two ways:
7
+
8
+ 1. Using the postMessage api, this window will respond to a
9
+ 'message' event with a post of all the data. (This can
10
+ be used by browsers other than IE if this window was
11
+ opened with window.open())
12
+ 2. This window has a function called requestCredentials which,
13
+ when called, will return the data. (This can be
14
+ used if this window was opened in an inAppBrowser using
15
+ Cordova / PhoneGap)
16
+ */
17
+
18
+ var data = <%= json_post_data %>;
19
+
20
+ window.addEventListener("message", function(ev) {
21
+ if (ev.data === "requestCredentials") {
22
+ ev.source.postMessage(data, '*');
23
+ window.close();
24
+ }
25
+ });
26
+ function requestCredentials() {
27
+ return data;
28
+ }
29
+ setTimeout(function() {
30
+ document.getElementById('text').innerHTML = (data && data.error) || 'Redirecting...';
31
+ }, 1000);
32
+ </script>
33
+ </head>
34
+ <body>
35
+ <pre id="text">
36
+ </pre>
37
+ </body>
38
+ </html>