├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── ROADMAP.md ├── Rakefile ├── app ├── controllers │ └── knock │ │ ├── application_controller.rb │ │ └── auth_token_controller.rb └── model │ └── knock │ └── auth_token.rb ├── bin └── rails ├── config └── routes.rb ├── gemfiles ├── rails_5.gemfile └── rails_5.gemfile.lock ├── knock.gemspec ├── lib ├── knock.rb ├── knock │ ├── authenticable.rb │ ├── authenticable │ │ └── getter_name.rb │ ├── engine.rb │ ├── tokenizable.rb │ └── version.rb └── tasks │ └── knock_tasks.rake └── test ├── dummy ├── README.rdoc ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── images │ │ │ └── .keep │ │ ├── javascripts │ │ │ └── application.js │ │ └── stylesheets │ │ │ └── application.css │ ├── controllers │ │ ├── admin_protected_controller.rb │ │ ├── admin_token_controller.rb │ │ ├── application_controller.rb │ │ ├── composite_name_entity_protected_controller.rb │ │ ├── concerns │ │ │ └── .keep │ │ ├── current_users_controller.rb │ │ ├── custom_unauthorized_entity_controller.rb │ │ ├── guest_protected_controller.rb │ │ ├── protected_resources_controller.rb │ │ ├── v1 │ │ │ └── test_namespaced_controller.rb │ │ ├── vendor_protected_controller.rb │ │ └── vendor_token_controller.rb │ ├── helpers │ │ └── application_helper.rb │ ├── mailers │ │ └── .keep │ ├── models │ │ ├── .keep │ │ ├── admin.rb │ │ ├── composite_name_entity.rb │ │ ├── concerns │ │ │ └── .keep │ │ ├── guest.rb │ │ ├── user.rb │ │ ├── v1 │ │ │ └── user.rb │ │ └── vendor.rb │ └── views │ │ └── layouts │ │ └── application.html.erb ├── bin │ ├── bundle │ ├── rails │ ├── rake │ └── setup ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── knock.rb │ │ ├── mime_types.rb │ │ ├── session_store.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── routes.rb │ └── secrets.yml ├── db │ ├── migrate │ │ ├── 20150713101607_create_users.rb │ │ ├── 20160519075733_create_admins.rb │ │ ├── 20160522051816_create_vendors.rb │ │ ├── 20160522181712_create_composite_name_entities.rb │ │ └── 20161127203222_create_v1_users.rb │ └── schema.rb ├── lib │ └── assets │ │ └── .keep ├── log │ └── .keep ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ └── favicon.ico └── test │ ├── controllers │ ├── admin_protected_controller_test.rb │ ├── admin_token_controller_test.rb │ ├── composite_name_entity_protected_controller_test.rb │ ├── current_users_controller_test.rb │ ├── custom_unauthorized_entity_controller_test.rb │ ├── guest_protected_controller_test.rb │ ├── protected_resources_controller_test.rb │ ├── v1 │ │ └── test_namespaced_controller_test.rb │ ├── vendor_protected_controller_test.rb │ └── vendor_token_controller_test.rb │ └── models │ ├── admin_test.rb │ ├── user_test.rb │ └── vendor_test.rb ├── fixtures ├── admins.yml ├── composite_name_entities.yml ├── users.yml ├── v1_users.yml └── vendors.yml ├── knock_test.rb ├── model └── knock │ └── auth_token_test.rb ├── support └── generators_test_helper.rb ├── test_helper.rb └── unit └── knock └── authenticable └── getter_name_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/tmp/ 5 | test/dummy/db/*.sqlite3 6 | test/dummy/db/*.sqlite3-journal 7 | test/dummy/log/*.log 8 | test/dummy/tmp/ 9 | test/dummy/.sass-cache 10 | coverage/ 11 | *.gem 12 | Gemfile.lock 13 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Style/StringLiterals: 2 | EnforcedStyle: double_quotes 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.6 4 | before_script: 5 | - bundle exec rake db:migrate RAILS_ENV=test 6 | addons: 7 | code_climate: 8 | repo_token: 61c3ea804322306871a6b7e06a0a6435672cd29aea0c2b286e6b837234ba9d90 9 | deploy: 10 | provider: rubygems 11 | api_key: 12 | secure: WxGCqYToLKgYediVNmHUQ5SkJ3hfKIIu6K8b8Fvd4nb2v6p9tUo+Cz4xN/mTU1tUT5xF4D+P3Oek9Ih5GCL56s+i5C8T60v71azupcQoJpr9AlUxNVipJy+BaaNWb/yH2c3GWYtVJmG44s/nBSgG2WJLhkHSjHM+0CuUF0PknW8WUY7+zkZW3XsVh3bv9+O32uSYizdNt2IiccvqcjEKtmD+mZ5U5/Zwosy1ExmGXFXgFCKhK1oBu/ejqQdwRzY3LwquaTg9tEqR3Cl2dwDboXN1Ga5V7Q3+zhqCYPho33CtNr9ZhaeWS/fI3VcUa7O3fzS71JWSn97gqfqhkehXuGbptebLmLOgRjfe7RloH2VVc8NoqTFLYyvptD3522TG/kSH+etOVQgBpTGXf6TDD6FybH/ZQ0efFRzm00feAEmrbW+AkdSonTCVr/LYePnYWBPuBOZmOf2wuV0+84E5Jlw1wFRLG2JzRDWzScREIbZ8Q4ZvUihEKHIy8a76N6TBTRnAQv/FMfpx4wDXWibP3ji7De9HADjgkUOtk5qTKn8flvNGtJ+phBHyNEg0dFVVQdvyxUuhHWZV1SqKZOy4ITRSGzruufnvTrg09HKDDx/jImSrK3mT/64NVLmmnXMCz/CkLXLMcl6cohgyQCugucsNocy66AZYh8fTB3t0LQ4= 13 | gem: knock 14 | on: 15 | tags: true 16 | repo: nsarno/knock 17 | notifications: 18 | slack: 19 | secure: Z/aucu3B6NfMxVOky4FK43hlhrArG7JbkCj6MZvldRzPP5Q/z2GD+isFvvvJVIO92Tu9NkBlGaSYmTKeQH8L5j2oqGKapVfK7zCUuLYuerP96P4OsLywHfH1E9T7xr9lRr/e/oEFmojN4WUoCXCtOUNVLr3e5vGJxmHIwyDKuLZDaTdKmeO6dVBwonkuccgM11d1CQmI9s5IGtv3jtQZGsccVEPQGbCDoD4s4xI8aEcf4YsXdSJ5v8vg0R3XEvc+GTRPvKAdPlKkFHfWgVyu1An0g0hIfX7cvcUEOHiCbstefhAi8AkI9muDqfa2GStOAp5lSdyRs2/MJCr2K7jVNgXfzAg+RarKMpbM9gNAMxL4Qa4O6lwrng5ynL9TUX28uvA4ceLNTycjX/WQdR1EiQf6zS6N/DqxFrCzTE/2JEY4fnaKTt9bmpYMvF+yGDE5TnmrwtUehqCuN8ViSAOFspr6KP947Fu5Q0gDaZAvhiOoDuSYxe1D04f6N+hdaVtrFvPVHWAXp1rR6oQbVAFxJAoh9sAHpI1Fa/y/x1hWJkRf6DFeyRadJblGQsN6894UFlCL6x5RPvT70sZ8EOx6F1EMJpQIKqN7z2bmMkQmsA1RvQm5ZzGqxiwA8ZyyKpUtVYM31hcH1qwGKIUcGkgp0Sv+BHCjW8prnDEK5SARfgI= 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [2.2.0] - Unreleased 7 | 8 | ### Fixed 9 | 10 | - Autoloading & Zeitwerk issues 11 | - Update dependency to JWT 2.2.1 12 | - Update dependency to Rails 5 13 | - Implement `#respond_to_missing?` in `Authenticable` module 14 | 15 | ### Removed 16 | 17 | - Generators 18 | 19 | ## [2.1.1] - 2017-02-11 20 | 21 | ### Fixed 22 | 23 | - Stop trying to retrieve user from empty payload when no token is given 24 | 25 | ## [2.1] - 2017-01-31 26 | 27 | ### Fixed 28 | 29 | - Parsing of token controller to handle namespaces correctly 30 | 31 | ### Added 32 | 33 | - Configurable default validations by adding `verify_options` parameter to AuthToken initializer 34 | 35 | ## [2.0] - 2016-10-23 36 | 37 | ### Added 38 | 39 | - Configurable unauthorized response by overriding `Authenticable#unauthorized_entity` 40 | 41 | ### Removed 42 | 43 | - Deprecated features (see deprecated features in version 1.5) 44 | 45 | ## [1.5] - 2016-05-29 46 | 47 | ### Added 48 | 49 | - Exception configuration option `Knock.not_found_exception_class_name` 50 | - Multiple entity authentication (e.g. User, Admin, etc) 51 | - Possibility to have permanent tokens 52 | - Adding config options for exception class 53 | - Generator for token controller. E.g. `rails g knock:token_controller user` 54 | 55 | ### Changed 56 | 57 | - Deprecated `Authenticable#authenticate` in favor of `Authenticable#authenticate_user` 58 | - Deprecated use of `Knock.current_user_from_token` in favor of `User.from_token_payload` 59 | - Deprecated use of direct route to `AuthTokenController` in favor of generating a token controller 60 | - No need to mount the engine in `config/routes.rb` anymore 61 | 62 | ## [1.4.2] - 2016-01-29 63 | 64 | ### Fixed 65 | 66 | - Allow use of any or no prefix in authorization header. 67 | This fixes an unwanted breaking change introduced in `1.4.0` forcing the use 68 | of the `Bearer` prefix. 69 | 70 | ## [1.4.1] - 2016-01-08 71 | 72 | ### Fixed 73 | 74 | - Use lambda for audience verification 75 | 76 | ## [1.4.0] - 2016-01-02 77 | 78 | ### Changed 79 | 80 | - Allow use of rails versions above 4.2 81 | 82 | ### Added 83 | 84 | - Travis integration 85 | - Contribution guidelines 86 | - URL authentication 87 | - Allow use of different encoding algorithm 88 | - Expose `current_user` in the controllers without authenticating 89 | 90 | ### Fixed 91 | 92 | - Audience verification in token 93 | - Use lambda syntax compatible with older ruby versions 94 | - A few typos 95 | 96 | ## [1.3.0] - 2015-07-23 97 | 98 | ### Added 99 | 100 | - Configuration option for how the current_user is retrieved when signing in. 101 | - Configuration option for the handle attribute (email by default). 102 | 103 | ## [1.2.0] - 2015-07-16 104 | 105 | ### Added 106 | 107 | - Configuration option for how the current_user is retrieved when validating 108 | a token. (#1) 109 | 110 | ### Changed 111 | 112 | - Use "sub" claim to store the user id by default instead of "user_id". (#1) 113 | 114 | ### Fixed 115 | 116 | - Decode auth0_client_secret in default configuration for Auth0 117 | 118 | ## [1.1.0] - 2015-07-15 119 | 120 | ## [1.1.0.rc1] - 2015-07-15 121 | 122 | ### Added 123 | 124 | - `Knock.token_lifetime` configuration variable 125 | - `Knock.token_secret_signature_key` configuration variable 126 | - `Knock.token_audience` configuration variable 127 | - audience claim verification when decoding token 128 | - `Knock.setup` method for configuration in `knock.rb` initializer 129 | - generator for initializer (rails g knock:install) 130 | 131 | ## [1.0.0] - 2015-07-14 132 | 133 | ## [1.0.0.rc1] - 2015-07-14 134 | 135 | ### Added 136 | 137 | - `Knock::Authenticable` to secure endpoints with `before_action :authenticate` 138 | - `AuthToken` model provides JWT encapsulation 139 | - `AuthTokenController` provides out of the box sign in implementation 140 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Contribution is always highly appreciated. Here are a few guidelines to make it 4 | easier and faster for pull requests to get merged and issues to get fixed. 5 | 6 | ### Issues 7 | 8 | - Provide steps to reproduce the issue 9 | - Even better if possible, provide a failing test 10 | 11 | ### Pull Requests 12 | 13 | - Provide test for any code you add or change 14 | - Add your changes to the changelog in the `Unreleased` section 15 | 16 | Thanks ♥️ 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Declare your gem's dependencies in knock.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use a debugger 14 | # gem 'byebug', group: [:development, :test] 15 | 16 | gem "simplecov", require: false, group: :test 17 | 18 | group :development do 19 | gem "bundler" 20 | gem "rake" 21 | end 22 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Arnaud MESUREUR 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## DISCLAIMER 2 | 3 | This project is not being maintained and I don't recommend using it in its current form. 4 | As an alternative, I recommend using the [jwt](https://github.com/jwt/ruby-jwt) gem directly. 5 | 6 | # knock 7 | 8 | [![Gem Version](https://badge.fury.io/rb/knock.svg)](http://badge.fury.io/rb/knock) 9 | [![Build Status](https://travis-ci.org/nsarno/knock.svg)](https://travis-ci.org/nsarno/knock) 10 | [![Code Climate](https://codeclimate.com/github/nsarno/knock/badges/gpa.svg)](https://codeclimate.com/github/nsarno/knock) 11 | 12 | Seamless JWT authentication for Rails API 13 | 14 | ## Description 15 | 16 | Knock is an authentication solution for Rails API-only application based on JSON Web Tokens. 17 | 18 | ## Getting Started 19 | 20 | ### Installation 21 | 22 | Add this line to your application's Gemfile: 23 | 24 | ```ruby 25 | gem 'knock' 26 | ``` 27 | 28 | Then execute: 29 | 30 | $ bundle install 31 | 32 | ### Requirements 33 | 34 | Knock makes one assumption about your user model: 35 | 36 | It must have an `authenticate` method, similar to the one added by [has_secure_password](http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password). 37 | 38 | ```ruby 39 | class User < ActiveRecord::Base 40 | has_secure_password 41 | end 42 | ``` 43 | 44 | Using `has_secure_password` is recommended, but you don't have to as long as your user model implements an `authenticate` instance method with the same behavior. 45 | 46 | ### Usage 47 | 48 | Include the `Knock::Authenticable` module in your `ApplicationController` 49 | 50 | ```ruby 51 | class ApplicationController < ActionController::API 52 | include Knock::Authenticable 53 | end 54 | ``` 55 | 56 | You can now protect your resources by calling `authenticate_user` as a before_action 57 | inside your controllers: 58 | 59 | ```ruby 60 | class SecuredController < ApplicationController 61 | before_action :authenticate_user 62 | 63 | def index 64 | # etc... 65 | end 66 | 67 | # etc... 68 | end 69 | ``` 70 | 71 | You can access the current user in your controller with `current_user`. 72 | 73 | If no valid token is passed with the request, Knock will respond with: 74 | 75 | ``` 76 | head :unauthorized 77 | ``` 78 | 79 | You can modify this behaviour by overriding `unauthorized_entity` in your controller. 80 | 81 | You also have access directly to `current_user` which will try to authenticate or return `nil`: 82 | 83 | ```ruby 84 | def index 85 | if current_user 86 | # do something 87 | else 88 | # do something else 89 | end 90 | end 91 | ``` 92 | 93 | _Note: the `authenticate_user` method uses the `current_user` method. Overwriting `current_user` may cause unexpected behaviour._ 94 | 95 | You can do the exact same thing for any entity. E.g. for `Admin`, use `authenticate_admin` and `current_admin` instead. 96 | 97 | If you're using a namespaced model, Knock won't be able to infer it automatically from the method name. Instead you can use `authenticate_for` directly like this: 98 | 99 | ```ruby 100 | class ApplicationController < ActionController::Base 101 | include Knock::Authenticable 102 | 103 | private 104 | 105 | def authenticate_v1_user 106 | authenticate_for V1::User 107 | end 108 | end 109 | ``` 110 | 111 | ```ruby 112 | class SecuredController < ApplicationController 113 | before_action :authenticate_v1_user 114 | end 115 | ``` 116 | 117 | Then you get the current user by calling `current_v1_user` instead of `current_user`. 118 | 119 | ### Configuration 120 | 121 | #### In the entity model 122 | 123 | The entity model (e.g. `User`) can implement specific methods to provide 124 | customization over different parts of the authentication process. 125 | 126 | - **Find the entity when creating the token (when signing in)** 127 | 128 | By default, Knock tries to find the entity by email. If you want to modify this 129 | behaviour, implement within your entity model a class method `from_token_request` 130 | that takes the request in argument. 131 | 132 | E.g. 133 | 134 | ```ruby 135 | class User < ActiveRecord::Base 136 | def self.from_token_request request 137 | # Returns a valid user, `nil` or raise `Knock.not_found_exception_class_name` 138 | # e.g. 139 | # email = request.params["auth"] && request.params["auth"]["email"] 140 | # self.find_by email: email 141 | end 142 | end 143 | ``` 144 | 145 | - **Find the authenticated entity from the token payload (when authenticating a request)** 146 | 147 | By default, Knock assumes the payload as a subject (`sub`) claim containing the entity's id 148 | and calls `find` on the model. If you want to modify this behaviour, implement within 149 | your entity model a class method `from_token_payload` that takes the 150 | payload in argument. 151 | 152 | E.g. 153 | 154 | ```ruby 155 | class User < ActiveRecord::Base 156 | def self.from_token_payload payload 157 | # Returns a valid user, `nil` or raise 158 | # e.g. 159 | # self.find payload["sub"] 160 | end 161 | end 162 | ``` 163 | 164 | - **Modify the token payload** 165 | 166 | By default the token payload contains the entity's id inside the subject (`sub`) claim. 167 | If you want to modify this behaviour, implement within your entity model an instance method 168 | `to_token_payload` that returns a hash representing the payload. 169 | 170 | E.g. 171 | 172 | ```ruby 173 | class User < ActiveRecord::Base 174 | def to_token_payload 175 | # Returns the payload as a hash 176 | end 177 | end 178 | ``` 179 | 180 | - **Token Lifetime** 181 | 182 | By default the generated tokens will be valid, after generated, for 1 day. 183 | You can change it in the Knock configuration file (config/knock.rb), 184 | setting the desired lifetime: 185 | 186 | E.g. 187 | 188 | ```ruby 189 | Knock.token_lifetime = 3.hours 190 | ``` 191 | 192 | If you are generating tokens for more than one entity, you can pass 193 | each lifetime in a hash, using the entities class names as keys, like: 194 | 195 | E.g. 196 | 197 | ```ruby 198 | # How long before a token is expired. If nil is provided, 199 | # token will last forever. 200 | Knock.token_lifetime = { 201 | user: 1.day 202 | admin: 30.minutes 203 | } 204 | ``` 205 | 206 | #### In the initializer 207 | 208 | Read [lib/knock.rb](https://github.com/nsarno/knock/blob/master/lib/knock.rb) to learn about all the possible configuration options and their default values. 209 | 210 | You can create an initializer like in the example below: 211 | 212 | Inside `config/initializers/knock.rb` 213 | 214 | ```ruby 215 | Knock.setup do |config| 216 | config.token_lifetime = 1.hour 217 | 218 | # For Auth0 219 | config.token_audience = -> { Rails.application.secrets.auth0_client_id } 220 | config.token_secret_signature_key = -> { JWT.base64url_decode Rails.application.secrets.auth0_client_secret } 221 | end 222 | ``` 223 | 224 | ### Authenticating from a web or mobile application 225 | 226 | Example request to get a token from your API: 227 | 228 | ``` 229 | POST /user_token 230 | {"auth": {"email": "foo@bar.com", "password": "secret"}} 231 | ``` 232 | 233 | Example response from the API: 234 | 235 | ``` 236 | 201 Created 237 | {"jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"} 238 | ``` 239 | 240 | To make an authenticated request to your API, you need to pass the token via the request header: 241 | 242 | ``` 243 | Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 244 | GET /my_resources 245 | ``` 246 | 247 | Knock responds with a `404 Not Found` when the user cannot be found or the password is invalid. This is a security best practice to avoid giving away information about the existence or not of a particular user. 248 | 249 | **NB:** HTTPS should always be enabled when sending a password or token in your request. 250 | 251 | ### Authenticated tests 252 | 253 | To authenticate within your tests: 254 | 255 | 1. Create a valid token 256 | 2. Pass it in your request 257 | 258 | e.g. 259 | 260 | ```ruby 261 | class SecuredResourcesControllerTest < ActionDispatch::IntegrationTest 262 | def authenticated_header 263 | token = Knock::AuthToken.new(payload: { sub: users(:one).id }).token 264 | 265 | { 266 | 'Authorization': "Bearer #{token}" 267 | } 268 | end 269 | 270 | it 'responds successfully' do 271 | get secured_resources_url, headers: authenticated_header 272 | 273 | assert_response :success 274 | end 275 | end 276 | ``` 277 | 278 | #### Without ActiveRecord 279 | 280 | If no ActiveRecord is used, then you will need to specify what Exception will be used when the user is not found with the given credentials. 281 | 282 | ```ruby 283 | Knock.setup do |config| 284 | 285 | # Exception Class 286 | # --------------- 287 | # 288 | # Configure the Exception to be used (raised and rescued) for User Not Found. 289 | # note: change this if ActiveRecord is not being used. 290 | # 291 | # Default: 292 | config.not_found_exception_class_name = 'MyCustomException' 293 | end 294 | ``` 295 | 296 | ### Algorithms 297 | 298 | The JWT spec supports different kind of cryptographic signing algorithms. 299 | You can set `token_signature_algorithm` to use the one you want in the 300 | initializer or do nothing and use the default one (HS256). 301 | 302 | You can specify any of the algorithms supported by the 303 | [jwt](https://github.com/jwt/ruby-jwt) gem. 304 | 305 | If the algorithm you use requires a public key, you also need to set 306 | `token_public_key` in the initializer. 307 | 308 | ## CORS 309 | 310 | To enable cross-origin resource sharing, check out the [rack-cors](https://github.com/cyu/rack-cors) gem. 311 | 312 | ## Related links 313 | 314 | - [10 things you should know about tokens](https://auth0.com/blog/2014/01/27/ten-things-you-should-know-about-tokens-and-cookies/) 315 | 316 | ## Contributing 317 | 318 | 1. Fork it ( https://github.com/nsarno/knock/fork ) 319 | 2. Create your feature branch (`git checkout -b my-new-feature`) 320 | 3. Commit your changes (`git commit -am 'Add some feature'`) 321 | 4. Push to the branch (`git push origin my-new-feature`) 322 | 5. Create a new Pull Request 323 | 324 | ## License 325 | 326 | MIT 327 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | ### Roadmap 2 | 3 | Keeping track here of changes considered for the future. 4 | 5 | - [ ] Support multiple token expiry times (#241) 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | 3 | begin 4 | require 'bundler/setup' 5 | rescue LoadError 6 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 7 | end 8 | 9 | require 'rdoc/task' 10 | 11 | RDoc::Task.new(:rdoc) do |rdoc| 12 | rdoc.rdoc_dir = 'rdoc' 13 | rdoc.title = 'Knock' 14 | rdoc.options << '--line-numbers' 15 | rdoc.rdoc_files.include('README.rdoc') 16 | rdoc.rdoc_files.include('lib/**/*.rb') 17 | end 18 | 19 | APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__) 20 | load 'rails/tasks/engine.rake' 21 | 22 | 23 | load 'rails/tasks/statistics.rake' 24 | 25 | 26 | 27 | Bundler::GemHelper.install_tasks 28 | 29 | require 'rake/testtask' 30 | 31 | Rake::TestTask.new(:test) do |t| 32 | t.libs << 'lib' 33 | t.libs << 'test' 34 | t.pattern = 'test/**/*_test.rb' 35 | t.verbose = false 36 | end 37 | 38 | 39 | task default: :test 40 | -------------------------------------------------------------------------------- /app/controllers/knock/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Knock 2 | class ApplicationController < ActionController::API 3 | rescue_from Knock.not_found_exception_class_name, with: :not_found 4 | 5 | private 6 | 7 | def not_found 8 | head :not_found 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/knock/auth_token_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency "knock/application_controller" 2 | 3 | module Knock 4 | class AuthTokenController < ApplicationController 5 | before_action :authenticate 6 | 7 | def create 8 | render json: auth_token, status: :created 9 | end 10 | 11 | private 12 | 13 | def authenticate 14 | unless entity.present? && entity.authenticate(auth_params[:password]) 15 | raise Knock.not_found_exception_class 16 | end 17 | end 18 | 19 | def auth_token 20 | if entity.respond_to? :to_token_payload 21 | AuthToken.new payload: entity.to_token_payload 22 | else 23 | AuthToken.new payload: { sub: entity.id } 24 | end 25 | end 26 | 27 | def entity 28 | @entity ||= 29 | if entity_class.respond_to? :from_token_request 30 | entity_class.from_token_request request 31 | else 32 | entity_class.find_by email: auth_params[:email] 33 | end 34 | end 35 | 36 | def entity_class 37 | entity_name.constantize 38 | end 39 | 40 | def entity_name 41 | self.class.name.scan(/\w+/).last.split('TokenController').first 42 | end 43 | 44 | def auth_params 45 | params.require(:auth).permit :email, :password 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/model/knock/auth_token.rb: -------------------------------------------------------------------------------- 1 | require "jwt" 2 | 3 | module Knock 4 | class AuthToken 5 | attr_reader :token 6 | attr_reader :payload 7 | attr_reader :entity_class_name 8 | 9 | def initialize(payload: {}, token: nil, verify_options: {}, entity_class_name: nil) 10 | @entity_class_name = entity_class_name 11 | if token.present? 12 | @payload, _ = JWT.decode token.to_s, decode_key, true, options.merge(verify_options) 13 | @token = token 14 | else 15 | @payload = claims.merge(payload) 16 | @token = JWT.encode @payload, 17 | secret_key, 18 | Knock.token_signature_algorithm 19 | end 20 | end 21 | 22 | def entity_for(entity_class) 23 | if entity_class.respond_to? :from_token_payload 24 | entity_class.from_token_payload @payload 25 | else 26 | entity_class.find @payload["sub"] 27 | end 28 | end 29 | 30 | def to_json(options = {}) 31 | { jwt: @token }.to_json 32 | end 33 | 34 | private 35 | 36 | def secret_key 37 | Knock.token_secret_signature_key.call 38 | end 39 | 40 | def decode_key 41 | Knock.token_public_key || secret_key 42 | end 43 | 44 | def options 45 | verify_claims.merge({ 46 | algorithm: Knock.token_signature_algorithm, 47 | }) 48 | end 49 | 50 | def claims 51 | _claims = {} 52 | _claims[:exp] = token_lifetime if verify_lifetime? 53 | _claims[:aud] = token_audience if verify_audience? 54 | _claims 55 | end 56 | 57 | def token_lifetime 58 | return unless verify_lifetime? 59 | 60 | if Knock.token_lifetime.is_a?(Hash) 61 | Knock.token_lifetime[entity_class_name].from_now.to_i 62 | else 63 | Knock.token_lifetime.from_now.to_i 64 | end 65 | end 66 | 67 | def verify_lifetime? 68 | !Knock.token_lifetime.nil? 69 | end 70 | 71 | def verify_claims 72 | { 73 | aud: token_audience, 74 | verify_aud: verify_audience?, 75 | verify_expiration: verify_lifetime?, 76 | } 77 | end 78 | 79 | def token_audience 80 | verify_audience? && Knock.token_audience.call 81 | end 82 | 83 | def verify_audience? 84 | Knock.token_audience.present? 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/knock/engine', __FILE__) 6 | 7 | # Set up gems listed in the Gemfile. 8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 10 | 11 | require 'rails/all' 12 | require 'rails/engine/commands' 13 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Knock::Engine.routes.draw do 2 | post 'auth_token' => 'auth_token#create' 3 | end 4 | -------------------------------------------------------------------------------- /gemfiles/rails_5.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "codeclimate-test-reporter", :group => :test, :require => nil 6 | gem "simplecov", :require => false, :group => :test 7 | gem "rails", "~> 5.0.0" 8 | 9 | group :development do 10 | gem "bundler" 11 | gem "rake" 12 | gem "appraisal" 13 | end 14 | 15 | gemspec :path => "../" 16 | -------------------------------------------------------------------------------- /gemfiles/rails_5.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../ 3 | specs: 4 | knock (1.5) 5 | bcrypt (~> 3.1) 6 | jwt (~> 1.5) 7 | rails (>= 4.2) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | actioncable (5.0.0.1) 13 | actionpack (= 5.0.0.1) 14 | nio4r (~> 1.2) 15 | websocket-driver (~> 0.6.1) 16 | actionmailer (5.0.0.1) 17 | actionpack (= 5.0.0.1) 18 | actionview (= 5.0.0.1) 19 | activejob (= 5.0.0.1) 20 | mail (~> 2.5, >= 2.5.4) 21 | rails-dom-testing (~> 2.0) 22 | actionpack (5.0.0.1) 23 | actionview (= 5.0.0.1) 24 | activesupport (= 5.0.0.1) 25 | rack (~> 2.0) 26 | rack-test (~> 0.6.3) 27 | rails-dom-testing (~> 2.0) 28 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 29 | actionview (5.0.0.1) 30 | activesupport (= 5.0.0.1) 31 | builder (~> 3.1) 32 | erubis (~> 2.7.0) 33 | rails-dom-testing (~> 2.0) 34 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 35 | activejob (5.0.0.1) 36 | activesupport (= 5.0.0.1) 37 | globalid (>= 0.3.6) 38 | activemodel (5.0.0.1) 39 | activesupport (= 5.0.0.1) 40 | activerecord (5.0.0.1) 41 | activemodel (= 5.0.0.1) 42 | activesupport (= 5.0.0.1) 43 | arel (~> 7.0) 44 | activesupport (5.0.0.1) 45 | concurrent-ruby (~> 1.0, >= 1.0.2) 46 | i18n (~> 0.7) 47 | minitest (~> 5.1) 48 | tzinfo (~> 1.1) 49 | appraisal (2.1.0) 50 | bundler 51 | rake 52 | thor (>= 0.14.0) 53 | arel (7.1.2) 54 | bcrypt (3.1.11) 55 | builder (3.2.2) 56 | codeclimate-test-reporter (0.6.0) 57 | simplecov (>= 0.7.1, < 1.0.0) 58 | concurrent-ruby (1.0.2) 59 | docile (1.1.5) 60 | erubis (2.7.0) 61 | globalid (0.3.7) 62 | activesupport (>= 4.1.0) 63 | i18n (0.7.0) 64 | json (2.0.2) 65 | jwt (1.5.6) 66 | loofah (2.0.3) 67 | nokogiri (>= 1.5.9) 68 | mail (2.6.4) 69 | mime-types (>= 1.16, < 4) 70 | method_source (0.8.2) 71 | mime-types (3.1) 72 | mime-types-data (~> 3.2015) 73 | mime-types-data (3.2016.0521) 74 | mini_portile2 (2.1.0) 75 | minitest (5.9.0) 76 | nio4r (1.2.1) 77 | nokogiri (1.6.8) 78 | mini_portile2 (~> 2.1.0) 79 | pkg-config (~> 1.1.7) 80 | pkg-config (1.1.7) 81 | rack (2.0.1) 82 | rack-test (0.6.3) 83 | rack (>= 1.0) 84 | rails (5.0.0.1) 85 | actioncable (= 5.0.0.1) 86 | actionmailer (= 5.0.0.1) 87 | actionpack (= 5.0.0.1) 88 | actionview (= 5.0.0.1) 89 | activejob (= 5.0.0.1) 90 | activemodel (= 5.0.0.1) 91 | activerecord (= 5.0.0.1) 92 | activesupport (= 5.0.0.1) 93 | bundler (>= 1.3.0, < 2.0) 94 | railties (= 5.0.0.1) 95 | sprockets-rails (>= 2.0.0) 96 | rails-dom-testing (2.0.1) 97 | activesupport (>= 4.2.0, < 6.0) 98 | nokogiri (~> 1.6.0) 99 | rails-html-sanitizer (1.0.3) 100 | loofah (~> 2.0) 101 | railties (5.0.0.1) 102 | actionpack (= 5.0.0.1) 103 | activesupport (= 5.0.0.1) 104 | method_source 105 | rake (>= 0.8.7) 106 | thor (>= 0.18.1, < 2.0) 107 | rake (11.3.0) 108 | simplecov (0.12.0) 109 | docile (~> 1.1.0) 110 | json (>= 1.8, < 3) 111 | simplecov-html (~> 0.10.0) 112 | simplecov-html (0.10.0) 113 | sprockets (3.7.0) 114 | concurrent-ruby (~> 1.0) 115 | rack (> 1, < 3) 116 | sprockets-rails (3.2.0) 117 | actionpack (>= 4.0) 118 | activesupport (>= 4.0) 119 | sprockets (>= 3.0.0) 120 | sqlite3 (1.3.11) 121 | thor (0.19.1) 122 | thread_safe (0.3.5) 123 | timecop (0.8.1) 124 | tzinfo (1.2.2) 125 | thread_safe (~> 0.1) 126 | websocket-driver (0.6.4) 127 | websocket-extensions (>= 0.1.0) 128 | websocket-extensions (0.1.2) 129 | 130 | PLATFORMS 131 | ruby 132 | 133 | DEPENDENCIES 134 | appraisal 135 | bundler 136 | codeclimate-test-reporter 137 | knock! 138 | rails (~> 5.0.0) 139 | rake 140 | simplecov 141 | sqlite3 (~> 1.3) 142 | timecop (~> 0.8.0) 143 | 144 | BUNDLED WITH 145 | 1.12.5 146 | -------------------------------------------------------------------------------- /knock.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "knock/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "knock" 9 | s.version = Knock::Version::VERSION 10 | s.authors = ["Arnaud MESUREUR", "Ghjuvan-Carlu BIANCHI"] 11 | s.email = ["arnaud.mesureur@gmail.com"] 12 | s.homepage = "https://github.com/nsarno/knock" 13 | s.summary = "Seamless JWT authentication for Rails API." 14 | s.description = "Authentication solution for Rails based on JWT" 15 | s.license = "MIT" 16 | 17 | s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"] 18 | s.test_files = Dir["test/**/*"] 19 | 20 | s.add_dependency "rails", ">= 5" 21 | s.add_dependency "jwt", "~> 2.2.1" 22 | s.add_dependency "bcrypt", "~> 3.1" 23 | 24 | s.add_development_dependency "sqlite3", "~> 1.3" 25 | s.add_development_dependency "timecop", "~> 0.8.0" 26 | end 27 | -------------------------------------------------------------------------------- /lib/knock.rb: -------------------------------------------------------------------------------- 1 | require "knock/engine" 2 | 3 | module Knock 4 | # How long before a token is expired. If nil is provided, 5 | # token will last forever. 6 | mattr_accessor :token_lifetime 7 | self.token_lifetime = 1.day 8 | 9 | # Configure the audience claim to identify the recipients that the token 10 | # is intended for. 11 | mattr_accessor :token_audience 12 | self.token_audience = nil 13 | 14 | # Configure the algorithm used to encode the token 15 | mattr_accessor :token_signature_algorithm 16 | self.token_signature_algorithm = "HS256" 17 | 18 | # Configure the key used to sign tokens. 19 | mattr_accessor :token_secret_signature_key 20 | self.token_secret_signature_key = -> do 21 | if Rails.application.respond_to?(:secret_key_base) 22 | Rails.application.secret_key_base 23 | else 24 | Rails.application.secrets.secret_key_base 25 | end 26 | end 27 | 28 | # Configure the public key used to decode tokens, when required. 29 | mattr_accessor :token_public_key 30 | self.token_public_key = nil 31 | 32 | # Configure the exception to be used when user cannot be found. 33 | mattr_accessor :not_found_exception_class_name 34 | self.not_found_exception_class_name = "ActiveRecord::RecordNotFound" 35 | 36 | def self.not_found_exception_class 37 | not_found_exception_class_name.to_s.constantize 38 | end 39 | 40 | # Default way to setup Knock. Run `rails generate knock:install` to create 41 | # a fresh initializer with all configuration values. 42 | def self.setup 43 | yield self 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/knock/authenticable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Knock 4 | # Include this module in your controller to enable authentication 5 | # for your endpoint. 6 | # 7 | # e.g. 8 | # Calling `authenticate_user` will try to find a valid `User` based on 9 | # the token payload. 10 | module Authenticable 11 | def authenticate_for(entity_class) 12 | getter_name = GetterName.new(entity_class).cleared 13 | define_current_entity_getter(entity_class, getter_name) 14 | public_send(getter_name) 15 | end 16 | 17 | private 18 | 19 | def token 20 | params[:token] || token_from_request_headers 21 | end 22 | 23 | def method_missing(method, *args) 24 | prefix, entity_name = method.to_s.split("_", 2) 25 | case prefix 26 | when "authenticate" 27 | unauthorized_entity(entity_name) unless authenticate_entity(entity_name) 28 | when "current" 29 | authenticate_entity(entity_name) 30 | else 31 | super 32 | end 33 | end 34 | 35 | def respond_to_missing?(method, *) 36 | prefix, = method.to_s.split("_", 2) 37 | case prefix 38 | when "authenticate" 39 | true 40 | else 41 | super 42 | end 43 | end 44 | 45 | def authenticate_entity(entity_name) 46 | return unless token 47 | 48 | entity_class = entity_name.camelize.constantize 49 | send(:authenticate_for, entity_class) 50 | end 51 | 52 | def unauthorized_entity(_entity_name) 53 | head(:unauthorized) 54 | end 55 | 56 | def token_from_request_headers 57 | request.headers["Authorization"]&.split&.last 58 | end 59 | 60 | # Dynamically defines a method similar to the example below. 61 | # 62 | # def current_user 63 | # @_current_user ||= fetch_entity_from_token(User) 64 | # end 65 | def define_current_entity_getter(entity_class, getter_name) 66 | return if respond_to?(getter_name) 67 | 68 | memoization_var_name = "@_#{getter_name}" 69 | self.class.send(:define_method, getter_name) do 70 | unless instance_variable_defined?(memoization_var_name) 71 | current = fetch_entity_from_token(entity_class) 72 | instance_variable_set(memoization_var_name, current) 73 | end 74 | instance_variable_get(memoization_var_name) 75 | end 76 | end 77 | 78 | def fetch_entity_from_token(entity_class) 79 | Knock::AuthToken.new(token: token).entity_for(entity_class) 80 | rescue Knock.not_found_exception_class, JWT::DecodeError, JWT::EncodeError 81 | nil 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/knock/authenticable/getter_name.rb: -------------------------------------------------------------------------------- 1 | module Knock 2 | module Authenticable 3 | class GetterName 4 | attr_reader :entity_class 5 | 6 | def initialize(entity_class) 7 | @entity_class = entity_class 8 | end 9 | 10 | def cleared 11 | "current_#{entity_class.to_s.gsub('::', '').underscore}" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/knock/engine.rb: -------------------------------------------------------------------------------- 1 | module Knock 2 | class Engine < ::Rails::Engine 3 | paths.add "lib", eager_load: true 4 | isolate_namespace Knock 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/knock/tokenizable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Knock 3 | # Include this module in your entity class (e.g. User) 4 | # for token serialization and deserialization. 5 | module Tokenizable 6 | def self.included(base) 7 | base.extends ClassMethods 8 | end 9 | 10 | module ClassMethods 11 | def from_token_payload(payload) 12 | find(payload["sub"]) 13 | end 14 | 15 | def from_token(token) 16 | auth_token = AuthToken.new(token: token) 17 | from_token_payload(auth_token.payload) 18 | end 19 | end 20 | 21 | def to_token_payload 22 | { sub: @object.id } 23 | end 24 | 25 | def to_token 26 | AuthToken.new(payload: to_token_payload).token 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/knock/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Knock 4 | class Version 5 | VERSION = "2.2.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/tasks/knock_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :knock do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /test/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsarno/knock/8e8b3e8d29eccb83a83ea2449e006250f025632a/test/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/admin_protected_controller.rb: -------------------------------------------------------------------------------- 1 | class AdminProtectedController < ApplicationController 2 | before_action :authenticate_admin 3 | 4 | def index 5 | head :ok 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/admin_token_controller.rb: -------------------------------------------------------------------------------- 1 | class AdminTokenController < Knock::AuthTokenController 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :null_session 5 | 6 | include Knock::Authenticable 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/composite_name_entity_protected_controller.rb: -------------------------------------------------------------------------------- 1 | class CompositeNameEntityProtectedController < ApplicationController 2 | before_action :authenticate_composite_name_entity 3 | 4 | def index 5 | head :ok 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsarno/knock/8e8b3e8d29eccb83a83ea2449e006250f025632a/test/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/controllers/current_users_controller.rb: -------------------------------------------------------------------------------- 1 | class CurrentUsersController < ApplicationController 2 | def show 3 | if current_user 4 | head :ok 5 | else 6 | head :not_found 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/custom_unauthorized_entity_controller.rb: -------------------------------------------------------------------------------- 1 | class CustomUnauthorizedEntityController < ApplicationController 2 | before_action :authenticate_user 3 | 4 | def index 5 | head :ok 6 | end 7 | 8 | private 9 | 10 | def unauthorized_entity(entity) 11 | head :not_found 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/guest_protected_controller.rb: -------------------------------------------------------------------------------- 1 | class GuestProtectedController < ApplicationController 2 | before_action :authenticate_guest 3 | 4 | def index 5 | head :ok 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/protected_resources_controller.rb: -------------------------------------------------------------------------------- 1 | class ProtectedResourcesController < ApplicationController 2 | before_action :authenticate_user 3 | 4 | def index 5 | head :ok 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/v1/test_namespaced_controller.rb: -------------------------------------------------------------------------------- 1 | module V1 2 | class TestNamespacedController < ApplicationController 3 | 4 | before_action :authenticate_v1_user 5 | 6 | def index 7 | head :ok 8 | end 9 | 10 | private 11 | 12 | def authenticate_v1_user 13 | authenticate_for V1::User 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/vendor_protected_controller.rb: -------------------------------------------------------------------------------- 1 | class VendorProtectedController < ApplicationController 2 | before_action :authenticate_vendor, only: [:index] 3 | before_action :some_missing_method, only: [:show] 4 | 5 | def index 6 | head :ok 7 | end 8 | 9 | def show 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/vendor_token_controller.rb: -------------------------------------------------------------------------------- 1 | class VendorTokenController < Knock::AuthTokenController 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsarno/knock/8e8b3e8d29eccb83a83ea2449e006250f025632a/test/dummy/app/mailers/.keep -------------------------------------------------------------------------------- /test/dummy/app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsarno/knock/8e8b3e8d29eccb83a83ea2449e006250f025632a/test/dummy/app/models/.keep -------------------------------------------------------------------------------- /test/dummy/app/models/admin.rb: -------------------------------------------------------------------------------- 1 | class Admin < ActiveRecord::Base 2 | has_secure_password 3 | 4 | def self.from_token_request request 5 | email = request.params["auth"] && request.params["auth"]["email"] 6 | self.find_by email: email 7 | end 8 | 9 | def self.from_token_payload payload 10 | self.find payload["sub"] 11 | end 12 | 13 | def to_token_payload 14 | {sub: id} 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/dummy/app/models/composite_name_entity.rb: -------------------------------------------------------------------------------- 1 | class CompositeNameEntity < ActiveRecord::Base 2 | has_secure_password 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsarno/knock/8e8b3e8d29eccb83a83ea2449e006250f025632a/test/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/models/guest.rb: -------------------------------------------------------------------------------- 1 | class Guest 2 | def self.from_token_payload _payload 3 | # This is to simulate the use of `find_or_create` 4 | # on an AR model, regardless of the payload content 5 | new 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | has_secure_password 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/v1/user.rb: -------------------------------------------------------------------------------- 1 | module V1 2 | class User < ActiveRecord::Base 3 | has_secure_password 4 | end 5 | end -------------------------------------------------------------------------------- /test/dummy/app/models/vendor.rb: -------------------------------------------------------------------------------- 1 | class Vendor < ActiveRecord::Base 2 | has_secure_password 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /test/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /test/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /test/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../boot", __FILE__) 2 | 3 | require "rails" 4 | require "active_model/railtie" 5 | require "active_record/railtie" 6 | require "action_controller/railtie" 7 | require "action_mailer/railtie" 8 | require "action_view/railtie" 9 | require "action_cable/engine" 10 | require "sprockets/railtie" 11 | 12 | Bundler.require(*Rails.groups) 13 | require "knock" 14 | 15 | module Dummy 16 | class Application < Rails::Application 17 | # Settings in config/environments/* take precedence over those specified here. 18 | # Application configuration should go into files in config/initializers 19 | # -- all .rb files in that directory are automatically loaded. 20 | 21 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 22 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 23 | # config.time_zone = 'Central Time (US & Canada)' 24 | 25 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 26 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 27 | # config.i18n.default_locale = :de 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Use default logging formatter so that PID and timestamp are not suppressed. 75 | config.log_formatter = ::Logger::Formatter.new 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | end 80 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | if Gem.loaded_specs["rails"].version.to_s.to_i >= 5 17 | config.public_file_server.enabled = true 18 | config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' } 19 | else 20 | config.serve_static_files = true 21 | config.static_cache_control = 'public, max-age=3600' 22 | end 23 | 24 | # Show full error reports and disable caching. 25 | config.consider_all_requests_local = true 26 | config.action_controller.perform_caching = false 27 | 28 | # Raise exceptions instead of rendering exception templates. 29 | config.action_dispatch.show_exceptions = false 30 | 31 | # Disable request forgery protection in test environment. 32 | config.action_controller.allow_forgery_protection = false 33 | 34 | # Tell Action Mailer not to deliver emails to the real world. 35 | # The :test delivery method accumulates sent emails in the 36 | # ActionMailer::Base.deliveries array. 37 | config.action_mailer.delivery_method = :test 38 | 39 | # Randomize the order test cases are executed. 40 | config.active_support.test_order = :random 41 | 42 | # Print deprecation notices to the stderr. 43 | config.active_support.deprecation = :stderr 44 | 45 | # Raises error for missing translations 46 | # config.action_view.raise_on_missing_translations = true 47 | end 48 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/knock.rb: -------------------------------------------------------------------------------- 1 | Knock.setup do |config| 2 | config.token_signature_algorithm = 'HS256' 3 | config.token_secret_signature_key = -> { Rails.application.secret_key_base } 4 | config.token_public_key = nil 5 | config.token_audience = nil 6 | 7 | config.not_found_exception_class_name = 'ActiveRecord::RecordNotFound' 8 | end 9 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | post 'admin_token' => 'admin_token#create' 3 | post 'vendor_token' => 'vendor_token#create' 4 | 5 | resource :current_user 6 | 7 | resources :admin_protected 8 | resources :composite_name_entity_protected 9 | resources :custom_unauthorized_entity 10 | resources :guest_protected 11 | resources :protected_resources 12 | resources :vendor_protected 13 | 14 | namespace :v1 do 15 | resources :test_namespaced 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 488bdeac12dec04d250e26f9066540eb82ca9972ad62dc7002a0434ea0573e584dba30dc62c38454cd915b453c02790ee81d41a0ba2c324b7d1db02ee116d412 15 | 16 | test: 17 | secret_key_base: d0b2c379485efedaf6ae2633caf9cb9f7f74297528ef736930c2adaf8e2e7d0654ff1e3f1634217244177511c545de40c84571c3e2be5fc0e6f783fd4d520080 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20150713101607_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :users do |t| 4 | t.string :email, unique: true, null: false 5 | t.string :password_digest, null: false 6 | 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20160519075733_create_admins.rb: -------------------------------------------------------------------------------- 1 | class CreateAdmins < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :admins do |t| 4 | t.string :email 5 | t.string :password_digest 6 | 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20160522051816_create_vendors.rb: -------------------------------------------------------------------------------- 1 | class CreateVendors < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :vendors do |t| 4 | t.string :email 5 | t.string :password_digest 6 | 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20160522181712_create_composite_name_entities.rb: -------------------------------------------------------------------------------- 1 | class CreateCompositeNameEntities < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :composite_name_entities do |t| 4 | t.string :email 5 | t.string :password_digest 6 | 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20161127203222_create_v1_users.rb: -------------------------------------------------------------------------------- 1 | class CreateV1Users < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :v1_users do |t| 4 | 5 | t.string :email, unique: true, null: false 6 | t.string :password_digest, null: false 7 | 8 | t.timestamps null: false 9 | 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2016_11_27_203222) do 14 | 15 | create_table "admins", force: :cascade do |t| 16 | t.string "email" 17 | t.string "password_digest" 18 | t.datetime "created_at", null: false 19 | t.datetime "updated_at", null: false 20 | end 21 | 22 | create_table "composite_name_entities", force: :cascade do |t| 23 | t.string "email" 24 | t.string "password_digest" 25 | t.datetime "created_at", null: false 26 | t.datetime "updated_at", null: false 27 | end 28 | 29 | create_table "users", force: :cascade do |t| 30 | t.string "email", null: false 31 | t.string "password_digest", null: false 32 | t.datetime "created_at", null: false 33 | t.datetime "updated_at", null: false 34 | end 35 | 36 | create_table "v1_users", force: :cascade do |t| 37 | t.string "email", null: false 38 | t.string "password_digest", null: false 39 | t.datetime "created_at", null: false 40 | t.datetime "updated_at", null: false 41 | end 42 | 43 | create_table "vendors", force: :cascade do |t| 44 | t.string "email" 45 | t.string "password_digest" 46 | t.datetime "created_at", null: false 47 | t.datetime "updated_at", null: false 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /test/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsarno/knock/8e8b3e8d29eccb83a83ea2449e006250f025632a/test/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /test/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsarno/knock/8e8b3e8d29eccb83a83ea2449e006250f025632a/test/dummy/log/.keep -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsarno/knock/8e8b3e8d29eccb83a83ea2449e006250f025632a/test/dummy/public/favicon.ico -------------------------------------------------------------------------------- /test/dummy/test/controllers/admin_protected_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AdminProtectedControllerTest < ActionController::TestCase 4 | def valid_auth 5 | @admin = admins(:one) 6 | @token = Knock::AuthToken.new(payload: { sub: @admin.id }).token 7 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{@token}" 8 | end 9 | 10 | def invalid_token_auth 11 | @token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' 12 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{@token}" 13 | end 14 | 15 | def invalid_entity_auth 16 | @token = Knock::AuthToken.new(payload: { sub: 0 }).token 17 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{@token}" 18 | end 19 | 20 | test "responds with unauthorized" do 21 | get :index 22 | assert_response :unauthorized 23 | end 24 | 25 | test "responds with unauthorized to invalid token" do 26 | invalid_token_auth 27 | get :index 28 | assert_response :unauthorized 29 | end 30 | 31 | test "responds with unauthorized to invalid entity" do 32 | invalid_entity_auth 33 | get :index 34 | assert_response :unauthorized 35 | end 36 | 37 | test "responds with success if authenticated" do 38 | valid_auth 39 | get :index 40 | assert_response :success 41 | end 42 | 43 | test "has a current_admin after authentication" do 44 | valid_auth 45 | get :index 46 | assert_response :success 47 | assert @controller.current_admin.id == @admin.id 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/dummy/test/controllers/admin_token_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AdminTokenControllerTest < ActionController::TestCase 4 | def setup 5 | @admin = admins(:one) 6 | end 7 | 8 | test "responds with 404 if user does not exist" do 9 | post :create, params: {auth: { email: 'wrong@example.net', password: '' }} 10 | assert_response :not_found 11 | end 12 | 13 | test "responds with 404 if password is invalid" do 14 | post :create, params: {auth: { email: @admin.email, password: 'wrong' }} 15 | assert_response :not_found 16 | end 17 | 18 | test "responds with 201" do 19 | post :create, params: {auth: { email: @admin.email, password: 'secret' }} 20 | assert_response :created 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/dummy/test/controllers/composite_name_entity_protected_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CompositeNameEntityProtectedControllerTest < ActionController::TestCase 4 | def valid_auth 5 | @composite_name_entity = composite_name_entities(:one) 6 | @token = Knock::AuthToken.new(payload: { sub: @composite_name_entity.id }).token 7 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{@token}" 8 | end 9 | 10 | def invalid_token_auth 11 | @token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' 12 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{@token}" 13 | end 14 | 15 | def invalid_entity_auth 16 | @token = Knock::AuthToken.new(payload: { sub: 0 }).token 17 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{@token}" 18 | end 19 | 20 | test "responds with unauthorized" do 21 | get :index 22 | assert_response :unauthorized 23 | end 24 | 25 | test "responds with unauthorized to invalid token" do 26 | invalid_token_auth 27 | get :index 28 | assert_response :unauthorized 29 | end 30 | 31 | test "responds with unauthorized to invalid entity" do 32 | invalid_entity_auth 33 | get :index 34 | assert_response :unauthorized 35 | end 36 | 37 | test "responds with success if authenticated" do 38 | valid_auth 39 | get :index 40 | assert_response :success 41 | end 42 | 43 | test "has a current_composite_name_entity after authentication" do 44 | valid_auth 45 | get :index 46 | assert_response :success 47 | assert @controller.current_composite_name_entity.id == @composite_name_entity.id 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/dummy/test/controllers/current_users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CurrentUsersControllerTest < ActionController::TestCase 4 | setup do 5 | @user = users(:one) 6 | @token = Knock::AuthToken.new(payload: { sub: @user.id }).token 7 | end 8 | 9 | def authenticate token: @token 10 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{token}" 11 | end 12 | 13 | test "responds with 404 if user is not logged in" do 14 | get :show 15 | assert_response :not_found 16 | end 17 | 18 | test "responds with 200" do 19 | authenticate 20 | get :show 21 | assert_response :success 22 | end 23 | 24 | # Run this test twice to validate that it still works 25 | # when the getter method has already been defined. 26 | test "responds with 200 #2" do 27 | authenticate 28 | get :show 29 | assert_response :success 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/dummy/test/controllers/custom_unauthorized_entity_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CustomUnauthorizedEntityControllerTest < ActionController::TestCase 4 | def valid_auth 5 | @user = users(:one) 6 | @token = Knock::AuthToken.new(payload: { sub: @user.id }).token 7 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{@token}" 8 | end 9 | 10 | def invalid_token_auth 11 | @token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' 12 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{@token}" 13 | end 14 | 15 | def invalid_entity_auth 16 | @token = Knock::AuthToken.new(payload: { sub: 0 }).token 17 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{@token}" 18 | end 19 | 20 | test "responds with not found" do 21 | get :index 22 | assert_response :not_found 23 | end 24 | 25 | test "responds with not found to invalid token" do 26 | invalid_token_auth 27 | get :index 28 | assert_response :not_found 29 | end 30 | 31 | test "responds with not found to invalid entity" do 32 | invalid_entity_auth 33 | get :index 34 | assert_response :not_found 35 | end 36 | 37 | test "responds with success if authenticated" do 38 | valid_auth 39 | get :index 40 | assert_response :success 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/dummy/test/controllers/guest_protected_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class GuestProtectedControllerTest < ActionController::TestCase 4 | def setup 5 | @token = Knock::AuthToken.new(payload: { sub: "1" }).token 6 | end 7 | 8 | def authenticate token: @token 9 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{token}" 10 | end 11 | 12 | test "responds with unauthorized when no token is provided" do 13 | get :index 14 | assert_response :unauthorized 15 | end 16 | 17 | test "responds with success with a valid token in the header" do 18 | authenticate 19 | get :index 20 | assert_response :success 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/dummy/test/controllers/protected_resources_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ProtectedResourcesControllerTest < ActionController::TestCase 4 | def setup 5 | @user = users(:one) 6 | @token = Knock::AuthToken.new(payload: { sub: @user.id }).token 7 | end 8 | 9 | def authenticate token: @token 10 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{token}" 11 | end 12 | 13 | test "responds with unauthorized" do 14 | get :index 15 | assert_response :unauthorized 16 | end 17 | 18 | test "responds with success with valid token in header" do 19 | authenticate 20 | get :index 21 | assert_response :success 22 | end 23 | 24 | test "responds with unauthorized with invalid token in header" do 25 | authenticate token: "invalid" 26 | get :index 27 | assert_response :unauthorized 28 | end 29 | 30 | test "responds with success with token in url" do 31 | get :index, params: {token: @token} 32 | assert_response :success 33 | end 34 | 35 | test "responds with unauthorized with invalid token in url" do 36 | get :index, params: {token: "invalid"} 37 | assert_response :unauthorized 38 | end 39 | 40 | test "has a current_user after authentication" do 41 | authenticate 42 | get :index 43 | assert_response :success 44 | assert @controller.current_user.id == @user.id 45 | end 46 | 47 | test "accepts any prefix in the authorization header" do 48 | @request.env['HTTP_AUTHORIZATION'] = "Other #{@token}" 49 | 50 | get :index 51 | 52 | assert_response :success 53 | end 54 | 55 | test "accepts authorization header without prefix" do 56 | @request.env['HTTP_AUTHORIZATION'] = "#{@token}" 57 | 58 | get :index 59 | 60 | assert_response :success 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/dummy/test/controllers/v1/test_namespaced_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | # require 'timecop' 3 | 4 | module Knock 5 | class TestNamespacedControllerTest < ActionDispatch::IntegrationTest 6 | 7 | setup do 8 | @user = V1::User.first 9 | end 10 | 11 | test "allow namespaced models" do 12 | token = Knock::AuthToken.new(payload: { sub: @user.id }).token 13 | get v1_test_namespaced_index_url, headers: {'Authorization': "Bearer #{token}"} 14 | assert_response :ok 15 | assert_equal @user, @controller.current_v1_user 16 | end 17 | 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/dummy/test/controllers/vendor_protected_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class VendorProtectedControllerTest < ActionController::TestCase 4 | def valid_auth 5 | @vendor = vendors(:one) 6 | @token = Knock::AuthToken.new(payload: { sub: @vendor.id }).token 7 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{@token}" 8 | end 9 | 10 | def invalid_token_auth 11 | @token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' 12 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{@token}" 13 | end 14 | 15 | def invalid_entity_auth 16 | @token = Knock::AuthToken.new(payload: { sub: 0 }).token 17 | @request.env['HTTP_AUTHORIZATION'] = "Bearer #{@token}" 18 | end 19 | 20 | test "responds with unauthorized" do 21 | get :index 22 | assert_response :unauthorized 23 | end 24 | 25 | test "responds with unauthorized to invalid token" do 26 | invalid_token_auth 27 | get :index 28 | assert_response :unauthorized 29 | end 30 | 31 | test "responds with unauthorized to invalid entity" do 32 | invalid_entity_auth 33 | get :index 34 | assert_response :unauthorized 35 | end 36 | 37 | test "responds with success if authenticated" do 38 | valid_auth 39 | get :index 40 | assert_response :success 41 | end 42 | 43 | test "has a current_vendor after authentication" do 44 | valid_auth 45 | get :index 46 | assert_response :success 47 | assert @controller.current_vendor.id == @vendor.id 48 | end 49 | 50 | test "raises method missing error appropriately" do 51 | assert_raises(NoMethodError) do 52 | get :show, params: {id: 1} 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/dummy/test/controllers/vendor_token_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class VendorTokenControllerTest < ActionController::TestCase 4 | def setup 5 | @vendor = vendors(:one) 6 | end 7 | 8 | test "responds with 404 if user does not exist" do 9 | post :create, params: {auth: { email: 'wrong@example.net', password: '' }} 10 | assert_response :not_found 11 | end 12 | 13 | test "responds with 404 if password is invalid" do 14 | post :create, params: {auth: { email: @vendor.email, password: 'wrong' }} 15 | assert_response :not_found 16 | end 17 | 18 | test "responds with 201" do 19 | post :create, params: {auth: { email: @vendor.email, password: 'secret' }} 20 | assert_response :created 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/dummy/test/models/admin_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AdminTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/test/models/vendor_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class VendorTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/admins.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | email: admin.one@example.net 5 | password_digest: <%= BCrypt::Password.create('secret', cost: 4) %> 6 | -------------------------------------------------------------------------------- /test/fixtures/composite_name_entities.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | email: composite_name_entity.one@example.net 5 | password_digest: <%= BCrypt::Password.create('secret', cost: 4) %> 6 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | email: one@example.net 5 | password_digest: <%= BCrypt::Password.create('secret', cost: 4) %> 6 | 7 | two: 8 | email: two@example.net 9 | password_digest: <%= BCrypt::Password.create('secret', cost: 4) %> 10 | -------------------------------------------------------------------------------- /test/fixtures/v1_users.yml: -------------------------------------------------------------------------------- 1 | 2 | one: 3 | email: v1user@example.net 4 | password_digest: <%= BCrypt::Password.create('secret', cost: 4) %> 5 | created_at: <%= Time.now.utc %> 6 | updated_at: <%= Time.now.utc %> -------------------------------------------------------------------------------- /test/fixtures/vendors.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | email: vendor.one@example.net 5 | password_digest: <%= BCrypt::Password.create('secret', cost: 4) %> 6 | -------------------------------------------------------------------------------- /test/knock_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class KnockTest < ActiveSupport::TestCase 4 | test 'setup block yields self' do 5 | Knock.setup do |config| 6 | assert_equal Knock, config 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/model/knock/auth_token_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'jwt' 3 | require 'timecop' 4 | 5 | module Knock 6 | class AuthTokenTest < ActiveSupport::TestCase 7 | setup do 8 | key = Knock.token_secret_signature_key.call 9 | @token = JWT.encode({ sub: '1' }, key, 'HS256') 10 | end 11 | 12 | test "verify algorithm" do 13 | Knock.token_signature_algorithm = 'RS256' 14 | 15 | assert_raises(JWT::IncorrectAlgorithm) { 16 | AuthToken.new(token: @token) 17 | } 18 | end 19 | 20 | test "decode RSA encoded tokens" do 21 | rsa_private = OpenSSL::PKey::RSA.generate 2048 22 | Knock.token_public_key = rsa_private.public_key 23 | Knock.token_signature_algorithm = 'RS256' 24 | 25 | token = JWT.encode({ sub: "1" }, rsa_private, 'RS256') 26 | 27 | assert_nothing_raised { AuthToken.new(token: token) } 28 | end 29 | 30 | test "encode tokens with RSA" do 31 | rsa_private = OpenSSL::PKey::RSA.generate 2048 32 | Knock.token_secret_signature_key = -> { rsa_private } 33 | Knock.token_signature_algorithm = 'RS256' 34 | 35 | token = AuthToken.new(payload: { sub: '1' }).token 36 | 37 | payload, header = JWT.decode token, rsa_private.public_key, true, { algorithm: 'RS256' } 38 | assert_equal payload['sub'], '1' 39 | assert_equal header['alg'], 'RS256' 40 | end 41 | 42 | test "verify audience when token_audience is present" do 43 | Knock.token_audience = -> { 'bar' } 44 | 45 | assert_raises(JWT::InvalidAudError) { 46 | AuthToken.new token: @token 47 | } 48 | end 49 | 50 | test "validate expiration claim by default" do 51 | token = AuthToken.new(payload: { sub: 'foo' }).token 52 | Timecop.travel(25.hours.from_now) do 53 | assert_raises(JWT::ExpiredSignature) { 54 | AuthToken.new(token: token) 55 | } 56 | end 57 | end 58 | 59 | test "does not validate expiration claim with a nil token_lifetime" do 60 | Knock.token_lifetime = nil 61 | 62 | token = AuthToken.new(payload: { sub: 'foo' }).token 63 | Timecop.travel(10.years.from_now) do 64 | assert_not AuthToken.new(token: token).payload.has_key?('exp') 65 | end 66 | end 67 | 68 | test "validate aud when verify_options[:verify_aud] is true" do 69 | verify_options = { 70 | verify_aud: true 71 | } 72 | Knock.token_audience = -> { 'bar' } 73 | Knock.token_secret_signature_key.call 74 | assert_raises(JWT::InvalidAudError) { 75 | AuthToken.new token: @token, verify_options: verify_options 76 | } 77 | end 78 | 79 | test "does not validate aud when verify_options[:verify_aud] is false" do 80 | verify_options = { 81 | verify_aud: false 82 | } 83 | Knock.token_audience = -> { 'bar' } 84 | Knock.token_secret_signature_key.call 85 | assert_not AuthToken.new(token: @token, verify_options: verify_options).payload.has_key?('aud') 86 | end 87 | 88 | test "validate expiration when verify_options[:verify_expiration] is true" do 89 | verify_options = { 90 | verify_expiration: true 91 | } 92 | token = AuthToken.new(payload: { sub: 'foo' }).token 93 | Timecop.travel(25.hours.from_now) do 94 | assert_raises(JWT::ExpiredSignature) { 95 | AuthToken.new(token: token, verify_options: verify_options) 96 | } 97 | end 98 | end 99 | 100 | test "does not validate expiration when verify_options[:verify_expiration] is false" do 101 | verify_options = { 102 | verify_expiration: false 103 | } 104 | token = AuthToken.new(payload: { sub: 'foo' }).token 105 | Timecop.travel(25.hours.from_now) do 106 | assert AuthToken.new(token: token, verify_options: verify_options).payload.has_key?('exp') 107 | end 108 | end 109 | 110 | test "Knock::AuthToken has all payloads" do 111 | Knock.token_lifetime = 7.days 112 | 113 | payload = Knock::AuthToken.new(payload: { sub: 'foo' }).payload 114 | assert payload.has_key?(:sub) 115 | assert payload.has_key?(:exp) 116 | end 117 | 118 | test "is serializable" do 119 | auth_token = AuthToken.new token: @token 120 | 121 | assert_equal("{\"jwt\":\"#{@token}\"}", auth_token.to_json) 122 | end 123 | 124 | test "returns the correct payload expiration value when token_lifetime is an integer" do 125 | lifespan = 7.days 126 | 127 | Knock.token_lifetime = lifespan 128 | 129 | auth_token = AuthToken.new 130 | 131 | assert auth_token.payload[:exp], lifespan.from_now.to_i 132 | end 133 | 134 | test "returns the correct payload expiration value when token_lifetime is a hash for default user" do 135 | user_lifespan = 7.days 136 | admin_lifespan = 1.hour 137 | 138 | Knock.token_lifetime = { 139 | user: user_lifespan, 140 | admin: admin_lifespan 141 | } 142 | 143 | auth_token = AuthToken.new(entity_class_name: :user) 144 | 145 | assert auth_token.payload[:exp], user_lifespan.from_now.to_i 146 | end 147 | 148 | test "returns the correct payload expiration value when token_lifetime is a hash for superuser" do 149 | user_lifespan = 7.days 150 | admin_lifespan = 1.hour 151 | 152 | Knock.token_lifetime = { 153 | user: user_lifespan, 154 | admin: admin_lifespan 155 | } 156 | auth_token = Knock::AuthToken.new(entity_class_name: :admin) 157 | 158 | assert auth_token.payload[:exp], admin_lifespan.from_now.to_i 159 | end 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /test/support/generators_test_helper.rb: -------------------------------------------------------------------------------- 1 | module GeneratorsTestHelper 2 | def copy_routes 3 | routes = File.expand_path("../../dummy/config/routes.rb", __FILE__) 4 | destination = File.join(destination_root, "config") 5 | 6 | FileUtils.mkdir_p(destination) 7 | FileUtils.cp routes, destination 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'bcrypt' 3 | 4 | SimpleCov.start 5 | 6 | # Configure Rails Environment 7 | ENV["RAILS_ENV"] = "test" 8 | 9 | require File.expand_path("../../test/dummy/config/environment.rb", __FILE__) 10 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../../test/dummy/db/migrate", __FILE__)] 11 | ActiveRecord::Migrator.migrations_paths << File.expand_path('../../db/migrate', __FILE__) 12 | require "rails/test_help" 13 | 14 | # Filter out Minitest backtrace while allowing backtrace from other libraries 15 | # to be shown. 16 | Minitest.backtrace_filter = Minitest::BacktraceFilter.new 17 | 18 | # Load support files 19 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 20 | 21 | # Load fixtures from the engine 22 | if ActiveSupport::TestCase.respond_to?(:fixture_path=) 23 | ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) 24 | ActiveSupport::TestCase.fixtures :all 25 | end 26 | 27 | module Knock 28 | class MyCustomException < StandardError 29 | end 30 | end 31 | 32 | # Make sure knock global configuration is reset before every tests 33 | # to avoid order dependent failures. 34 | class ActiveSupport::TestCase 35 | setup :reset_knock_configuration 36 | 37 | private 38 | 39 | def reset_knock_configuration 40 | Knock.token_signature_algorithm = 'HS256' 41 | Knock.token_secret_signature_key = -> { Rails.application.secret_key_base } 42 | Knock.token_public_key = nil 43 | Knock.token_audience = nil 44 | Knock.token_lifetime = 1.day 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/unit/knock/authenticable/getter_name_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Knock 4 | module Authenticable 5 | class GetterNameTest < ActiveSupport::TestCase 6 | test "check simple model names" do 7 | getter_name = GetterName.new("Test").cleared 8 | assert_equal getter_name, "current_test" 9 | end 10 | 11 | test "check pascal cased names" do 12 | getter_name = GetterName.new("TestModel").cleared 13 | assert_equal getter_name, "current_test_model" 14 | end 15 | 16 | test "check namespaced model names" do 17 | getter_name = GetterName.new("Test::Model").cleared 18 | assert_equal getter_name, "current_test_model" 19 | end 20 | 21 | test "check double namespaced model names" do 22 | getter_name = GetterName.new("Test::Double::Model").cleared 23 | assert_equal getter_name, "current_test_double_model" 24 | end 25 | end 26 | end 27 | end 28 | --------------------------------------------------------------------------------