├── .gitignore
├── .rspec
├── .rubocop.yml
├── .travis.yml
├── Gemfile
├── README.md
├── Rakefile
├── gemfiles
├── active_record.rb
├── mongoid.rb
└── sequel.rb
├── grape_oauth2.gemspec
├── grape_oauth2.png
├── lib
├── grape_oauth2.rb
└── grape_oauth2
│ ├── configuration.rb
│ ├── configuration
│ ├── class_accessors.rb
│ └── validation.rb
│ ├── endpoints
│ ├── authorize.rb
│ └── token.rb
│ ├── gem_version.rb
│ ├── generators
│ ├── authorization.rb
│ ├── base.rb
│ └── token.rb
│ ├── helpers
│ ├── access_token_helpers.rb
│ └── oauth_params.rb
│ ├── mixins
│ ├── active_record
│ │ ├── access_grant.rb
│ │ ├── access_token.rb
│ │ └── client.rb
│ ├── mongoid
│ │ ├── access_grant.rb
│ │ ├── access_token.rb
│ │ └── client.rb
│ └── sequel
│ │ ├── access_grant.rb
│ │ ├── access_token.rb
│ │ └── client.rb
│ ├── responses
│ ├── authorization.rb
│ ├── base.rb
│ └── token.rb
│ ├── scopes.rb
│ ├── strategies
│ ├── authorization_code.rb
│ ├── base.rb
│ ├── client_credentials.rb
│ ├── password.rb
│ └── refresh_token.rb
│ ├── unique_token.rb
│ └── version.rb
└── spec
├── configuration
├── config_spec.rb
└── version_spec.rb
├── dummy
├── endpoints
│ ├── custom_authorization.rb
│ ├── custom_token.rb
│ └── status.rb
├── grape_oauth2_config.rb
└── orm
│ ├── active_record
│ ├── app
│ │ ├── config
│ │ │ └── db.rb
│ │ ├── models
│ │ │ ├── access_code.rb
│ │ │ ├── access_token.rb
│ │ │ ├── application.rb
│ │ │ ├── application_record.rb
│ │ │ └── user.rb
│ │ └── twitter.rb
│ ├── config.ru
│ └── db
│ │ └── schema.rb
│ ├── mongoid
│ ├── app
│ │ ├── config
│ │ │ ├── db.rb
│ │ │ └── mongoid.yml
│ │ ├── models
│ │ │ ├── access_code.rb
│ │ │ ├── access_token.rb
│ │ │ ├── application.rb
│ │ │ └── user.rb
│ │ └── twitter.rb
│ └── config.ru
│ └── sequel
│ ├── app
│ ├── config
│ │ └── db.rb
│ ├── models
│ │ ├── access_code.rb
│ │ ├── access_token.rb
│ │ ├── application.rb
│ │ ├── application_record.rb
│ │ └── user.rb
│ └── twitter.rb
│ ├── config.ru
│ └── db
│ └── schema.rb
├── lib
└── scopes_spec.rb
├── mixins
├── active_record
│ ├── access_token_spec.rb
│ └── client_spec.rb
├── mongoid
│ ├── access_token_spec.rb
│ └── client_spec.rb
└── sequel
│ ├── access_token_spec.rb
│ └── client_spec.rb
├── requests
├── flows
│ ├── authorization_code_spec.rb
│ ├── client_credentials_spec.rb
│ ├── password_spec.rb
│ ├── refresh_token_spec.rb
│ └── revoke_token_spec.rb
└── protected_resources_spec.rb
├── spec_helper.rb
└── support
└── api_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | .bundle/
2 | .rbx
3 | *.rbc
4 | log/*.log
5 | .rvmrc
6 | /.idea
7 | gemfiles/*.lock
8 | Gemfile.lock
9 | coverage/
10 | .yardoc/
11 | doc/
12 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format=documentation
3 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | LineLength:
2 | Max: 120
3 | AllCops:
4 | Exclude:
5 | - 'spec/**/*'
6 | DisplayCopNames: true
7 | Rails:
8 | Enabled: false
9 | Documentation:
10 | Enabled: false
11 | Style/EndOfLine:
12 | Enabled: false
13 | Rails/TimeZone:
14 | Enabled: false
15 | Metrics/BlockLength:
16 | Exclude:
17 | - lib/grape_oauth2/mixins/**/*
18 | - lib/grape_oauth2/endpoints/*
19 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | sudo: false
3 | cache: bundler
4 | bundler_args: --without yard guard benchmarks
5 |
6 | services:
7 | - mongodb
8 |
9 | before_install:
10 | - gem install bundler -v '~> 1.10'
11 |
12 | matrix:
13 | allow_failures:
14 | - rvm: ruby-head
15 | include:
16 | - rvm: 2.2
17 | gemfile: gemfiles/active_record.rb
18 | env: ORM=active_record
19 | - rvm: 2.2
20 | gemfile: gemfiles/sequel.rb
21 | env: ORM=sequel
22 | - rvm: 2.2
23 | gemfile: gemfiles/mongoid.rb
24 | env: ORM=mongoid
25 | - rvm: 2.3
26 | gemfile: gemfiles/active_record.rb
27 | env: ORM=active_record
28 | - rvm: 2.3
29 | gemfile: gemfiles/sequel.rb
30 | env: ORM=sequel
31 | - rvm: 2.3
32 | gemfile: gemfiles/mongoid.rb
33 | env: ORM=mongoid
34 | - rvm: 2.4
35 | gemfile: gemfiles/active_record.rb
36 | env: ORM=active_record
37 | - rvm: 2.4
38 | gemfile: gemfiles/sequel.rb
39 | env: ORM=sequel
40 | - rvm: ruby-head
41 | gemfile: gemfiles/active_record.rb
42 | env: ORM=active_record
43 | - rvm: ruby-head
44 | gemfile: gemfiles/sequel.rb
45 | env: ORM=sequel
46 | - rvm: ruby-head
47 | gemfile: gemfiles/mongoid.rb
48 | env: ORM=mongoid
49 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
5 | gem 'grape', '~> 1.0'
6 | gem 'rack-oauth2'
7 |
8 | gem 'activerecord'
9 | gem 'bcrypt'
10 |
11 | group :test do
12 | platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do
13 | gem 'sqlite3'
14 | end
15 |
16 | gem 'coveralls', require: false
17 | gem 'database_cleaner'
18 | gem 'otr-activerecord'
19 | gem 'rack-test', require: 'rack/test'
20 | gem 'rspec-rails', '~> 3.5'
21 | end
22 |
23 | gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Grape::OAuth2
6 | [](http://badge.fury.io/rb/grape_oauth2)
7 | [](https://travis-ci.org/nbulaj/grape_oauth2)
8 | [](https://coveralls.io/github/nbulaj/grape_oauth2)
9 | [](https://codeclimate.com/github/nbulaj/grape_oauth2)
10 | [](http://inch-ci.org/github/nbulaj/grape_oauth2)
11 | [](#license)
12 |
13 | This gem adds a flexible OAuth2 ([RFC 6749](http://www.rfc-editor.org/rfc/rfc6749.txt)) server authorization and
14 | endpoints protection to your [Grape](https://github.com/ruby-grape/grape) API project with any ORM / ODM / PORO.
15 |
16 | Implemented features (flows):
17 |
18 | - Resource Owner Password Credentials
19 | - Client Credentials
20 | - Refresh token
21 | - Token revocation
22 | - Access Token Scopes
23 |
24 | Supported token types:
25 |
26 | * Bearer
27 |
28 | _In progress_ (check [Contributing](#contributing) section if yoy want to help with it):
29 |
30 | - Authorization Code Flow (partially implemented)
31 | - Access Grants
32 | - Implicit Grant
33 |
34 | ## Documentation valid for `master` branch
35 |
36 | Please check the documentation for the version of `Grape::OAuth2` you are using in:
37 | https://github.com/nbulaj/grape_oauth2/releases
38 |
39 | - See the [Wiki](https://github.com/nbulaj/grape_oauth2/wiki)
40 |
41 | ## Table of Contents
42 |
43 | - [Installation](#installation)
44 | - [Configuration](#configuration)
45 | - [ActiveRecord](#activerecord)
46 | - [Sequel](#sequel)
47 | - [Mongoid](#mongoid)
48 | - [Other ORMs](#other-orms)
49 | - [Client](#client)
50 | - [AccessToken](#accesstoken)
51 | - [ResourceOwner](#resourceowner)
52 | - [Usage examples](#usage-examples)
53 | - [I'm lazy, give me all out of the box!](#im-lazy-give-me-all-out-of-the-box)
54 | - [Hey, I wanna control all the authentication process!](#hey-i-wanna-control-all-the-authentication-process)
55 | - [Override default mixins](#override-default-mixins)
56 | - [Custom authentication endpoints](#custom-authentication-endpoints)
57 | - [Custom Access Token authenticator](#custom-access-token-authenticator)
58 | - [Custom scopes validation](#custom-scopes-validation)
59 | - [Custom token generator](#custom-token-generator)
60 | - [Process token on Refresh (protect against Replay Attacks)](#process-token-on-refresh-protect-against-replay-attacks)
61 | - [Errors (exceptions) handling](#errors-exceptions-handling)
62 | - [Example App](#example-app)
63 | - [Contributing](#contributing)
64 | - [License](#license)
65 |
66 | ## Installation
67 |
68 | **Grape::OAuth2** gem requires only `Grape` and `Rack::OAuth2` gems as the dependency.
69 | Yes, no Rails, ActiveRecord or any other libs or huge frameworks :+1:
70 |
71 | If you are using bundler, first add `'grape_oauth2'` to your Gemfile:
72 |
73 | ```ruby
74 |
75 | gem 'grape', '~> 1.0'
76 | gem 'grape_oauth2', '~> 0.2'
77 |
78 | # or
79 |
80 | gem 'grape_oauth2', git: 'https://github.com/nbulaj/grape_oauth2.git'
81 | ```
82 |
83 | And run:
84 |
85 | ```sh
86 | bundle install
87 | ```
88 |
89 | If you running your Grape API with `rackup` and using the [gem from git source](http://bundler.io/git.html), then
90 | you need to explicitly require bundler in the `config.ru`:
91 |
92 | ```ruby
93 | require 'bundler/setup'
94 | Bundler.setup
95 | ```
96 |
97 | or run your app with bundle exec command:
98 |
99 | ```
100 | > bundle exec rackup config.ru
101 | [2016-11-19 02:35:33] INFO WEBrick 1.3.1
102 | [2016-11-19 02:35:33] INFO ruby 2.3.1 (2016-04-26) [i386-mingw32]
103 | [2016-11-19 02:35:33] INFO WEBrick::HTTPServer#start: pid=5472 port=9292
104 | ```
105 |
106 | ## Configuration
107 |
108 | Main `Grape::OAuth2` configuration must be placed in `config/initializers/` (in case you are using [Rails](https://github.com/rails/rails))
109 | or in some place, that will be processed at the application startup:
110 |
111 | ```ruby
112 | Grape::OAuth2.configure do |config|
113 | # Access Tokens lifetime (expires in)
114 | config.access_token_lifetime = 7200 # in seconds (2.hours for Rails), `nil` if never expires
115 |
116 | # Authorization Code lifetime
117 | # config.authorization_code_lifetime = 7200 # in seconds (2.hours for Rails)
118 |
119 | # Allowed OAuth2 Authorization Grants (default is %w(password client_credentials)
120 | config.allowed_grant_types = %w(password client_credentials refresh_token)
121 |
122 | # Issue access tokens with refresh token (default is false)
123 | config.issue_refresh_token = true
124 |
125 | # Process Access Token that was used for the Refresh Token Flow (default is :nothing).
126 | # Could be a symbol (Access Token instance must respond to it)
127 | # or block with refresh token as an argument.
128 | # config.on_refresh = :nothing
129 |
130 | # WWW-Authenticate Realm (default is "OAuth 2.0")
131 | # config.realm = 'My API'
132 |
133 | # Access Token authenticator block.
134 | # config.token_authenticator do |request|
135 | # AccessToken.authenticate(request.access_token) || request.invalid_token!
136 | # end
137 |
138 | # Scopes validator class (default is Grape::OAuth2::Scopes).
139 | # config.scopes_validator_class_name = 'MyCustomValidator'
140 |
141 | # Token generator class (default is Grape::OAuth2::UniqueToken).
142 | # Must respond to `self.generate(payload = {}, options = {})`.
143 | # config.token_generator_class_name = 'JWTGenerator'
144 |
145 | # Classes for OAuth2 Roles
146 | config.client_class_name = 'Application'
147 | config.access_token_class_name = 'AccessToken'
148 | config.resource_owner_class_name = 'User'
149 | end
150 | ```
151 |
152 | Currently implemented (partly on completely) grant types: _password, client_credentials, refresh_token_.
153 |
154 | As you know, OAuth2 workflow implies the existence of the next three roles: **Access Token**, **Client** and **Resource Owner**.
155 | So your project must include 3 classes (models) - _AccessToken_, _Application_ and _User_ for example. The gem needs to know
156 | what classes it work, so you need to create them and configure `Grape::OAuth2`.
157 |
158 | `resource_owner_class` must have a `self.oauth_authenticate(client, username, password)` method, that returns an instance of the
159 | class if authentication successful (`username` and `password` matches for example) and `false` or `nil` in other cases.
160 |
161 | ```ruby
162 | # app/models/user.rb
163 | class User < ApplicationRecord
164 | has_secure_password
165 |
166 | def self.oauth_authenticate(_client, username, password)
167 | # find the user by it username
168 | user = find_by(username: username)
169 | return if user.nil?
170 |
171 | # check the password
172 | user.authenticate(password)
173 | end
174 | end
175 | ```
176 |
177 | `client_class`, `access_token_class` and `resource_owner_class` objects must contain a specific set of API (methods), that are
178 | called by the gem. `Grape::OAuth2` includes predefined mixins for the projects that use the `ActiveRecord` or `Sequel` ORMs,
179 | and you can just include them into your models.
180 |
181 | ### ActiveRecord
182 |
183 | ```ruby
184 | # app/models/access_token.rb
185 | class AccessToken < ApplicationRecord
186 | include Grape::OAuth2::ActiveRecord::AccessToken
187 | end
188 |
189 | # app/models/application.rb
190 | class Application < ApplicationRecord
191 | include Grape::OAuth2::ActiveRecord::Client
192 | end
193 | ```
194 |
195 | Migration for the simplest use case of the gem looks as follows:
196 |
197 | ```ruby
198 | ActiveRecord::Schema.define(version: 3) do
199 | # All the columns are custom
200 | create_table :users do |t|
201 | t.string :name
202 | t.string :username
203 | t.string :password_digest
204 | end
205 |
206 | # Required columns: :key & :secret
207 | create_table :applications do |t|
208 | t.string :name
209 | t.string :key
210 | t.string :secret
211 |
212 | t.timestamps null: false
213 | end
214 |
215 | add_index :applications, :key, unique: true
216 |
217 | # Required columns: :client_id, :resource_owner_id, :token, :expires_at, :revoked_at, :refresh_token
218 | create_table :access_tokens do |t|
219 | t.integer :resource_owner_id
220 | t.integer :client_id
221 |
222 | t.string :token, null: false
223 | t.string :refresh_token
224 | t.string :scopes, default: ''
225 |
226 | t.datetime :expires_at
227 | t.datetime :revoked_at
228 | t.datetime :created_at, null: false
229 | end
230 |
231 | add_index :access_tokens, :token, unique: true
232 | add_index :access_tokens, :resource_owner_id
233 | add_index :access_tokens, :client_id
234 | add_index :access_tokens, :refresh_token, unique: true
235 | end
236 | ```
237 |
238 | ### Sequel
239 |
240 | ```ruby
241 | # app/models/access_token.rb
242 | class AccessToken < Sequel::Model
243 | include Grape::OAuth2::Sequel::AccessToken
244 | end
245 |
246 | # app/models/application.rb
247 | class Application < Sequel::Model
248 | include Grape::OAuth2::Sequel::Client
249 | end
250 | ```
251 |
252 | Migration for the simplest use case of the gem looks as follows:
253 |
254 | ```ruby
255 | DB.create_table :applications do
256 | primary_key :id
257 |
258 | column :name, String, size: 255, null: false
259 | column :key, String, size: 255, null: false, index: { unique: true }
260 | column :secret, String, size: 255, null: false
261 |
262 |
263 | column :redirect_uri, String
264 |
265 | column :created_at, DateTime
266 | column :updated_at, DateTime
267 | end
268 |
269 | DB.create_table :access_tokens do
270 | primary_key :id
271 | column :client_id, Integer
272 | column :resource_owner_id, Integer, index: true
273 |
274 | column :token, String, size: 255, null: false, index: { unique: true }
275 |
276 | column :refresh_token, String, size: 255, index: { unique: true }
277 |
278 | column :expires_at, DateTime
279 | column :revoked_at, DateTime
280 | column :created_at, DateTime, null: false
281 | column :scopes, String, size: 255
282 | end
283 |
284 | DB.create_table :users do
285 | primary_key :id
286 | column :name, String, size: 255
287 | column :username, String, size: 255
288 | column :created_at, DateTime
289 | column :updated_at, DateTime
290 | column :password_digest, String, size: 255
291 | end
292 | ```
293 |
294 | ### Mongoid
295 |
296 | ```ruby
297 | # app/models/access_token.rb
298 | class AccessToken
299 | include Grape::OAuth2::Mongoid::AccessToken
300 | end
301 |
302 | # app/models/application.rb
303 | class Application
304 | include Grape::OAuth2::Mongoid::Client
305 | end
306 |
307 | # app/models/user.rb
308 | class User
309 | include Mongoid::Document
310 | include Mongoid::Timestamps
311 |
312 | field :username, type: String
313 | field :password, type: String
314 |
315 | def self.oauth_authenticate(_client, username, password)
316 | find_by(username: username, password: password)
317 | end
318 | end
319 | ```
320 |
321 | ### Other ORMs
322 |
323 | If you want to use `Grape::OAuth2` gem, but your project doesn't use `ActiveRecord`, `Sequel` or `Mongoid`, then you can
324 | create at least 3 classes (models) to cover OAuth2 roles and define a specific set ot API for them as described below.
325 |
326 | #### Client
327 |
328 | Class that represents an OAuth2 Client should contain the following API:
329 |
330 | ```ruby
331 | class Client
332 | # ...
333 |
334 | def self.authenticate(key, secret = nil)
335 | # Should return a Client instance matching the
336 | # key & secret provided (`secret` is optional).
337 | end
338 | end
339 | ```
340 |
341 | #### AccessToken
342 |
343 | For the class that represents an OAuth2 Access Token you must define the next API:
344 |
345 | ```ruby
346 | class AccessToken
347 | # ...
348 |
349 | def self.create_for(client, resource_owner, scopes = nil)
350 | # Creates the record in the database for the provided client and
351 | # resource owner with specific scopes (if present).
352 | # Returns an instance of that record.
353 | end
354 |
355 | def self.authenticate(token, type: :access_token)
356 | # Returns an Access Token instance matching the token provided.
357 | # Access Token can be searched by token or refresh token value. In the
358 | # first case :type option must be set to :access_token (default), in
359 | # the second case - to the :refresh_token.
360 | # Note that you MAY include expired access tokens in the result
361 | # of this method so long as you implement an instance `#expired?`
362 | # method.
363 | end
364 |
365 | def client
366 | # Returns associated Client instance. Always must be present!
367 | # For ORM objects it can be an association (`belongs_to :client` for ActiveRecord).
368 | end
369 |
370 | def resource_owner
371 | # Returns associated Resource Owner instance.
372 | # Can return `nil` (for Client Credentials flow as an example).
373 | # For ORM objects it can be an association (`belongs_to :resource_owner` for ActiveRecord).
374 | end
375 |
376 | def scopes
377 | # Returns Access Token authorised set of scopes. Can be a space-separated String,
378 | # Array or any object, that responds to `to_a`.
379 | end
380 |
381 | def expired?
382 | # true if the Access Token has reached its expiration.
383 | end
384 |
385 | def revoked?
386 | # true if the Access Token was revoked.
387 | end
388 |
389 | def revoke!(revoked_at = Time.now)
390 | # Revokes an Access Token (by setting its :revoked_at attribute to the
391 | # specific time for example).
392 | end
393 |
394 | def to_bearer_token
395 | # Returns a Hash of Bearer token attributes like the following:
396 | # access_token: '', # - required
397 | # refresh_token: '', # - optional
398 | # token_type: 'bearer', # - required
399 | # expires_in: '', # - required
400 | # scope: '' # - optional
401 | end
402 | end
403 | ```
404 |
405 | You can take a look at the [Grape::OAuth2 mixins](https://github.com/nbulaj/grape_oauth2/tree/master/lib/grape_oauth2/mixins)
406 | to understand what they are doing and what they are returning.
407 |
408 | #### ResourceOwner
409 |
410 | As was said before, Resource Owner class (`User` model for example) must contain only one class method
411 | (**only for** Password Authorization Grant): `self.oauth_authenticate(client, username, password)`.
412 |
413 | ```ruby
414 | class User
415 | # ...
416 |
417 | def self.oauth_authenticate(client, username, password)
418 | # Returns an instance of the User class with matching username
419 | # and password. If there is no such User or password doesn't match
420 | # then returns nil.
421 | end
422 | end
423 | ```
424 |
425 | ## Usage examples
426 | ### I'm lazy, give me all out of the box!
427 |
428 | If you need a common OAuth2 authentication then you can use default gem endpoints for it. First of all you
429 | will need to configure Grape::OAuth2 as described above (create models, migrations, configure the gem).
430 | For `ActiveRecord` it would be as follows:
431 |
432 | ```ruby
433 | # app/models/access_token.rb
434 | class AccessToken < ApplicationRecord
435 | include Grape::OAuth2::ActiveRecord::AccessToken
436 | end
437 |
438 | # app/models/application.rb
439 | class Application < ApplicationRecord
440 | include Grape::OAuth2::ActiveRecord::Client
441 | end
442 |
443 | # app/models/user.rb
444 | class User < ApplicationRecord
445 | has_secure_password
446 |
447 | # Don't forget to setup this method for your Resource Owner model!
448 | def self.oauth_authenticate(_client, username, password)
449 | user = find_by(username: username)
450 | return if user.nil?
451 |
452 | user.authenticate(password)
453 | end
454 | end
455 |
456 | # config/oauth2.rb
457 | Grape::OAuth2.configure do |config|
458 | # Classes for OAuth2 Roles
459 | config.client_class_name = 'Application'
460 | config.access_token_class_name = 'AccessToken'
461 | config.resource_owner_class_name = 'User'
462 | end
463 | ```
464 |
465 | And just inject `Grape::OAuth2` into your main API class:
466 |
467 | ```ruby
468 | # app/twitter.rb
469 | module Twitter
470 | class API < Grape::API
471 | version 'v1', using: :path
472 | format :json
473 | prefix :api
474 |
475 | # Mount all endpoints by default.
476 | # You can define a custom one you want to use by providing them
477 | # as an argument:
478 | # include Grape::OAuth2.api :token, :authorize
479 | #
480 | include Grape::OAuth2.api
481 |
482 | # mount any other endpoints
483 | # ...
484 | end
485 | end
486 | ```
487 |
488 | The `include Grape::OAuth2.api` could be replaced with the next (as it does the same):
489 |
490 |
491 | ```ruby
492 | # app/twitter.rb
493 | module Twitter
494 | class API < Grape::API
495 | version 'v1', using: :path
496 | format :json
497 | prefix :api
498 |
499 | # Add OAuth2 helpers
500 | helpers Grape::OAuth2::Helpers::AccessTokenHelpers
501 |
502 | # Inject token authentication middleware
503 | use *Grape::OAuth2.middleware
504 |
505 | # Mount default Grape::OAuth2 Token endpoint
506 | mount Grape::OAuth2::Endpoints::Token
507 | # Mount default Grape::OAuth2 Authorization endpoint
508 | mount Grape::OAuth2::Endpoints::Authorize
509 |
510 | # mount any other endpoints
511 | # ...
512 | end
513 | end
514 | ```
515 |
516 | And that is all! Use the next available routes to get the Access Token:
517 |
518 | ```
519 | POST /oauth/token
520 | POST /oauth/revoke
521 | ```
522 |
523 | Now you can protect your endpoints with `access_token_required!` method:
524 |
525 | ```ruby
526 | module Twitter
527 | module Endpoints
528 | class Status < Grape::API
529 | resources :status do
530 | get do
531 | # public resource, no scopes required
532 | access_token_required!
533 |
534 | present(:status, current_user.status)
535 | end
536 |
537 | post do
538 | # requires 'write' scope to exist in Access Token
539 | access_token_required! :write
540 |
541 | status = current_user.statuses.create!(body: 'Hi man!')
542 | present(:status, status, with: V1::Entities::Status)
543 | end
544 | end
545 | end
546 | end
547 | end
548 | ```
549 |
550 | If you need to protect all the routes in the endpoint, but it's requires different scopes, than you can
551 | add `access_token_required!` helper to the `before` filter and setup required scopes directly for the endpoints:
552 |
553 | ```ruby
554 | module Twitter
555 | module Endpoints
556 | class Status < Grape::API
557 | before do
558 | access_token_required!
559 | end
560 |
561 | resources :status do
562 | # public endpoint - no scopes required
563 | get do
564 | present(:status, current_user.status)
565 | end
566 |
567 | # private endpoint - requires :write scope
568 | put ':id', scopes: [:write] do
569 | status = current_user.statuses.create!(body: 'Hi man!')
570 | present(:status, status, with: V1::Entities::Status)
571 | end
572 | end
573 | end
574 | end
575 | end
576 | ```
577 |
578 | ### Hey, I wanna control all the authentication process!
579 | #### Override default mixins
580 |
581 | If you need to do some special things (check if `key` starts with _'MyAPI'_ word for example) and don't want to
582 | write your own authentication endpoints, then you can just override default authentication methods in models
583 | (only if you are using gem mixins, in other cases you **MUST** write them by yourself):
584 |
585 | ```ruby
586 | # app/models/application.rb
587 | class Application < ApplicationRecord
588 | include Grape::OAuth2::ActiveRecord::Client
589 |
590 | class << self
591 | def self.authenticate(key, secret = nil)
592 | # My custom condition for successful authentication
593 | return nil unless key.start_with?('MyAPI')
594 |
595 | if secret.present?
596 | find_by(key: key, secret: secret)
597 | else
598 | find_by(key: key)
599 | end
600 | end
601 | end
602 | end
603 | ```
604 |
605 | #### Custom authentication endpoints
606 |
607 | Besides, you can create your own API endpoints for OAuth2 authentication and use `grape_oauth2` gem functionality.
608 | In that case you will get a full control over the authentication proccess and can do anything in it. Just create
609 | a common Grape API class, set optional OAuth2 params and process the request with the `Grape::OAuth2::Generators::Token`
610 | generator for example (for issuing an access token):
611 |
612 | ```ruby
613 | # api/oauth2.rb
614 | module MyAPI
615 | class OAuth2 < Grape::API
616 | helpers Grape::OAuth2::Helpers::OAuthParams
617 |
618 | namespace :oauth do
619 | params do
620 | use :oauth_token_params
621 | end
622 |
623 | post :token do
624 | token_response = Grape::OAuth2::Generators::Token.generate_for(env) do |request, response|
625 | # You can use default authentication if you don't need to change this part:
626 | # client = Grape::OAuth2::Strategies::Base.authenticate_client(request)
627 |
628 | # Or write your custom client authentication:
629 | client = Application.find_by(key: request.client_id, active: true)
630 | request.invalid_client! unless client
631 |
632 | # You can use default Resource Owner authentication if you don't need to change this part:
633 | # resource_owner = Grape::OAuth2::Strategies::Base.authenticate_resource_owner(client, request)
634 |
635 | # Or define your custom resource owner authentication:
636 | resource_owner = User.find_by(username: request.username)
637 | request.invalid_grant! if resource_owner.nil? || resource_owner.inactive?
638 |
639 | # You can create an Access Token as you want:
640 | token = MyAwesomeAccessToken.create(client: client,
641 | resource_owner: resource_owner,
642 | scope: request.scope)
643 |
644 | response.access_token = Grape::OAuth2::Strategies::Base.expose_to_bearer_token(token)
645 | end
646 |
647 | # If request is successful, then return it
648 | status token_response.status
649 |
650 | token_response.headers.each do |key, value|
651 | header key, value
652 | end
653 |
654 | body token_response.access_token
655 | end
656 |
657 | desc 'OAuth 2.0 Token Revocation'
658 |
659 | params do
660 | use :oauth_token_revocation_params
661 | end
662 |
663 | post :revoke do
664 | # ...
665 | end
666 | end
667 | end
668 | end
669 | ```
670 |
671 | ## Custom Access Token authenticator
672 |
673 | If you don't want to use default `Grape::OAuth2` Access Token authenticator then you can define your own in the
674 | configuration (it must be a `proc` or `lambda`):
675 |
676 | ```ruby
677 | Grape::OAuth2.configure do |config|
678 | config.token_authenticator do |request|
679 | AccessToken.find_by(token: request.access_token) || request.invalid_token!
680 | end
681 |
682 | # or config.token_authenticator = lambda { |request| ... }
683 | end
684 | ```
685 |
686 | Don't forget to add the middleware to your root API class (`use *Grape::OAuth2.middleware`, see below).
687 |
688 | ## Custom scopes validation
689 |
690 | If you want to control the process of scopes validation (for protected endpoints for example) then you must implement
691 | your own class that will implement the following API:
692 |
693 | ```ruby
694 | class CustomScopesValidator
695 | # `scopes' is the set of required scopes that must be
696 | # present in the Access Token instance.
697 | def initialize(scopes)
698 | @scopes = scopes || []
699 | # ...some custom processing of scopes if required ...
700 | end
701 |
702 | def valid_for?(access_token)
703 | # custom scopes validation implementation...
704 | end
705 | end
706 | ```
707 |
708 | And set that class as scopes validator in the Grape::OAuth2 config:
709 |
710 | ```ruby
711 | Grape::OAuth2.configure do |config|
712 | # ...
713 |
714 | config.scopes_validator_class_name = 'CustomScopesValidator'
715 | end
716 | ```
717 |
718 | ## Custom token generator
719 |
720 | If you want to generate your own tokens for Access Tokens and Authorization Codes then you need to write your own generator:
721 |
722 | ```ruby
723 | class SomeTokenGenerator
724 | # @param payload [Hash]
725 | # Access Token payload (attributes before creation for example)
726 | #
727 | # @param options [Hash]
728 | # Options for Generator
729 | #
730 | def self.generate(payload = {}, options = {})
731 | # Returns a generated token string.
732 | end
733 | end
734 | ```
735 |
736 | And set it as a token generator class in the Grape::OAuth2 config:
737 |
738 | ```ruby
739 | Grape::OAuth2.configure do |config|
740 | # ...
741 |
742 | config.token_generator_class_name = 'SomeTokenGenerator'
743 | end
744 | ```
745 |
746 | ## Process token on Refresh (protect against Replay Attacks)
747 |
748 | If you want to do something with the original Access Token that was used with the Refresh Token Flow, then you need to
749 | setup `on_refresh` configuration option. By default `Grape::OAuth2` gem does nothing on token refresh and that
750 | option is set to `:nothing`. You can set it to the symbol (in that case `Access Token` instance must respond to it)
751 | or block. Look at the examples:
752 |
753 | ```ruby
754 | Grape::OAuth2.configure do |config|
755 | # ...
756 |
757 | config.on_refresh = :destroy # will call :destroy method (`refresh_token.destroy`)
758 | end
759 | ```
760 |
761 | ```ruby
762 | Grape::OAuth2.configure do |config|
763 | # ...
764 |
765 | config.on_refresh do |refresh_token|
766 | refresh_token.destroy
767 |
768 | MyAwesomeLogger.info("Token ##{refresh_token.id} was destroyed on refresh!")
769 | end
770 | end
771 | ```
772 |
773 | ## Errors (exceptions) handling
774 |
775 | You can add any exception class from the [`rack-oauth2`](https://github.com/nov/rack-oauth2) gem (like `Rack::OAuth2::Server::Resource::Bearer::Unauthorized`)
776 | to the `rescue_from` if you need to return some special response.
777 |
778 | Example:
779 |
780 | ```ruby
781 | module Twitter
782 | class API < Grape::API
783 | include Grape::OAuth2.api
784 |
785 | # ...
786 |
787 | rescue_from Rack::OAuth2::Server::Authorize::BadRequest do |e|
788 | error!({ status: e.status, description: e.description, error: e.error}, 400)
789 | end
790 | end
791 | end
792 | ```
793 |
794 | Do not forget to meet the OAuth 2.0 specification.
795 |
796 | ## Example App
797 |
798 | If you want to see the gem in action then you can look at [sample app](https://github.com/grape-oauth2/grape-oauth2-sample) (deployable to Heroku).
799 |
800 | Or you can take a look at the [sample applications](https://github.com/nbulaj/grape_oauth2/tree/master/spec/dummy) in the "_spec/dummy_" project directory.
801 |
802 | ## Contributing
803 |
804 | You are very welcome to help improve `grape_oauth2` if you have suggestions for features that other people can use.
805 |
806 | To contribute:
807 |
808 | 1. Fork the project.
809 | 1. Create your feature branch (`git checkout -b my-new-feature`).
810 | 1. Implement your feature or bug fix.
811 | 1. Add documentation for your feature or bug fix.
812 | 1. Add tests for your feature or bug fix.
813 | 1. Run `rake` to make sure all tests pass.
814 | 1. Commit your changes (`git commit -am 'Add new feature'`).
815 | 1. Push to the branch (`git push origin my-new-feature`).
816 | 1. Create new pull request.
817 |
818 | Thanks.
819 |
820 | ## License
821 |
822 | `Grape::OAuth2` gem is released under the [MIT License](http://www.opensource.org/licenses/MIT).
823 |
824 | Copyright (c) 2014-2016 Nikita Bulai (bulajnikita@gmail.com).
825 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/setup'
2 | require 'rspec/core/rake_task'
3 |
4 | desc 'Default: run specs.'
5 | task default: :spec
6 |
7 | RSpec::Core::RakeTask.new(:spec) do |config|
8 | config.verbose = false
9 | end
10 |
11 | Bundler::GemHelper.install_tasks
12 |
--------------------------------------------------------------------------------
/gemfiles/active_record.rb:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec path: '../'
4 |
5 | platforms :jruby do
6 | gem 'jdbc-sqlite3'
7 | end
8 |
9 | platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do
10 | gem 'sqlite3'
11 | end
12 |
13 | gem 'otr-activerecord'
14 |
15 | gem 'activerecord'
16 | gem 'bcrypt'
17 |
18 | group :test do
19 | gem 'rspec-rails', '~> 3.6'
20 | gem 'database_cleaner'
21 | gem 'rack-test', require: 'rack/test'
22 | gem 'coveralls', require: false
23 | end
24 |
25 | gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
26 |
--------------------------------------------------------------------------------
/gemfiles/mongoid.rb:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec path: '../'
4 |
5 | gem 'mongoid', '~> 6'
6 |
7 | group :test do
8 | gem 'rspec-rails', '~> 3.6'
9 | gem 'database_cleaner'
10 | gem 'rack-test', require: 'rack/test'
11 | gem 'coveralls', require: false
12 | end
13 |
14 | gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
15 |
--------------------------------------------------------------------------------
/gemfiles/sequel.rb:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec path: '../'
4 |
5 | platforms :jruby do
6 | gem 'jdbc-sqlite3'
7 | end
8 |
9 | platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do
10 | gem 'sqlite3'
11 | end
12 |
13 | gem 'bcrypt'
14 | gem 'sequel'
15 | gem 'sequel_secure_password'
16 |
17 | group :test do
18 | gem 'rspec-rails', '~> 3.6'
19 | gem 'database_cleaner'
20 | gem 'rack-test', require: 'rack/test'
21 | gem 'coveralls', require: false
22 | end
23 |
24 | gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
25 |
--------------------------------------------------------------------------------
/grape_oauth2.gemspec:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2 |
3 | require 'grape_oauth2/version'
4 |
5 | Gem::Specification.new do |gem|
6 | gem.name = 'grape_oauth2'
7 | gem.version = Grape::OAuth2.gem_version
8 | gem.authors = ['Nikita Bulai']
9 | gem.email = ['bulajnikita@gmail.com']
10 | gem.homepage = 'http://github.com/nbulaj/grape-oauth2'
11 | gem.summary = 'Grape OAuth2 provider'
12 | gem.description = 'Flexible, ORM-agnostic, fully customizable and simple OAuth2 provider support for Grape APIs'
13 | gem.license = 'MIT'
14 |
15 | gem.require_paths = %w[lib]
16 | gem.files = `git ls-files`.split($RS) - ['README.md', 'grape_oauth2.png', 'gemfiles', '.travis.yml', '.rubocop.yml']
17 | gem.test_files = Dir['spec/**/*']
18 |
19 | gem.required_ruby_version = '>= 2.2.2'
20 |
21 | gem.add_runtime_dependency 'grape', '~> 1.0', '>= 1.0'
22 | gem.add_runtime_dependency 'rack-oauth2', '~> 1.6.0', '>= 1.6.0'
23 |
24 | gem.add_development_dependency 'rspec-rails', '~> 3.6.0', '>= 3.6.0'
25 | gem.add_development_dependency 'database_cleaner', '~> 1.5.0', '>= 1.5.0'
26 | end
27 |
--------------------------------------------------------------------------------
/grape_oauth2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grape-oauth2/grape_oauth2/e55107db1230f5d356a35d2ecb45f16aa4319180/grape_oauth2.png
--------------------------------------------------------------------------------
/lib/grape_oauth2.rb:
--------------------------------------------------------------------------------
1 | require 'grape'
2 | require 'rack/oauth2'
3 |
4 | require 'grape_oauth2/version'
5 | require 'grape_oauth2/configuration/validation'
6 | require 'grape_oauth2/configuration/class_accessors'
7 | require 'grape_oauth2/configuration'
8 | require 'grape_oauth2/scopes'
9 | require 'grape_oauth2/unique_token'
10 |
11 | # NOTE: Extract to separate gems!!!
12 | # This gem should contains only the core functionality and all mixins
13 | # need to be moved to their own repos with their own tests.
14 |
15 | # Mixins
16 | if defined?(ActiveRecord::Base)
17 | require 'grape_oauth2/mixins/active_record/access_token'
18 | require 'grape_oauth2/mixins/active_record/access_grant'
19 | require 'grape_oauth2/mixins/active_record/client'
20 | end
21 |
22 | if defined?(Sequel::Model)
23 | require 'grape_oauth2/mixins/sequel/access_token'
24 | require 'grape_oauth2/mixins/sequel/access_grant'
25 | require 'grape_oauth2/mixins/sequel/client'
26 | end
27 |
28 | if defined?(Mongoid::Document)
29 | require 'grape_oauth2/mixins/mongoid/access_token'
30 | require 'grape_oauth2/mixins/mongoid/access_grant'
31 | require 'grape_oauth2/mixins/mongoid/client'
32 | end
33 |
34 | # Authorization Grants aka Flows (Strategies)
35 | require 'grape_oauth2/strategies/base'
36 | require 'grape_oauth2/strategies/authorization_code'
37 | require 'grape_oauth2/strategies/password'
38 | require 'grape_oauth2/strategies/client_credentials'
39 | require 'grape_oauth2/strategies/refresh_token'
40 |
41 | # Generators
42 | require 'grape_oauth2/generators/base'
43 | require 'grape_oauth2/generators/token'
44 | require 'grape_oauth2/generators/authorization'
45 |
46 | # Grape Helpers
47 | require 'grape_oauth2/helpers/access_token_helpers'
48 | require 'grape_oauth2/helpers/oauth_params'
49 |
50 | # Responses
51 | require 'grape_oauth2/responses/base'
52 | require 'grape_oauth2/responses/authorization'
53 | require 'grape_oauth2/responses/token'
54 |
55 | # Grape Endpoints
56 | require 'grape_oauth2/endpoints/token'
57 | require 'grape_oauth2/endpoints/authorize'
58 |
59 | # Use Grape namespace for the gem.
60 | module Grape
61 | # Main Grape::OAuth2 module.
62 | module OAuth2
63 | class << self
64 | # Grape::OAuth2 configuration.
65 | #
66 | # @return [Grape::OAuth2::Configuration]
67 | # configuration object
68 | #
69 | def config
70 | @config ||= Grape::OAuth2::Configuration.new
71 | end
72 |
73 | # Configures Grape::OAuth2.
74 | # Yields Grape::OAuth2::Configuration instance to the block.
75 | def configure
76 | yield config
77 | end
78 |
79 | # Validates Grape::OAuth2 configuration to be set correctly.
80 | def check_configuration!
81 | config.check!
82 | end
83 |
84 | # Grape::OAuth2 default middleware.
85 | def middleware
86 | [Rack::OAuth2::Server::Resource::Bearer, config.realm, config.token_authenticator]
87 | end
88 |
89 | # Method for injecting Grape::OAuth2 endpoints and helpers
90 | # into Grape API class. Automatically set required middleware,
91 | # OAuth2 helpers and mounts all (or configured) endpoints.
92 | #
93 | # @param endpoints [Array, Array] endpoints to add
94 | #
95 | def api(*endpoints)
96 | inject_to_api do |api|
97 | api.use(*Grape::OAuth2.middleware)
98 | api.helpers(Grape::OAuth2::Helpers::AccessTokenHelpers)
99 |
100 | (endpoints.presence || endpoints_mapping.keys).each do |name|
101 | endpoint = endpoints_mapping[name.to_sym]
102 | raise ArgumentError, "Unrecognized endpoint: #{endpoint}" if endpoint.nil?
103 |
104 | api.mount(endpoint)
105 | end
106 | end
107 | end
108 |
109 | private
110 |
111 | def endpoints_mapping
112 | {
113 | token: ::Grape::OAuth2::Endpoints::Token,
114 | authorize: ::Grape::OAuth2::Endpoints::Authorize
115 | }
116 | end
117 |
118 | def inject_to_api(&_block)
119 | raise ArgumentError, 'block must be specified!' unless block_given?
120 |
121 | Module.new do |mod|
122 | mod.define_singleton_method :included do |base|
123 | yield base
124 | end
125 | end
126 | end
127 | end
128 | end
129 | end
130 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/configuration.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | # Grape::OAuth2 configuration class.
4 | # Contains default or customized options that would be used
5 | # in OAuth2 endpoints and helpers.
6 | class Configuration
7 | # Default Grape::OAuth2 configuration error class.
8 | Error = Class.new(StandardError)
9 | # Grape::OAuth2 configuration error for missing API required for OAuth2 classes.
10 | APIMissing = Class.new(Error)
11 |
12 | include Validation
13 | include ClassAccessors
14 |
15 | # Default Access Token TTL (in seconds)
16 | DEFAULT_TOKEN_LIFETIME = 7200
17 | # Default Authorization Code TTL ()in seconds)
18 | DEFAULT_CODE_LIFETIME = 1800
19 |
20 | # Default realm value
21 | DEFAULT_REALM = 'OAuth 2.0'.freeze
22 |
23 | # Currently supported (by the gem) OAuth2 grant types
24 | SUPPORTED_GRANT_TYPES = %w[password client_credentials refresh_token].freeze
25 |
26 | # The names of the classes that represents OAuth2 roles
27 | #
28 | # @return [String] class name
29 | #
30 | attr_accessor :access_token_class_name, :access_grant_class_name,
31 | :client_class_name, :resource_owner_class_name
32 |
33 | # Class name for the OAuth2 helper class that validates requested scopes against Access Token scopes
34 | #
35 | # @return [String] scopes validator class name
36 | #
37 | attr_accessor :scopes_validator_class_name
38 |
39 | # Class name for the OAuth2 helper class that generates unique token values
40 | #
41 | # @return [String] token generator class name
42 | #
43 | attr_accessor :token_generator_class_name
44 |
45 | # OAuth2 grant types (flows) allowed to be processed
46 | #
47 | # @return [Array] grant types
48 | #
49 | attr_accessor :allowed_grant_types
50 |
51 | # Access Token and Authorization Code lifetime in seconds
52 | attr_accessor :authorization_code_lifetime, :access_token_lifetime
53 |
54 | # Specifies whether to generate a Refresh Token when creating an Access Token
55 | #
56 | # @return [Boolean] true if need to generate refresh token, false in other case
57 | #
58 | attr_accessor :issue_refresh_token
59 |
60 | # Realm value
61 | #
62 | # @return [String] realm
63 | #
64 | attr_accessor :realm
65 |
66 | # Access Token authenticator block option for customization
67 | attr_accessor :token_authenticator
68 |
69 | # Callback that would be invoked during processing of Refresh Token request for
70 | # the original Access Token found by token value
71 | attr_accessor :on_refresh
72 |
73 | def initialize
74 | reset!
75 | end
76 |
77 | # Default Access Token authenticator block.
78 | # Validates token value passed with the request params.
79 | def default_token_authenticator
80 | lambda do |request|
81 | access_token_class.authenticate(request.access_token) || request.invalid_token!
82 | end
83 | end
84 |
85 | # Accessor for Access Token authenticator block. Set it to proc
86 | # if called with block or returns current value of the accessor.
87 | def token_authenticator(&block)
88 | if block_given?
89 | instance_variable_set(:'@token_authenticator', block)
90 | else
91 | instance_variable_get(:'@token_authenticator')
92 | end
93 | end
94 |
95 | # Accessor for on_refresh callback. Set callback proc
96 | # if called with block or returns current value of the accessor.
97 | def on_refresh(&block)
98 | if block_given?
99 | instance_variable_set(:'@on_refresh', block)
100 | else
101 | instance_variable_get(:'@on_refresh')
102 | end
103 | end
104 |
105 | # Indicates if on_refresh callback can be invoked.
106 | #
107 | # @return [Boolean]
108 | # true if callback can be invoked and false in other cases
109 | #
110 | def on_refresh_runnable?
111 | !on_refresh.nil? && on_refresh != :nothing
112 | end
113 |
114 | # Reset configuration to default options values.
115 | def reset!
116 | initialize_classes
117 | initialize_authenticators
118 |
119 | self.access_token_lifetime = DEFAULT_TOKEN_LIFETIME
120 | self.authorization_code_lifetime = DEFAULT_CODE_LIFETIME
121 | self.allowed_grant_types = %w[password client_credentials]
122 |
123 | self.issue_refresh_token = false
124 | self.on_refresh = :nothing
125 |
126 | self.realm = DEFAULT_REALM
127 | end
128 |
129 | private
130 |
131 | # Sets OAuth2 helpers classes to gem defaults.
132 | def initialize_classes
133 | self.scopes_validator_class_name = Grape::OAuth2::Scopes.name
134 | self.token_generator_class_name = Grape::OAuth2::UniqueToken.name
135 | end
136 |
137 | # Sets authenticators to gem defaults.
138 | def initialize_authenticators
139 | self.token_authenticator = default_token_authenticator
140 | end
141 | end
142 | end
143 | end
144 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/configuration/class_accessors.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | # Grape::OAuth2 accessors for configured classes.
4 | module ClassAccessors
5 | # Returns Access Token class by configured name
6 | def access_token_class
7 | @_access_token_class ||= access_token_class_name.constantize
8 | end
9 |
10 | # Returns Resource Owner class by configured name
11 | def resource_owner_class
12 | @_resource_owner_class ||= resource_owner_class_name.constantize
13 | end
14 |
15 | # Returns Client class by configured name
16 | def client_class
17 | @_client_class ||= client_class_name.constantize
18 | end
19 |
20 | # Returns Access Grant class by configured name
21 | def access_grant_class
22 | @_access_grant_class ||= access_grant_class_name.constantize
23 | end
24 |
25 | # Returns Scopes Validator class by configured name
26 | def scopes_validator
27 | scopes_validator_class_name.constantize
28 | end
29 |
30 | # Returns Token Generator class by configured name
31 | def token_generator
32 | token_generator_class_name.constantize
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/configuration/validation.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | class Configuration
4 | # Validates Grape::OAuth2 configuration.
5 | module Validation
6 | # Checks configuration to be set correctly
7 | # (required classes must be defined and implement specific set of API).
8 | def check!
9 | check_required_classes!
10 | check_required_classes_api!
11 | end
12 |
13 | private
14 |
15 | # API mapping.
16 | # Classes, that represents OAuth2 roles, must have described methods.
17 | REQUIRED_CLASSES_API = {
18 | access_token_class: {
19 | class_methods: %i[authenticate create_for],
20 | instance_methods: %i[expired? revoked? revoke! to_bearer_token]
21 | },
22 | client_class: {
23 | class_methods: %i[authenticate]
24 | },
25 | token_generator: {
26 | class_methods: %i[generate]
27 | },
28 | scopes_validator: {
29 | instance_methods: %i[valid_for?]
30 | }
31 | }.freeze
32 |
33 | # Validates that required classes defined.
34 | def check_required_classes!
35 | REQUIRED_CLASSES_API.keys.each do |klass|
36 | begin
37 | object = send(klass)
38 | rescue NoMethodError
39 | raise Error, "'#{klass}' must be defined!" if object.nil? || !defined?(object)
40 | end
41 | end
42 | end
43 |
44 | # Validates that required classes have all the API.
45 | def check_required_classes_api!
46 | REQUIRED_CLASSES_API.each do |klass, api_methods|
47 | check_class_methods(klass, api_methods[:class_methods])
48 | check_instance_methods(klass, api_methods[:instance_methods])
49 | end
50 | end
51 |
52 | # Validates that required classes have required class methods.
53 | def check_class_methods(klass, required_methods)
54 | (required_methods || []).each do |method|
55 | method_exist = send(klass).respond_to?(method)
56 | raise APIMissing, "Class method '#{method}' must be defined for the '#{klass}'!" unless method_exist
57 | end
58 | end
59 |
60 | # Validates that required classes have required instance methods.
61 | def check_instance_methods(klass, required_methods)
62 | (required_methods || []).each do |method|
63 | unless send(klass).method_defined?(method)
64 | raise APIMissing, "Instance method '#{method}' must be defined for the '#{klass}'!"
65 | end
66 | end
67 | end
68 | end
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/endpoints/authorize.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | # Grape::OAuth2 endpoints namespace
4 | module Endpoints
5 | # OAuth2 Grape authorization endpoint.
6 | class Authorize < ::Grape::API
7 | helpers Grape::OAuth2::Helpers::OAuthParams
8 |
9 | namespace :oauth do
10 | desc 'OAuth 2.0 Authorization Endpoint'
11 |
12 | params do
13 | use :oauth_authorization_params
14 | end
15 |
16 | post :authorize do
17 | response = Grape::OAuth2::Generators::Authorization.generate_for(env)
18 |
19 | # Status
20 | status response.status
21 |
22 | # Headers
23 | response.headers.each do |key, value|
24 | header key, value
25 | end
26 |
27 | # Body
28 | body response.body
29 | end
30 | end
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/endpoints/token.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | # Grape::OAuth2 endpoints namespace
4 | module Endpoints
5 | # OAuth2 Grape token endpoint.
6 | class Token < ::Grape::API
7 | helpers Grape::OAuth2::Helpers::OAuthParams
8 |
9 | namespace :oauth do
10 | # @see https://tools.ietf.org/html/rfc6749#section-3.2
11 | #
12 | desc 'OAuth 2.0 Token Endpoint'
13 |
14 | params do
15 | use :oauth_token_params
16 | end
17 |
18 | post :token do
19 | token_response = Grape::OAuth2::Generators::Token.generate_for(env)
20 |
21 | # Status
22 | status token_response.status
23 |
24 | # Headers
25 | token_response.headers.each do |key, value|
26 | header key, value
27 | end
28 |
29 | # Body
30 | body token_response.body
31 | end
32 |
33 | desc 'OAuth 2.0 Token Revocation'
34 |
35 | params do
36 | use :oauth_token_revocation_params
37 | end
38 |
39 | post :revoke do
40 | access_token = Grape::OAuth2.config.access_token_class.authenticate(params[:token],
41 | type: params[:token_type_hint])
42 |
43 | if access_token
44 | if access_token.client
45 | request = Rack::OAuth2::Server::Token::Request.new(env)
46 |
47 | # The authorization server, if applicable, first authenticates the client
48 | # and checks its ownership of the provided token.
49 | client = Grape::OAuth2::Strategies::Base.authenticate_client(request)
50 | request.invalid_client! if client.nil?
51 |
52 | access_token.revoke! if client && client == access_token.client
53 | else
54 | # Access token is public
55 | access_token.revoke!
56 | end
57 | end
58 |
59 | # The authorization server responds with HTTP status code 200 if the token
60 | # has been revoked successfully or if the client submitted an invalid
61 | # token.
62 | #
63 | # @see https://tools.ietf.org/html/rfc7009#section-2.2 Revocation Response
64 | #
65 | status 200
66 | {}
67 | end
68 | end
69 | end
70 | end
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/gem_version.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | # Grape::OAuth2 version.
4 | # @return [Gem::Version] version of the gem
5 | #
6 | def self.gem_version
7 | Gem::Version.new VERSION::STRING
8 | end
9 |
10 | # Grape::OAuth2 semantic versioning module.
11 | # Contains detailed info about gem version.
12 | module VERSION
13 | # Major version of the gem
14 | MAJOR = 0
15 | # Minor version of the gem
16 | MINOR = 2
17 | # Tiny version of the gem
18 | TINY = 0
19 |
20 | # Full gem version string
21 | STRING = [MAJOR, MINOR, TINY].compact.join('.')
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/generators/authorization.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | module Generators
4 | # OAuth2 Authorization generator class.
5 | # Processes the request and builds the response.
6 | class Authorization < Base
7 | class << self
8 | # Generates Authorization Response based on the request.
9 | #
10 | # @return [Grape::OAuth2::Responses::Authorization] response
11 | #
12 | def generate_for(env, &_block)
13 | authorization = Rack::OAuth2::Server::Authorize.new do |request, response|
14 | if block_given?
15 | yield request, response
16 | else
17 | execute_default(request, response)
18 | end
19 | end
20 |
21 | Grape::OAuth2::Responses::Authorization.new(authorization.call(env))
22 | rescue Rack::OAuth2::Server::Authorize::BadRequest => error
23 | error_response(error)
24 | end
25 |
26 | private
27 |
28 | def error_response(error)
29 | response = Rack::Response.new
30 | response.status = error.status
31 | response.header['Content-Type'] = 'application/json'
32 | response.write(JSON.dump(Rack::OAuth2::Util.compact_hash(error.protocol_params)))
33 |
34 | Grape::OAuth2::Responses::Authorization.new(response.finish)
35 | end
36 |
37 | def execute_default(request, response)
38 | Grape::OAuth2::Strategies::AuthorizationCode.process(request, response)
39 | end
40 | end
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/generators/base.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | module Generators
4 | # Base class for Grape::OAuth2 generators.
5 | # Grape::OAuth2 generators processes the requests and
6 | # generates responses with Access Token or Authorization Code.
7 | class Base
8 | class << self
9 | # Allowed grant types from the Grape::OAuth2 configuration.
10 | #
11 | # @return [Array]
12 | # allowed grant types
13 | #
14 | def allowed_grants
15 | config.allowed_grant_types
16 | end
17 |
18 | # Short getter for Grape::OAuth2 configuration.
19 | def config
20 | Grape::OAuth2.config
21 | end
22 | end
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/generators/token.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | module Generators
4 | # OAuth2 Token generator class.
5 | # Processes the request by required Grant Type and builds the response.
6 | class Token < Base
7 | # Grant type => OAuth2 strategy class
8 | STRATEGY_CLASSES = {
9 | password: Grape::OAuth2::Strategies::Password,
10 | client_credentials: Grape::OAuth2::Strategies::ClientCredentials,
11 | refresh_token: Grape::OAuth2::Strategies::RefreshToken
12 | }.freeze
13 |
14 | class << self
15 | # Generates Token Response based on the request.
16 | #
17 | # @return [Grape::OAuth2::Responses::Token] response
18 | #
19 | def generate_for(env, &_block)
20 | token = Rack::OAuth2::Server::Token.new do |request, response|
21 | request.unsupported_grant_type! unless allowed_grants.include?(request.grant_type.to_s)
22 |
23 | if block_given?
24 | yield request, response
25 | else
26 | execute_default(request, response)
27 | end
28 | end
29 |
30 | Grape::OAuth2::Responses::Token.new(token.call(env))
31 | end
32 |
33 | protected
34 |
35 | # Runs default Grape::OAuth2 functionality for Token endpoint.
36 | # In common it authenticates client (or/and any other objects) and
37 | # grants the Access Token or Auth Code.
38 | #
39 | # @param request [Rack::Request] request object
40 | # @param response [Rack::Response] response object
41 | #
42 | def execute_default(request, response)
43 | strategy = find_strategy(request.grant_type) || request.invalid_grant!
44 | response.access_token = strategy.process(request)
45 | end
46 |
47 | # Returns Grape::OAuth2 strategy class by Grant Type.
48 | #
49 | # @param grant_type [Symbol]
50 | # grant type value
51 | #
52 | # @return [Password, ClientCredentials, RefreshToken]
53 | # strategy class
54 | #
55 | def find_strategy(grant_type)
56 | STRATEGY_CLASSES[grant_type]
57 | end
58 | end
59 | end
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/helpers/access_token_helpers.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | module Helpers
4 | # Set of Grape OAuth2 helpers.
5 | module AccessTokenHelpers
6 | extend ::Grape::API::Helpers
7 |
8 | # Adds OAuth2 Access Token protection for Grape routes.
9 | #
10 | # @param scopes [Array]
11 | # set of scopes required to access the endpoint
12 | #
13 | # @raise [Rack::OAuth2::Server::Resource::Bearer::Unauthorized]
14 | # invalid Access Token value
15 | # @raise [Rack::OAuth2::Server::Resource::Bearer::Forbidden]
16 | # Access Token expired, revoked or does't have required scopes
17 | #
18 | def access_token_required!(*scopes)
19 | endpoint_scopes = env['api.endpoint'].options[:route_options][:scopes]
20 | required_scopes = endpoint_scopes.presence || scopes
21 |
22 | raise Rack::OAuth2::Server::Resource::Bearer::Unauthorized if current_access_token.nil?
23 | raise Rack::OAuth2::Server::Resource::Bearer::Forbidden unless valid_access_token?(required_scopes)
24 | end
25 |
26 | # Returns Resource Owner from the Access Token
27 | # found by access_token value passed with the request.
28 | def current_resource_owner
29 | @_current_resource_owner ||= current_access_token.resource_owner
30 | end
31 |
32 | # Returns Access Token instance found by
33 | # access_token value passed with the request.
34 | def current_access_token
35 | @_current_access_token ||= request.env[Rack::OAuth2::Server::Resource::ACCESS_TOKEN]
36 | end
37 |
38 | # Validate current access token not to be expired or revoked
39 | # and has all the requested scopes.
40 | #
41 | # @return [Boolean]
42 | # true if current Access Token not expired, not revoked and scopes match
43 | # false in other cases.
44 | #
45 | def valid_access_token?(scopes)
46 | !current_access_token.revoked? && !current_access_token.expired? &&
47 | Grape::OAuth2.config.scopes_validator.new(scopes).valid_for?(current_access_token)
48 | end
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/helpers/oauth_params.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | module Helpers
4 | # Grape Helper object for OAuth2 requests params.
5 | # Used fin default Grape::OAuth2 gem endpoints and can be used
6 | # for custom one.
7 | module OAuthParams
8 | extend ::Grape::API::Helpers
9 |
10 | # Params are optional in order to process them correctly in accordance
11 | # with the RFC 6749 (invalid_client, unsupported_grant_type, etc.)
12 | params :oauth_token_params do
13 | optional :grant_type, type: String, desc: 'Grant type'
14 | optional :client_id, type: String, desc: 'Client ID'
15 | optional :client_secret, type: String, desc: 'Client secret'
16 | optional :refresh_token, type: String, desc: 'Refresh Token'
17 | end
18 |
19 | # Params for authorization request.
20 | # @see https://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-4.1.1 Authorization Request
21 | params :oauth_authorization_params do
22 | optional :response_type, type: String, desc: 'Response type'
23 | optional :client_id, type: String, desc: 'Client ID'
24 | optional :redirect_uri, type: String, desc: 'Redirect URI'
25 | optional :scope, type: String, desc: 'Authorization scopes'
26 | optional :state, type: String, desc: 'State'
27 | end
28 |
29 | # Params for token revocation.
30 | # @see https://tools.ietf.org/html/rfc7009#section-2.1 OAuth 2.0 Token Revocation
31 | params :oauth_token_revocation_params do
32 | requires :token, type: String, desc: 'The token that the client wants to get revoked'
33 | optional :token_type_hint, type: String,
34 | values: %w[access_token refresh_token],
35 | default: 'access_token',
36 | desc: 'A hint about the type of the token submitted for revocation'
37 | end
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/mixins/active_record/access_grant.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | module ActiveRecord
4 | # Grape::OAuth2 Authorization Grant role mixin for ActiveRecord.
5 | # Includes all the required API, associations, validations and callbacks.
6 | module AccessGrant
7 | extend ActiveSupport::Concern
8 |
9 | included do
10 | belongs_to :client, class_name: Grape::OAuth2.config.client_class_name,
11 | foreign_key: :client_id
12 |
13 | belongs_to :resource_owner, class_name: Grape::OAuth2.config.resource_owner_class_name,
14 | foreign_key: :resource_owner_id
15 |
16 | # resource_owner_id - required!
17 | validates :client_id, :redirect_uri, presence: true
18 | validates :token, presence: true, uniqueness: true
19 |
20 | before_validation :generate_token, on: :create
21 | before_validation :setup_expiration, on: :create
22 |
23 | class << self
24 | def create_for(client, resource_owner, redirect_uri, scopes = nil)
25 | create(
26 | client_id: client.id,
27 | resource_owner_id: resource_owner && resource_owner.id,
28 | redirect_uri: redirect_uri,
29 | scopes: scopes.to_s
30 | )
31 | end
32 | end
33 |
34 | protected
35 |
36 | def generate_token
37 | self.token = Grape::OAuth2.config.token_generator.generate(attributes)
38 | end
39 |
40 | def setup_expiration
41 | self.expires_at = Time.now.utc + Grape::OAuth2.config.authorization_code_lifetime if expires_at.nil?
42 | end
43 | end
44 | end
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/mixins/active_record/access_token.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | module ActiveRecord
4 | # Grape::OAuth2 Access Token role mixin for ActiveRecord.
5 | # Includes all the required API, associations, validations and callbacks.
6 | module AccessToken
7 | extend ActiveSupport::Concern
8 |
9 | included do
10 | belongs_to :client, class_name: Grape::OAuth2.config.client_class_name,
11 | foreign_key: :client_id
12 |
13 | belongs_to :resource_owner, class_name: Grape::OAuth2.config.resource_owner_class_name,
14 | foreign_key: :resource_owner_id
15 |
16 | validates :token, presence: true, uniqueness: true
17 |
18 | before_validation :setup_expiration, on: :create
19 | before_validation :generate_tokens, on: :create
20 |
21 | class << self
22 | def create_for(client, resource_owner, scopes = nil)
23 | create(
24 | client: client,
25 | resource_owner: resource_owner,
26 | scopes: scopes.to_s
27 | )
28 | end
29 |
30 | def authenticate(token, type: :access_token)
31 | if type && type.to_sym == :refresh_token
32 | find_by(refresh_token: token.to_s)
33 | else
34 | find_by(token: token.to_s)
35 | end
36 | end
37 | end
38 |
39 | def expired?
40 | !expires_at.nil? && Time.now.utc > expires_at
41 | end
42 |
43 | def revoked?
44 | !revoked_at.nil? && revoked_at <= Time.now.utc
45 | end
46 |
47 | def revoke!(revoked_at = Time.now)
48 | update_column :revoked_at, revoked_at.utc
49 | end
50 |
51 | def to_bearer_token
52 | {
53 | access_token: token,
54 | expires_in: expires_at && Grape::OAuth2.config.access_token_lifetime.to_i,
55 | refresh_token: refresh_token,
56 | scope: scopes
57 | }
58 | end
59 |
60 | protected
61 |
62 | def generate_tokens
63 | self.token = Grape::OAuth2.config.token_generator.generate(attributes) if token.blank?
64 | self.refresh_token = Grape::OAuth2::UniqueToken.generate if Grape::OAuth2.config.issue_refresh_token
65 | end
66 |
67 | def setup_expiration
68 | expires_in = Grape::OAuth2.config.access_token_lifetime
69 | self.expires_at = Time.now + expires_in if expires_at.nil? && !expires_in.nil?
70 | end
71 | end
72 | end
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/mixins/active_record/client.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | module ActiveRecord
4 | # Grape::OAuth2 Client role mixin for ActiveRecord.
5 | # Includes all the required API, associations, validations and callbacks.
6 | module Client
7 | extend ActiveSupport::Concern
8 |
9 | included do
10 | has_many :access_tokens, class_name: Grape::OAuth2.config.access_token_class_name,
11 | foreign_key: :client_id, dependent: :delete_all
12 |
13 | validates :key, :secret, presence: true
14 | validates :key, uniqueness: true
15 |
16 | before_validation :generate_keys, on: :create
17 |
18 | def self.authenticate(key, secret = nil)
19 | if secret.nil?
20 | find_by(key: key)
21 | else
22 | find_by(key: key, secret: secret)
23 | end
24 | end
25 |
26 | protected
27 |
28 | def generate_keys
29 | self.key = Grape::OAuth2::UniqueToken.generate if key.blank?
30 | self.secret = Grape::OAuth2::UniqueToken.generate if secret.blank?
31 | end
32 | end
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/mixins/mongoid/access_grant.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | module Mongoid
4 | # Grape::OAuth2 Authorization Grant role mixin for Mongoid ORM.
5 | # Includes all the required API, associations, validations and callbacks.
6 | module AccessGrant
7 | extend ActiveSupport::Concern
8 |
9 | included do
10 | include ::Mongoid::Document
11 | include ::Mongoid::Timestamps
12 |
13 | field :resource_owner_id, type: BSON::ObjectId
14 | field :client_id, type: BSON::ObjectId
15 |
16 | field :token, type: String
17 | field :scopes, type: String
18 | field :redirect_uri, type: String
19 |
20 | field :expires_at, type: DateTime
21 |
22 | belongs_to :client, class_name: Grape::OAuth2.config.client_class_name,
23 | foreign_key: :client_id
24 |
25 | belongs_to :resource_owner, class_name: Grape::OAuth2.config.resource_owner_class_name,
26 | foreign_key: :resource_owner_id, optional: true # required!
27 |
28 | before_validation :generate_token, on: :create
29 | before_validation :setup_expiration, on: :create
30 |
31 | index({ token: 1 }, unique: true)
32 | index({ refresh_token: 1 }, unique: true, sparse: true)
33 |
34 | class << self
35 | def create_for(client, resource_owner, redirect_uri, scopes = nil)
36 | create(
37 | client_id: client.id,
38 | resource_owner_id: resource_owner && resource_owner.id,
39 | redirect_uri: redirect_uri,
40 | scopes: scopes.to_s
41 | )
42 | end
43 | end
44 |
45 | protected
46 |
47 | def generate_token
48 | self.token = Grape::OAuth2.config.token_generator.generate(attributes)
49 | end
50 |
51 | def setup_expiration
52 | self.expires_at = Time.now.utc + Grape::OAuth2.config.authorization_code_lifetime if expires_at.nil?
53 | end
54 | end
55 | end
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/mixins/mongoid/access_token.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | module Mongoid
4 | # Grape::OAuth2 Access Token role mixin for Mongoid ORM.
5 | # Includes all the required API, associations, validations and callbacks.
6 | module AccessToken
7 | extend ActiveSupport::Concern
8 |
9 | included do
10 | include ::Mongoid::Document
11 | include ::Mongoid::Timestamps
12 |
13 | field :resource_owner_id, type: BSON::ObjectId
14 | field :client_id, type: BSON::ObjectId
15 |
16 | belongs_to :client, class_name: Grape::OAuth2.config.client_class_name, foreign_key: :client_id, optional: true
17 | belongs_to :resource_owner, class_name: Grape::OAuth2.config.resource_owner_class_name, foreign_key: :resource_owner_id, optional: true
18 |
19 | field :token, type: String
20 | field :refresh_token, type: String
21 | field :scopes, type: String
22 |
23 | field :expires_at, type: DateTime
24 | field :revoked_at, type: DateTime
25 |
26 | index({ token: 1 }, unique: true)
27 | index({ refresh_token: 1 }, unique: true, sparse: true)
28 |
29 | before_validation :setup_expiration, on: :create
30 | before_validation :generate_tokens, on: :create
31 |
32 | validates :token, presence: true, uniqueness: true
33 |
34 | class << self
35 | def create_for(client, resource_owner, scopes = nil)
36 | create(
37 | client: client,
38 | resource_owner: resource_owner,
39 | scopes: scopes.to_s
40 | )
41 | end
42 |
43 | def authenticate(token, type: :access_token)
44 | if type && type.to_sym == :refresh_token
45 | find_by(refresh_token: token.to_s)
46 | else
47 | find_by(token: token.to_s)
48 | end
49 | end
50 | end
51 |
52 | def expired?
53 | !expires_at.nil? && Time.now.utc > expires_at
54 | end
55 |
56 | def revoked?
57 | !revoked_at.nil? && revoked_at <= Time.now.utc
58 | end
59 |
60 | def revoke!(revoked_at = Time.now)
61 | update_attribute :revoked_at, revoked_at.utc
62 | end
63 |
64 | def to_bearer_token
65 | {
66 | access_token: token,
67 | expires_in: expires_at && Grape::OAuth2.config.access_token_lifetime.to_i,
68 | refresh_token: refresh_token,
69 | scope: scopes
70 | }
71 | end
72 |
73 | protected
74 |
75 | def generate_tokens
76 | self.token = Grape::OAuth2.config.token_generator.generate(attributes) if token.blank?
77 | self.refresh_token = Grape::OAuth2::UniqueToken.generate if Grape::OAuth2.config.issue_refresh_token
78 | end
79 |
80 | def setup_expiration
81 | expires_in = Grape::OAuth2.config.access_token_lifetime
82 | self.expires_at = Time.now + expires_in if expires_at.nil? && !expires_in.nil?
83 | end
84 | end
85 | end
86 | end
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/mixins/mongoid/client.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | module Mongoid
4 | # Grape::OAuth2 Client role mixin for Mongoid ORM.
5 | # Includes all the required API, associations, validations and callbacks.
6 | module Client
7 | extend ActiveSupport::Concern
8 |
9 | included do
10 | include ::Mongoid::Document
11 | include ::Mongoid::Timestamps
12 |
13 | has_many :access_tokens, class_name: Grape::OAuth2.config.access_token_class_name,
14 | foreign_key: :client_id, dependent: :delete
15 |
16 | field :name, type: String
17 | field :key, type: String
18 | field :secret, type: String
19 | field :redirect_uri, type: String
20 |
21 | before_validation :generate_keys, on: :create
22 |
23 | validates :key, :secret, presence: true
24 | validates :key, uniqueness: true
25 |
26 | def self.authenticate(key, secret = nil)
27 | if secret.nil?
28 | find_by(key: key)
29 | else
30 | find_by(key: key, secret: secret)
31 | end
32 | end
33 |
34 | protected
35 |
36 | def generate_keys
37 | self.key = Grape::OAuth2::UniqueToken.generate if key.blank?
38 | self.secret = Grape::OAuth2::UniqueToken.generate if secret.blank?
39 | end
40 | end
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/mixins/sequel/access_grant.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | module Sequel
4 | # Grape::OAuth2 Authorization Grant role mixin for Sequel toolkit.
5 | # Includes all the required API, associations, validations and callbacks.
6 | module AccessGrant
7 | extend ActiveSupport::Concern
8 |
9 | included do
10 | plugin :validation_helpers
11 | plugin :timestamps
12 |
13 | many_to_one :client, class: Grape::OAuth2.config.client_class_name, key: :client_id
14 | many_to_one :resource_owner, class: Grape::OAuth2.config.resource_owner_class_name, key: :resource_owner_id
15 |
16 | def before_validation
17 | if new?
18 | generate_token
19 | setup_expiration
20 | end
21 |
22 | super
23 | end
24 |
25 | class << self
26 | def create_for(client, resource_owner, redirect_uri, scopes = nil)
27 | create(
28 | client_id: client.id,
29 | resource_owner_id: resource_owner && resource_owner.id,
30 | redirect_uri: redirect_uri,
31 | scopes: scopes.to_s
32 | )
33 | end
34 | end
35 |
36 | def validate
37 | super
38 | validates_presence %i[token client_id]
39 | validates_unique [:token]
40 | end
41 |
42 | def expired?
43 | expires_at && Time.now.utc > expires_at
44 | end
45 |
46 | def revoked?
47 | revoked_at && revoked_at <= Time.now.utc
48 | end
49 |
50 | def revoke!(revoked_at = Time.now)
51 | set(revoked_at: revoked_at.utc)
52 | save(columns: [:revoked_at], validate: false)
53 | end
54 |
55 | protected
56 |
57 | def generate_token
58 | self.token = Grape::OAuth2.config.token_generator.generate(values)
59 | end
60 |
61 | def setup_expiration
62 | self.expires_at = Time.now.utc + Grape::OAuth2.config.authorization_code_lifetime if expires_at.nil?
63 | end
64 | end
65 | end
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/mixins/sequel/access_token.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | module Sequel
4 | # Grape::OAuth2 Access Token role mixin for Sequel toolkit.
5 | # Includes all the required API, associations, validations and callbacks.
6 | module AccessToken
7 | extend ActiveSupport::Concern
8 |
9 | included do
10 | plugin :validation_helpers
11 | plugin :timestamps
12 |
13 | many_to_one :client, class: Grape::OAuth2.config.client_class_name, key: :client_id
14 | many_to_one :resource_owner, class: Grape::OAuth2.config.resource_owner_class_name, key: :resource_owner_id
15 |
16 | def before_validation
17 | if new?
18 | setup_expiration
19 | generate_tokens
20 | end
21 |
22 | super
23 | end
24 |
25 | def validate
26 | super
27 | validates_presence :token
28 | validates_unique :token
29 | end
30 |
31 | class << self
32 | def create_for(client, resource_owner, scopes = nil)
33 | create(
34 | client: client,
35 | resource_owner: resource_owner,
36 | scopes: scopes.to_s
37 | )
38 | end
39 |
40 | def authenticate(token, type: :access_token)
41 | if type && type.to_sym == :refresh_token
42 | first(refresh_token: token.to_s)
43 | else
44 | first(token: token.to_s)
45 | end
46 | end
47 | end
48 |
49 | def expired?
50 | !expires_at.nil? && Time.now.utc > expires_at.utc
51 | end
52 |
53 | def revoked?
54 | !revoked_at.nil? && revoked_at <= Time.now.utc
55 | end
56 |
57 | def revoke!(revoked_at = Time.now)
58 | set(revoked_at: revoked_at.utc)
59 | save(columns: [:revoked_at], validate: false)
60 | end
61 |
62 | def to_bearer_token
63 | {
64 | access_token: token,
65 | expires_in: expires_at && Grape::OAuth2.config.access_token_lifetime.to_i,
66 | refresh_token: refresh_token,
67 | scope: scopes
68 | }
69 | end
70 |
71 | protected
72 |
73 | def generate_tokens
74 | self.token = Grape::OAuth2.config.token_generator.generate(values) if token.blank?
75 | self.refresh_token = Grape::OAuth2::UniqueToken.generate if Grape::OAuth2.config.issue_refresh_token
76 | end
77 |
78 | def setup_expiration
79 | expires_in = Grape::OAuth2.config.access_token_lifetime
80 | self.expires_at = Time.now + expires_in if expires_at.nil? && !expires_in.nil?
81 | end
82 | end
83 | end
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/mixins/sequel/client.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | module Sequel
4 | # Grape::OAuth2 Client role mixin for Sequel toolkit.
5 | # Includes all the required API, associations, validations and callbacks.
6 | module Client
7 | extend ActiveSupport::Concern
8 |
9 | included do
10 | plugin :validation_helpers
11 | plugin :timestamps
12 | plugin :association_dependencies
13 |
14 | # Sequel 4.47 deprecated #set_allowed_columns
15 | if (::Sequel::MAJOR >= 4 && ::Sequel::MINOR >= 47) || ::Sequel::MAJOR >= 5
16 | plugin :whitelist_security
17 | end
18 |
19 | set_allowed_columns :name, :redirect_uri
20 |
21 | one_to_many :access_tokens, class: Grape::OAuth2.config.access_token_class_name, key: :client_id
22 |
23 | add_association_dependencies access_tokens: :delete
24 |
25 | def before_validation
26 | generate_keys if new?
27 | super
28 | end
29 |
30 | def validate
31 | super
32 | validates_presence %i[key secret]
33 | validates_unique :key
34 | end
35 |
36 | def self.authenticate(key, secret = nil)
37 | if secret.nil?
38 | find(key: key)
39 | else
40 | find(key: key, secret: secret)
41 | end
42 | end
43 |
44 | protected
45 |
46 | def generate_keys
47 | self.key = Grape::OAuth2::UniqueToken.generate if key.blank?
48 | self.secret = Grape::OAuth2::UniqueToken.generate if secret.blank?
49 | end
50 | end
51 | end
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/responses/authorization.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | # Grape::OAuth2 responses namespace.
4 | module Responses
5 | # Authorization response.
6 | class Authorization < Base
7 | # [IMPORTANT]: need to be implemented!
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/responses/base.rb:
--------------------------------------------------------------------------------
1 | module Grape
2 | module OAuth2
3 | # Grape::OAuth2 responses namespace.
4 | module Responses
5 | # Base class for Grape::OAuth2 endpoints responses.
6 | # Processes raw Rack Responses and contains helper methods.
7 | class Base
8 | # Raw Rack::Response to process
9 | #
10 | # @return [Array] Rack response
11 | #
12 | # @example
13 | # response = Grape::OAuth2::Responses::Base.new([200, {}, Rack::BodyProxy.new('Test')])
14 | # response.rack_response
15 | #
16 | # #=> [200, {}, Rack::BodyProxy.new('Test')]
17 | #
18 | attr_reader :rack_response
19 |
20 | # OAuth2 response class.
21 | #
22 | # @param rack_response [Array]
23 | # raw Rack::Response object
24 | #
25 | def initialize(rack_response)
26 | # Rack Body:
27 | # [Status Code, Headers, Body]
28 | @rack_response = rack_response
29 | end
30 |
31 | # Response status
32 | def status
33 | @rack_response[0]
34 | end
35 |
36 | # Response headers
37 | def headers
38 | @rack_response[1]
39 | end
40 |
41 | # Raw Rack body
42 | def raw_body
43 | @rack_response[2].body
44 | end
45 |
46 | # JSON-parsed body
47 | def body
48 | response_body = raw_body.first
49 | return {} if response_body.nil? || response_body.empty?
50 |
51 | JSON.parse(response_body)
52 | end
53 | end
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/responses/token.rb:
--------------------------------------------------------------------------------
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
11 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/scopes.rb:
--------------------------------------------------------------------------------
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] 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]
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
75 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/strategies/authorization_code.rb:
--------------------------------------------------------------------------------
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
39 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/strategies/base.rb:
--------------------------------------------------------------------------------
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
48 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/strategies/client_credentials.rb:
--------------------------------------------------------------------------------
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
21 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/strategies/password.rb:
--------------------------------------------------------------------------------
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
23 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/strategies/refresh_token.rb:
--------------------------------------------------------------------------------
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
48 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/unique_token.rb:
--------------------------------------------------------------------------------
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
21 |
--------------------------------------------------------------------------------
/lib/grape_oauth2/version.rb:
--------------------------------------------------------------------------------
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
15 |
--------------------------------------------------------------------------------
/spec/configuration/config_spec.rb:
--------------------------------------------------------------------------------
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?(*)
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
232 |
--------------------------------------------------------------------------------
/spec/configuration/version_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Grape::OAuth2 Version' do
4 | it 'has a version string' do
5 | expect(Grape::OAuth2::VERSION::STRING).to be_present
6 | end
7 |
8 | it 'returns version as an instance of Gem::Version' do
9 | expect(Grape::OAuth2.gem_version).to be_an_instance_of(Gem::Version)
10 | expect(Grape::OAuth2.version).to be_an_instance_of(Gem::Version)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/dummy/endpoints/custom_authorization.rb:
--------------------------------------------------------------------------------
1 | module Twitter
2 | module Endpoints
3 | class CustomAuthorization < ::Grape::API
4 | helpers Grape::OAuth2::Helpers::OAuthParams
5 |
6 | namespace :oauth do
7 | params do
8 | use :oauth_authorization_params
9 | end
10 |
11 | post :custom_authorize do
12 | response = Grape::OAuth2::Generators::Authorization.generate_for(env) do |request, response|
13 | request.unsupported_response_type!
14 | end
15 |
16 | # Status
17 | status response.status
18 |
19 | # Body
20 | body response.body
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/dummy/endpoints/custom_token.rb:
--------------------------------------------------------------------------------
1 | module Twitter
2 | module Endpoints
3 | class CustomToken < ::Grape::API
4 | helpers Grape::OAuth2::Helpers::OAuthParams
5 |
6 | namespace :oauth do
7 | params do
8 | use :oauth_token_params
9 | end
10 |
11 | post :custom_token do
12 | token_response = Grape::OAuth2::Generators::Token.generate_for(env) do |request, response|
13 | # Custom client authentication:
14 | client = Grape::OAuth2::Strategies::Base.authenticate_client(request)
15 | request.invalid_client! if client.nil? || client.name != 'Admin'
16 |
17 | resource_owner = Grape::OAuth2::Strategies::Base.authenticate_resource_owner(client, request)
18 | request.invalid_grant! if resource_owner.nil?
19 |
20 | token = AccessToken.create_for(client, resource_owner, request.scope.join(' '))
21 | response.access_token = Grape::OAuth2::Strategies::Base.expose_to_bearer_token(token)
22 | end
23 |
24 | status token_response.status
25 |
26 | token_response.headers.each do |key, value|
27 | header key, value
28 | end
29 |
30 | body token_response.body
31 | end
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/dummy/endpoints/status.rb:
--------------------------------------------------------------------------------
1 | module Twitter
2 | module Endpoints
3 | class Status < Grape::API
4 | before do
5 | access_token_required!
6 | end
7 |
8 | resources :status do
9 | get do
10 | { value: 'Nice day!', current_user: current_resource_owner.username }
11 | end
12 |
13 | get :single_scope, scopes: [:read] do
14 | { value: 'Access granted' }
15 | end
16 |
17 | get :multiple_scopes, scopes: [:read, :write] do
18 | access_token_required!
19 |
20 | { value: 'Access granted' }
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/dummy/grape_oauth2_config.rb:
--------------------------------------------------------------------------------
1 | # Common config across all the ORMs
2 | Grape::OAuth2.configure do |config|
3 | config.client_class_name = 'Application'
4 | config.access_token_class_name = 'AccessToken'
5 | config.resource_owner_class_name = 'User'
6 | config.access_grant_class_name = 'AccessCode'
7 |
8 | config.realm = 'Custom Realm'
9 |
10 | config.allowed_grant_types << 'refresh_token'
11 | end
12 |
--------------------------------------------------------------------------------
/spec/dummy/orm/active_record/app/config/db.rb:
--------------------------------------------------------------------------------
1 | OTR::ActiveRecord.configure_from_hash!(adapter: 'sqlite3', database: ':memory:')
2 |
3 | ::ActiveRecord::Base.default_timezone = :utc
4 | ::ActiveRecord::Base.logger = ENV['RAILS_ENV'] == 'test' ? nil : Logger.new(STDOUT)
5 |
6 | ::ActiveRecord::Migration.verbose = false
7 | load File.expand_path('../../../db/schema.rb', __FILE__)
8 |
--------------------------------------------------------------------------------
/spec/dummy/orm/active_record/app/models/access_code.rb:
--------------------------------------------------------------------------------
1 | class AccessCode < ApplicationRecord
2 | include Grape::OAuth2::ActiveRecord::AccessGrant
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/orm/active_record/app/models/access_token.rb:
--------------------------------------------------------------------------------
1 | class AccessToken < ApplicationRecord
2 | include Grape::OAuth2::ActiveRecord::AccessToken
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/orm/active_record/app/models/application.rb:
--------------------------------------------------------------------------------
1 | class Application < ApplicationRecord
2 | include Grape::OAuth2::ActiveRecord::Client
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/orm/active_record/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ::ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/orm/active_record/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ApplicationRecord
2 | has_secure_password
3 |
4 | def self.oauth_authenticate(_client, username, password)
5 | user = find_by(username: username)
6 | return if user.nil?
7 |
8 | user.authenticate(password)
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/dummy/orm/active_record/app/twitter.rb:
--------------------------------------------------------------------------------
1 | require 'otr-activerecord'
2 | require 'grape'
3 |
4 | require File.expand_path('../../../../../../lib/grape_oauth2', __FILE__)
5 |
6 | # Database
7 | load File.expand_path('../config/db.rb', __FILE__)
8 |
9 | # Grape::OAuth2 config
10 | load File.expand_path('../../../../grape_oauth2_config.rb', __FILE__)
11 |
12 | # Models
13 | require_relative 'models/application_record'
14 | require_relative 'models/access_token'
15 | require_relative 'models/access_code'
16 | require_relative 'models/application'
17 | require_relative 'models/user'
18 |
19 | # Twitter Endpoints
20 | require_relative '../../../endpoints/custom_token'
21 | require_relative '../../../endpoints/custom_authorization'
22 | require_relative '../../../endpoints/status'
23 |
24 | module Twitter
25 | class API < Grape::API
26 | version 'v1', using: :path
27 | format :json
28 | prefix :api
29 |
30 | include Grape::OAuth2.api
31 |
32 | mount Twitter::Endpoints::Status
33 | mount Twitter::Endpoints::CustomToken
34 | mount Twitter::Endpoints::CustomAuthorization
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/spec/dummy/orm/active_record/config.ru:
--------------------------------------------------------------------------------
1 | $:.unshift(File.dirname(__FILE__))
2 |
3 | require 'app/twitter'
4 |
5 | use OTR::ActiveRecord::ConnectionManagement
6 |
7 | run Twitter::API
8 |
--------------------------------------------------------------------------------
/spec/dummy/orm/active_record/db/schema.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define(version: 3) do
2 | create_table :users do |t|
3 | t.string :name
4 | t.string :username
5 | t.string :password_digest
6 | end
7 |
8 | create_table :applications do |t|
9 | t.string :name
10 | t.string :key
11 | t.string :secret
12 | t.string :redirect_uri
13 |
14 | t.timestamps null: false
15 | end
16 |
17 | add_index :applications, :key, unique: true
18 |
19 | create_table :access_tokens do |t|
20 | t.integer :resource_owner_id
21 | t.integer :client_id
22 |
23 | t.string :token, null: false
24 | t.string :refresh_token
25 | t.string :scopes
26 |
27 | t.datetime :expires_at
28 | t.datetime :revoked_at
29 | t.datetime :created_at, null: false
30 | end
31 |
32 | create_table :access_codes do |t|
33 | t.integer :resource_owner_id
34 | t.integer :client_id
35 |
36 | t.string :token, null: false
37 | t.string :redirect_uri
38 | t.string :scopes
39 |
40 | t.datetime :expires_at
41 | t.datetime :revoked_at
42 | t.datetime :created_at, null: false
43 | end
44 |
45 | add_index :access_tokens, :token, unique: true
46 | add_index :access_tokens, :resource_owner_id
47 | add_index :access_tokens, :client_id
48 | add_index :access_tokens, :refresh_token, unique: true
49 |
50 | add_index :access_codes, :token, unique: true
51 | add_index :access_codes, :resource_owner_id
52 | add_index :access_codes, :client_id
53 | end
54 |
--------------------------------------------------------------------------------
/spec/dummy/orm/mongoid/app/config/db.rb:
--------------------------------------------------------------------------------
1 | Mongoid.load!(File.expand_path('../mongoid.yml', __FILE__), :test)
2 |
3 | Mongoid.raise_not_found_error = false
4 |
5 | Mongoid.logger.level = Logger::ERROR
6 | Mongo::Logger.logger.level = Logger::ERROR
7 |
--------------------------------------------------------------------------------
/spec/dummy/orm/mongoid/app/config/mongoid.yml:
--------------------------------------------------------------------------------
1 | development:
2 | clients:
3 | default:
4 | database: mongoid-dev
5 | hosts:
6 | - localhost:27017
7 | options:
8 | raise_not_found_error: false
9 | write:
10 | w: 1
11 |
12 | test:
13 | clients:
14 | default:
15 | database: mongoid-test
16 | hosts:
17 | - localhost:27017
18 | options:
19 | raise_not_found_error: false
20 | write:
21 | w: 1
22 |
--------------------------------------------------------------------------------
/spec/dummy/orm/mongoid/app/models/access_code.rb:
--------------------------------------------------------------------------------
1 | class AccessCode
2 | include Grape::OAuth2::Mongoid::AccessGrant
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/orm/mongoid/app/models/access_token.rb:
--------------------------------------------------------------------------------
1 | class AccessToken
2 | include Grape::OAuth2::Mongoid::AccessToken
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/orm/mongoid/app/models/application.rb:
--------------------------------------------------------------------------------
1 | class Application
2 | include Grape::OAuth2::Mongoid::Client
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/orm/mongoid/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User
2 | include Mongoid::Document
3 | include Mongoid::Timestamps
4 |
5 | field :username, type: String
6 | field :password, type: String
7 |
8 | def self.oauth_authenticate(_client, username, password)
9 | find_by(username: username, password: password)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/dummy/orm/mongoid/app/twitter.rb:
--------------------------------------------------------------------------------
1 | require 'grape'
2 |
3 | require File.expand_path('../../../../../../lib/grape_oauth2', __FILE__)
4 |
5 | # Database
6 | load File.expand_path('../config/db.rb', __FILE__)
7 |
8 | # Grape::OAuth2 config
9 | load File.expand_path('../../../../grape_oauth2_config.rb', __FILE__)
10 |
11 | # Models
12 | require_relative 'models/access_token'
13 | require_relative 'models/access_code'
14 | require_relative 'models/application'
15 | require_relative 'models/user'
16 |
17 | # Twitter Endpoints
18 | require_relative '../../../endpoints/custom_token'
19 | require_relative '../../../endpoints/custom_authorization'
20 | require_relative '../../../endpoints/status'
21 |
22 | module Twitter
23 | class API < Grape::API
24 | version 'v1', using: :path
25 | format :json
26 | prefix :api
27 |
28 | include Grape::OAuth2.api
29 |
30 | mount Twitter::Endpoints::Status
31 | mount Twitter::Endpoints::CustomToken
32 | mount Twitter::Endpoints::CustomAuthorization
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/spec/dummy/orm/mongoid/config.ru:
--------------------------------------------------------------------------------
1 | $:.unshift(File.dirname(__FILE__))
2 |
3 | require 'app/twitter'
4 |
5 | run Twitter::API
6 |
--------------------------------------------------------------------------------
/spec/dummy/orm/sequel/app/config/db.rb:
--------------------------------------------------------------------------------
1 | load File.expand_path('../../../db/schema.rb', __FILE__)
2 |
--------------------------------------------------------------------------------
/spec/dummy/orm/sequel/app/models/access_code.rb:
--------------------------------------------------------------------------------
1 | class AccessCode < ::Sequel::Model
2 | set_dataset :access_codes
3 | include Grape::OAuth2::Sequel::AccessGrant
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/orm/sequel/app/models/access_token.rb:
--------------------------------------------------------------------------------
1 | class AccessToken < ::Sequel::Model
2 | set_dataset :access_tokens
3 | include Grape::OAuth2::Sequel::AccessToken
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/orm/sequel/app/models/application.rb:
--------------------------------------------------------------------------------
1 | class Application < ::Sequel::Model
2 | set_dataset :applications
3 | include Grape::OAuth2::Sequel::Client
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/orm/sequel/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/spec/dummy/orm/sequel/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ::Sequel::Model
2 | set_dataset :users
3 | plugin :secure_password, include_validations: false
4 |
5 | def self.oauth_authenticate(_client, username, password)
6 | user = find(username: username)
7 | return if user.nil?
8 |
9 | user.authenticate(password)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/dummy/orm/sequel/app/twitter.rb:
--------------------------------------------------------------------------------
1 | require 'grape'
2 |
3 | require File.expand_path('../../../../../../lib/grape_oauth2', __FILE__)
4 |
5 | # SQLite memory database
6 | DB = if defined?(JRUBY_VERSION)
7 | Sequel.connect('jdbc:sqlite::memory:')
8 | else
9 | Sequel.sqlite
10 | end
11 |
12 | # Database
13 | load File.expand_path('../config/db.rb', __FILE__)
14 |
15 | # Grape::OAuth2 config
16 | load File.expand_path('../../../../grape_oauth2_config.rb', __FILE__)
17 |
18 | # Models
19 | require_relative 'models/application_record'
20 | require_relative 'models/access_token'
21 | require_relative 'models/access_code'
22 | require_relative 'models/application'
23 | require_relative 'models/user'
24 |
25 | # Twitter Endpoints
26 | require_relative '../../../endpoints/custom_token'
27 | require_relative '../../../endpoints/custom_authorization'
28 | require_relative '../../../endpoints/status'
29 |
30 | module Twitter
31 | class API < Grape::API
32 | version 'v1', using: :path
33 | format :json
34 | prefix :api
35 |
36 | use *Grape::OAuth2.middleware
37 |
38 | helpers Grape::OAuth2::Helpers::AccessTokenHelpers
39 |
40 | mount Grape::OAuth2::Endpoints::Token
41 | mount Grape::OAuth2::Endpoints::Authorize
42 |
43 | mount Twitter::Endpoints::Status
44 | mount Twitter::Endpoints::CustomToken
45 | mount Twitter::Endpoints::CustomAuthorization
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/dummy/orm/sequel/config.ru:
--------------------------------------------------------------------------------
1 | $:.unshift(File.dirname(__FILE__))
2 |
3 | require 'app/twitter'
4 |
5 | run Twitter::API
6 |
--------------------------------------------------------------------------------
/spec/dummy/orm/sequel/db/schema.rb:
--------------------------------------------------------------------------------
1 | DB.create_table :applications do
2 | primary_key :id
3 |
4 | column :name, String, size: 255, null: false
5 | column :key, String, size: 255, null: false, index: { unique: true }
6 | column :secret, String, size: 255, null: false
7 |
8 |
9 | column :redirect_uri, String
10 |
11 | column :created_at, DateTime
12 | column :updated_at, DateTime
13 | end
14 |
15 | DB.create_table :access_tokens do
16 | primary_key :id
17 | column :client_id, Integer
18 | column :resource_owner_id, Integer, index: true
19 |
20 | column :token, String, size: 255, null: false, index: { unique: true }
21 |
22 | column :refresh_token, String, size: 255, index: { unique: true }
23 |
24 | column :expires_at, DateTime
25 | column :revoked_at, DateTime
26 | column :created_at, DateTime, null: false
27 | column :scopes, String, size: 255
28 | end
29 |
30 | DB.create_table :access_codes do
31 | primary_key :id
32 | column :client_id, Integer
33 | column :resource_owner_id, Integer, index: true
34 |
35 | column :token, String, size: 255, null: false, index: { unique: true }
36 | column :redirect_uri, String, size: 255, index: { unique: true }
37 |
38 | column :expires_at, DateTime
39 | column :created_at, DateTime, null: false
40 | column :scopes, String, size: 255
41 | end
42 |
43 | DB.create_table :users do
44 | primary_key :id
45 | column :name, String, size: 255
46 | column :username, String, size: 255
47 | column :created_at, DateTime
48 | column :updated_at, DateTime
49 | column :password_digest, String, size: 255
50 | end
51 |
--------------------------------------------------------------------------------
/spec/lib/scopes_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Grape::OAuth2::Scopes do
4 | context '#valid_for?' do
5 | it 'true when all the requested scopes included in the Access Token scopes' do
6 | scopes = described_class.new(['read', 'write'])
7 | access_token = double('AccessToken', scopes: 'read write')
8 |
9 | expect(scopes.valid_for?(access_token)).to be_truthy
10 | end
11 |
12 | it 'true when requested scopes are empty' do
13 | scopes = described_class.new([])
14 | access_token = double('AccessToken', scopes: 'read write')
15 |
16 | expect(scopes.valid_for?(access_token)).to be_truthy
17 | end
18 |
19 | it 'false when some of the requested scopes does not included in the Access Token scopes' do
20 | scopes = described_class.new(['read', 'write', 'destroy'])
21 | access_token = double('AccessToken', scopes: 'read write')
22 |
23 | expect(scopes.valid_for?(access_token)).to be_falsey
24 | end
25 | end
26 |
27 | context '#to_array' do
28 | let(:scopes) { described_class.new([]) }
29 |
30 | it 'converts the String scopes to an Array' do
31 | expect(scopes.send(:to_array, 'read write delete')).to eq(%w[read write delete])
32 | end
33 |
34 | it 'converts the object that responds to `to_a` to an Array' do
35 | custom_scopes = double('CustomScopes')
36 | allow(custom_scopes).to receive(:to_a).and_return(%w(read write))
37 |
38 | expect(scopes.send(:to_array, custom_scopes)).to eq(%w(read write))
39 | end
40 |
41 | it 'returns an Array of String values if Array was passed' do
42 | expect(scopes.send(:to_array, %w(read write delete))).to eq(%w[read write delete])
43 | expect(scopes.send(:to_array, %i(read write delete))).to eq(%w[read write delete])
44 | end
45 |
46 | it 'raises an error if scopes type is not supported' do
47 | expect { scopes.send(:to_array, :read) }.to raise_error(ArgumentError)
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/mixins/active_record/access_token_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Grape::OAuth2::ActiveRecord::AccessToken', skip_if: ENV['ORM'] != 'active_record' do
4 | let(:application) { Application.create(name: 'Test') }
5 | let(:user) { User.create(username: 'test', password: '123123') }
6 | let(:access_token) { AccessToken.create(client: application, resource_owner: user) }
7 |
8 | let(:token) { SecureRandom.hex(16) }
9 |
10 | describe 'validations' do
11 | it 'validate token uniqueness' do
12 | another_token = AccessToken.create(client: application)
13 | token = AccessToken.new(client: application, token: another_token.token)
14 |
15 | expect(token).not_to be_valid
16 | expect(token.errors.messages).to include(:token)
17 | end
18 | end
19 |
20 | describe '#to_bearer_token' do
21 | context 'config with refresh token' do
22 | before do
23 | Grape::OAuth2.config.issue_refresh_token = true
24 | end
25 |
26 | after do
27 | Grape::OAuth2.config.issue_refresh_token = false
28 | end
29 |
30 | it 'returns refresh token' do
31 | expect(access_token.to_bearer_token[:access_token]).not_to be_blank
32 | end
33 | end
34 |
35 | context 'config without refresh token' do
36 | before do
37 | Grape::OAuth2.configure do |config|
38 | config.issue_refresh_token = false
39 | end
40 | end
41 |
42 | it 'returns blank refresh token' do
43 | expect(access_token.to_bearer_token[:refresh_token]).to be_blank
44 | end
45 | end
46 | end
47 |
48 | describe '#authenticate' do
49 | it 'returns an instance if authenticated successfully' do
50 | access_token.token = token
51 | access_token.save
52 |
53 | expect(AccessToken.authenticate(token)).to eq(access_token)
54 | end
55 |
56 | it 'returns nil if authentication failed' do
57 | access_token.token = token
58 | access_token.save
59 |
60 | expect(AccessToken.authenticate("invalid-#{token}")).to be_nil
61 | end
62 |
63 | it 'returns an instance by refresh token' do
64 | refresh_token = SecureRandom.hex(6)
65 | token = AccessToken.create(client: application, refresh_token: refresh_token)
66 |
67 | expect(AccessToken.authenticate(refresh_token, type: :refresh_token)).to eq(token)
68 | expect(AccessToken.authenticate(refresh_token, type: 'refresh_token')).to eq(token)
69 | end
70 | end
71 |
72 | describe '#create_for?' do
73 | it 'creates a record only for Client' do
74 | token = AccessToken.create_for(application, nil)
75 |
76 | expect(token.client).not_to be_nil
77 | expect(token.resource_owner).to be_nil
78 | end
79 |
80 | it 'creates a record for Client and Resource Owner' do
81 | token = AccessToken.create_for(application, user)
82 |
83 | expect(token.client).to eq(application)
84 | expect(token.resource_owner).to eq(user)
85 | end
86 |
87 | it 'creates a record with scopes' do
88 | scopes = 'write read'
89 | token = AccessToken.create_for(application, user, scopes)
90 |
91 | expect(token.client).to eq(application)
92 | expect(token.resource_owner).to eq(user)
93 | expect(token.scopes).to eq(scopes)
94 | end
95 | end
96 |
97 | describe '#expired?' do
98 | it 'return false if expires_at nil' do
99 | access_token.update_column(:expires_at, nil)
100 |
101 | expect(access_token.expired?).to be_falsey
102 | end
103 |
104 | it 'return false if expires_at < Time.now' do
105 | expect(access_token.expired?).to be_falsey
106 | end
107 |
108 | it 'return false if expires_at > Time.now' do
109 | expired_at = Time.now.utc - Grape::OAuth2.config.access_token_lifetime + 1
110 | access_token.update_column(:expires_at, expired_at)
111 |
112 | expect(access_token.expired?).to be_truthy
113 | end
114 | end
115 |
116 | describe '#revoked?' do
117 | it 'return false if revoked_at nil' do
118 | access_token.update_column(:revoked_at, nil)
119 |
120 | expect(access_token.revoked?).to be_falsey
121 | end
122 |
123 | it 'return false if revoked_at present' do
124 | access_token.update_column(:revoked_at, Time.now.utc)
125 | expect(access_token.revoked?).to be_truthy
126 | end
127 | end
128 |
129 | describe '#revoke!' do
130 | it 'update :revoked_at attribute' do
131 | expect { access_token.revoke! }.to change { access_token.revoked? }.from(false).to(true)
132 | end
133 |
134 | it 'update :revoked_at attribute with custom value' do
135 | custom_time = Time.now - 7200
136 | access_token.revoke!(custom_time)
137 |
138 | expect(access_token.revoked_at).to eq(custom_time.utc)
139 | end
140 | end
141 |
142 | describe 'token generation' do
143 | it 'generates a new token before saving if token is blank' do
144 | token = AccessToken.new(client: application, resource_owner: user)
145 |
146 | expect(token.token).to be_blank
147 |
148 | token.save
149 |
150 | expect(token.token).not_to be_blank
151 | end
152 |
153 | it 'does not change token value on saving if token is present' do
154 | token = AccessToken.new(client: application, resource_owner: user, token: 'abcdef')
155 |
156 | expect(token.token).not_to be_blank
157 |
158 | token.save
159 |
160 | expect(token.token).to eq('abcdef')
161 | end
162 | end
163 |
164 | describe 'expiration' do
165 | it 'set to nil if configuration option set to nil' do
166 | Grape::OAuth2.config.access_token_lifetime = nil
167 |
168 | token = AccessToken.create(client: application, resource_owner: user)
169 | expect(token.expires_at).to be_nil
170 |
171 | Grape::OAuth2.config.access_token_lifetime = Grape::OAuth2::Configuration::DEFAULT_TOKEN_LIFETIME
172 | end
173 |
174 | it 'set to specific time if configuration option set to some value' do
175 | current_time = Time.now.utc
176 | Grape::OAuth2.config.access_token_lifetime = 3500
177 |
178 | token = AccessToken.create(client: application, resource_owner: user)
179 | expect(token.expires_at).not_to be_nil
180 | expect(token.expires_at).to be_within(1).of(current_time + 3500)
181 |
182 | Grape::OAuth2.config.access_token_lifetime = Grape::OAuth2::Configuration::DEFAULT_TOKEN_LIFETIME
183 | end
184 | end
185 | end
186 |
--------------------------------------------------------------------------------
/spec/mixins/active_record/client_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Grape::OAuth2::ActiveRecord::Client', skip_if: ENV['ORM'] != 'active_record' do
4 | let(:client) { Application.new }
5 |
6 | let(:key) { SecureRandom.hex(8) }
7 | let(:secret) { SecureRandom.hex(8) }
8 |
9 | it 'generates key on create' do
10 | expect(client.key).to be_nil
11 | client.save
12 | expect(client.key).not_to be_nil
13 | end
14 |
15 | it 'generates key on create if an empty string' do
16 | client.key = ''
17 | client.save
18 | expect(client.key).not_to be_blank
19 | end
20 |
21 | it 'generates key on create unless one is set' do
22 | client.key = key
23 | client.save
24 | expect(client.key).to eq(key)
25 | end
26 |
27 | it 'is invalid without key' do
28 | client.save
29 | client.key = nil
30 | expect(client).not_to be_valid
31 | end
32 |
33 | it 'checks uniqueness of key' do
34 | app1 = Application.create
35 | app2 = Application.create
36 | app2.key = app1.key
37 | expect(app2).not_to be_valid
38 | end
39 |
40 | it 'expects database to throw an error when keys are the same' do
41 | app1 = Application.create
42 | app2 = Application.create
43 | app2.key = app1.key
44 | expect { app2.save!(validate: false) }.to raise_error(ActiveRecord::RecordNotUnique)
45 | end
46 |
47 | it 'generate secret on create' do
48 | expect(client.secret).to be_nil
49 | client.save
50 | expect(client.secret).not_to be_nil
51 | end
52 |
53 | it 'generate secret on create if is blank string' do
54 | client.secret = ''
55 | client.save
56 | expect(client.secret).not_to be_blank
57 | end
58 |
59 | it 'generate secret on create unless one is set' do
60 | client.secret = secret
61 | client.save
62 | expect(client.secret).to eq(secret)
63 | end
64 |
65 | it 'is invalid without secret' do
66 | client.save
67 | client.secret = nil
68 | expect(client).not_to be_valid
69 | end
70 |
71 | describe '#authenticate' do
72 | it 'returns a class instance if authenticated successfully' do
73 | client.key = key
74 | client.secret = secret
75 | client.save
76 |
77 | expect(Application.authenticate(key, secret)).to eq(client)
78 | end
79 |
80 | it 'returns a class instance if only key specified' do
81 | client.key = key
82 | client.save
83 |
84 | expect(Application.authenticate(key)).to eq(client)
85 | end
86 |
87 | it 'returns nil if authentication failed' do
88 | client.key = key
89 | client.secret = secret
90 | client.save
91 |
92 | expect(Application.authenticate(key, 'invalid-')).to be_nil
93 | end
94 |
95 | it 'delete all the associated access tokens on destroy' do
96 | user = User.create!(username: 'John', password: '123123')
97 | app = Application.create!(name: 'app1', redirect_uri: 'https://google.com')
98 |
99 | 3.times { AccessToken.create(resource_owner_id: user.id, client_id: app.id) }
100 |
101 | expect { app.reload.destroy }.to change { AccessToken.count }.from(3).to(0)
102 | end
103 | end
104 | end
105 |
--------------------------------------------------------------------------------
/spec/mixins/mongoid/access_token_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Grape::OAuth2::Mongoid::AccessToken', skip_if: ENV['ORM'] != 'mongoid' do
4 | let(:application) { Application.create(name: 'Test') }
5 | let(:user) { User.create(username: 'test', password: '123123') }
6 | let(:access_token) { AccessToken.create(client: application, resource_owner: user) }
7 |
8 | let(:token) { SecureRandom.hex(16) }
9 |
10 | describe 'validations' do
11 | it 'validate token uniqueness' do
12 | another_token = AccessToken.create(client: application)
13 | token = AccessToken.new(client: application, token: another_token.token)
14 |
15 | expect(token).not_to be_valid
16 | expect(token.errors.messages).to include(:token)
17 | end
18 | end
19 |
20 | describe '#to_bearer_token' do
21 | context 'config with refresh token' do
22 | before do
23 | Grape::OAuth2.config.issue_refresh_token = true
24 | end
25 |
26 | after do
27 | Grape::OAuth2.config.issue_refresh_token = false
28 | end
29 |
30 | it 'returns refresh token' do
31 | expect(access_token.to_bearer_token[:access_token]).not_to be_blank
32 | end
33 | end
34 |
35 | context 'config without refresh token' do
36 | before do
37 | Grape::OAuth2.configure do |config|
38 | config.issue_refresh_token = false
39 | end
40 | end
41 |
42 | it 'returns blank refresh token' do
43 | expect(access_token.to_bearer_token[:refresh_token]).to be_blank
44 | end
45 | end
46 | end
47 |
48 | describe '#authenticate' do
49 | it 'returns an instance if authenticated successfully' do
50 | access_token.token = token
51 | access_token.save
52 |
53 | expect(AccessToken.authenticate(token)).to eq(access_token)
54 | end
55 |
56 | it 'returns nil if authentication failed' do
57 | access_token.token = token
58 | access_token.save
59 |
60 | expect(AccessToken.authenticate("invalid-#{token}")).to be_nil
61 | end
62 |
63 | it 'returns an instance by refresh token' do
64 | refresh_token = SecureRandom.hex(6)
65 | token = AccessToken.create(client: application, refresh_token: refresh_token)
66 |
67 | expect(AccessToken.authenticate(refresh_token, type: :refresh_token)).to eq(token)
68 | expect(AccessToken.authenticate(refresh_token, type: 'refresh_token')).to eq(token)
69 | end
70 | end
71 |
72 | describe '#create_for?' do
73 | it 'creates a record only for Client' do
74 | token = AccessToken.create_for(application, nil)
75 |
76 | expect(token.client).not_to be_nil
77 | expect(token.resource_owner).to be_nil
78 | end
79 |
80 | it 'creates a record for Client and Resource Owner' do
81 | token = AccessToken.create_for(application, user)
82 |
83 | expect(token.client).to eq(application)
84 | expect(token.resource_owner).to eq(user)
85 | end
86 |
87 | it 'creates a record with scopes' do
88 | scopes = 'write read'
89 | token = AccessToken.create_for(application, user, scopes)
90 |
91 | expect(token.client).to eq(application)
92 | expect(token.resource_owner).to eq(user)
93 | expect(token.scopes).to eq(scopes)
94 | end
95 | end
96 |
97 | describe '#expired?' do
98 | it 'return false if expires_at nil' do
99 | access_token.update_attribute(:expires_at, nil)
100 |
101 | expect(access_token.expired?).to be_falsey
102 | end
103 |
104 | it 'return false if expires_at < Time.now' do
105 | expect(access_token.expired?).to be_falsey
106 | end
107 |
108 | it 'return false if expires_at > Time.now' do
109 | expired_at = Time.now.utc - Grape::OAuth2.config.access_token_lifetime + 1
110 | access_token.update_attribute(:expires_at, expired_at)
111 |
112 | expect(access_token.expired?).to be_truthy
113 | end
114 | end
115 |
116 | describe '#revoked?' do
117 | it 'return false if revoked_at nil' do
118 | access_token.update_attribute(:revoked_at, nil)
119 |
120 | expect(access_token.revoked?).to be_falsey
121 | end
122 |
123 | it 'return false if revoked_at present' do
124 | access_token.update_attribute(:revoked_at, Time.now.utc)
125 | expect(access_token.revoked?).to be_truthy
126 | end
127 | end
128 |
129 | describe '#revoke!' do
130 | it 'update :revoked_at attribute' do
131 | expect { access_token.revoke! }.to change { access_token.revoked? }.from(false).to(true)
132 | end
133 |
134 | it 'update :revoked_at attribute with custom value' do
135 | custom_time = Time.now - 7200
136 | access_token.revoke!(custom_time)
137 |
138 | expect(access_token.revoked_at).to eq(custom_time.utc)
139 | end
140 | end
141 |
142 | describe 'token generation' do
143 | it 'generates a new token before saving if token is blank' do
144 | token = AccessToken.new(client: application, resource_owner: user)
145 |
146 | expect(token.token).to be_blank
147 |
148 | token.save
149 |
150 | expect(token.token).not_to be_blank
151 | end
152 |
153 | it 'does not change token value on saving if token is present' do
154 | token = AccessToken.new(client: application, resource_owner: user, token: 'abcdef')
155 |
156 | expect(token.token).not_to be_blank
157 |
158 | token.save
159 |
160 | expect(token.token).to eq('abcdef')
161 | end
162 | end
163 |
164 | describe 'expiration' do
165 | it 'set to nil if configuration option set to nil' do
166 | Grape::OAuth2.config.access_token_lifetime = nil
167 |
168 | token = AccessToken.create(client: application, resource_owner: user)
169 | expect(token.expires_at).to be_nil
170 |
171 | Grape::OAuth2.config.access_token_lifetime = Grape::OAuth2::Configuration::DEFAULT_TOKEN_LIFETIME
172 | end
173 |
174 | it 'set to specific time if configuration option set to some value' do
175 | current_time = Time.now.utc
176 | Grape::OAuth2.config.access_token_lifetime = 3500
177 |
178 | token = AccessToken.create(client: application, resource_owner: user)
179 | expect(token.expires_at).not_to be_nil
180 | expect(token.expires_at.to_i).to be_within(1).of((current_time + 3500).to_i)
181 |
182 | Grape::OAuth2.config.access_token_lifetime = Grape::OAuth2::Configuration::DEFAULT_TOKEN_LIFETIME
183 | end
184 | end
185 | end
186 |
--------------------------------------------------------------------------------
/spec/mixins/mongoid/client_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Grape::OAuth2::Mongoid::Client', skip_if: ENV['ORM'] != 'mongoid' do
4 | let(:client) { Application.new }
5 |
6 | let(:key) { SecureRandom.hex(8) }
7 | let(:secret) { SecureRandom.hex(8) }
8 |
9 | it 'generates key on create' do
10 | expect(client.key).to be_nil
11 | client.save
12 | expect(client.key).not_to be_nil
13 | end
14 |
15 | it 'generates key on create if an empty string' do
16 | client.key = ''
17 | client.save
18 | expect(client.key).not_to be_blank
19 | end
20 |
21 | it 'generates key on create unless one is set' do
22 | client.key = key
23 | client.save
24 | expect(client.key).to eq(key)
25 | end
26 |
27 | it 'is invalid without key' do
28 | client.save
29 | client.key = nil
30 | expect(client).not_to be_valid
31 | end
32 |
33 | it 'checks uniqueness of key' do
34 | app1 = Application.create
35 | app2 = Application.create
36 | app2.key = app1.key
37 | expect(app2).not_to be_valid
38 | end
39 |
40 | it 'expects database to throw an error when keys are the same' do
41 | app1 = Application.create
42 | app2 = Application.create
43 | app2.key = app1.key
44 | expect { app2.save! }.to raise_error(Mongoid::Errors::Validations)
45 | end
46 |
47 | it 'generate secret on create' do
48 | expect(client.secret).to be_nil
49 | client.save
50 | expect(client.secret).not_to be_nil
51 | end
52 |
53 | it 'generate secret on create if is blank string' do
54 | client.secret = ''
55 | client.save
56 | expect(client.secret).not_to be_blank
57 | end
58 |
59 | it 'generate secret on create unless one is set' do
60 | client.secret = secret
61 | client.save
62 | expect(client.secret).to eq(secret)
63 | end
64 |
65 | it 'is invalid without secret' do
66 | client.save
67 | client.secret = nil
68 | expect(client).not_to be_valid
69 | end
70 |
71 | describe '#authenticate' do
72 | it 'returns a class instance if authenticated successfully' do
73 | client.key = key
74 | client.secret = secret
75 | client.save
76 |
77 | expect(Application.authenticate(key, secret)).to eq(client)
78 | end
79 |
80 | it 'returns a class instance if only key specified' do
81 | client.key = key
82 | client.save
83 |
84 | expect(Application.authenticate(key)).to eq(client)
85 | end
86 |
87 | it 'returns nil if authentication failed' do
88 | client.key = key
89 | client.secret = secret
90 | client.save
91 |
92 | expect(Application.authenticate(key, 'invalid-')).to be_nil
93 | end
94 |
95 | it 'delete all the associated access tokens on destroy' do
96 | user = User.create(username: 'John', password: '123123')
97 | app = Application.create(name: 'app1', redirect_uri: 'https://google.com')
98 |
99 | 3.times { AccessToken.create(resource_owner_id: user.id, client_id: app.id) }
100 |
101 | expect { app.reload.destroy }.to change { AccessToken.count }.from(3).to(0)
102 | end
103 | end
104 | end
105 |
--------------------------------------------------------------------------------
/spec/mixins/sequel/access_token_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Grape::OAuth2::Sequel::AccessToken', skip_if: ENV['ORM'] != 'sequel' do
4 | let(:application) { Application.create(name: 'Test') }
5 | let(:user) { User.create(username: 'test', password: '123123') }
6 | let(:access_token) { AccessToken.create(client: application, resource_owner: user) }
7 |
8 | let(:token) { SecureRandom.hex(16) }
9 |
10 | describe 'validations' do
11 | it 'validate token uniqueness' do
12 | another_token = AccessToken.create(client: application)
13 | token = AccessToken.new(client: application, token: another_token.token)
14 |
15 | expect(token).not_to be_valid
16 | expect(token.errors).to include(:token)
17 | end
18 | end
19 |
20 | describe '#to_bearer_token' do
21 | context 'config with refresh token' do
22 | before do
23 | Grape::OAuth2.config.issue_refresh_token = true
24 | end
25 |
26 | after do
27 | Grape::OAuth2.config.issue_refresh_token = false
28 | end
29 |
30 | it 'returns refresh token' do
31 | expect(access_token.to_bearer_token[:access_token]).not_to be_blank
32 | end
33 | end
34 |
35 | context 'config without refresh token' do
36 | before do
37 | Grape::OAuth2.configure do |config|
38 | config.issue_refresh_token = false
39 | end
40 | end
41 |
42 | it 'returns blank refresh token' do
43 | expect(access_token.to_bearer_token[:refresh_token]).to be_blank
44 | end
45 | end
46 | end
47 |
48 | describe '#authenticate' do
49 | it 'returns an instance if authenticated successfully' do
50 | access_token.token = token
51 | access_token.save
52 |
53 | expect(AccessToken.authenticate(token)).to eq(access_token)
54 | end
55 |
56 | it 'returns nil if authentication failed' do
57 | access_token.token = token
58 | access_token.save
59 |
60 | expect(AccessToken.authenticate("invalid-#{token}")).to be_nil
61 | end
62 |
63 | it 'returns an instance by refresh token' do
64 | refresh_token = SecureRandom.hex(6)
65 | token = AccessToken.create(client: application, refresh_token: refresh_token)
66 |
67 | expect(AccessToken.authenticate(refresh_token, type: :refresh_token)).to eq(token)
68 | expect(AccessToken.authenticate(refresh_token, type: 'refresh_token')).to eq(token)
69 | end
70 | end
71 |
72 | describe '#create_for?' do
73 | it 'creates a record only for Client' do
74 | token = AccessToken.create_for(application, nil)
75 |
76 | expect(token.client).not_to be_nil
77 | expect(token.resource_owner).to be_nil
78 | end
79 |
80 | it 'creates a record for Client and Resource Owner' do
81 | token = AccessToken.create_for(application, user)
82 |
83 | expect(token.client).to eq(application)
84 | expect(token.resource_owner).to eq(user)
85 | end
86 |
87 | it 'creates a record with scopes' do
88 | scopes = 'write read'
89 | token = AccessToken.create_for(application, user, scopes)
90 |
91 | expect(token.client).to eq(application)
92 | expect(token.resource_owner).to eq(user)
93 | expect(token.scopes).to eq(scopes)
94 | end
95 | end
96 |
97 | describe '#expired?' do
98 | it 'return false if expires_at nil' do
99 | access_token.update_fields({ expires_at: nil }, [:expires_at])
100 |
101 | expect(access_token.expired?).to be_falsey
102 | end
103 |
104 | it 'return false if expires_at < Time.now' do
105 | expect(access_token.expired?).to be_falsey
106 | end
107 |
108 | it 'return false if expires_at > Time.now' do
109 | expires_at = Time.now.utc - Grape::OAuth2.config.access_token_lifetime + 1
110 | access_token.update_fields({ expires_at: expires_at }, [:expires_at])
111 |
112 | expect(access_token.expired?).to be_truthy
113 | end
114 | end
115 |
116 | describe '#revoked?' do
117 | it 'return false if revoked_at nil' do
118 | access_token.update_fields({ revoked_at: nil }, [:revoked_at])
119 |
120 | expect(access_token.revoked?).to be_falsey
121 | end
122 |
123 | it 'return false if revoked_at present' do
124 | access_token.update_fields({ revoked_at: Time.now.utc }, [:revoked_at])
125 | expect(access_token.revoked?).to be_truthy
126 | end
127 | end
128 |
129 | describe '#revoke!' do
130 | it 'update :revoked_at attribute' do
131 | expect { access_token.revoke! }.to change { access_token.revoked? }.from(false).to(true)
132 | end
133 |
134 | it 'update :revoked_at attribute with custom value' do
135 | custom_time = Time.now - 7200
136 | access_token.revoke!(custom_time)
137 |
138 | expect(access_token.revoked_at).to eq(custom_time.utc)
139 | end
140 | end
141 |
142 | describe 'token generation' do
143 | it 'generates a new token before saving if token is blank' do
144 | token = AccessToken.new(client: application, resource_owner: user)
145 |
146 | expect(token.token).to be_blank
147 |
148 | token.save
149 |
150 | expect(token.token).not_to be_blank
151 | end
152 |
153 | it 'does not change token value on saving if token is present' do
154 | token = AccessToken.new(client: application, resource_owner: user, token: 'abcdef')
155 |
156 | expect(token.token).not_to be_blank
157 |
158 | token.save
159 |
160 | expect(token.token).to eq('abcdef')
161 | end
162 | end
163 |
164 | describe 'expiration' do
165 | it 'set to nil if configuration option set to nil' do
166 | Grape::OAuth2.config.access_token_lifetime = nil
167 |
168 | token = AccessToken.create(client: application, resource_owner: user)
169 | expect(token.expires_at).to be_nil
170 |
171 | Grape::OAuth2.config.access_token_lifetime = Grape::OAuth2::Configuration::DEFAULT_TOKEN_LIFETIME
172 | end
173 |
174 | it 'set to specific time if configuration option set to some value' do
175 | current_time = Time.now.utc
176 | Grape::OAuth2.config.access_token_lifetime = 3500
177 |
178 | token = AccessToken.create(client: application, resource_owner: user)
179 | expect(token.expires_at).not_to be_nil
180 | expect(token.expires_at).to be_within(1).of(current_time + 3500)
181 |
182 | Grape::OAuth2.config.access_token_lifetime = Grape::OAuth2::Configuration::DEFAULT_TOKEN_LIFETIME
183 | end
184 | end
185 | end
186 |
--------------------------------------------------------------------------------
/spec/mixins/sequel/client_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Grape::OAuth2::Sequel::Client', skip_if: ENV['ORM'] != 'sequel' do
4 | let(:client) { Application.new(name: 'Test') }
5 |
6 | let(:key) { SecureRandom.hex(8) }
7 | let(:secret) { SecureRandom.hex(8) }
8 |
9 | it 'generates key on create' do
10 | expect(client.key).to be_nil
11 | client.save
12 | expect(client.key).not_to be_nil
13 | end
14 |
15 | it 'generates key on create if an empty string' do
16 | client.key = ''
17 | client.save
18 | expect(client.key).not_to be_blank
19 | end
20 |
21 | it 'generates key on create unless one is set' do
22 | client.key = key
23 | client.save
24 | expect(client.key).to eq(key)
25 | end
26 |
27 | it 'is invalid without key' do
28 | client.save
29 | client.key = nil
30 | expect(client).not_to be_valid
31 | end
32 |
33 | it 'checks uniqueness of key' do
34 | app1 = Application.create(name: 'app1')
35 | app2 = Application.create(name: 'app2')
36 | app2.key = app1.key
37 | expect(app2).not_to be_valid
38 | expect(app2.errors).to include(:key)
39 | end
40 |
41 | it 'expects database to throw an error when keys are the same' do
42 | app1 = Application.create(name: 'app1')
43 | app2 = Application.create(name: 'app2')
44 | app2.key = app1.key
45 | expect { app2.save }.to raise_error(Sequel::ValidationFailed)
46 | end
47 |
48 | it 'generate secret on create' do
49 | expect(client.secret).to be_nil
50 | client.save
51 | expect(client.secret).not_to be_nil
52 | end
53 |
54 | it 'generate secret on create if is blank string' do
55 | client.secret = ''
56 | client.save
57 | expect(client.secret).not_to be_blank
58 | end
59 |
60 | it 'generate secret on create unless one is set' do
61 | client.secret = secret
62 | client.save
63 | expect(client.secret).to eq(secret)
64 | end
65 |
66 | it 'is invalid without secret' do
67 | client.save
68 | client.secret = nil
69 | expect(client).not_to be_valid
70 | end
71 |
72 | describe '#authenticate' do
73 | it 'returns a class instance if authenticated successfully' do
74 | client.key = key
75 | client.secret = secret
76 | client.save
77 |
78 | expect(Application.authenticate(key, secret)).to eq(client)
79 | end
80 |
81 | it 'returns a class instance if only key specified' do
82 | client.key = key
83 | client.save
84 |
85 | expect(Application.authenticate(key)).to eq(client)
86 | end
87 |
88 | it 'returns nil if authentication failed' do
89 | client.key = key
90 | client.secret = secret
91 | client.save
92 |
93 | expect(Application.authenticate(key, 'invalid-')).to be_nil
94 | end
95 |
96 | it 'delete all the associated access tokens on destroy' do
97 | user = User.create(username: 'John', password: '123123')
98 | app = Application.create(name: 'app1', redirect_uri: 'https://google.com')
99 |
100 | 3.times { AccessToken.create(resource_owner_id: user.id, client_id: app.id) }
101 |
102 | expect { app.refresh.destroy }.to change { AccessToken.count }.from(3).to(0)
103 | end
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/spec/requests/flows/authorization_code_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Authorization Code flow' do
4 | let(:redirect_uri) { 'http://localhost:3000/home' }
5 | let(:application) { Application.create(name: 'App1', redirect_uri: redirect_uri) }
6 |
7 | describe 'POST /oauth/authorize' do
8 | let(:authorize_url) { '/api/v1/oauth/authorize' }
9 |
10 | context 'with valid params' do
11 | context 'when response_type is :code' do
12 | it 'should be success' do
13 | expect {
14 | post authorize_url,
15 | client_id: application.key,
16 | redirect_uri: redirect_uri,
17 | response_type: 'code'
18 | }.to change { AccessCode.count }.from(0).to(1)
19 |
20 | expect(last_response.status).to eq 302
21 | end
22 | end
23 |
24 | context 'when response_type is :token' do
25 | it 'should be success' do
26 | expect {
27 | post authorize_url,
28 | client_id: application.key,
29 | redirect_uri: redirect_uri,
30 | response_type: 'token'
31 | }.to change { AccessToken.count }.from(0).to(1)
32 | end
33 | end
34 | end
35 |
36 | context 'with invalid params' do
37 | it 'should fail without response_type' do
38 | post authorize_url,
39 | client_id: application.key
40 |
41 | expect(last_response.status).to eq 400
42 | expect(json_body[:error]).to eq('invalid_request')
43 | end
44 |
45 | it 'should fail with unsupported response_type' do
46 | post authorize_url,
47 | client_id: application.key,
48 | redirect_uri: redirect_uri,
49 | response_type: 'invalid'
50 |
51 | expect(last_response.status).to eq 400
52 | expect(json_body[:error]).to eq('unsupported_response_type')
53 | end
54 | end
55 | end
56 |
57 | describe 'POST /oauth/custom_authorize' do
58 | it 'invokes custom block' do
59 | post '/api/v1/oauth/custom_authorize',
60 | client_id: application.key,
61 | redirect_uri: redirect_uri,
62 | response_type: 'code'
63 |
64 | expect(last_response.status).to eq(400)
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/spec/requests/flows/client_credentials_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Token Endpoint' do
4 | describe 'POST /oauth/token' do
5 | describe 'Client Credentials flow' do
6 | context 'with valid params' do
7 | let(:authentication_url) { '/api/v1/oauth/token' }
8 | let(:application) { Application.create(name: 'App1') }
9 | let(:user) { User.create(username: 'test', password: '12345678') }
10 |
11 | context 'when request is invalid' do
12 | it 'fails without Grant Type' do
13 | post authentication_url,
14 | client_id: application.key,
15 | client_secret: application.secret
16 |
17 | expect(AccessToken.all).to be_empty
18 |
19 | expect(json_body[:error]).to eq('invalid_request')
20 | expect(last_response.status).to eq 400
21 | end
22 |
23 | it 'fails with invalid Grant Type' do
24 | post authentication_url,
25 | grant_type: 'invalid'
26 |
27 | expect(AccessToken.all).to be_empty
28 |
29 | expect(json_body[:error]).to eq('unsupported_grant_type')
30 | expect(last_response.status).to eq 400
31 | end
32 |
33 | it 'fails without Client Credentials' do
34 | post authentication_url,
35 | grant_type: 'client_credentials'
36 |
37 | expect(AccessToken.all).to be_empty
38 |
39 | expect(json_body[:error]).to eq('invalid_request')
40 | expect(last_response.status).to eq 400
41 | end
42 |
43 | it 'fails with invalid Client Credentials' do
44 | post authentication_url,
45 | grant_type: 'client_credentials',
46 | client_id: 'blah-blah',
47 | client_secret: application.secret
48 |
49 | expect(AccessToken.all).to be_empty
50 |
51 | expect(json_body[:error]).to eq('invalid_client')
52 | expect(last_response.status).to eq 401
53 | end
54 | end
55 |
56 | context 'with valid data' do
57 | context 'when scopes requested' do
58 | it 'returns an Access Token with scopes' do
59 | post authentication_url,
60 | grant_type: 'client_credentials',
61 | scope: 'read write',
62 | client_id: application.key,
63 | client_secret: application.secret
64 |
65 | expect(AccessToken.count).to eq 1
66 | expect(AccessToken.first.client_id).to eq application.id
67 |
68 | expect(json_body[:access_token]).to be_present
69 | expect(json_body[:token_type]).to eq 'bearer'
70 | expect(json_body[:expires_in]).to eq 7200
71 | expect(json_body[:refresh_token]).to be_nil
72 | expect(json_body[:scope]).to eq('read write')
73 |
74 | expect(last_response.status).to eq 200
75 | end
76 | end
77 |
78 | context 'without scopes' do
79 | it 'returns an Access Token without scopes' do
80 | post authentication_url,
81 | grant_type: 'client_credentials',
82 | client_id: application.key,
83 | client_secret: application.secret
84 |
85 | expect(AccessToken.count).to eq 1
86 | expect(AccessToken.first.client_id).to eq application.id
87 |
88 | expect(json_body[:access_token]).to be_present
89 | expect(json_body[:token_type]).to eq 'bearer'
90 | expect(json_body[:expires_in]).to eq 7200
91 | expect(json_body[:refresh_token]).to be_nil
92 | expect(json_body[:scope]).to be_nil
93 |
94 | expect(last_response.status).to eq 200
95 | end
96 | end
97 | end
98 | end
99 | end
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/spec/requests/flows/password_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Token Endpoint' do
4 | let(:application) { Application.create(name: 'App1') }
5 | let(:user) { User.create(username: 'test', password: '12345678') }
6 |
7 | describe 'Resource Owner Password Credentials flow' do
8 | describe 'POST /oauth/token' do
9 | let(:authentication_url) { '/api/v1/oauth/token' }
10 |
11 | context 'with valid params' do
12 | context 'when request is invalid' do
13 | it 'fails without Grant Type' do
14 | post authentication_url,
15 | username: user.username,
16 | password: '12345678',
17 | client_id: application.key,
18 | client_secret: application.secret
19 |
20 | expect(AccessToken.all).to be_empty
21 |
22 | expect(json_body[:error]).to eq('invalid_request')
23 | expect(last_response.status).to eq 400
24 | end
25 |
26 | it 'fails with invalid Grant Type' do
27 | post authentication_url,
28 | grant_type: 'invalid',
29 | username: user.username,
30 | password: '12345678'
31 |
32 | expect(AccessToken.all).to be_empty
33 |
34 | expect(json_body[:error]).to eq('unsupported_grant_type')
35 | expect(last_response.status).to eq 400
36 | end
37 |
38 | it 'fails without Client Credentials' do
39 | post authentication_url,
40 | grant_type: 'password',
41 | username: user.username,
42 | password: '12345678'
43 |
44 | expect(AccessToken.all).to be_empty
45 |
46 | expect(json_body[:error]).to eq('invalid_request')
47 | expect(last_response.status).to eq 400
48 | end
49 |
50 | it 'fails with invalid Client Credentials' do
51 | post authentication_url,
52 | grant_type: 'password',
53 | username: user.username,
54 | password: '12345678',
55 | client_id: 'blah-blah',
56 | client_secret: application.secret
57 |
58 | expect(AccessToken.all).to be_empty
59 |
60 | expect(json_body[:error]).to eq('invalid_client')
61 | expect(last_response.status).to eq 401
62 | end
63 |
64 | it 'fails without Resource Owner credentials' do
65 | post authentication_url,
66 | grant_type: 'password',
67 | client_id: application.key,
68 | client_secret: application.secret
69 |
70 | expect(json_body[:error]).to eq('invalid_request')
71 | expect(json_body[:error_description]).not_to be_blank
72 | expect(last_response.status).to eq 400
73 | end
74 |
75 | it 'fails with invalid Resource Owner credentials' do
76 | post authentication_url,
77 | grant_type: 'password',
78 | username: 'invalid@example.com',
79 | password: 'invalid',
80 | client_id: application.key,
81 | client_secret: application.secret
82 |
83 | expect(json_body[:error]).to eq('invalid_grant')
84 | expect(json_body[:error_description]).not_to be_blank
85 | expect(last_response.status).to eq 400
86 | end
87 | end
88 |
89 | context 'with valid data' do
90 | context 'when scopes requested' do
91 | it 'returns an Access Token with scopes' do
92 | post authentication_url,
93 | grant_type: 'password',
94 | username: user.username,
95 | password: '12345678',
96 | scope: 'read write',
97 | client_id: application.key,
98 | client_secret: application.secret
99 |
100 | expect(AccessToken.count).to eq 1
101 | expect(AccessToken.first.client_id).to eq application.id
102 |
103 | expect(json_body[:access_token]).to be_present
104 | expect(json_body[:token_type]).to eq 'bearer'
105 | expect(json_body[:expires_in]).to eq 7200
106 | expect(json_body[:refresh_token]).to be_nil
107 | expect(json_body[:scope]).to eq('read write')
108 |
109 | expect(last_response.status).to eq 200
110 | end
111 | end
112 |
113 | context 'without scopes' do
114 | it 'returns an Access Token without scopes' do
115 | post authentication_url,
116 | grant_type: 'password',
117 | username: user.username,
118 | password: '12345678',
119 | client_id: application.key,
120 | client_secret: application.secret
121 |
122 | expect(AccessToken.count).to eq 1
123 | expect(AccessToken.first.client_id).to eq application.id
124 |
125 | expect(json_body[:access_token]).to be_present
126 | expect(json_body[:token_type]).to eq 'bearer'
127 | expect(json_body[:expires_in]).to eq 7200
128 | expect(json_body[:refresh_token]).to be_nil
129 | expect(json_body[:scope]).to be_nil
130 |
131 | expect(last_response.status).to eq 200
132 | end
133 | end
134 | end
135 |
136 | context 'when Token endpoint not mounted' do
137 | before do
138 | Twitter::API.reset!
139 | Twitter::API.change!
140 |
141 | # Mount only Authorization Endpoint
142 | Twitter::API.send(:include, Grape::OAuth2.api(:authorize))
143 | end
144 |
145 | after do
146 | Twitter::API.reset!
147 | Twitter::API.change!
148 |
149 | Twitter::API.send(:include, Grape::OAuth2.api)
150 | Twitter::API.mount(Twitter::Endpoints::Status)
151 | Twitter::API.mount(Twitter::Endpoints::CustomToken)
152 | Twitter::API.mount(Twitter::Endpoints::CustomAuthorization)
153 | end
154 |
155 | it 'returns 404' do
156 | post authentication_url,
157 | grant_type: 'password',
158 | username: user.username,
159 | password: '12345678',
160 | client_id: application.key,
161 | client_secret: application.secret
162 |
163 | expect(last_response.status).to eq 404
164 | end
165 | end
166 | end
167 | end
168 | end
169 |
170 | describe 'POST /oauth/custom_token' do
171 | context 'when block processed successfully' do
172 | it 'returns an access token' do
173 | application.update(name: 'Admin')
174 |
175 | post '/api/v1/oauth/custom_token',
176 | grant_type: 'password',
177 | username: user.username,
178 | password: '12345678',
179 | client_id: application.key,
180 | client_secret: application.secret
181 |
182 | expect(last_response.status).to eq 200
183 |
184 | expect(AccessToken.count).to eq 1
185 | expect(AccessToken.first.client_id).to eq application.id
186 |
187 | expect(json_body[:access_token]).to be_present
188 | expect(json_body[:token_type]).to eq 'bearer'
189 | expect(json_body[:expires_in]).to eq 7200
190 | end
191 | end
192 |
193 | context 'when authentication failed' do
194 | it 'returns an error' do
195 | application.update(name: 'Admin')
196 |
197 | post '/api/v1/oauth/custom_token',
198 | grant_type: 'password',
199 | username: 'invalid@example.com',
200 | password: 'invalid',
201 | client_id: application.key,
202 | client_secret: application.secret
203 |
204 | expect(json_body[:error]).to eq('invalid_grant')
205 | expect(json_body[:error_description]).not_to be_blank
206 | expect(last_response.status).to eq 400
207 | end
208 | end
209 | end
210 | end
211 |
--------------------------------------------------------------------------------
/spec/requests/flows/refresh_token_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Token Endpoint' do
4 | describe 'POST /oauth/token' do
5 | describe 'Refresh Token flow' do
6 | context 'with valid params' do
7 | let(:api_url) { '/api/v1/oauth/token' }
8 | let(:application) { Application.create(name: 'App1') }
9 | let(:user) { User.create(username: 'test', password: '12345678') }
10 |
11 | context 'when request is invalid' do
12 | it 'fails without Grant Type' do
13 | post api_url,
14 | client_id: application.key,
15 | client_secret: application.secret
16 |
17 | expect(AccessToken.all).to be_empty
18 |
19 | expect(json_body[:error]).to eq('invalid_request')
20 | expect(last_response.status).to eq 400
21 | end
22 |
23 | it 'fails with invalid Grant Type' do
24 | post api_url,
25 | grant_type: 'invalid'
26 |
27 | expect(AccessToken.all).to be_empty
28 |
29 | expect(json_body[:error]).to eq('unsupported_grant_type')
30 | expect(last_response.status).to eq 400
31 | end
32 |
33 | it 'fails without Client Credentials' do
34 | post api_url,
35 | grant_type: 'refresh_token'
36 |
37 | expect(AccessToken.all).to be_empty
38 |
39 | expect(json_body[:error]).to eq('invalid_request')
40 | expect(last_response.status).to eq 400
41 | end
42 |
43 | it 'fails with invalid Client Credentials' do
44 | post api_url,
45 | grant_type: 'refresh_token',
46 | refresh_token: SecureRandom.hex(6),
47 | client_id: 'blah-blah',
48 | client_secret: application.secret
49 |
50 | expect(AccessToken.all).to be_empty
51 |
52 | expect(json_body[:error]).to eq('invalid_client')
53 | expect(last_response.status).to eq 401
54 | end
55 |
56 | it 'fails when Access Token was issued to another client' do
57 | allow(Grape::OAuth2.config).to receive(:issue_refresh_token).and_return(true)
58 |
59 | another_client = Application.create(name: 'Some')
60 | token = AccessToken.create_for(another_client, user)
61 | expect(token.refresh_token).not_to be_nil
62 |
63 | post api_url,
64 | grant_type: 'refresh_token',
65 | refresh_token: token.refresh_token,
66 | client_id: application.key,
67 | client_secret: application.secret
68 |
69 | expect(json_body[:error]).to eq('unauthorized_client')
70 | expect(last_response.status).to eq 400
71 |
72 | expect(AccessToken.count).to eq(1)
73 | end
74 | end
75 |
76 | context 'with valid data' do
77 | before { allow(Grape::OAuth2.config).to receive(:issue_refresh_token).and_return(true) }
78 |
79 | it 'returns a new Access Token' do
80 | token = AccessToken.create_for(application, user)
81 | expect(token.refresh_token).not_to be_nil
82 |
83 | post api_url,
84 | grant_type: 'refresh_token',
85 | refresh_token: token.refresh_token,
86 | client_id: application.key,
87 | client_secret: application.secret
88 |
89 | expect(last_response.status).to eq 200
90 |
91 | expect(AccessToken.count).to eq 2
92 | expect(AccessToken.last.client_id).to eq application.id
93 | expect(AccessToken.last.resource_owner_id).to eq user.id
94 |
95 | expect(json_body[:access_token]).to eq AccessToken.last.token
96 | expect(json_body[:token_type]).to eq 'bearer'
97 | expect(json_body[:expires_in]).to eq 7200
98 | expect(json_body[:refresh_token]).to eq AccessToken.last.refresh_token
99 | end
100 |
101 | it 'revokes old Access Token if it is configured' do
102 | allow(Grape::OAuth2.config).to receive(:on_refresh).and_return(:revoke!)
103 |
104 | token = AccessToken.create_for(application, user)
105 | expect(token.refresh_token).not_to be_nil
106 |
107 | post api_url,
108 | grant_type: 'refresh_token',
109 | refresh_token: token.refresh_token,
110 | client_id: application.key,
111 | client_secret: application.secret
112 |
113 | expect(last_response.status).to eq 200
114 |
115 | expect(AccessToken.count).to eq 2
116 | expect(AccessToken.last.client).to eq application
117 | expect(AccessToken.last.resource_owner).to eq user
118 |
119 | expect(token.reload.revoked?).to be_truthy
120 |
121 | expect(json_body[:access_token]).to eq AccessToken.last.token
122 | expect(json_body[:refresh_token]).to eq AccessToken.last.refresh_token
123 | end
124 |
125 | it 'destroy old Access Token if it is configured' do
126 | allow(Grape::OAuth2.config).to receive(:on_refresh).and_return(:destroy)
127 |
128 | token = AccessToken.create_for(application, user)
129 | expect(token.refresh_token).not_to be_nil
130 |
131 | post api_url,
132 | grant_type: 'refresh_token',
133 | refresh_token: token.refresh_token,
134 | client_id: application.key,
135 | client_secret: application.secret
136 |
137 | expect(last_response.status).to eq 200
138 |
139 | expect(AccessToken.count).to eq 1
140 | expect(AccessToken.where(token: token.token).first).to be_nil
141 | end
142 |
143 | it 'calls custom block on token refresh if it is configured' do
144 | scopes = 'just for test'
145 | allow(Grape::OAuth2.config).to receive(:on_refresh).and_return(->(token) { token.update(scopes: scopes) })
146 |
147 | token = AccessToken.create_for(application, user)
148 | expect(token.refresh_token).not_to be_nil
149 |
150 | post api_url,
151 | grant_type: 'refresh_token',
152 | refresh_token: token.refresh_token,
153 | client_id: application.key,
154 | client_secret: application.secret
155 |
156 | expect(last_response.status).to eq 200
157 |
158 | expect(AccessToken.count).to eq 2
159 | expect(token.reload.scopes).to eq(scopes)
160 | end
161 |
162 | it 'does nothing on token refresh if :on_refresh is equal to :nothing or nil' do
163 | allow(Grape::OAuth2.config).to receive(:on_refresh).and_return(:nothing)
164 |
165 | token = AccessToken.create_for(application, user)
166 | expect(token.refresh_token).not_to be_nil
167 |
168 | # Check for :nothing
169 | expect(Grape::OAuth2::Strategies::RefreshToken).not_to receive(:run_on_refresh_callback)
170 |
171 | post api_url,
172 | grant_type: 'refresh_token',
173 | refresh_token: token.refresh_token,
174 | client_id: application.key,
175 | client_secret: application.secret
176 |
177 | expect(last_response.status).to eq 200
178 |
179 | allow(Grape::OAuth2.config).to receive(:on_refresh).and_return(nil)
180 |
181 | token = AccessToken.create_for(application, user)
182 | expect(token.refresh_token).not_to be_nil
183 |
184 | # Check for nil
185 | expect(Grape::OAuth2::Strategies::RefreshToken).not_to receive(:run_on_refresh_callback)
186 |
187 | post api_url,
188 | grant_type: 'refresh_token',
189 | refresh_token: token.refresh_token,
190 | client_id: application.key,
191 | client_secret: application.secret
192 |
193 | expect(last_response.status).to eq 200
194 | end
195 |
196 | it 'returns a new Access Token even if used token is expired' do
197 | token = AccessToken.create_for(application, user)
198 | token.update(expires_at: Time.now - 604800) # - 7 days
199 | expect(token.refresh_token).not_to be_nil
200 |
201 | post api_url,
202 | grant_type: 'refresh_token',
203 | refresh_token: token.refresh_token,
204 | client_id: application.key,
205 | client_secret: application.secret
206 |
207 | expect(last_response.status).to eq 200
208 |
209 | expect(AccessToken.count).to eq 2
210 | expect(AccessToken.last.client_id).to eq application.id
211 | expect(AccessToken.last.resource_owner_id).to eq user.id
212 |
213 | expect(json_body[:access_token]).to eq AccessToken.last.token
214 | expect(json_body[:token_type]).to eq 'bearer'
215 | expect(json_body[:expires_in]).to eq 7200
216 | expect(json_body[:refresh_token]).to eq AccessToken.last.refresh_token
217 | end
218 | end
219 | end
220 | end
221 | end
222 | end
223 |
--------------------------------------------------------------------------------
/spec/requests/flows/revoke_token_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Token Endpoint' do
4 | describe 'POST /oauth/revoke' do
5 | describe 'Revoke Token flow' do
6 | context 'with valid params' do
7 | let(:api_url) { '/api/v1/oauth/revoke' }
8 | let(:application) { Application.create(name: 'App1') }
9 | let(:user) { User.create(username: 'test', password: '12345678') }
10 |
11 | let(:headers) { { 'HTTP_AUTHORIZATION' => ('Basic ' + Base64::encode64("#{application.key}:#{application.secret}")) } }
12 |
13 | describe 'for public token' do
14 | context 'when request is invalid' do
15 | before { AccessToken.create_for(application, user) }
16 |
17 | it 'does nothing' do
18 | expect {
19 | post api_url, { token: 'invalid token' }, headers
20 | }.not_to change { AccessToken.count }
21 |
22 | expect(json_body).to eq({})
23 | expect(last_response.status).to eq 200
24 |
25 | expect(AccessToken.last).not_to be_revoked
26 | end
27 |
28 | it 'returns an error with invalid token_type_hint' do
29 | expect {
30 | post api_url, { token: AccessToken.last.token, token_type_hint: 'undefined' }, headers
31 | }.not_to change { AccessToken.count }
32 |
33 | expect(last_response.status).to eq 400
34 | end
35 | end
36 |
37 | context 'with valid data' do
38 | # Token doesn't belongs to anybody
39 | before { AccessToken.create_for(nil, nil) }
40 |
41 | it 'revokes Access Token by its token' do
42 | expect {
43 | post api_url, { token: AccessToken.last.token }, headers
44 | }.to change { AccessToken.where(revoked_at: nil).count }.from(1).to(0)
45 |
46 | expect(json_body).to eq({})
47 | expect(last_response.status).to eq 200
48 |
49 | expect(AccessToken.last).to be_revoked
50 | end
51 |
52 | it 'revokes Access Token by its refresh token' do
53 | refresh_token = SecureRandom.hex(16)
54 | AccessToken.last.update(refresh_token: refresh_token)
55 |
56 | expect {
57 | post api_url, { token: refresh_token, token_type_hint: 'refresh_token' }, headers
58 | }.to change { AccessToken.where(revoked_at: nil).count }.from(1).to(0)
59 |
60 | expect(json_body).to eq({})
61 | expect(last_response.status).to eq 200
62 |
63 | expect(AccessToken.last).to be_revoked
64 | end
65 | end
66 | end
67 |
68 | describe 'for private token' do
69 | before { AccessToken.create_for(application, user) }
70 |
71 | context 'with valid data' do
72 | it 'revokes token with client authorization' do
73 | expect {
74 | post api_url, { token: AccessToken.last.token}, headers
75 | }.to change { AccessToken.where(revoked_at: nil).count }.from(1).to(0)
76 | end
77 | end
78 |
79 | context 'with invalid data' do
80 | it 'does not revokes Access Token when credentials is invalid' do
81 | expect {
82 | post api_url, token: AccessToken.last.token
83 | }.to_not change { AccessToken.where(revoked_at: nil).count }
84 |
85 | expect(json_body[:error]).to eq('invalid_client')
86 | end
87 |
88 | it 'does not revokes Access Token when token was issued to another client' do
89 | another_client = Application.create(name: 'Some')
90 | AccessToken.last.update(client_id: another_client.id)
91 |
92 | expect {
93 | post api_url, token: AccessToken.last.token
94 | }.to_not change { AccessToken.where(revoked_at: nil).count }
95 |
96 | expect(json_body[:error]).to eq('invalid_client')
97 | end
98 | end
99 | end
100 | end
101 | end
102 | end
103 | end
104 |
--------------------------------------------------------------------------------
/spec/requests/protected_resources_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'GET Protected Resources' do
4 | let(:application) { Application.create(name: 'App1') }
5 | let(:user) { User.create(username: 'Jack Sparrow', password: '12345678') }
6 | let(:access_token) { AccessToken.create_for(application, user) }
7 |
8 | context 'with invalid data' do
9 | it 'returns Unauthorized without Access Token' do
10 | get 'api/v1/status'
11 |
12 | expect(last_response.status).to eq 401
13 |
14 | expect(json_body[:error]).to eq('unauthorized')
15 | expect(last_response.headers['WWW-Authenticate']).to eq('Bearer realm="Custom Realm"')
16 | end
17 |
18 | it 'returns Unauthorized when token scopes are blank' do
19 | get 'api/v1/status/single_scope', access_token: access_token.token
20 |
21 | expect(last_response.status).to eq 403
22 |
23 | expect(json_body[:error]).not_to be_blank
24 | end
25 |
26 | it "returns Unauthorized when token scopes doesn't match required scopes" do
27 | access_token.update(scopes: 'read')
28 | get 'api/v1/status/multiple_scopes', access_token: access_token.token
29 |
30 | expect(last_response.status).to eq 403
31 |
32 | expect(json_body[:error]).not_to be_blank
33 | end
34 | end
35 |
36 | context 'with valid data' do
37 | it "returns status for endpoint that doesn't requires any scope" do
38 | get 'api/v1/status', access_token: access_token.token
39 |
40 | expect(last_response.status).to eq 200
41 |
42 | expect(json_body[:value]).to eq('Nice day!')
43 | expect(json_body[:current_user]).to eq('Jack Sparrow')
44 | end
45 |
46 | it 'returns status for endpoint with specific scope' do
47 | access_token.update(scopes: 'read public')
48 | get 'api/v1/status/single_scope', access_token: access_token.token
49 |
50 | expect(last_response.status).to eq 200
51 |
52 | expect(json_body[:value]).to eq('Access granted')
53 | end
54 |
55 | it 'returns status for endpoint with specific set of scopes' do
56 | access_token.update(scopes: 'read write public')
57 | get 'api/v1/status/multiple_scopes', access_token: access_token.token
58 |
59 | expect(last_response.status).to eq 200
60 |
61 | expect(json_body[:value]).to eq('Access granted')
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] ||= 'test'
2 | ENV['ORM'] ||= 'active_record'
3 |
4 | puts "Configured ORM: '#{ENV['ORM']}'"
5 |
6 | require 'coveralls'
7 |
8 | if Coveralls.should_run?
9 | Coveralls.wear!
10 | else
11 | require 'simplecov'
12 | SimpleCov.start
13 | end
14 |
15 | require 'bundler/setup'
16 | Bundler.setup
17 |
18 | require 'rack/test'
19 | require 'database_cleaner'
20 |
21 | ORM_GEMS_MAPPING = {
22 | 'sequel' => 'sequel',
23 | 'active_record' => 'active_record',
24 | 'mongoid' => 'mongoid'
25 | }.freeze
26 |
27 | require ORM_GEMS_MAPPING[ENV['ORM']]
28 |
29 | require 'grape_oauth2'
30 |
31 | # Require Rack app by ORM
32 | require File.expand_path("../dummy/orm/#{ENV['ORM']}/app/twitter", __FILE__)
33 |
34 | TWITTER_APP = Rack::Builder.parse_file(File.expand_path("../dummy/orm/#{ENV['ORM']}/config.ru", __FILE__)).first
35 |
36 | require 'support/api_helper'
37 |
38 | RSpec.configure do |config|
39 | config.include ApiHelper
40 |
41 | config.filter_run_excluding skip_if: true
42 |
43 | config.order = 'random'
44 |
45 | config.before(:suite) do
46 | if ENV['ORM'] == 'mongoid'
47 | DatabaseCleaner[:mongoid].strategy = :truncation
48 | DatabaseCleaner[:mongoid].clean_with :truncation
49 | else
50 | DatabaseCleaner.strategy = :transaction
51 | DatabaseCleaner.clean_with(:deletion)
52 | end
53 | end
54 |
55 | config.around(:example) do |example|
56 | DatabaseCleaner.cleaning do
57 | example.run
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/spec/support/api_helper.rb:
--------------------------------------------------------------------------------
1 | module ApiHelper
2 | include Rack::Test::Methods
3 |
4 | def app
5 | TWITTER_APP
6 | end
7 |
8 | def json_body
9 | JSON.parse(last_response.body, symbolize_names: true) rescue fail StandardError, 'API request returned invalid json'
10 | end
11 | end
12 |
--------------------------------------------------------------------------------