grape_oauth2 0.1.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.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +18 -0
  5. data/.travis.yml +42 -0
  6. data/Gemfile +23 -0
  7. data/README.md +820 -0
  8. data/Rakefile +11 -0
  9. data/gemfiles/active_record.rb +25 -0
  10. data/gemfiles/mongoid.rb +14 -0
  11. data/gemfiles/sequel.rb +24 -0
  12. data/grape_oauth2.gemspec +27 -0
  13. data/grape_oauth2.png +0 -0
  14. data/lib/grape_oauth2.rb +129 -0
  15. data/lib/grape_oauth2/configuration.rb +143 -0
  16. data/lib/grape_oauth2/configuration/class_accessors.rb +36 -0
  17. data/lib/grape_oauth2/configuration/validation.rb +71 -0
  18. data/lib/grape_oauth2/endpoints/authorize.rb +34 -0
  19. data/lib/grape_oauth2/endpoints/token.rb +72 -0
  20. data/lib/grape_oauth2/gem_version.rb +24 -0
  21. data/lib/grape_oauth2/generators/authorization.rb +44 -0
  22. data/lib/grape_oauth2/generators/base.rb +26 -0
  23. data/lib/grape_oauth2/generators/token.rb +62 -0
  24. data/lib/grape_oauth2/helpers/access_token_helpers.rb +54 -0
  25. data/lib/grape_oauth2/helpers/oauth_params.rb +41 -0
  26. data/lib/grape_oauth2/mixins/active_record/access_grant.rb +47 -0
  27. data/lib/grape_oauth2/mixins/active_record/access_token.rb +75 -0
  28. data/lib/grape_oauth2/mixins/active_record/client.rb +35 -0
  29. data/lib/grape_oauth2/mixins/mongoid/access_grant.rb +58 -0
  30. data/lib/grape_oauth2/mixins/mongoid/access_token.rb +88 -0
  31. data/lib/grape_oauth2/mixins/mongoid/client.rb +41 -0
  32. data/lib/grape_oauth2/mixins/sequel/access_grant.rb +68 -0
  33. data/lib/grape_oauth2/mixins/sequel/access_token.rb +86 -0
  34. data/lib/grape_oauth2/mixins/sequel/client.rb +46 -0
  35. data/lib/grape_oauth2/responses/authorization.rb +10 -0
  36. data/lib/grape_oauth2/responses/base.rb +56 -0
  37. data/lib/grape_oauth2/responses/token.rb +10 -0
  38. data/lib/grape_oauth2/scopes.rb +74 -0
  39. data/lib/grape_oauth2/strategies/authorization_code.rb +38 -0
  40. data/lib/grape_oauth2/strategies/base.rb +47 -0
  41. data/lib/grape_oauth2/strategies/client_credentials.rb +20 -0
  42. data/lib/grape_oauth2/strategies/password.rb +22 -0
  43. data/lib/grape_oauth2/strategies/refresh_token.rb +47 -0
  44. data/lib/grape_oauth2/unique_token.rb +20 -0
  45. data/lib/grape_oauth2/version.rb +14 -0
  46. data/spec/configuration/config_spec.rb +231 -0
  47. data/spec/configuration/version_spec.rb +12 -0
  48. data/spec/dummy/endpoints/custom_authorization.rb +25 -0
  49. data/spec/dummy/endpoints/custom_token.rb +35 -0
  50. data/spec/dummy/endpoints/status.rb +25 -0
  51. data/spec/dummy/grape_oauth2_config.rb +11 -0
  52. data/spec/dummy/orm/active_record/app/config/db.rb +7 -0
  53. data/spec/dummy/orm/active_record/app/models/access_code.rb +3 -0
  54. data/spec/dummy/orm/active_record/app/models/access_token.rb +3 -0
  55. data/spec/dummy/orm/active_record/app/models/application.rb +3 -0
  56. data/spec/dummy/orm/active_record/app/models/application_record.rb +3 -0
  57. data/spec/dummy/orm/active_record/app/models/user.rb +10 -0
  58. data/spec/dummy/orm/active_record/app/twitter.rb +36 -0
  59. data/spec/dummy/orm/active_record/config.ru +7 -0
  60. data/spec/dummy/orm/active_record/db/schema.rb +53 -0
  61. data/spec/dummy/orm/mongoid/app/config/db.rb +6 -0
  62. data/spec/dummy/orm/mongoid/app/config/mongoid.yml +21 -0
  63. data/spec/dummy/orm/mongoid/app/models/access_code.rb +3 -0
  64. data/spec/dummy/orm/mongoid/app/models/access_token.rb +3 -0
  65. data/spec/dummy/orm/mongoid/app/models/application.rb +3 -0
  66. data/spec/dummy/orm/mongoid/app/models/user.rb +11 -0
  67. data/spec/dummy/orm/mongoid/app/twitter.rb +34 -0
  68. data/spec/dummy/orm/mongoid/config.ru +5 -0
  69. data/spec/dummy/orm/sequel/app/config/db.rb +1 -0
  70. data/spec/dummy/orm/sequel/app/models/access_code.rb +4 -0
  71. data/spec/dummy/orm/sequel/app/models/access_token.rb +4 -0
  72. data/spec/dummy/orm/sequel/app/models/application.rb +4 -0
  73. data/spec/dummy/orm/sequel/app/models/application_record.rb +2 -0
  74. data/spec/dummy/orm/sequel/app/models/user.rb +11 -0
  75. data/spec/dummy/orm/sequel/app/twitter.rb +47 -0
  76. data/spec/dummy/orm/sequel/config.ru +5 -0
  77. data/spec/dummy/orm/sequel/db/schema.rb +50 -0
  78. data/spec/lib/scopes_spec.rb +50 -0
  79. data/spec/mixins/active_record/access_token_spec.rb +185 -0
  80. data/spec/mixins/active_record/client_spec.rb +95 -0
  81. data/spec/mixins/mongoid/access_token_spec.rb +185 -0
  82. data/spec/mixins/mongoid/client_spec.rb +95 -0
  83. data/spec/mixins/sequel/access_token_spec.rb +185 -0
  84. data/spec/mixins/sequel/client_spec.rb +96 -0
  85. data/spec/requests/flows/authorization_code_spec.rb +67 -0
  86. data/spec/requests/flows/client_credentials_spec.rb +101 -0
  87. data/spec/requests/flows/password_spec.rb +210 -0
  88. data/spec/requests/flows/refresh_token_spec.rb +222 -0
  89. data/spec/requests/flows/revoke_token_spec.rb +103 -0
  90. data/spec/requests/protected_resources_spec.rb +64 -0
  91. data/spec/spec_helper.rb +60 -0
  92. data/spec/support/api_helper.rb +11 -0
  93. metadata +257 -0
@@ -0,0 +1,10 @@
1
+ module Grape
2
+ module OAuth2
3
+ # Grape::OAuth2 responses namespace.
4
+ module Responses
5
+ # Token response.
6
+ class Token < Base
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,74 @@
1
+ module Grape
2
+ module OAuth2
3
+ # OAuth2 helper for scopes validation
4
+ # (between requested and presented in Access Token).
5
+ class Scopes
6
+ # Array of requested scopes
7
+ #
8
+ # @return [Array<String>] scopes
9
+ #
10
+ attr_reader :scopes
11
+
12
+ # Helper class initializer.
13
+ #
14
+ # @param scopes [Array, String, #to_a]
15
+ # array, string of any object that responds to `to_a`
16
+ #
17
+ def initialize(scopes)
18
+ @scopes = to_array(scopes || [])
19
+ end
20
+
21
+ # Checks if requested scopes (passed and processed on initialization)
22
+ # are presented in the Access Token.
23
+ #
24
+ # @param access_token [Object]
25
+ # instance of the Access Token class that responds to `scopes`
26
+ #
27
+ # @return [Boolean]
28
+ # true if requested scopes are empty or present in access token scopes
29
+ # and false in other cases
30
+ #
31
+ def valid_for?(access_token)
32
+ scopes.empty? || present_in?(access_token.scopes)
33
+ end
34
+
35
+ private
36
+
37
+ # Checks if scopes present in Access Token scopes.
38
+ #
39
+ # @param token_scopes [Array, String, #to_a]
40
+ # array, string of any object that responds to `to_a`
41
+ #
42
+ # @return [Boolean]
43
+ # true if requested scopes present in Access Token and false in other cases
44
+ #
45
+ def present_in?(token_scopes)
46
+ required_scopes = Set.new(to_array(scopes))
47
+ authorized_scopes = Set.new(to_array(token_scopes))
48
+
49
+ authorized_scopes >= required_scopes
50
+ end
51
+
52
+ # Converts scopes set to the array.
53
+ #
54
+ # @param scopes [Array, String, #to_a]
55
+ # string, array or object that responds to `to_a`
56
+ # @return [Array<String>]
57
+ # array of scopes
58
+ #
59
+ def to_array(scopes)
60
+ return [] if scopes.nil?
61
+
62
+ collection = if scopes.is_a?(Array) || scopes.respond_to?(:to_a)
63
+ scopes.to_a
64
+ elsif scopes.is_a?(String)
65
+ scopes.split
66
+ else
67
+ raise ArgumentError, 'scopes class is not supported!'
68
+ end
69
+
70
+ collection.map(&:to_s)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,38 @@
1
+ module Grape
2
+ module OAuth2
3
+ module Strategies
4
+ # Auth Code strategy class.
5
+ # Processes request and responds with Token or Code
6
+ # (depend on requested response type).
7
+ class AuthorizationCode < Base
8
+ class << self
9
+ # Processes Authorization request.
10
+ def process(request, response)
11
+ client = authenticate_client(request)
12
+ request.bad_request! if client.nil?
13
+
14
+ response.redirect_uri = request.verify_redirect_uri!(client.redirect_uri)
15
+
16
+ # TODO: verify scopes if they valid
17
+ # scopes = request.scope
18
+ # request.invalid_scope! "Unknown scope: #{scope}"
19
+
20
+ case request.response_type
21
+ when :code
22
+ # resource owner can't be nil!
23
+ authorization_code = config.access_grant_class.create_for(client, nil, response.redirect_uri)
24
+ response.code = authorization_code.token
25
+ when :token
26
+ # resource owner can't be nil!
27
+ access_token = config.access_token_class.create_for(client, nil, scopes_from(request))
28
+ response.access_token = expose_to_bearer_token(access_token)
29
+ end
30
+
31
+ response.approve!
32
+ response
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,47 @@
1
+ module Grape
2
+ module OAuth2
3
+ # Grape::OAuth2 strategies namespace
4
+ module Strategies
5
+ # Base Grape::OAuth2 Strategies class .
6
+ # Contains common functionality for all the descendants.
7
+ class Base
8
+ class << self
9
+ # Authenticates Client from the request.
10
+ def authenticate_client(request)
11
+ config.client_class.authenticate(request.client_id, request.try(:client_secret))
12
+ end
13
+
14
+ # Authenticates Resource Owner from the request.
15
+ def authenticate_resource_owner(client, request)
16
+ config.resource_owner_class.oauth_authenticate(client, request.username, request.password)
17
+ end
18
+
19
+ # Short getter for Grape::OAuth2 configuration
20
+ def config
21
+ Grape::OAuth2.config
22
+ end
23
+
24
+ # Converts scopes from the request string. Separate them by the whitespace.
25
+ # @return [String] scopes string
26
+ #
27
+ def scopes_from(request)
28
+ return nil if request.scope.nil?
29
+
30
+ Array(request.scope).join(' ')
31
+ end
32
+
33
+ # Exposes token object to Bearer token.
34
+ #
35
+ # @param token [#to_bearer_token]
36
+ # any object that responds to `to_bearer_token`
37
+ # @return [Rack::OAuth2::AccessToken::Bearer]
38
+ # bearer token instance
39
+ #
40
+ def expose_to_bearer_token(token)
41
+ Rack::OAuth2::AccessToken::Bearer.new(token.to_bearer_token)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,20 @@
1
+ module Grape
2
+ module OAuth2
3
+ module Strategies
4
+ # Client Credentials strategy class.
5
+ # Processes request and respond with Access Token.
6
+ class ClientCredentials < Base
7
+ class << self
8
+ # Processes Client Credentials request.
9
+ def process(request)
10
+ client = authenticate_client(request)
11
+ request.invalid_client! if client.nil?
12
+
13
+ token = config.access_token_class.create_for(client, nil, scopes_from(request))
14
+ expose_to_bearer_token(token)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ module Grape
2
+ module OAuth2
3
+ module Strategies
4
+ # Resource Owner Password Credentials strategy class.
5
+ # Processes request and respond with Access Token.
6
+ class Password < Base
7
+ class << self
8
+ # Processes Password request.
9
+ def process(request)
10
+ client = authenticate_client(request) || request.invalid_client!
11
+ resource_owner = authenticate_resource_owner(client, request)
12
+
13
+ request.invalid_grant! if resource_owner.nil?
14
+
15
+ token = config.access_token_class.create_for(client, resource_owner, scopes_from(request))
16
+ expose_to_bearer_token(token)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,47 @@
1
+ module Grape
2
+ module OAuth2
3
+ module Strategies
4
+ # Refresh Token strategy class.
5
+ # Processes request and respond with Access Token.
6
+ class RefreshToken < Base
7
+ class << self
8
+ # Processes Refresh Token request.
9
+ def process(request)
10
+ client = authenticate_client(request)
11
+
12
+ request.invalid_client! if client.nil?
13
+
14
+ refresh_token = config.access_token_class.authenticate(request.refresh_token, type: :refresh_token)
15
+ request.invalid_grant! if refresh_token.nil?
16
+ request.unauthorized_client! if refresh_token && refresh_token.client != client
17
+
18
+ token = config.access_token_class.create_for(client, refresh_token.resource_owner)
19
+ run_on_refresh_callback(refresh_token) if config.on_refresh_runnable?
20
+
21
+ expose_to_bearer_token(token)
22
+ end
23
+
24
+ private
25
+
26
+ # Invokes custom callback on Access Token refresh.
27
+ # If callback is a proc, then call it with token.
28
+ # If access token responds to callback value (symbol for example), then call it from the token.
29
+ #
30
+ # @param access_token [Object] Access Token instance
31
+ #
32
+ def run_on_refresh_callback(access_token)
33
+ callback = config.on_refresh
34
+
35
+ if callback.respond_to?(:call)
36
+ callback.call(access_token)
37
+ elsif access_token.respond_to?(callback)
38
+ access_token.send(callback)
39
+ else
40
+ raise ArgumentError, ":on_refresh is not a block and Access Token class doesn't respond to #{callback}!"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,20 @@
1
+ module Grape
2
+ module OAuth2
3
+ # OAuth2 helper for generation of unique token values.
4
+ # Can process custom payload and options.
5
+ module UniqueToken
6
+ # Generates unique token value.
7
+ #
8
+ # @param _payload [Hash]
9
+ # payload
10
+ # @param options [Hash]
11
+ # options for generator
12
+ #
13
+ # @return [String]
14
+ # unique token value
15
+ def self.generate(_payload = {}, options = {})
16
+ SecureRandom.hex(options.delete(:size) || 32)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ require_relative 'gem_version'
2
+
3
+ module Grape
4
+ module OAuth2
5
+ # Grape::OAuth2 gem version.
6
+ #
7
+ # @return [Gem::Version]
8
+ # version value
9
+ #
10
+ def self.version
11
+ gem_version
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,231 @@
1
+ require 'spec_helper'
2
+
3
+ describe Grape::OAuth2::Configuration do
4
+ let(:config) { described_class.new }
5
+
6
+ # Refactor: Mock it
7
+ class CustomClient
8
+ def self.authenticate(key, secret = nil)
9
+ 'Test'
10
+ end
11
+ end
12
+
13
+ class CustomAccessToken
14
+ def self.create_for(client, resource_owner, scopes = nil)
15
+ end
16
+
17
+ def self.authenticate(token, type: :access_token)
18
+ 'Test'
19
+ end
20
+
21
+ def client
22
+ end
23
+
24
+ def resource_owner
25
+ end
26
+
27
+ def expired?
28
+ end
29
+
30
+ def revoked?
31
+ end
32
+
33
+ def revoke!(revoked_at = Time.now)
34
+ end
35
+
36
+ def to_bearer_token
37
+ end
38
+ end
39
+
40
+ class CustomResourceOwner
41
+ def self.oauth_authenticate(client, username, password)
42
+ 'Test'
43
+ end
44
+ end
45
+
46
+ context 'default config' do
47
+ it 'setup config with default values' do
48
+ expect(config.access_token_lifetime).to eq(7200)
49
+ expect(config.authorization_code_lifetime).to eq(1800)
50
+
51
+ expect(config.realm).to eq(Grape::OAuth2::Configuration::DEFAULT_REALM)
52
+ expect(config.allowed_grant_types).to eq(%w(password client_credentials))
53
+
54
+ expect(config.issue_refresh_token).to be_falsey
55
+ expect(config.on_refresh).to eq(:nothing)
56
+
57
+ expect(config.scopes_validator_class_name).to eq(Grape::OAuth2::Scopes.name)
58
+ end
59
+ end
60
+
61
+ context 'custom config' do
62
+ class CustomScopesValidator
63
+ def initialize(scopes)
64
+ @scopes = scopes
65
+ end
66
+
67
+ def valid_for?(access_token)
68
+ false
69
+ end
70
+ end
71
+
72
+ class CustomTokenGenerator
73
+ def self.generate(options = {})
74
+ if options[:custom]
75
+ 'custom_token'
76
+ else
77
+ 'default_token'
78
+ end
79
+ end
80
+ end
81
+
82
+ before do
83
+ config.access_token_class_name = 'CustomAccessToken'
84
+ config.resource_owner_class_name = 'CustomResourceOwner'
85
+ config.client_class_name = 'CustomClient'
86
+ config.access_grant_class_name = 'Object'
87
+ config.scopes_validator_class_name = 'CustomScopesValidator'
88
+ end
89
+
90
+ it 'invokes custom scopes validator' do
91
+ expect(config.scopes_validator.new([]).valid_for?(nil)).to be_falsey
92
+ end
93
+
94
+ it 'works with custom Access Token class' do
95
+ expect(config.access_token_class.authenticate('')).to eq('Test')
96
+ end
97
+
98
+ it 'works with custom Client class' do
99
+ expect(config.client_class.authenticate('')).to eq('Test')
100
+ end
101
+
102
+ it 'works with custom Resource Owner class' do
103
+ expect(config.resource_owner_class.oauth_authenticate('', '', '')).to eq('Test')
104
+ end
105
+
106
+ it 'works with custom token authenticator' do
107
+ # before
108
+ Grape::OAuth2.configure do |config|
109
+ config.token_authenticator do |request|
110
+ raise ArgumentError, 'Test'
111
+ end
112
+ end
113
+
114
+ expect { config.token_authenticator.call }.to raise_error(ArgumentError)
115
+
116
+ # after
117
+ Grape::OAuth2.configure do |config|
118
+ config.token_authenticator = config.default_token_authenticator
119
+ end
120
+ end
121
+
122
+ it 'works with custom token generator' do
123
+ # before
124
+ Grape::OAuth2.configure do |config|
125
+ config.token_generator_class_name = 'CustomTokenGenerator'
126
+ end
127
+
128
+ expect(Grape::OAuth2.config.token_generator.generate).to eq('default_token')
129
+ expect(Grape::OAuth2.config.token_generator.generate(custom: true)).to eq('custom_token')
130
+
131
+ # after
132
+ Grape::OAuth2.configure do |config|
133
+ config.token_generator_class_name = Grape::OAuth2::UniqueToken.name
134
+ end
135
+ end
136
+
137
+ it 'works with custom on_refresh callback' do
138
+ token = AccessToken.create
139
+
140
+ # before
141
+ Grape::OAuth2.configure do |config|
142
+ config.on_refresh do |access_token|
143
+ access_token.update(scopes: 'test')
144
+ end
145
+ end
146
+
147
+ expect {
148
+ Grape::OAuth2::Strategies::RefreshToken.send(:run_on_refresh_callback, token)
149
+ }.to change { token.scopes }.to('test')
150
+
151
+ # after
152
+ Grape::OAuth2.configure do |config|
153
+ config.on_refresh = :nothing
154
+ end
155
+ end
156
+
157
+ it 'raises an error with invalid on_refresh callback' do
158
+ # before
159
+ Grape::OAuth2.configure do |config|
160
+ config.on_refresh = 'invalid'
161
+ end
162
+
163
+ expect {
164
+ Grape::OAuth2::Strategies::RefreshToken.send(:run_on_refresh_callback, nil)
165
+ }.to raise_error(ArgumentError)
166
+
167
+ # after
168
+ Grape::OAuth2.configure do |config|
169
+ config.on_refresh = :nothing
170
+ end
171
+ end
172
+ end
173
+
174
+ context 'validation' do
175
+ context 'with invalid config options' do
176
+ it 'raises an error for default configuration' do
177
+ expect { config.check! }.to raise_error(Grape::OAuth2::Configuration::Error)
178
+ end
179
+
180
+ it "raises an error if configured classes doesn't have an instance methods" do
181
+ class InvalidAccessToken
182
+ # Only class methods
183
+ def self.create_for(client, resource_owner, scopes = nil)
184
+ end
185
+
186
+ def self.authenticate(token, type: :access_token)
187
+ 'Test'
188
+ end
189
+ end
190
+
191
+ config.access_token_class_name = 'InvalidAccessToken'
192
+ config.resource_owner_class_name = 'CustomResourceOwner'
193
+ config.client_class_name = 'CustomClient'
194
+ config.access_grant_class_name = 'Object'
195
+
196
+ expect { config.check! }.to raise_error(Grape::OAuth2::Configuration::APIMissing) do |error|
197
+ expect(error.message).to include('access_token_class')
198
+ expect(error.message).to include('Instance method')
199
+ end
200
+ end
201
+
202
+ it "raises an error if configured classes doesn't have a class methods" do
203
+ class InvalidClient
204
+ end
205
+
206
+ config.access_token_class_name = 'CustomAccessToken'
207
+ config.resource_owner_class_name = 'CustomResourceOwner'
208
+ config.client_class_name = 'InvalidClient'
209
+ config.access_grant_class_name = 'Object'
210
+
211
+ expect { config.check! }.to raise_error(Grape::OAuth2::Configuration::APIMissing) do |error|
212
+ expect(error.message).to include('client_class')
213
+ expect(error.message).to include('Class method')
214
+ end
215
+ end
216
+ end
217
+
218
+ context 'with valid config options' do
219
+ before do
220
+ config.access_token_class_name = 'CustomAccessToken'
221
+ config.resource_owner_class_name = 'CustomResourceOwner'
222
+ config.client_class_name = 'CustomClient'
223
+ config.access_grant_class_name = 'Object'
224
+ end
225
+
226
+ it 'successfully pass' do
227
+ expect { config.check! }.not_to raise_error
228
+ end
229
+ end
230
+ end
231
+ end