├── .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 | Grape::OAuth2 - OAuth2 provider for Grape 3 |

4 | 5 | # Grape::OAuth2 6 | [![Gem Version](https://badge.fury.io/rb/grape_oauth2.svg)](http://badge.fury.io/rb/grape_oauth2) 7 | [![Build Status](https://travis-ci.org/nbulaj/grape_oauth2.svg?branch=master)](https://travis-ci.org/nbulaj/grape_oauth2) 8 | [![Coverage Status](https://coveralls.io/repos/github/nbulaj/grape_oauth2/badge.svg)](https://coveralls.io/github/nbulaj/grape_oauth2) 9 | [![Code Climate](https://codeclimate.com/github/nbulaj/grape_oauth2/badges/gpa.svg)](https://codeclimate.com/github/nbulaj/grape_oauth2) 10 | [![Inline docs](http://inch-ci.org/github/nbulaj/grape_oauth2.svg?branch=master)](http://inch-ci.org/github/nbulaj/grape_oauth2) 11 | [![License](http://img.shields.io/badge/license-MIT-brightgreen.svg)](#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 | --------------------------------------------------------------------------------