grape_oauth2 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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