├── .codeclimate.yml ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ └── lint.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── devise-jwt.gemspec ├── docker-compose.yml ├── issue_template.md ├── lib └── devise │ ├── jwt.rb │ └── jwt │ ├── defaults_generator.rb │ ├── mapping_inspector.rb │ ├── models.rb │ ├── models │ └── jwt_authenticatable.rb │ ├── railtie.rb │ ├── revocation_strategies.rb │ ├── revocation_strategies │ ├── allowlist.rb │ ├── denylist.rb │ ├── jti_matcher.rb │ └── null.rb │ ├── test_helpers.rb │ └── version.rb └── spec ├── devise ├── devise_spec.rb ├── jwt │ ├── defaults_generator_spec.rb │ ├── mapping_inspector_spec.rb │ ├── railtie_spec.rb │ ├── revocation_strategies │ │ ├── allowlist_spec.rb │ │ ├── denylist_spec.rb │ │ ├── jti_matcher_spec.rb │ │ └── null_spec.rb │ └── test_helpers_spec.rb ├── jwt_spec.rb └── models │ └── jwt_authenticatable_spec.rb ├── features ├── authorization_spec.rb ├── token_dispatch_spec.rb └── token_revocation_spec.rb ├── fixtures └── rails_app │ ├── Gemfile │ ├── Gemfile.lock │ ├── README.md │ ├── Rakefile │ ├── app │ ├── controllers │ │ ├── application_controller.rb │ │ └── registrations_controller.rb │ ├── jobs │ │ └── application_job.rb │ └── models │ │ ├── allowlisted_jwt.rb │ │ ├── application_record.rb │ │ ├── jwt_denylist.rb │ │ ├── jwt_with_allowlist_user.rb │ │ ├── jwt_with_denylist_user.rb │ │ ├── jwt_with_jti_matcher_user.rb │ │ ├── jwt_with_null_user.rb │ │ └── no_jwt_user.rb │ ├── bin │ ├── bundle │ ├── rails │ ├── rake │ ├── setup │ └── update │ ├── config.ru │ ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── application_controller_renderer.rb │ │ ├── backtrace_silencers.rb │ │ ├── cors.rb │ │ ├── devise.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ ├── devise.en.yml │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ └── secrets.yml │ ├── db │ ├── migrate │ │ ├── 20170113091441_devise_create_jwt_users.rb │ │ ├── 20170113150139_devise_create_no_jwt_users.rb │ │ ├── 20170119120658_add_jti_to_jwt_users.rb │ │ ├── 20170119143719_rename_jwt_user_to_jwt_with_jti_matcher_users.rb │ │ ├── 20170119145401_create_blacklist.rb │ │ ├── 20170120092719_create_jwt_with_blacklist_users.rb │ │ ├── 20170120101213_devise_create_jwt_with_null_users.rb │ │ ├── 20170802000822_add_expiration_time_to_blacklist.rb │ │ ├── 20170804032446_rename_blacklist_table_to_jwt_blacklist.rb │ │ ├── 20171209162000_devise_create_jwt_with_whitelist_users.rb │ │ ├── 20171209162500_create_whitelisted_jwts.rb │ │ ├── 20171211114759_add_exp_to_whitelisted_jwts.rb │ │ ├── 20171230182243_add_unique_index_to_blacklist_jti.rb │ │ ├── 20200603051918_rename_blacklist_to_denylist.rb │ │ └── 20200603053415_rename_whitelist_to_allowlist.rb │ ├── schema.rb │ └── seeds.rb │ ├── public │ └── robots.txt │ └── test │ ├── fixtures │ ├── jwt_users.yml │ └── no_jwt_users.yml │ ├── models │ ├── jwt_with_jti_matcher_user_test.rb │ └── no_jwt_user_test.rb │ └── test_helper.rb ├── spec_helper.rb └── support └── shared_contexts ├── feature.rb └── fixtures.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | duplication: 3 | enabled: true 4 | config: 5 | languages: 6 | - ruby 7 | fixme: 8 | enabled: true 9 | rubocop: 10 | enabled: true 11 | ratings: 12 | paths: 13 | - "**.rb" 14 | exclude_paths: 15 | - spec/ 16 | - Gemfile 17 | - bin/console 18 | - devise-jwt.gemspec 19 | - vendor/ 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: waiting-for-dev 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | ruby-version: ['3.2', '3.3', '3.4', ruby-head] 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Ruby ${{ matrix.ruby-version }} 15 | uses: ruby/setup-ruby@v1 16 | with: 17 | ruby-version: ${{ matrix.ruby-version }} 18 | bundler-cache: true # 'bundle install' and cache 19 | - name: Run specs 20 | run: | 21 | cd spec/fixtures/rails_app && bundle && bundle exec rails db:setup && cd - 22 | bundle exec rspec 23 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Set up Ruby ${{ matrix.ruby-version }} 11 | uses: ruby/setup-ruby@v1 12 | with: 13 | ruby-version: 3.4 14 | bundler-cache: true # 'bundle install' and cache 15 | - name: Run specs 16 | run: | 17 | bundle exec rubocop 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /spec/fixtures/rails_app/db/test.sqlite3* 10 | /spec/fixtures/rails_app/db/development.sqlite3* 11 | /spec/fixtures/rails_app/tmp 12 | /tmp/ 13 | .overcommit_gems.rb.lock 14 | *.log 15 | *sqlite3-journal 16 | /tags 17 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: rubocop-rspec 2 | AllCops: 3 | TargetRubyVersion: 3.0 4 | Exclude: 5 | - Gemfile 6 | - devise-jwt.gemspec 7 | - spec/fixtures/rails_app/**/* 8 | - vendor/**/* 9 | RSpec/NestedGroups: 10 | Max: 3 11 | RSpec/MessageExpectation: 12 | EnforcedStyle: 'expect' 13 | RSpec/ContextWording: 14 | Exclude: 15 | - "spec/support/shared_contexts/*rb" 16 | - "spec/features/*rb" 17 | Metrics/BlockLength: 18 | Exclude: 19 | - "spec/**/*.rb" 20 | Style/SafeNavigation: 21 | Enabled: false 22 | Layout/EmptyLinesAroundAttributeAccessor: 23 | Enabled: true 24 | Layout/SpaceAroundMethodCallOperator: 25 | Enabled: true 26 | Lint/DeprecatedOpenSSLConstant: 27 | Enabled: true 28 | Lint/MixedRegexpCaptureTypes: 29 | Enabled: true 30 | Lint/RaiseException: 31 | Enabled: true 32 | Lint/StructNewOverride: 33 | Enabled: true 34 | Style/AccessorGrouping: 35 | Enabled: true 36 | Style/BisectedAttrAccessor: 37 | Enabled: true 38 | Style/ExponentialNotation: 39 | Enabled: true 40 | Style/HashEachMethods: 41 | Enabled: true 42 | Style/HashTransformKeys: 43 | Enabled: true 44 | Style/HashTransformValues: 45 | Enabled: true 46 | Style/RedundantAssignment: 47 | Enabled: true 48 | Style/RedundantFetchBlock: 49 | Enabled: true 50 | Style/RedundantRegexpCharacterClass: 51 | Enabled: true 52 | Style/RedundantRegexpEscape: 53 | Enabled: true 54 | Style/SlicingWithRange: 55 | Enabled: true 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | [#](#) Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [0.12.1] - 2024-07-12 8 | - Fix properly support for `token_header` & `issuer` config options 9 | 10 | ## [0.12.0] - 2024-07-10 11 | ### Added 12 | - Add support for `token_header` config 13 | - Add support for `issuer` config 14 | 15 | ## [0.11.0] - 2023-05-10 16 | ### Added 17 | - Add support for rotation_secret 18 | 19 | ## [0.10.0] - 2022-09-16 20 | ### Added 21 | - Enable support for asymmetric algorithms 22 | 23 | ### Fixed 24 | - FIX: "No verification key available" on token decode 25 | 26 | ## [0.9.0] - 2021-09-21 27 | ### Fixed 28 | - Fix compatibility with dry-configurable 0.13 29 | 30 | ## [0.8.1] - 2021-02-14 31 | ### Fixed 32 | - Fix behaviour on code reload 33 | - Support ruby 3.0 and deprecate ruby 2.5 34 | 35 | ## [0.8.0] - 2020-07-06 36 | ### Fixed 37 | - Fix compatibility with last version of dry-configurable 38 | 39 | ## [0.7.0] - 2020-06-03 40 | ### Fixed 41 | - Replace whitelist/blacklist terminology with allowlist/denylist 42 | 43 | ## [0.6.0] - 2019-08-01 44 | ### Fixed 45 | - Update warden-jwt_auth dependency to v0.4.0 so that now it is possible to configure algorithm. 46 | 47 | ## [0.5.9] - 2019-03-29 48 | ### Fixed 49 | - Update dependencies. 50 | 51 | ## [0.5.8] - 2018-09-07 52 | ### Fixed 53 | - Fix test helper to persist whitelisted tokens. 54 | 55 | ## [0.5.7] - 2018-06-22 56 | ### Added 57 | - Use `primary_key` instead of `id` to fetch resource. 58 | 59 | ## [0.5.6] - 2018-02-22 60 | ### Fixed 61 | - Work with more than one `sign_out_via` configured 62 | 63 | ## [0.5.5] - 2018-01-30 64 | ### Fixed 65 | - Update `warden-jwt_auth` dependency to reenable JWT scopes being stored to 66 | the session and inform the user. 67 | 68 | ## [0.5.4] - 2018-01-09 69 | ### Fixed 70 | - Update `warden-jwt_auth` dependency to allow a JWT scope to be fetched from 71 | session in a standard AJAX request 72 | 73 | ## [0.5.3] - 2017-12-31 74 | ### Fixed 75 | - Do not crash for consecutive revocations of same token in blacklist & 76 | whitelist strategies 77 | - Update `warden-jwt_auth` dependency to allow a JWT scope to be fetched from 78 | session in a html request 79 | 80 | ## [0.5.2] - 2017-12-23 81 | ### Added 82 | - Added a test helper to authenticate request headers 83 | 84 | ## [0.5.1] - 2017-12-11 85 | ### Added 86 | - Update `warden-jwt_auth` dependency to ensure JWT scopes are not fetched from 87 | session 88 | 89 | ## [0.5.0] - 2017-12-11 90 | ### Added 91 | - Added whitelist strategy 92 | - Update `warden-jwt_auth` dependency 93 | 94 | ## [0.4.4] - 2017-12-04 95 | ### Fixed 96 | - Configure classes as strings to avoid problems with Rails STI 97 | 98 | ## [0.4.3] - 2017-11-23 99 | ### Fixed 100 | - Return `nil` and not raise when user is not found in model 101 | 102 | ## [0.4.2] - 2017-11-21 103 | ### Fixed 104 | - Update `warden-jwt_auth` dependency 105 | 106 | ## [0.4.1] - 2017-10-03 107 | ### Fixed 108 | - Do not generate double slash paths when one segment is blank 109 | 110 | ## [0.4.0] - 2017-08-07 111 | 112 | ### Added 113 | - Store `exp` in the blacklist strategy to easy cleaning tasks 114 | 115 | ## [0.3.0] - 2017-06-07 116 | ### Fixed 117 | - Allow configuring request formats to take into account through 118 | `request_formats` configuration option 119 | 120 | ## [0.2.1] - 2017-04-13 121 | ### Fixed 122 | - Ignore expired token revocation 123 | 124 | ## [0.2.0] - 2017-02-28 125 | ### Added 126 | - Dispatch token on sign up 127 | - Speed up initialization 128 | 129 | ### Fixed 130 | - Do not depend on assumed helpers to build default paths 131 | - Use `sign_out_via` devise option to set revocation request methods 132 | - Take routes with scopes into account 133 | 134 | ## [0.1.1] - 2017-01-26 135 | ### Fixed 136 | - Request method configuration for Rails < 5 137 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at marc@lamarciana.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.0.0 2 | ENV APP_USER devise_jwt_user 3 | RUN apt-get update -qq && \ 4 | apt-get install -y build-essential sqlite3 libsqlite3-dev 5 | RUN useradd -ms /bin/bash $APP_USER 6 | USER $APP_USER 7 | WORKDIR /home/$APP_USER/app 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in devise-jwt.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2020 Marc Busqué 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Devise::JWT 2 | 3 | [![Gem Version](https://badge.fury.io/rb/devise-jwt.svg)](https://badge.fury.io/rb/devise-jwt) 4 | [![Build Status](https://travis-ci.org/waiting-for-dev/devise-jwt.svg?branch=master)](https://travis-ci.org/waiting-for-dev/devise-jwt) 5 | [![Code Climate](https://codeclimate.com/github/waiting-for-dev/devise-jwt/badges/gpa.svg)](https://codeclimate.com/github/waiting-for-dev/devise-jwt) 6 | [![Test Coverage](https://codeclimate.com/github/waiting-for-dev/devise-jwt/badges/coverage.svg)](https://codeclimate.com/github/waiting-for-dev/devise-jwt/coverage) 7 | 8 | `devise-jwt` is a [Devise](https://github.com/plataformatec/devise) extension which uses [JWT](https://jwt.io/) tokens for user authentication. It follows [secure by default](https://en.wikipedia.org/wiki/Secure_by_default) principle. 9 | 10 | This gem is just a replacement for cookies when these can't be used. As with 11 | cookies, a `devise-jwt` token will mandatorily have an expiration 12 | time. If you need that your users never sign out, you will be better off with a 13 | solution using refresh tokens, like some implementation of OAuth2. 14 | 15 | You can read about which security concerns this library takes into account and about JWT generic secure usage in the following series of posts: 16 | 17 | - [Stand Up for JWT Revocation](http://waiting-for-dev.github.io/blog/2017/01/23/stand_up_for_jwt_revocation) 18 | - [JWT Revocation Strategies](http://waiting-for-dev.github.io/blog/2017/01/24/jwt_revocation_strategies) 19 | - [JWT Secure Usage](http://waiting-for-dev.github.io/blog/2017/01/25/jwt_secure_usage) 20 | - [A secure JWT authentication implementation for Rack and Rails](http://waiting-for-dev.github.io/blog/2017/01/26/a_secure_jwt_authentication_implementation_for_rack_and_rails) 21 | 22 | `devise-jwt` is just a thin layer on top of [`warden-jwt_auth`](https://github.com/waiting-for-dev/warden-jwt_auth) that configures it to be used out of the box with Devise and Rails. 23 | 24 | ## Upgrade notes 25 | 26 | ### v0.7.0 27 | 28 | Since version v0.7.0 `Blacklist` revocation strategy has been renamed to `Denylist` while `Whitelist` has been renamed to `Allowlist`. 29 | 30 | For `Denylist`, you only need to update the `include` line you're using in your revocation strategy model: 31 | 32 | ```ruby 33 | # include Devise::JWT::RevocationStrategies::Blacklist # before 34 | include Devise::JWT::RevocationStrategies::Denylist 35 | ``` 36 | 37 | For `Allowlist`, you need to update the `include` line you're using in your user model: 38 | 39 | ```ruby 40 | # include Devise::JWT::RevocationStrategies::Whitelist # before 41 | include Devise::JWT::RevocationStrategies::Allowlist 42 | ``` 43 | 44 | You also have to rename your `WhitelistedJwt` model to `AllowlistedJwt`, rename `model/whitelisted_jwt.rb` to `model/allowlisted_jwt.rb` and change the underlying database table to `allowlisted_jwts` (or configure the model to keep using the old name). 45 | 46 | ## Installation 47 | 48 | Add this line to your application's Gemfile: 49 | 50 | ```ruby 51 | gem 'devise-jwt' 52 | ``` 53 | 54 | And then execute: 55 | 56 | $ bundle 57 | 58 | Or install it yourself as: 59 | 60 | $ gem install devise-jwt 61 | 62 | ## Usage 63 | 64 | First, you need to configure Devise to work in an API application. You can follow the instructions in this project wiki page [Configuring Devise for APIs](https://github.com/waiting-for-dev/devise-jwt/wiki/Configuring-devise-for-APIs) (you are more than welcome to improve them). 65 | 66 | ### Secret key configuration 67 | 68 | You have to configure the secret key that will be used to sign generated tokens. You can do it in the Devise initializer: 69 | 70 | ```ruby 71 | Devise.setup do |config| 72 | # ... 73 | config.jwt do |jwt| 74 | jwt.secret = ENV['DEVISE_JWT_SECRET_KEY'] 75 | end 76 | end 77 | ``` 78 | 79 | If you are using Encrypted Credentials (Rails 5.2+), you can store the secret key in `config/credentials.yml.enc`. 80 | 81 | Open your credentials editor using `bin/rails credentials:edit` and add `devise_jwt_secret_key`. 82 | 83 | > **Note** you may need to set `$EDITOR` depending on your specific environment. 84 | 85 | ```yml 86 | 87 | # Other secrets... 88 | 89 | # Used as the base secret for Devise JWT 90 | devise_jwt_secret_key: abc...xyz 91 | ``` 92 | 93 | Add the following to the Devise initializer. 94 | 95 | ```ruby 96 | Devise.setup do |config| 97 | # ... 98 | config.jwt do |jwt| 99 | jwt.secret = Rails.application.credentials.devise_jwt_secret_key! 100 | end 101 | end 102 | ``` 103 | 104 | > **Important:** You are encouraged to use a secret different than your application `secret_key_base`. It is quite possible that some other component of your system is already using it. If several components share the same secret key, chances that a vulnerability in one of them has a wider impact increase. In rails, generating new secrets is as easy as `rails secret`. Also, never share your secrets pushing it to a remote repository, you are better off using an environment variable like in the example. 105 | 106 | Currently, HS256 algorithm is the one in use. You may configure a matching secret and algorithm name to use a different one (see [ruby-jwt](https://github.com/jwt/ruby-jwt#algorithms-and-usage) to see which are supported): 107 | 108 | ```ruby 109 | Devise.setup do |config| 110 | # ... 111 | config.jwt do |jwt| 112 | jwt.secret = OpenSSL::PKey::RSA.new(Rails.application.credentials.devise_jwt_secret_key!) 113 | jwt.algorithm = Rails.application.credentials.devise_jwt_algorithm! 114 | end 115 | end 116 | ``` 117 | 118 | If the algorithm is asymmetric (e.g. RS256) which necessitates a different decoding secret, configure the `decoding_secret` setting as well: 119 | 120 | ```ruby 121 | Devise.setup do |config| 122 | # ... 123 | config.jwt do |jwt| 124 | jwt.secret = OpenSSL::PKey::RSA.new(Rails.application.credentials.devise_jwt_private_key!) 125 | jwt.decoding_secret = OpenSSL::PKey::RSA.new(Rails.application.credentials.devise_jwt_public_key!) 126 | jwt.algorithm = 'RS256' # or some other asymmetric algorithm 127 | end 128 | end 129 | ``` 130 | 131 | ### Model configuration 132 | 133 | You have to tell which user models you want to be able to authenticate with JWT tokens. For them, the authentication process will be like this: 134 | 135 | - A user authenticates through Devise create session request (for example, using the standard `:database_authenticatable` module). 136 | - If the authentication succeeds, a JWT token is dispatched to the client in the `Authorization` response header, with format `Bearer #{token}` (tokens are also dispatched on a successful sign up). 137 | - The client can use this token to authenticate following requests for the same user, providing it in the `Authorization` request header, also with format `Bearer #{token}` 138 | - When the client visits Devise destroy session request, the token is revoked. 139 | 140 | See [request_formats](#request_formats) configuration option if you are using paths with a format segment (like `.json`) in order to use it properly. 141 | 142 | As you see, unlike other JWT authentication libraries, it is expected that tokens will be revoked by the server. I wrote about [why I think JWT revocation is needed and useful](http://waiting-for-dev.github.io/blog/2017/01/23/stand_up_for_jwt_revocation). 143 | 144 | An example configuration: 145 | 146 | ```ruby 147 | class User < ApplicationRecord 148 | devise :database_authenticatable, 149 | :jwt_authenticatable, jwt_revocation_strategy: Denylist 150 | end 151 | ``` 152 | 153 | If you need to add something to the JWT payload, you can do it by defining a `jwt_payload` method in the user model. It must return a `Hash`. For instance: 154 | 155 | ```ruby 156 | def jwt_payload 157 | { 'foo' => 'bar' } 158 | end 159 | ``` 160 | 161 | You can add a hook method `on_jwt_dispatch` on the user model. It is executed when a token dispatched for that user instance, and it takes `token` and `payload` as parameters. 162 | 163 | ```ruby 164 | def on_jwt_dispatch(token, payload) 165 | do_something(token, payload) 166 | end 167 | ``` 168 | 169 | Note: if you are making cross-domain requests, make sure that you add `Authorization` header to the list of allowed request headers and exposed response headers. You can use something like [rack-cors](https://github.com/cyu/rack-cors) for that, for example: 170 | 171 | ```ruby 172 | config.middleware.insert_before 0, Rack::Cors do 173 | allow do 174 | origins 'http://your.frontend.domain.com' 175 | resource '/api/*', 176 | headers: %w(Authorization), 177 | methods: :any, 178 | expose: %w(Authorization), 179 | max_age: 600 180 | end 181 | end 182 | ``` 183 | 184 | #### Session storage caveat 185 | 186 | If you are working with a Rails application that has session storage enabled 187 | and a default Devise setup, chances are the same origin requests will be 188 | authenticated from the session regardless of a token being present in the 189 | headers or not. 190 | 191 | This is so because of the following default Devise workflow: 192 | 193 | - When a user signs in with `:database_authenticatable` strategy, the user is 194 | stored in the session unless one of the following conditions is met: 195 | - Session is disabled. 196 | - Devise `config.skip_session_storage` includes `:params_auth`. 197 | - [Rails Request forgery 198 | protection](http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html) 199 | handles an unverified request (but this is usually deactivated for API 200 | requests). 201 | - Warden (the engine below Devise), authenticates any request that the user has 202 | in the session without requiring a strategy (`:jwt_authenticatable` 203 | in our case). 204 | 205 | So, if you want to avoid this caveat you have five options: 206 | 207 | - Disable the session. If you are developing an API, you probably don't need 208 | it. In order to disable it, change `config/initializers/session_store.rb` to: 209 | 210 | ```ruby 211 | Rails.application.config.session_store :disabled 212 | ``` 213 | Notice that if you created the application with the `--api` flag you already 214 | have the session disabled. 215 | - If you still need the session for any other purpose, disable 216 | `:database_authenticatable` user storage. In `config/initializers/devise.rb`: 217 | 218 | ```ruby 219 | config.skip_session_storage = [:http_auth, :params_auth] 220 | ``` 221 | - If you are using Devise for another model (e.g. `AdminUser`) and doesn't want 222 | to disable session storage for Devise entirely, you can disable it on a 223 | per-model basis: 224 | 225 | ```ruby 226 | class User < ApplicationRecord 227 | devise :database_authenticatable #, your other enabled modules... 228 | self.skip_session_storage = [:http_auth, :params_auth] 229 | end 230 | ``` 231 | - If you need the session for some of the controllers, you are able to disable it at 232 | the controller level for those controllers which don't need it: 233 | 234 | ```ruby 235 | class AdminsController < ApplicationController 236 | before_action :drop_session_cookie 237 | 238 | private 239 | 240 | def drop_session_cookie 241 | request.session_options[:skip] = true 242 | end 243 | ``` 244 | - As the last option you can tell Devise to not store the user in the Warden session 245 | if you override default Devise `SessionsController` with your own one, and pass 246 | `store: false` attribute to the `sign_in`, `sign_in_and_redirect`, `bypass_sign_in` 247 | methods: 248 | 249 | ```ruby 250 | sign_in user, store: false 251 | ``` 252 | 253 | ### Revocation strategies 254 | 255 | `devise-jwt` comes with three revocation strategies out of the box. Some of them are implementations of what is discussed in the blog post [JWT Revocation Strategies](http://waiting-for-dev.github.io/blog/2017/01/24/jwt_revocation_strategies), where I also talk about their pros and cons. 256 | 257 | #### JTIMatcher 258 | 259 | Here, the model class acts as the revocation strategy. It needs a new string column named `jti` to be added to the user. `jti` stands for JWT ID, and it is a standard claim meant to uniquely identify a token. 260 | 261 | It works like the following: 262 | 263 | - When a token is dispatched for a user, the `jti` claim is taken from the `jti` column in the model (which has been initialized when the record has been created). 264 | - At every authenticated action, the incoming token `jti` claim is matched against the `jti` column for that user. The authentication only succeeds if they are the same. 265 | - When the user requests to sign out its `jti` column changes, so that provided token won't be valid anymore. 266 | 267 | In order to use it, you need to add the `jti` column to the user model. So, you have to set something like the following in a migration: 268 | 269 | ```ruby 270 | def change 271 | add_column :users, :jti, :string, null: false 272 | add_index :users, :jti, unique: true 273 | # If you already have user records, you will need to initialize its `jti` column before setting it to not nullable. Your migration will look this way: 274 | # add_column :users, :jti, :string 275 | # User.all.each { |user| user.update_column(:jti, SecureRandom.uuid) } 276 | # change_column_null :users, :jti, false 277 | # add_index :users, :jti, unique: true 278 | end 279 | ``` 280 | 281 | **Important:** You are encouraged to set a unique index in the `jti` column. This way we can be sure at the database level that there aren't two valid tokens with same `jti` at the same time. 282 | 283 | Then, you have to add the strategy to the model class and configure it accordingly: 284 | 285 | ```ruby 286 | class User < ApplicationRecord 287 | include Devise::JWT::RevocationStrategies::JTIMatcher 288 | 289 | devise :database_authenticatable, 290 | :jwt_authenticatable, jwt_revocation_strategy: self 291 | end 292 | ``` 293 | 294 | Be aware that this strategy makes uses of `jwt_payload` method in the user model, so if you need to use it don't forget to call `super`: 295 | 296 | ```ruby 297 | def jwt_payload 298 | super.merge('foo' => 'bar') 299 | end 300 | ``` 301 | 302 | #### Denylist 303 | 304 | In this strategy, a database table is used as a list of revoked JWT tokens. The `jti` claim, which uniquely identifies a token, is persisted. The `exp` claim is also stored to allow the clean-up of stale tokens. 305 | 306 | In order to use it, you need to create the denylist table in a migration: 307 | 308 | ```ruby 309 | def change 310 | create_table :jwt_denylist do |t| 311 | t.string :jti, null: false 312 | t.datetime :exp, null: false 313 | end 314 | add_index :jwt_denylist, :jti 315 | end 316 | ``` 317 | For performance reasons, it is better if the `jti` column is an index. 318 | 319 | Note: if you used the denylist strategy before version 0.4.0 you may not have the field *exp.* If not, run the following migration: 320 | 321 | ```ruby 322 | class AddExpirationTimeToJWTDenylist < ActiveRecord::Migration 323 | def change 324 | add_column :jwt_denylist, :exp, :datetime, null: false 325 | end 326 | end 327 | 328 | ``` 329 | 330 | Then, you need to create the corresponding model and include the strategy: 331 | 332 | ```ruby 333 | class JwtDenylist < ApplicationRecord 334 | include Devise::JWT::RevocationStrategies::Denylist 335 | 336 | self.table_name = 'jwt_denylist' 337 | end 338 | ``` 339 | 340 | Last, configure the user model to use it: 341 | 342 | ```ruby 343 | class User < ApplicationRecord 344 | devise :database_authenticatable, 345 | :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist 346 | end 347 | ``` 348 | 349 | #### Allowlist 350 | 351 | Here, the model itself also acts as a revocation strategy, but it needs to have 352 | a one-to-many association with another table which stores the tokens (in fact 353 | their `jti` claim, which uniquely identifies them) that are valid for each user record. 354 | 355 | The workflow is as the following: 356 | 357 | - Once a token is dispatched for a user, its `jti` claim is stored in the 358 | associated table. 359 | - At every authentication, the incoming token `jti` is matched against all the 360 | `jti` associated to that user. The authentication only succeeds if one of 361 | them matches. 362 | - On a sign out, the token `jti` is deleted from the associated table. 363 | 364 | In fact, besides the `jti` claim, the `aud` claim is also stored and matched at 365 | every authentication. This, together with the [aud_header](#aud_header) 366 | configuration parameter, can be used to differentiate between clients or 367 | devices for the same user. 368 | 369 | The `exp` claim is also stored to allow the clean-up of staled tokens. 370 | 371 | In order to use it, you have to create the associated table and model. 372 | The association table must be called `allowlisted_jwts`: 373 | 374 | ```ruby 375 | def change 376 | create_table :allowlisted_jwts do |t| 377 | t.string :jti, null: false 378 | t.string :aud 379 | # If you want to leverage the `aud` claim, add to it a `NOT NULL` constraint: 380 | # t.string :aud, null: false 381 | t.datetime :exp, null: false 382 | t.references :your_user_table, foreign_key: { on_delete: :cascade }, null: false 383 | end 384 | 385 | add_index :allowlisted_jwts, :jti, unique: true 386 | end 387 | ``` 388 | Important: You are encouraged to set a unique index in the `jti` column. This way we can be sure at the database level that there aren't two valid tokens with the same `jti` at the same time. Defining `foreign_key: { on_delete: :cascade }, null: false` on `t.references :your_user_table` helps to keep referential integrity of your database. 389 | 390 | And then, the model: 391 | 392 | ```ruby 393 | class AllowlistedJwt < ApplicationRecord 394 | end 395 | ``` 396 | 397 | Finally, include the strategy in the model and configure it: 398 | 399 | ```ruby 400 | class User < ApplicationRecord 401 | include Devise::JWT::RevocationStrategies::Allowlist 402 | 403 | devise :database_authenticatable, 404 | :jwt_authenticatable, jwt_revocation_strategy: self 405 | end 406 | ``` 407 | 408 | Be aware that this strategy makes uses of `on_jwt_dispatch` method in the user model, so if you need to use it don't forget to call `super`: 409 | 410 | ```ruby 411 | def on_jwt_dispatch(token, payload) 412 | super 413 | do_something(token, payload) 414 | end 415 | ``` 416 | 417 | #### Null strategy 418 | 419 | A [null object pattern](https://en.wikipedia.org/wiki/Null_Object_pattern) strategy, which does not revoke tokens, is provided out of the box just in case you are absolutely sure you don't need token revocation. It is recommended **not to use it**. 420 | 421 | ```ruby 422 | class User < ApplicationRecord 423 | devise :database_authenticatable, 424 | :jwt_authenticatable, jwt_revocation_strategy: Devise::JWT::RevocationStrategies::Null 425 | end 426 | ``` 427 | 428 | #### Custom strategies 429 | 430 | You can also implement your own strategies. They just need to implement two methods: `jwt_revoked?` and `revoke_jwt`, both of them accept the JWT payload and the user record as parameters, in this order. 431 | 432 | For instance: 433 | 434 | ```ruby 435 | module MyCustomStrategy 436 | def self.jwt_revoked?(payload, user) 437 | # Does something to check whether the JWT token is revoked for given user 438 | end 439 | 440 | def self.revoke_jwt(payload, user) 441 | # Does something to revoke the JWT token for given user 442 | end 443 | end 444 | 445 | class User < ApplicationRecord 446 | devise :database_authenticatable, 447 | :jwt_authenticatable, jwt_revocation_strategy: MyCustomStrategy 448 | end 449 | ``` 450 | 451 | ### Testing 452 | 453 | Models configured with `:jwt_authenticatable` usually won't be retrieved from 454 | the session. For this reason, `sign_in` Devise testing helper methods won't 455 | work as expected. 456 | 457 | What you need to do to authenticate test environment requests is the 458 | same that you will do in production: to provide a valid token in the 459 | `Authorization` header (in the form of `Bearer #{token}`) at every request. 460 | 461 | There are two ways you can get a valid token: 462 | 463 | - Inspecting the `Authorization` response header after a valid sign in request. 464 | - Manually creating it. 465 | 466 | The first option tests the real workflow of your application, but it can slow 467 | things if you perform it at every test. 468 | 469 | For the second option, a test helper is provided in order to add the 470 | `Authorization` name/value pair to given request headers. You can use it as in 471 | the following example: 472 | 473 | ```ruby 474 | # First, require the helper module 475 | require 'devise/jwt/test_helpers' 476 | 477 | # ... 478 | 479 | it 'tests something' do 480 | user = fetch_my_user() 481 | headers = { 'Accept' => 'application/json', 'Content-Type' => 'application/json' } 482 | # This will add a valid token for `user` in the `Authorization` header 483 | auth_headers = Devise::JWT::TestHelpers.auth_headers(headers, user) 484 | 485 | get '/my/end_point', headers: auth_headers 486 | 487 | expect_something() 488 | end 489 | ``` 490 | 491 | Usually you will wrap this in your own test helper. 492 | 493 | ### Configuration reference 494 | 495 | This library can be configured calling `jwt` on Devise config object: 496 | 497 | ```ruby 498 | Devise.setup do |config| 499 | config.jwt do |jwt| 500 | # ... 501 | end 502 | end 503 | ``` 504 | #### secret 505 | 506 | Secret key is used to sign generated JWT tokens. You must set it. 507 | 508 | #### rotation_secret 509 | 510 | Allow rotating secrets. Set a new value to `secret` and copy the old secret to `rotation_secret`. 511 | 512 | #### expiration_time 513 | 514 | Number of seconds while a JWT is valid after its generation. After that, it won't be valid anymore, even if it hasn't been revoked. 515 | 516 | Defaults to 3600 seconds (1 hour). 517 | 518 | #### dispatch_requests 519 | 520 | Besides the create session one, there are additional requests where JWT tokens should be dispatched. 521 | 522 | It must be a bidimensional array, each item being an array of two elements: the request method and a regular expression that must match the request path. 523 | 524 | For example: 525 | 526 | ```ruby 527 | jwt.dispatch_requests = [ 528 | ['POST', %r{^/dispatch_path_1$}], 529 | ['GET', %r{^/dispatch_path_2$}], 530 | ] 531 | ``` 532 | 533 | **Important**: You are encouraged to delimit your regular expression with `^` and `$` to avoid unintentional matches. 534 | 535 | #### revocation_requests 536 | 537 | Besides the destroy session one, there are additional requests where JWT tokens should be revoked. 538 | 539 | It must be a bidimensional array, each item being an array of two elements: the request method and a regular expression that must match the request path. 540 | 541 | For example: 542 | 543 | ```ruby 544 | jwt.revocation_requests = [ 545 | ['DELETE', %r{^/revocation_path_1$}], 546 | ['GET', %r{^/revocation_path_2$}], 547 | ] 548 | ``` 549 | 550 | **Important**: You are encouraged to delimit your regular expression with `^` and `$` to avoid unintentional matches. 551 | 552 | #### request_formats 553 | 554 | Request formats that must be processed (in order to dispatch or revoke tokens). 555 | 556 | It must be a hash of Devise scopes as keys and an array of request formats as 557 | values. When a scope is not present or if it has a nil item, requests without 558 | format will be taken into account. 559 | 560 | For example, with following configuration, `user` scope would dispatch and 561 | revoke tokens in `json` requests (as in `/users/sign_in.json`), while 562 | `admin_user` would do it in `xml` and with no format (as in 563 | `/admin_user/sign_in.xml` and `/admin_user/sign_in`). 564 | 565 | ```ruby 566 | jwt.request_formats = { 567 | user: [:json], 568 | admin_user: [nil, :xml] 569 | } 570 | ``` 571 | 572 | By default, only requests without format are processed. 573 | 574 | #### token_header 575 | 576 | Request/response header which will transmit the JWT token. 577 | 578 | Defaults to 'Authorization' 579 | 580 | #### issuer 581 | 582 | Expected issuer claim. If present, it will be checked against the incoming 583 | token issuer claim and authorization will be skipped if they don't match. 584 | 585 | Defaults to nil. 586 | 587 | #### aud_header 588 | 589 | Request header which content will be stored to the `aud` claim in the payload. 590 | 591 | It is used to validate whether an incoming token was originally issued to the 592 | same client, checking if `aud` and the `aud_header` header value match. If you 593 | don't want to differentiate between clients, you don't need to provide that 594 | header. 595 | 596 | **Important:** Be aware that this workflow is not bullet proof. In some 597 | scenarios a user can handcraft the request headers, therefore being able to 598 | impersonate any client. In such cases you could need something more robust, 599 | like an OAuth workflow with client id and client secret. 600 | 601 | Defaults to `JWT_AUD`. 602 | 603 | #### token_header 604 | 605 | Request header containing the token in the format of `Bearer #{token}`. 606 | 607 | Defaults to `Authorization`. 608 | 609 | #### issuer 610 | 611 | The [issuer claim in the token](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1). 612 | 613 | If present, it will be checked against the incoming token issuer claim and 614 | authorization will be skipped if they don't match. 615 | 616 | Defaults to `nil`. 617 | 618 | ```ruby 619 | jwt.issuer = 'http://myapp.com' 620 | ``` 621 | 622 | ## Development 623 | 624 | There are docker and docker-compose files configured to create a development environment for this gem. So, if you use Docker you only need to run: 625 | 626 | `docker-compose up -d` 627 | 628 | An then, for example: 629 | 630 | `docker-compose exec app rspec` 631 | 632 | ## Contributing 633 | 634 | Bug reports and pull requests are welcome on GitHub at https://github.com/waiting-for-dev/devise-jwt. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 635 | 636 | ## Release Policy 637 | 638 | `devise-jwt` follows the principles of [semantic versioning](http://semver.org/). 639 | 640 | ## License 641 | 642 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 643 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'devise/jwt' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require 'pry' 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /devise-jwt.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'devise/jwt/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "devise-jwt" 8 | spec.version = Devise::JWT::VERSION 9 | spec.authors = ["Marc Busqué"] 10 | spec.email = ["marc@lamarciana.com"] 11 | 12 | spec.summary = %q{JWT authentication for devise} 13 | spec.description = %q{JWT authentication for devise with configurable token revocation strategies} 14 | spec.homepage = "https://github.com/waiting-for-dev/devise-jwt" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|features)/}) 19 | end 20 | spec.bindir = "exe" 21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 22 | spec.require_paths = ["lib"] 23 | 24 | spec.add_dependency 'devise', '~> 4.0' 25 | spec.add_dependency 'warden-jwt_auth', '~> 0.10' 26 | 27 | spec.add_development_dependency "bundler", "> 1" 28 | spec.add_development_dependency "rake", "~> 13.0" 29 | spec.add_development_dependency "rspec" 30 | # Needed to test the rails fixture application 31 | spec.add_development_dependency 'rails' 32 | spec.add_development_dependency 'sqlite3' 33 | spec.add_development_dependency 'rspec-rails' 34 | # Cops 35 | spec.add_development_dependency 'rubocop' 36 | spec.add_development_dependency 'rubocop-rspec' 37 | # Test reporting 38 | spec.add_development_dependency 'simplecov' 39 | spec.add_development_dependency 'codeclimate-test-reporter' 40 | end 41 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | app: 4 | build: . 5 | image: devise_jwt 6 | command: bash -c "bundle && tail -f Gemfile" 7 | volumes: 8 | - .:/home/devise_jwt_user/app 9 | tty: true 10 | stdin_open: true 11 | tmpfs: 12 | - /tmp 13 | -------------------------------------------------------------------------------- /issue_template.md: -------------------------------------------------------------------------------- 1 | Please, for a bug report fill in the following template. Before that, make sure to read the whole [README](https://github.com/waiting-for-dev/devise-jwt/blob/master/README.md) and check if your issue is not related with [CORS](https://github.com/waiting-for-dev/devise-jwt#model-configuration). 2 | 3 | Feature requests and questions about `devise-jwt` are also accepted. It isn't the place for generic questions about using `devise` with an API. For that, read our [wiki page](https://github.com/waiting-for-dev/devise-jwt/wiki/Configuring-devise-for-APIs) or ask somewhere else like [stackoverflow](https://stackoverflow.com/) 4 | 5 | ## Expected behavior 6 | 7 | ## Actual behavior 8 | 9 | ## Steps to Reproduce the Problem 10 | 11 | 1. 12 | 2. 13 | 3. 14 | 15 | ## Debugging information 16 | 17 | Provide following information. Please, format pasted output as code. Feel free to remove the secret key value. 18 | 19 | - Version of `devise-jwt` in use 20 | - Version of `rails` in use 21 | - Version of `warden-jwt_auth` in use 22 | - Output of `Devise::JWT.config` 23 | - Output of `Warden::JWTAuth.config` 24 | - Output of `Devise.mappings` 25 | - If your issue is related with not getting a JWT from the server: 26 | - Involved request path, method and request headers 27 | - Response headers for that request 28 | - If your issue is related with not being able to revoke a JWT: 29 | - Involved request path, method and request headers 30 | -------------------------------------------------------------------------------- /lib/devise/jwt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'forwardable' 4 | require 'devise' 5 | require 'active_support/core_ext/module/attribute_accessors' 6 | require 'warden/jwt_auth' 7 | require 'devise/jwt/version' 8 | require 'devise/jwt/mapping_inspector' 9 | require 'devise/jwt/defaults_generator' 10 | require 'devise/jwt/railtie' 11 | require 'devise/jwt/models' 12 | require 'devise/jwt/revocation_strategies' 13 | 14 | # Authentication library 15 | module Devise 16 | # Yields to Warden::JWTAuth.config 17 | # 18 | # @see Warden::JWTAuth 19 | def self.jwt 20 | yield(Devise::JWT.config) 21 | end 22 | 23 | add_module(:jwt_authenticatable, strategy: :jwt) 24 | 25 | # JWT extension for devise 26 | module JWT 27 | extend Dry::Configurable 28 | 29 | def self.forward_to_warden(setting, value) 30 | default = Warden::JWTAuth.config.send(setting) 31 | Warden::JWTAuth.config.send("#{setting}=", value || default) 32 | Warden::JWTAuth.config.send(setting) 33 | end 34 | 35 | setting(:secret, 36 | default: Warden::JWTAuth.config.secret, 37 | constructor: ->(value) { forward_to_warden(:secret, value) }) 38 | 39 | setting(:rotation_secret, 40 | default: Warden::JWTAuth.config.rotation_secret, 41 | constructor: ->(value) { forward_to_warden(:rotation_secret, value) }) 42 | 43 | setting(:decoding_secret, 44 | constructor: ->(value) { forward_to_warden(:decoding_secret, value) }) 45 | 46 | setting(:algorithm, 47 | constructor: ->(value) { forward_to_warden(:algorithm, value) }) 48 | 49 | setting(:expiration_time, 50 | default: Warden::JWTAuth.config.expiration_time, 51 | constructor: ->(value) { forward_to_warden(:expiration_time, value) }) 52 | 53 | setting(:dispatch_requests, 54 | default: Warden::JWTAuth.config.dispatch_requests, 55 | constructor: ->(value) { forward_to_warden(:dispatch_requests, value) }) 56 | 57 | setting(:revocation_requests, 58 | default: Warden::JWTAuth.config.revocation_requests, 59 | constructor: ->(value) { forward_to_warden(:revocation_requests, value) }) 60 | 61 | setting(:token_header, 62 | default: Warden::JWTAuth.config.token_header, 63 | constructor: ->(value) { forward_to_warden(:token_header, value) }) 64 | 65 | setting(:issuer, 66 | default: Warden::JWTAuth.config.issuer, 67 | constructor: ->(value) { forward_to_warden(:issuer, value) }) 68 | 69 | setting(:aud_header, 70 | default: Warden::JWTAuth.config.aud_header, 71 | constructor: ->(value) { forward_to_warden(:aud_header, value) }) 72 | 73 | # A hash of warden scopes as keys and an array of request formats that will 74 | # be processed as values. When a scope is not present or if it has a nil 75 | # item, requests without format will be taken into account. 76 | # 77 | # For example, with following configuration, `user` scope would dispatch and 78 | # revoke tokens in `json` requests, while `admin_user` would do it in `xml` 79 | # and with no format. 80 | # 81 | # @example 82 | # { 83 | # user: [:json], 84 | # admin_user: [nil, :xml] 85 | # } 86 | setting :request_formats, default: {} 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/devise/jwt/defaults_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module JWT 5 | # Generate defaults to be used in the configuration for the Devise 6 | # installation in a Rails app 7 | # 8 | # @see Warden::JWTAuth 9 | class DefaultsGenerator 10 | attr_reader :devise_mappings, :defaults 11 | 12 | def self.call 13 | new.call 14 | end 15 | 16 | def initialize 17 | @devise_mappings = Devise.mappings 18 | @defaults = { 19 | mappings: {}, 20 | revocation_strategies: {}, 21 | dispatch_requests: [], 22 | revocation_requests: [] 23 | } 24 | end 25 | 26 | def call 27 | devise_mappings.each_key do |scope| 28 | inspector = MappingInspector.new(scope) 29 | next unless inspector.jwt? 30 | 31 | add_defaults(inspector) 32 | end 33 | defaults 34 | end 35 | 36 | private 37 | 38 | def add_defaults(inspector) 39 | add_mapping(inspector) 40 | add_revocation_strategy(inspector) 41 | add_dispatch_requests(inspector) 42 | add_revocation_requests(inspector) 43 | end 44 | 45 | def add_mapping(inspector) 46 | scope = inspector.scope 47 | model = inspector.model 48 | defaults[:mappings][scope] = model.name 49 | end 50 | 51 | def add_revocation_strategy(inspector) 52 | scope = inspector.scope 53 | strategy = inspector.model.jwt_revocation_strategy 54 | defaults[:revocation_strategies][scope] = strategy.name 55 | end 56 | 57 | def add_dispatch_requests(inspector) 58 | add_sign_in_request(inspector) 59 | add_registration_request(inspector) 60 | end 61 | 62 | def add_sign_in_request(inspector) 63 | return unless inspector.session? 64 | 65 | defaults[:dispatch_requests].push(*sign_in_requests(inspector)) 66 | end 67 | 68 | def add_registration_request(inspector) 69 | return unless inspector.registration? 70 | 71 | defaults[:dispatch_requests].push(*registration_requests(inspector)) 72 | end 73 | 74 | def add_revocation_requests(inspector) 75 | return unless inspector.session? 76 | 77 | defaults[:revocation_requests].push(*sign_out_requests(inspector)) 78 | end 79 | 80 | def sign_in_requests(inspector) 81 | requests(inspector, :sign_in) 82 | end 83 | 84 | def sign_out_requests(inspector) 85 | requests(inspector, :sign_out) 86 | end 87 | 88 | def registration_requests(inspector) 89 | requests(inspector, :registration) 90 | end 91 | 92 | def requests(inspector, name) 93 | path = inspector.path(name) 94 | methods = inspector.methods(name) 95 | inspector.formats.each_with_object([]) do |format, requests| 96 | requests.push(*requests_for_format(path, methods, format)) 97 | end 98 | end 99 | 100 | def requests_for_format(path, methods, format) 101 | path_regexp = format ? /^#{path}.#{format}$/ : /^#{path}$/ 102 | methods.map do |method| 103 | [method, path_regexp] 104 | end 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/devise/jwt/mapping_inspector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module JWT 5 | # Inspect and extract information from a Devise mapping 6 | class MappingInspector 7 | attr_reader :scope, :mapping 8 | 9 | def initialize(scope) 10 | @scope = scope 11 | @mapping = Devise.mappings[scope] 12 | end 13 | 14 | def jwt? 15 | mapping.modules.member?(:jwt_authenticatable) 16 | end 17 | 18 | def session? 19 | routes?(:session) 20 | end 21 | 22 | def registration? 23 | routes?(:registration) 24 | end 25 | 26 | def model 27 | mapping.to 28 | end 29 | 30 | def path(name) 31 | prefix, scope, request = path_parts(name) 32 | [prefix, scope, request].delete_if do |item| 33 | !item || item.empty? 34 | end.join('/').prepend('/').gsub('//', '/') 35 | end 36 | 37 | def methods(name) 38 | method = case name 39 | when :sign_in then 'POST' 40 | when :sign_out then sign_out_via 41 | when :registration then 'POST' 42 | end 43 | Array(method) 44 | end 45 | 46 | def formats 47 | JWT.config.request_formats[scope] || default_formats 48 | end 49 | 50 | private 51 | 52 | def path_parts(name) 53 | prefix = mapping.instance_variable_get(:@path_prefix) 54 | path = mapping.path 55 | path_name = mapping.path_names[name] 56 | [prefix, path, path_name] 57 | end 58 | 59 | def routes?(name) 60 | mapping.routes.member?(name) 61 | end 62 | 63 | def sign_out_via 64 | Array(mapping.sign_out_via).map do |method| 65 | method.to_s.upcase 66 | end 67 | end 68 | 69 | def default_formats 70 | [nil] 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/devise/jwt/models.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'devise/jwt/models/jwt_authenticatable' 4 | 5 | module Devise 6 | # Devise models 7 | # 8 | # @see Devise::Models 9 | module Models 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/devise/jwt/models/jwt_authenticatable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/concern' 4 | 5 | module Devise 6 | module Models 7 | # Model that will be authenticatable with the JWT strategy 8 | # 9 | # @see [Warden::JWTAuth::Interfaces::UserRepository] 10 | # @see [Warden::JWTAuth::Interfaces::User] 11 | module JwtAuthenticatable 12 | extend ActiveSupport::Concern 13 | 14 | class_methods do 15 | Devise::Models.config(self, :jwt_revocation_strategy) 16 | end 17 | 18 | included do 19 | def self.find_for_jwt_authentication(sub) 20 | find_by(primary_key => sub) 21 | end 22 | end 23 | 24 | def jwt_subject 25 | id 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/devise/jwt/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails/railtie' 4 | 5 | module Devise 6 | module JWT 7 | # Pluck to rails 8 | class Railtie < Rails::Railtie 9 | initializer 'devise-jwt-middleware' do |app| 10 | app.middleware.use Warden::JWTAuth::Middleware 11 | 12 | config.after_initialize do 13 | Rails.application.reload_routes! 14 | 15 | Warden::JWTAuth.configure do |config| 16 | defaults = DefaultsGenerator.call 17 | 18 | config.mappings = defaults[:mappings] 19 | config.dispatch_requests.push(*defaults[:dispatch_requests]) 20 | config.revocation_requests.push(*defaults[:revocation_requests]) 21 | config.revocation_strategies = defaults[:revocation_strategies] 22 | end 23 | end 24 | 25 | ActiveSupport::Reloader.to_prepare do 26 | Warden::JWTAuth.configure do |config| 27 | defaults = DefaultsGenerator.call 28 | 29 | config.mappings = defaults[:mappings] 30 | config.revocation_strategies = defaults[:revocation_strategies] 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/devise/jwt/revocation_strategies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'devise/jwt/revocation_strategies/jti_matcher' 4 | require 'devise/jwt/revocation_strategies/denylist' 5 | require 'devise/jwt/revocation_strategies/allowlist' 6 | require 'devise/jwt/revocation_strategies/null' 7 | 8 | module Devise 9 | module JWT 10 | # Pre-build revocation strategies 11 | # 12 | # @see Warden::JWTAuth::Interfaces::RevocationStrategy 13 | module RevocationStrategies 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/devise/jwt/revocation_strategies/allowlist.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/concern' 4 | 5 | module Devise 6 | module JWT 7 | module RevocationStrategies 8 | # This strategy must be included in the user model. 9 | # 10 | # The JwtAllowlist table must include `jti`, `aud`, `exp` and `user_id` 11 | # columns 12 | # 13 | # In order to tell whether a token is revoked, it just tries to find the 14 | # `jti` and `aud` values from the token on the `allowlisted_jwts` 15 | # table for the respective user. 16 | # 17 | # If the values don't exist means the token was revoked. 18 | # On revocation, it deletes the matching record from the 19 | # `allowlisted_jwts` table. 20 | # 21 | # On sign in, it creates a new record with the `jti` and `aud` values. 22 | module Allowlist 23 | extend ActiveSupport::Concern 24 | 25 | included do 26 | has_many :allowlisted_jwts, dependent: :destroy 27 | 28 | # @see Warden::JWTAuth::Interfaces::RevocationStrategy#jwt_revoked? 29 | def self.jwt_revoked?(payload, user) 30 | !user.allowlisted_jwts.exists?(payload.slice('jti', 'aud')) 31 | end 32 | 33 | # @see Warden::JWTAuth::Interfaces::RevocationStrategy#revoke_jwt 34 | def self.revoke_jwt(payload, user) 35 | jwt = user.allowlisted_jwts.find_by(payload.slice('jti', 'aud')) 36 | jwt.destroy! if jwt 37 | end 38 | end 39 | 40 | # Warden::JWTAuth::Interfaces::User#on_jwt_dispatch 41 | def on_jwt_dispatch(_token, payload) 42 | allowlisted_jwts.create!( 43 | jti: payload['jti'], 44 | aud: payload['aud'], 45 | exp: Time.at(payload['exp'].to_i) 46 | ) 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/devise/jwt/revocation_strategies/denylist.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/concern' 4 | 5 | module Devise 6 | module JWT 7 | module RevocationStrategies 8 | # This strategy must be included in an ActiveRecord model, and requires 9 | # that it has a `jti` column. 10 | # 11 | # In order to tell whether a token is revoked, it just checks whether 12 | # `jti` is in the table. On revocation, creates a new record with it. 13 | module Denylist 14 | extend ActiveSupport::Concern 15 | 16 | included do 17 | # @see Warden::JWTAuth::Interfaces::RevocationStrategy#jwt_revoked? 18 | def self.jwt_revoked?(payload, _user) 19 | exists?(jti: payload['jti']) 20 | end 21 | 22 | # @see Warden::JWTAuth::Interfaces::RevocationStrategy#revoke_jwt 23 | def self.revoke_jwt(payload, _user) 24 | find_or_create_by!(jti: payload['jti'], 25 | exp: Time.at(payload['exp'].to_i)) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/devise/jwt/revocation_strategies/jti_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/concern' 4 | require 'securerandom' 5 | 6 | module Devise 7 | module JWT 8 | module RevocationStrategies 9 | # This strategy must be included in the user model, and requires that it 10 | # has a `jti` column. It adds the value of the `jti` column as the `jti` 11 | # claim in dispatched tokens. 12 | # 13 | # In order to tell whether a token is revoked, it just compares both `jti` 14 | # values. On revocation, it changes column value so that the token is no 15 | # longer valid. 16 | module JTIMatcher 17 | extend ActiveSupport::Concern 18 | 19 | included do 20 | before_create :initialize_jti 21 | 22 | # @see Warden::JWTAuth::Interfaces::RevocationStrategy#jwt_revoked? 23 | def self.jwt_revoked?(payload, user) 24 | payload['jti'] != user.jti 25 | end 26 | 27 | # @see Warden::JWTAuth::Interfaces::RevocationStrategy#revoke_jwt 28 | def self.revoke_jwt(_payload, user) 29 | user.update_column(:jti, generate_jti) 30 | end 31 | 32 | # Generates a random and unique string to be used as jti 33 | def self.generate_jti 34 | SecureRandom.uuid 35 | end 36 | end 37 | 38 | # Warden::JWTAuth::Interfaces::User#jwt_payload 39 | def jwt_payload 40 | { 'jti' => jti } 41 | end 42 | 43 | private 44 | 45 | def initialize_jti 46 | self.jti = self.class.generate_jti 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/devise/jwt/revocation_strategies/null.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/concern' 4 | 5 | module Devise 6 | module JWT 7 | module RevocationStrategies 8 | # This strategy is just a null object pattern strategy, so it does not 9 | # revoke anything 10 | module Null 11 | # @see Warden::JWTAuth::Interfaces::RevocationStrategy#jwt_revoked? 12 | def self.jwt_revoked?(_payload, _user) 13 | false 14 | end 15 | 16 | # @see Warden::JWTAuth::Interfaces::RevocationStrategy#revoke_jwt 17 | def self.revoke_jwt(_payload, _user); end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/devise/jwt/test_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module JWT 5 | # Helpers to make testing authorization through JWT easier 6 | module TestHelpers 7 | # Returns headers with a valid token in the `Authorization` header 8 | # added. 9 | # 10 | # Side effects could happen if you have implemented 11 | # `on_jwt_dispatch` method on the user model (as it happens in 12 | # the allowlist revocation strategy). 13 | # 14 | # Be aware that a fresh copy of `headers` is returned with the new 15 | # key/value pair added, instead of modifying given argument. 16 | # 17 | # @param headers [Hash] Headers to which add the `Authorization` item. 18 | # @param user [ActiveRecord::Base] The user to authenticate. 19 | # @param scope [Symbol] The warden scope. If `nil` it will be 20 | # autodetected. 21 | # @param aud [String] The aud claim. If `nil` it will be autodetected from 22 | # the header name configured in `Devise::JWT.config.aud_header`. 23 | def self.auth_headers(headers, user, scope: nil, aud: nil) 24 | scope ||= Devise::Mapping.find_scope!(user) 25 | aud ||= headers[Warden::JWTAuth.config.aud_header] 26 | token, payload = Warden::JWTAuth::UserEncoder.new.call( 27 | user, scope, aud 28 | ) 29 | user.on_jwt_dispatch(token, payload) if user.respond_to?(:on_jwt_dispatch) 30 | Warden::JWTAuth::HeaderParser.to_headers(headers, token) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/devise/jwt/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Devise 4 | module JWT 5 | VERSION = '0.12.1' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/devise/devise_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Devise do 6 | describe '::jwt' do 7 | it 'yields to Devise::JWT.config' do 8 | described_class.jwt { |jwt| jwt.expiration_time = 900 } 9 | 10 | expect(Devise::JWT.config.expiration_time).to eq(900) 11 | end 12 | end 13 | 14 | it 'adds jwt_authenticatable module' do 15 | expect(described_class::ALL).to include(:jwt_authenticatable) 16 | end 17 | 18 | it 'associates jwt_authenticatable module with jwt strategy' do 19 | expect(described_class::STRATEGIES[:jwt_authenticatable]).to eq(:jwt) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/devise/jwt/defaults_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Devise::JWT::DefaultsGenerator do 6 | subject(:defaults) { described_class.call } 7 | 8 | describe 'mappings' do 9 | # rubocop:disable RSpec/ExampleLength 10 | it 'adds devise models with jwt as strings' do 11 | expect(defaults[:mappings]).to eq( 12 | jwt_with_jti_matcher_user: 'JwtWithJtiMatcherUser', 13 | jwt_with_denylist_user: 'JwtWithDenylistUser', 14 | jwt_with_allowlist_user: 'JwtWithAllowlistUser', 15 | jwt_with_null_user: 'JwtWithNullUser' 16 | ) 17 | end 18 | # rubocop:enable RSpec/ExampleLength 19 | end 20 | 21 | describe 'dispatch_requests' do 22 | it 'adds create session requests for devise models with jwt' do 23 | expect(defaults[:dispatch_requests]).to include( 24 | ['POST', %r{^/jwt_with_jti_matcher_users/sign_in$}] 25 | ) 26 | end 27 | 28 | # rubocop:disable RSpec/ExampleLength, RSpec/MultipleExpectations 29 | it 'respects configured format segment for create session requests' do 30 | expect(defaults[:dispatch_requests]).to include( 31 | ['POST', %r{^/jwt_with_denylist_users/sign_in.json$}], 32 | ['POST', %r{^/jwt_with_denylist_users/sign_in.xml$}] 33 | ) 34 | expect(defaults[:dispatch_requests]).not_to include( 35 | ['POST', %r{^/jwt_with_denylist_users/sign_in$}] 36 | ) 37 | end 38 | # rubocop:enable RSpec/ExampleLength, RSpec/MultipleExpectations 39 | 40 | it 'respect route scopes for create session requests' do 41 | expect(defaults[:dispatch_requests]).to include( 42 | ['POST', %r{^/a/scope/jwt_with_null_users/sign_in$}] 43 | ) 44 | end 45 | 46 | it 'does not add create session requests for devise models without jwt' do 47 | expect(defaults[:dispatch_requests]).not_to include( 48 | ['POST', %r{^/no_jwt_users/sign_in$}] 49 | ) 50 | end 51 | 52 | it 'adds registration requests for devise models with jwt' do 53 | expect(defaults[:dispatch_requests]).to include( 54 | ['POST', %r{^/jwt_with_jti_matcher_users$}] 55 | ) 56 | end 57 | 58 | # rubocop:disable RSpec/ExampleLength, RSpec/MultipleExpectations 59 | it 'respects configured format segment for registration requests' do 60 | expect(defaults[:dispatch_requests]).to include( 61 | ['POST', %r{^/jwt_with_denylist_users.json$}], 62 | ['POST', %r{^/jwt_with_denylist_users.xml$}] 63 | ) 64 | expect(defaults[:dispatch_requests]).not_to include( 65 | ['POST', %r{^/jwt_with_denylist_users$}] 66 | ) 67 | end 68 | # rubocop:enable RSpec/ExampleLength, RSpec/MultipleExpectations 69 | 70 | it 'respect route scopes for registration requests' do 71 | expect(defaults[:dispatch_requests]).to include( 72 | ['POST', %r{^/a/scope/jwt_with_null_users$}] 73 | ) 74 | end 75 | 76 | it 'does not add registration requests for devise models without jwt' do 77 | expect(defaults[:dispatch_requests]).not_to include( 78 | ['POST', %r{^/no_jwt_users$}] 79 | ) 80 | end 81 | end 82 | 83 | describe 'revocation_requests' do 84 | it 'adds destroy session requests for devise models with jwt' do 85 | expect(defaults[:revocation_requests]).to include( 86 | ['DELETE', %r{^/jwt_with_jti_matcher_users/sign_out$}] 87 | ) 88 | end 89 | 90 | # rubocop:disable RSpec/ExampleLength, RSpec/MultipleExpectations 91 | it 'respects configured format segment for destroy session requests' do 92 | expect(defaults[:revocation_requests]).to include( 93 | ['POST', %r{^/jwt_with_denylist_users/sign_out.json$}], 94 | ['POST', %r{^/jwt_with_denylist_users/sign_out.xml$}] 95 | ) 96 | expect(defaults[:revocation_requests]).not_to include( 97 | ['POST', %r{^/jwt_with_denylist_users/sign_out$}] 98 | ) 99 | end 100 | # rubocop:enable RSpec/ExampleLength, RSpec/MultipleExpectations 101 | 102 | it 'respect sign_out_via configuration for destroy session requests' do 103 | expect(defaults[:revocation_requests]).to include( 104 | ['POST', %r{^/jwt_with_denylist_users/sign_out.json$}], 105 | ['GET', %r{^/jwt_with_allowlist_users/sign_out$}], 106 | ['DELETE', %r{^/jwt_with_allowlist_users/sign_out$}] 107 | ) 108 | end 109 | 110 | it 'respect route scopes for destroy session requests' do 111 | expect(defaults[:revocation_requests]).to include( 112 | ['DELETE', %r{^/a/scope/jwt_with_null_users/sign_out$}] 113 | ) 114 | end 115 | 116 | it 'does not add destroy session requests for devise models without jwt' do 117 | expect(defaults[:revocation_requests]).not_to include( 118 | ['DELETE', %r{^/no_jwt_users/sign_out$}] 119 | ) 120 | end 121 | end 122 | 123 | describe 'revocation_strategies' do 124 | # rubocop:disable RSpec/ExampleLength 125 | it 'adds strategies configured for each devise model with jwt' do 126 | expect(defaults[:revocation_strategies]).to eq( 127 | jwt_with_jti_matcher_user: 'JwtWithJtiMatcherUser', 128 | jwt_with_denylist_user: 'JwtDenylist', 129 | jwt_with_allowlist_user: 'JwtWithAllowlistUser', 130 | jwt_with_null_user: 'Devise::JWT::RevocationStrategies::Null' 131 | ) 132 | end 133 | # rubocop:enable RSpec/ExampleLength 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /spec/devise/jwt/mapping_inspector_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Devise::JWT::MappingInspector do 6 | let(:jwt_with_jti_matcher_inspector) do 7 | described_class.new(:jwt_with_jti_matcher_user) 8 | end 9 | 10 | let(:jwt_with_null_inspector) do 11 | described_class.new(:jwt_with_null_user) 12 | end 13 | 14 | let(:jwt_with_denylist_inspector) do 15 | described_class.new(:jwt_with_denylist_user) 16 | end 17 | 18 | let(:jwt_with_allowlist_inspector) do 19 | described_class.new(:jwt_with_allowlist_user) 20 | end 21 | 22 | let(:no_jwt_inspector) { described_class.new(:no_jwt_user) } 23 | 24 | describe '#jwt?' do 25 | context 'when jwt_authenticatable module is included' do 26 | it 'returns true' do 27 | expect(jwt_with_jti_matcher_inspector.jwt?).to be(true) 28 | end 29 | end 30 | 31 | context 'when jwt_authenticatable module is not included' do 32 | it 'returns false' do 33 | expect(no_jwt_inspector.jwt?).to be(false) 34 | end 35 | end 36 | end 37 | 38 | describe '#session?' do 39 | context 'when session routes are included' do 40 | it 'returns true' do 41 | expect(jwt_with_jti_matcher_inspector.session?).to be(true) 42 | end 43 | end 44 | 45 | context 'when session routes are not included' do 46 | it 'returns true' do 47 | jwt_with_jti_matcher_inspector.mapping.routes.delete(:session) 48 | 49 | expect(jwt_with_jti_matcher_inspector.session?).to be(false) 50 | end 51 | end 52 | end 53 | 54 | describe '#registration?' do 55 | context 'when registration routes are included' do 56 | it 'returns true' do 57 | expect(jwt_with_jti_matcher_inspector.registration?).to be(true) 58 | end 59 | end 60 | 61 | context 'when registration routes are not included' do 62 | it 'returns true' do 63 | jwt_with_jti_matcher_inspector.mapping.routes.delete(:registration) 64 | 65 | expect(jwt_with_jti_matcher_inspector.registration?).to be(false) 66 | end 67 | end 68 | end 69 | 70 | describe '#model' do 71 | it 'returns associated model' do 72 | expect(jwt_with_jti_matcher_inspector.model).to eq(JwtWithJtiMatcherUser) 73 | end 74 | end 75 | 76 | describe '#path' do 77 | it 'returns path for given name' do 78 | expect(jwt_with_jti_matcher_inspector.path(:sign_in)).to eq( 79 | '/jwt_with_jti_matcher_users/sign_in' 80 | ) 81 | end 82 | 83 | it 'respects scope parts' do 84 | expect(jwt_with_null_inspector.path(:sign_in)).to eq( 85 | '/a/scope/jwt_with_null_users/sign_in' 86 | ) 87 | end 88 | end 89 | 90 | describe '#methods' do 91 | context 'when name is :sign_in' do 92 | it "returns ['POST']" do 93 | expect(jwt_with_jti_matcher_inspector.methods(:sign_in)).to eq(['POST']) 94 | end 95 | end 96 | 97 | context 'when name is :registration' do 98 | it "returns ['POST']" do 99 | expect(jwt_with_jti_matcher_inspector.methods(:registration)).to eq( 100 | ['POST'] 101 | ) 102 | end 103 | end 104 | 105 | context 'when name is :sign_out' do 106 | it 'returns mapping sign_out_via option as upcased string' do 107 | expect(jwt_with_jti_matcher_inspector.methods(:sign_out)).to eq( 108 | ['DELETE'] 109 | ) 110 | end 111 | 112 | it 'respects when sign_out_via option is not the default' do 113 | expect(jwt_with_denylist_inspector.methods(:sign_out)).to eq( 114 | ['POST'] 115 | ) 116 | end 117 | 118 | it 'accepts sign_out_via option when it contains multiple methods' do 119 | expect(jwt_with_allowlist_inspector.methods(:sign_out)).to match_array( 120 | %w[GET DELETE] 121 | ) 122 | end 123 | end 124 | end 125 | 126 | describe '#formats' do 127 | context 'when scope has no configured formats' do 128 | it 'returns an array with a nil element' do 129 | expect(jwt_with_jti_matcher_inspector.formats).to eq([nil]) 130 | end 131 | end 132 | 133 | context 'when scope has configured formats' do 134 | it 'returns them' do 135 | expect(jwt_with_denylist_inspector.formats).to eq(%i[json xml]) 136 | end 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /spec/devise/jwt/railtie_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Devise::JWT::Railtie do 6 | let(:rails) { Rails } 7 | let(:rails_config) { Rails.configuration } 8 | 9 | it 'adds JWTAuth middleware' do 10 | expect(rails_config.middleware).to include(Warden::JWTAuth::Middleware) 11 | end 12 | 13 | it 'configure mappings using defaults' do 14 | expect(Warden::JWTAuth.config.mappings).to include( 15 | jwt_with_jti_matcher_user: JwtWithJtiMatcherUser 16 | ) 17 | end 18 | 19 | it 'configures dispatch_requests using defaults' do 20 | expect(Warden::JWTAuth.config.dispatch_requests).to include( 21 | ['POST', %r{^/jwt_with_jti_matcher_users/sign_in$}] 22 | ) 23 | end 24 | 25 | it 'does not override user defined dispatch_requests' do 26 | expect(Warden::JWTAuth.config.dispatch_requests).to include( 27 | ['GET', %r{^/foo_path$}] 28 | ) 29 | end 30 | 31 | it 'configures revocation_requests using defaults' do 32 | expect(Warden::JWTAuth.config.revocation_requests).to include( 33 | ['DELETE', %r{^/jwt_with_jti_matcher_users/sign_out$}] 34 | ) 35 | end 36 | 37 | it 'does not override user defined revocation' do 38 | expect(Warden::JWTAuth.config.revocation_requests).to include( 39 | ['GET', %r{^/bar_path$}] 40 | ) 41 | end 42 | 43 | it 'configures revocation_strategies using defaults' do 44 | expect(Warden::JWTAuth.config.revocation_strategies).to include( 45 | jwt_with_jti_matcher_user: JwtWithJtiMatcherUser 46 | ) 47 | end 48 | 49 | it 'configures algorithm using defaults' do 50 | expect(Warden::JWTAuth.config.algorithm).to eq('HS256') 51 | end 52 | 53 | it 'configures decoding_secret using defaults' do 54 | expect(Warden::JWTAuth.config.decoding_secret).to eq( 55 | Warden::JWTAuth.config.secret 56 | ) 57 | end 58 | 59 | context 'when asymmetric algorithm is user configured' do 60 | let(:rsa_secret) { OpenSSL::PKey::RSA.generate 2048 } 61 | let(:decoding_secret) { rsa_secret.public_key } 62 | 63 | before do 64 | Rails.configuration.devise.jwt do |jwt| 65 | jwt.algorithm = 'RS256' 66 | jwt.secret = rsa_secret 67 | jwt.decoding_secret = decoding_secret 68 | end 69 | end 70 | 71 | it 'does not override user defined algorithm' do 72 | expect(Warden::JWTAuth.config.algorithm).to eq('RS256') 73 | end 74 | 75 | it 'does not override user defined secret' do 76 | expect(Warden::JWTAuth.config.secret).to eq(rsa_secret) 77 | end 78 | 79 | it 'does not override user defined decoding_secret' do 80 | expect(Warden::JWTAuth.config.decoding_secret).to eq(decoding_secret) 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/devise/jwt/revocation_strategies/allowlist_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Devise::JWT::RevocationStrategies::Allowlist do 6 | subject(:strategy) { JwtWithAllowlistUser } 7 | 8 | include_context 'fixtures' 9 | 10 | let(:model) { JwtWithAllowlistUser } 11 | 12 | let(:user) { jwt_with_allowlist_user } 13 | 14 | let(:payload) do 15 | { 'jti' => '123', 'aud' => 'client1', 'exp' => Time.at(1_501_717_440) } 16 | end 17 | 18 | describe '#jwt_revoked?(payload, user)' do 19 | context 'when jti and aud in payload exist on jwt_allowlist' do 20 | before { user.allowlisted_jwts.create(payload) } 21 | 22 | it 'returns false' do 23 | expect(strategy.jwt_revoked?(payload, user)).to be(false) 24 | end 25 | end 26 | 27 | context 'when jti and aud payload does not exist on jwt_allowlist' do 28 | it 'returns true' do 29 | expect(strategy.jwt_revoked?(payload, user)).to be(true) 30 | end 31 | end 32 | end 33 | 34 | describe '#revoke_jwt(payload, user)' do 35 | before { user.allowlisted_jwts.create(payload) } 36 | 37 | it 'deletes matching jwt_allowlist record' do 38 | expect { strategy.revoke_jwt(payload, user) } 39 | .to(change { user.allowlisted_jwts.count }.by(-1)) 40 | end 41 | 42 | it 'does not crash when token has already been revoked' do 43 | expect do 44 | strategy.revoke_jwt(payload, user) 45 | 46 | strategy.revoke_jwt(payload, user) 47 | end.not_to raise_error 48 | end 49 | end 50 | 51 | describe '#on_jwt_dispatch(token, payload)' do 52 | it 'creates allowlisted_jwt record from the payload' do 53 | jwt_with_allowlist_user.on_jwt_dispatch(:token, payload) 54 | 55 | expect( 56 | jwt_with_allowlist_user.allowlisted_jwts.exists?(payload) 57 | ).to be(true) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/devise/jwt/revocation_strategies/denylist_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Devise::JWT::RevocationStrategies::Denylist do 6 | subject(:strategy) { JwtDenylist } 7 | 8 | let(:payload) { { 'jti' => '123', 'exp' => '1501717440' } } 9 | 10 | describe '#jwt_revoked?(payload, user)' do 11 | context 'when payload jti is in the denylist' do 12 | it 'returns true' do 13 | strategy.create(jti: '123') 14 | 15 | expect(strategy.jwt_revoked?(payload, :whatever)).to be(true) 16 | end 17 | end 18 | 19 | context 'when payload jti is not in the denylist' do 20 | it 'returns true' do 21 | expect(strategy.jwt_revoked?(payload, :whatever)).to be(false) 22 | end 23 | end 24 | end 25 | 26 | describe '#revoke_jwt(payload, user)' do 27 | it 'adds payload jti to the denylist' do 28 | strategy.revoke_jwt(payload, :whatever) 29 | 30 | expect(strategy.find_by(jti: '123')).not_to be_nil 31 | end 32 | 33 | it 'populates exp (expiration_time)' do 34 | strategy.revoke_jwt(payload, :whatever) 35 | 36 | exp = strategy.find_by(jti: '123').exp 37 | expect(exp).to eq(Time.at(payload['exp'].to_i)) 38 | end 39 | 40 | it 'does not crash when token has already been revoked' do 41 | expect do 42 | strategy.revoke_jwt(payload, :whatever) 43 | 44 | strategy.revoke_jwt(payload, :whatever) 45 | end.not_to raise_error 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/devise/jwt/revocation_strategies/jti_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Devise::JWT::RevocationStrategies::JTIMatcher do 6 | subject(:strategy) { JwtWithJtiMatcherUser } 7 | 8 | include_context 'fixtures' 9 | 10 | let(:model) { JwtWithJtiMatcherUser } 11 | 12 | let(:user) { jwt_with_jti_matcher_user } 13 | 14 | describe 'Callbacks' do 15 | context 'with before create' do 16 | it 'initializes jti' do 17 | expect(user.jti).not_to be_nil 18 | end 19 | end 20 | end 21 | 22 | describe '#jwt_revoked?(payload, user)' do 23 | let(:payload) { { 'jti' => jti } } 24 | 25 | context 'when jti in payload matches user jti column' do 26 | let(:jti) { user.jti } 27 | 28 | it 'returns false' do 29 | expect(strategy.jwt_revoked?(payload, user)).to be(false) 30 | end 31 | end 32 | 33 | context 'when jti in payload does not match user jti column' do 34 | let(:jti) { '123' } 35 | 36 | it 'returns true' do 37 | expect(strategy.jwt_revoked?(payload, user)).to be(true) 38 | end 39 | end 40 | end 41 | 42 | describe '#revoke_jwt(payload, user)' do 43 | it 'changes user jti column' do 44 | expect { strategy.revoke_jwt('whatever', user) }.to(change(user, :jti)) 45 | end 46 | end 47 | 48 | describe '#jwt_payload' do 49 | it 'includes jti column in jti claim' do 50 | expect(user.jwt_payload['jti']).to eq(user.jti) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/devise/jwt/revocation_strategies/null_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Devise::JWT::RevocationStrategies::Null do 6 | subject(:strategy) { described_class } 7 | 8 | describe '#jwt_revoked?(payload, user)' do 9 | it 'returns false' do 10 | expect(strategy.jwt_revoked?(:whatever, :whatever)).to be(false) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/devise/jwt/test_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'devise/jwt/test_helpers' 5 | 6 | describe Devise::JWT::TestHelpers do 7 | include_context 'fixtures' 8 | 9 | let(:headers) { { 'foo' => 'bar', 'JWT_AUD' => 'client' } } 10 | let(:user) { jwt_with_jti_matcher_user } 11 | 12 | def payload_from_headers(headers) 13 | _method, token = headers['Authorization'].split 14 | Warden::JWTAuth::TokenDecoder.new.call(token) 15 | end 16 | 17 | describe '::auth_headers(headers, user, scope:, aud:)' do 18 | it 'adds a valid token in the Authorization header' do 19 | auth_headers = described_class.auth_headers(headers, user) 20 | payload = payload_from_headers(auth_headers) 21 | 22 | expect(payload['sub']).to eq(user.id.to_s) 23 | end 24 | 25 | it 'preserves present headers' do 26 | auth_headers = described_class.auth_headers(headers, user) 27 | 28 | expect(auth_headers['foo']).to eq('bar') 29 | end 30 | 31 | it 'detects user scope' do 32 | auth_headers = described_class.auth_headers(headers, user) 33 | payload = payload_from_headers(auth_headers) 34 | 35 | expect(payload['scp']).to eq('jwt_with_jti_matcher_user') 36 | end 37 | 38 | it 'can manually specify scope' do 39 | auth_headers = described_class.auth_headers(headers, user, scope: :foo) 40 | payload = payload_from_headers(auth_headers) 41 | 42 | expect(payload['scp']).to eq('foo') 43 | end 44 | 45 | it 'detects aud header' do 46 | auth_headers = described_class.auth_headers(headers, user) 47 | payload = payload_from_headers(auth_headers) 48 | 49 | expect(payload['aud']).to eq('client') 50 | end 51 | 52 | it 'can manually specify aud claim' do 53 | auth_headers = described_class.auth_headers(headers, user, aud: 'foo') 54 | payload = payload_from_headers(auth_headers) 55 | 56 | expect(payload['aud']).to eq('foo') 57 | end 58 | 59 | it 'calls on_jwt_dispatch method on the user model' do 60 | user = jwt_with_allowlist_user 61 | auth_headers = described_class.auth_headers(headers, user) 62 | payload = payload_from_headers(auth_headers) 63 | 64 | expect(user.allowlisted_jwts.first.jti).to eq(payload['jti']) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/devise/jwt_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Devise::JWT do 6 | it 'has a version number' do 7 | expect(Devise::JWT::VERSION).not_to be_nil 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/devise/models/jwt_authenticatable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Devise::Models::JwtAuthenticatable do 6 | include_context 'fixtures' 7 | 8 | let(:model) { jwt_with_jti_matcher_model } 9 | let(:user) { jwt_with_jti_matcher_user } 10 | 11 | describe '#find_for_jwt_authentication(sub)' do 12 | it 'finds record which has given `sub` as `id`' do 13 | expect(model.find_for_jwt_authentication(user.id)).to eq(user) 14 | end 15 | 16 | it 'returns nil when user is not found' do 17 | expect(model.find_for_jwt_authentication('none')).to be_nil 18 | end 19 | end 20 | 21 | describe '#jwt_subject' do 22 | it 'returns id' do 23 | expect(user.jwt_subject).to eq(user.id) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/features/authorization_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'Authorization', type: :request do 6 | include_context 'feature' 7 | include_context 'fixtures' 8 | 9 | context 'JWT user with JTI matcher revocation' do 10 | let(:user) { jwt_with_jti_matcher_user } 11 | let(:user_params) do 12 | { 13 | jwt_with_jti_matcher_user: { 14 | email: user.email, 15 | password: user.password 16 | } 17 | } 18 | end 19 | 20 | it 'authorizes requests with a valid token' do 21 | auth = sign_in(jwt_with_jti_matcher_user_session_path, user_params) 22 | 23 | get_with_auth('/jwt_with_jti_matcher_user_auth_action', auth) 24 | 25 | expect(response.status).to eq(200) 26 | end 27 | 28 | it 'unauthorizes requests with an invalid token' do 29 | auth = 'Bearer 123' 30 | 31 | get_with_auth('/jwt_with_jti_matcher_user_auth_action', auth) 32 | 33 | expect(response.status).to eq(401) 34 | end 35 | end 36 | 37 | context 'JWT user with allowlist revocation' do 38 | let(:user) { jwt_with_allowlist_user } 39 | let(:user_params) do 40 | { 41 | jwt_with_allowlist_user: { 42 | email: user.email, 43 | password: user.password 44 | } 45 | } 46 | end 47 | 48 | it 'authorizes requests with a valid token' do 49 | auth = sign_in(jwt_with_allowlist_user_session_path, user_params) 50 | 51 | get_with_auth('/jwt_with_allowlist_user_auth_action', auth) 52 | 53 | expect(response.status).to eq(200) 54 | end 55 | 56 | it 'unauthorizes requests with an invalid token' do 57 | auth = 'Bearer 123' 58 | 59 | get_with_auth('/jwt_with_allowlist_user_auth_action', auth) 60 | 61 | expect(response.status).to eq(401) 62 | end 63 | end 64 | 65 | context 'JWT user with Denylist revocation' do 66 | let(:user) { jwt_with_denylist_user } 67 | let(:user_params) do 68 | { 69 | jwt_with_denylist_user: { 70 | email: user.email, 71 | password: user.password 72 | } 73 | } 74 | end 75 | 76 | it 'authorizes requests with a valid token' do 77 | auth = sign_in( 78 | jwt_with_denylist_user_session_path, user_params, format: :json 79 | ) 80 | 81 | get_with_auth('/jwt_with_denylist_user_auth_action', auth) 82 | 83 | expect(response.status).to eq(200) 84 | end 85 | 86 | it 'unauthorizes requests with an invalid token' do 87 | auth = 'Bearer 123' 88 | 89 | get_with_auth('/jwt_with_denylist_user_auth_action', auth) 90 | 91 | expect(response.status).to eq(401) 92 | end 93 | end 94 | 95 | context 'JWT user with Null revocation' do 96 | let(:user) { jwt_with_null_user } 97 | let(:user_params) do 98 | { 99 | jwt_with_null_user: { 100 | email: user.email, 101 | password: user.password 102 | } 103 | } 104 | end 105 | 106 | it 'authorizes requests with a valid token' do 107 | auth = sign_in(jwt_with_null_user_session_path, user_params) 108 | 109 | get_with_auth('/jwt_with_null_user_auth_action', auth) 110 | 111 | expect(response.status).to eq(200) 112 | end 113 | 114 | it 'unauthorizes requests with an invalid token' do 115 | auth = 'Bearer 123' 116 | 117 | get_with_auth('/jwt_with_null_user_auth_action', auth) 118 | 119 | expect(response.status).to eq(401) 120 | end 121 | end 122 | 123 | context 'when a token from another scope and same id is given' do 124 | let(:user) { jwt_with_jti_matcher_user } 125 | let(:user_params) do 126 | { 127 | jwt_with_jti_matcher_user: { 128 | email: user.email, 129 | password: user.password 130 | } 131 | } 132 | end 133 | 134 | it 'unauthorizes' do 135 | auth = sign_in(jwt_with_jti_matcher_user_session_path, user_params) 136 | jwt_with_denylist_user.update(id: user.id) 137 | 138 | get_with_auth('/jwt_with_denylist_user_auth_action', auth) 139 | 140 | expect(response.status).to eq(401) 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /spec/features/token_dispatch_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'Token dispatch', type: :request do 6 | include_context 'feature' 7 | include_context 'fixtures' 8 | 9 | let(:generic_registration_params) do 10 | { 11 | email: 'registration@email.com', 12 | password: 'password' 13 | } 14 | end 15 | 16 | context 'JWT user with JTI matcher revocation' do 17 | let(:user) { jwt_with_jti_matcher_user } 18 | let(:sign_in_params) do 19 | { 20 | jwt_with_jti_matcher_user: { 21 | email: user.email, 22 | password: user.password 23 | } 24 | } 25 | end 26 | let(:registration_params) do 27 | { 28 | jwt_with_jti_matcher_user: generic_registration_params 29 | } 30 | end 31 | 32 | it 'dispatches JWT in sign_in requests' do 33 | sign_in(jwt_with_jti_matcher_user_session_path, sign_in_params) 34 | 35 | expect(response.headers['Authorization']).not_to be_nil 36 | end 37 | 38 | it 'dispatches JWT in registration requests' do 39 | sign_up(jwt_with_jti_matcher_user_registration_path, registration_params) 40 | 41 | expect(response.headers['Authorization']).not_to be_nil 42 | end 43 | end 44 | 45 | context 'JWT user with Denylist revocation' do 46 | let(:user) { jwt_with_denylist_user } 47 | let(:sign_in_params) do 48 | { 49 | jwt_with_denylist_user: { 50 | email: user.email, 51 | password: user.password 52 | } 53 | } 54 | end 55 | let(:registration_params) do 56 | { 57 | jwt_with_denylist_user: generic_registration_params 58 | } 59 | end 60 | 61 | it 'dispatches JWT in sign_in requests' do 62 | sign_in( 63 | jwt_with_denylist_user_session_path, sign_in_params, format: :json 64 | ) 65 | 66 | expect(response.headers['Authorization']).not_to be_nil 67 | end 68 | 69 | it 'dispatches JWT in registration requests' do 70 | sign_up( 71 | jwt_with_denylist_user_registration_path, 72 | registration_params, format: :json 73 | ) 74 | 75 | expect(response.headers['Authorization']).not_to be_nil 76 | end 77 | end 78 | 79 | context 'JWT user with allowlist revocation' do 80 | let(:user) { jwt_with_allowlist_user } 81 | let(:sign_in_params) do 82 | { 83 | jwt_with_allowlist_user: { 84 | email: user.email, 85 | password: user.password 86 | } 87 | } 88 | end 89 | let(:registration_params) do 90 | { 91 | jwt_with_allowlist_user: generic_registration_params 92 | } 93 | end 94 | 95 | it 'dispatches JWT in sign_in requests' do 96 | sign_in(jwt_with_allowlist_user_session_path, sign_in_params) 97 | 98 | expect(response.headers['Authorization']).not_to be_nil 99 | end 100 | 101 | it 'dispatches JWT in registration requests' do 102 | sign_up(jwt_with_allowlist_user_registration_path, registration_params) 103 | 104 | expect(response.headers['Authorization']).not_to be_nil 105 | end 106 | end 107 | 108 | context 'JWT user with Null revocation' do 109 | let(:user) { jwt_with_null_user } 110 | let(:sign_in_params) do 111 | { 112 | jwt_with_null_user: { 113 | email: user.email, 114 | password: user.password 115 | } 116 | } 117 | end 118 | let(:registration_params) do 119 | { 120 | jwt_with_null_user: generic_registration_params 121 | } 122 | end 123 | 124 | it 'dispatches JWT in sign_in requests' do 125 | sign_in(jwt_with_null_user_session_path, sign_in_params) 126 | 127 | expect(response.headers['Authorization']).not_to be_nil 128 | end 129 | 130 | it 'dispatches JWT in registration requests' do 131 | sign_up(jwt_with_null_user_registration_path, registration_params) 132 | 133 | expect(response.headers['Authorization']).not_to be_nil 134 | end 135 | end 136 | 137 | context 'no JWT user' do 138 | let(:user) { no_jwt_user } 139 | let(:sign_in_params) do 140 | { 141 | no_jwt_user: { 142 | email: user.email, 143 | password: user.password 144 | } 145 | } 146 | end 147 | 148 | it 'does not dispatch JWT in sign_in requests' do 149 | sign_in(no_jwt_user_session_path, sign_in_params) 150 | 151 | expect(response.headers['Authorization']).to be_nil 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /spec/features/token_revocation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'Token revocation', type: :request do 6 | include_context 'feature' 7 | include_context 'fixtures' 8 | 9 | context 'JWT user with JTI matcher revocation' do 10 | let(:user) { jwt_with_jti_matcher_user } 11 | let(:user_params) do 12 | { 13 | jwt_with_jti_matcher_user: { 14 | email: user.email, 15 | password: user.password 16 | } 17 | } 18 | end 19 | 20 | it 'revokes JWT in sign_out' do 21 | auth = sign_in(jwt_with_jti_matcher_user_session_path, user_params) 22 | sign_out(destroy_jwt_with_jti_matcher_user_session_path, auth) 23 | 24 | get_with_auth('/jwt_with_jti_matcher_user_auth_action', auth) 25 | 26 | expect(response.status).to eq(401) 27 | end 28 | end 29 | 30 | context 'JWT user with Denylist revocation' do 31 | let(:user) { jwt_with_denylist_user } 32 | let(:user_params) do 33 | { 34 | jwt_with_denylist_user: { 35 | email: user.email, 36 | password: user.password 37 | } 38 | } 39 | end 40 | 41 | # rubocop:disable RSpec/ExampleLength 42 | it 'revokes JWT in sign_out' do 43 | auth = sign_in( 44 | jwt_with_denylist_user_session_path, user_params, format: :json 45 | ) 46 | sign_out( 47 | destroy_jwt_with_denylist_user_session_path, auth, :post, format: :json 48 | ) 49 | 50 | get_with_auth('/jwt_with_denylist_user_auth_action', auth) 51 | 52 | expect(response.status).to eq(401) 53 | end 54 | # rubocop:enable RSpec/ExampleLength 55 | end 56 | 57 | context 'JWT user with allowlist revocation' do 58 | let(:user) { jwt_with_allowlist_user } 59 | let(:user_params) do 60 | { 61 | jwt_with_allowlist_user: { 62 | email: user.email, 63 | password: user.password 64 | } 65 | } 66 | end 67 | 68 | it 'revokes JWT in sign_out' do 69 | auth = sign_in(jwt_with_allowlist_user_session_path, user_params) 70 | sign_out(destroy_jwt_with_allowlist_user_session_path, auth) 71 | 72 | get_with_auth('/jwt_with_allowlist_user_auth_action', auth) 73 | 74 | expect(response.status).to eq(401) 75 | end 76 | end 77 | 78 | context 'JWT user with Null revocation' do 79 | let(:user) { jwt_with_null_user } 80 | let(:user_params) do 81 | { 82 | jwt_with_null_user: { 83 | email: user.email, 84 | password: user.password 85 | } 86 | } 87 | end 88 | 89 | it 'does not revoke JWT' do 90 | auth = sign_in(jwt_with_null_user_session_path, user_params) 91 | sign_out(destroy_jwt_with_null_user_session_path, auth) 92 | 93 | get_with_auth('/jwt_with_null_user_auth_action', auth) 94 | 95 | expect(response.status).to eq(200) 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) do |repo_name| 4 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 5 | "https://github.com/#{repo_name}.git" 6 | end 7 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 8 | gem 'rails' 9 | # Use sqlite3 as the database for Active Record 10 | gem 'sqlite3' 11 | # Use Puma as the app server 12 | gem 'puma' 13 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 14 | # gem 'jbuilder', '~> 2.5' 15 | # Use ActiveModel has_secure_password 16 | # gem 'bcrypt', '~> 3.1.7' 17 | 18 | # Use Capistrano for deployment 19 | # gem 'capistrano-rails', group: :development 20 | 21 | # gem 'rack-cors' 22 | # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible 23 | 24 | group :development, :test do 25 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 26 | # gem 'byebug', platform: :mri 27 | end 28 | 29 | group :development do 30 | end 31 | 32 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 33 | # gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 34 | 35 | # We want to test the behavior of this gem in a rails app 36 | gem 'devise-jwt', path: '../../../' 37 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../../.. 3 | specs: 4 | devise-jwt (0.12.1) 5 | devise (~> 4.0) 6 | warden-jwt_auth (~> 0.10) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (8.0.2) 12 | actionpack (= 8.0.2) 13 | activesupport (= 8.0.2) 14 | nio4r (~> 2.0) 15 | websocket-driver (>= 0.6.1) 16 | zeitwerk (~> 2.6) 17 | actionmailbox (8.0.2) 18 | actionpack (= 8.0.2) 19 | activejob (= 8.0.2) 20 | activerecord (= 8.0.2) 21 | activestorage (= 8.0.2) 22 | activesupport (= 8.0.2) 23 | mail (>= 2.8.0) 24 | actionmailer (8.0.2) 25 | actionpack (= 8.0.2) 26 | actionview (= 8.0.2) 27 | activejob (= 8.0.2) 28 | activesupport (= 8.0.2) 29 | mail (>= 2.8.0) 30 | rails-dom-testing (~> 2.2) 31 | actionpack (8.0.2) 32 | actionview (= 8.0.2) 33 | activesupport (= 8.0.2) 34 | nokogiri (>= 1.8.5) 35 | rack (>= 2.2.4) 36 | rack-session (>= 1.0.1) 37 | rack-test (>= 0.6.3) 38 | rails-dom-testing (~> 2.2) 39 | rails-html-sanitizer (~> 1.6) 40 | useragent (~> 0.16) 41 | actiontext (8.0.2) 42 | actionpack (= 8.0.2) 43 | activerecord (= 8.0.2) 44 | activestorage (= 8.0.2) 45 | activesupport (= 8.0.2) 46 | globalid (>= 0.6.0) 47 | nokogiri (>= 1.8.5) 48 | actionview (8.0.2) 49 | activesupport (= 8.0.2) 50 | builder (~> 3.1) 51 | erubi (~> 1.11) 52 | rails-dom-testing (~> 2.2) 53 | rails-html-sanitizer (~> 1.6) 54 | activejob (8.0.2) 55 | activesupport (= 8.0.2) 56 | globalid (>= 0.3.6) 57 | activemodel (8.0.2) 58 | activesupport (= 8.0.2) 59 | activerecord (8.0.2) 60 | activemodel (= 8.0.2) 61 | activesupport (= 8.0.2) 62 | timeout (>= 0.4.0) 63 | activestorage (8.0.2) 64 | actionpack (= 8.0.2) 65 | activejob (= 8.0.2) 66 | activerecord (= 8.0.2) 67 | activesupport (= 8.0.2) 68 | marcel (~> 1.0) 69 | activesupport (8.0.2) 70 | base64 71 | benchmark (>= 0.3) 72 | bigdecimal 73 | concurrent-ruby (~> 1.0, >= 1.3.1) 74 | connection_pool (>= 2.2.5) 75 | drb 76 | i18n (>= 1.6, < 2) 77 | logger (>= 1.4.2) 78 | minitest (>= 5.1) 79 | securerandom (>= 0.3) 80 | tzinfo (~> 2.0, >= 2.0.5) 81 | uri (>= 0.13.1) 82 | base64 (0.2.0) 83 | bcrypt (3.1.20) 84 | benchmark (0.4.0) 85 | bigdecimal (3.1.9) 86 | builder (3.3.0) 87 | concurrent-ruby (1.3.5) 88 | connection_pool (2.5.1) 89 | crass (1.0.6) 90 | date (3.4.1) 91 | devise (4.9.4) 92 | bcrypt (~> 3.0) 93 | orm_adapter (~> 0.1) 94 | railties (>= 4.1.0) 95 | responders 96 | warden (~> 1.2.3) 97 | drb (2.2.1) 98 | dry-auto_inject (1.1.0) 99 | dry-core (~> 1.1) 100 | zeitwerk (~> 2.6) 101 | dry-configurable (1.3.0) 102 | dry-core (~> 1.1) 103 | zeitwerk (~> 2.6) 104 | dry-core (1.1.0) 105 | concurrent-ruby (~> 1.0) 106 | logger 107 | zeitwerk (~> 2.6) 108 | erubi (1.13.1) 109 | globalid (1.2.1) 110 | activesupport (>= 6.1) 111 | i18n (1.14.7) 112 | concurrent-ruby (~> 1.0) 113 | io-console (0.8.0) 114 | irb (1.15.2) 115 | pp (>= 0.6.0) 116 | rdoc (>= 4.0.0) 117 | reline (>= 0.4.2) 118 | jwt (2.10.1) 119 | base64 120 | logger (1.7.0) 121 | loofah (2.24.0) 122 | crass (~> 1.0.2) 123 | nokogiri (>= 1.12.0) 124 | mail (2.8.1) 125 | mini_mime (>= 0.1.1) 126 | net-imap 127 | net-pop 128 | net-smtp 129 | marcel (1.0.4) 130 | mini_mime (1.1.5) 131 | mini_portile2 (2.8.8) 132 | minitest (5.25.5) 133 | net-imap (0.5.7) 134 | date 135 | net-protocol 136 | net-pop (0.1.2) 137 | net-protocol 138 | net-protocol (0.2.2) 139 | timeout 140 | net-smtp (0.5.1) 141 | net-protocol 142 | nio4r (2.7.4) 143 | nokogiri (1.18.8) 144 | mini_portile2 (~> 2.8.2) 145 | racc (~> 1.4) 146 | orm_adapter (0.5.0) 147 | pp (0.6.2) 148 | prettyprint 149 | prettyprint (0.2.0) 150 | psych (5.2.3) 151 | date 152 | stringio 153 | puma (6.6.0) 154 | nio4r (~> 2.0) 155 | racc (1.8.1) 156 | rack (3.1.13) 157 | rack-session (2.1.0) 158 | base64 (>= 0.1.0) 159 | rack (>= 3.0.0) 160 | rack-test (2.2.0) 161 | rack (>= 1.3) 162 | rackup (2.2.1) 163 | rack (>= 3) 164 | rails (8.0.2) 165 | actioncable (= 8.0.2) 166 | actionmailbox (= 8.0.2) 167 | actionmailer (= 8.0.2) 168 | actionpack (= 8.0.2) 169 | actiontext (= 8.0.2) 170 | actionview (= 8.0.2) 171 | activejob (= 8.0.2) 172 | activemodel (= 8.0.2) 173 | activerecord (= 8.0.2) 174 | activestorage (= 8.0.2) 175 | activesupport (= 8.0.2) 176 | bundler (>= 1.15.0) 177 | railties (= 8.0.2) 178 | rails-dom-testing (2.2.0) 179 | activesupport (>= 5.0.0) 180 | minitest 181 | nokogiri (>= 1.6) 182 | rails-html-sanitizer (1.6.2) 183 | loofah (~> 2.21) 184 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) 185 | railties (8.0.2) 186 | actionpack (= 8.0.2) 187 | activesupport (= 8.0.2) 188 | irb (~> 1.13) 189 | rackup (>= 1.0.0) 190 | rake (>= 12.2) 191 | thor (~> 1.0, >= 1.2.2) 192 | zeitwerk (~> 2.6) 193 | rake (13.2.1) 194 | rdoc (6.13.1) 195 | psych (>= 4.0.0) 196 | reline (0.6.1) 197 | io-console (~> 0.5) 198 | responders (3.1.1) 199 | actionpack (>= 5.2) 200 | railties (>= 5.2) 201 | securerandom (0.4.1) 202 | sqlite3 (2.6.0) 203 | mini_portile2 (~> 2.8.0) 204 | stringio (3.1.7) 205 | thor (1.3.2) 206 | timeout (0.4.3) 207 | tzinfo (2.0.6) 208 | concurrent-ruby (~> 1.0) 209 | uri (1.0.3) 210 | useragent (0.16.11) 211 | warden (1.2.9) 212 | rack (>= 2.0.9) 213 | warden-jwt_auth (0.11.0) 214 | dry-auto_inject (>= 0.8, < 2) 215 | dry-configurable (>= 0.13, < 2) 216 | jwt (~> 2.1) 217 | warden (~> 1.2) 218 | websocket-driver (0.7.7) 219 | base64 220 | websocket-extensions (>= 0.1.0) 221 | websocket-extensions (0.1.5) 222 | zeitwerk (2.7.2) 223 | 224 | PLATFORMS 225 | ruby 226 | 227 | DEPENDENCIES 228 | devise-jwt! 229 | puma 230 | rails 231 | sqlite3 232 | 233 | BUNDLED WITH 234 | 2.6.7 235 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/README.md: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/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_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::API 2 | include ActionController::MimeResponds 3 | 4 | respond_to :json 5 | 6 | def jwt_with_jti_matcher_user_auth_action 7 | head :ok 8 | end 9 | before_action :authenticate_jwt_with_jti_matcher_user!, 10 | only: :jwt_with_jti_matcher_user_auth_action 11 | 12 | def jwt_with_denylist_user_auth_action 13 | head :ok 14 | end 15 | before_action :authenticate_jwt_with_denylist_user!, 16 | only: :jwt_with_denylist_user_auth_action 17 | 18 | def jwt_with_allowlist_user_auth_action 19 | head :ok 20 | end 21 | before_action :authenticate_jwt_with_allowlist_user!, 22 | only: :jwt_with_allowlist_user_auth_action 23 | 24 | def jwt_with_null_user_auth_action 25 | head :ok 26 | end 27 | before_action :authenticate_jwt_with_null_user!, 28 | only: :jwt_with_null_user_auth_action 29 | end 30 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/app/controllers/registrations_controller.rb: -------------------------------------------------------------------------------- 1 | class RegistrationsController < Devise::RegistrationsController 2 | # Since this is an API only app we disable session storing on sign up 3 | # but we still allow signing in to set the `Authorization` header with JWT 4 | # strategy 5 | # Unfortunately there is no config to disable session storage on the 6 | # registerable module. 7 | # 8 | # https://github.com/heartcombo/devise/blob/fec67f98f26fcd9a79072e4581b1bd40d0c7fa1d/app/controllers/devise/registrations_controller.rb#L103-L107 9 | def sign_up(resource_name, resource) 10 | sign_in(resource_name, resource, store: false) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/app/models/allowlisted_jwt.rb: -------------------------------------------------------------------------------- 1 | class AllowlistedJwt < ApplicationRecord 2 | end 3 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/app/models/jwt_denylist.rb: -------------------------------------------------------------------------------- 1 | class JwtDenylist < ApplicationRecord 2 | include Devise::JWT::RevocationStrategies::Denylist 3 | 4 | self.table_name = 'jwt_denylist' 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/app/models/jwt_with_allowlist_user.rb: -------------------------------------------------------------------------------- 1 | class JwtWithAllowlistUser < ApplicationRecord 2 | include Devise::JWT::RevocationStrategies::Allowlist 3 | 4 | devise :database_authenticatable, 5 | :registerable, 6 | :jwt_authenticatable, 7 | jwt_revocation_strategy: self 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/app/models/jwt_with_denylist_user.rb: -------------------------------------------------------------------------------- 1 | class JwtWithDenylistUser < ApplicationRecord 2 | devise :database_authenticatable, 3 | :registerable, 4 | :jwt_authenticatable, 5 | jwt_revocation_strategy: JwtDenylist 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/app/models/jwt_with_jti_matcher_user.rb: -------------------------------------------------------------------------------- 1 | class JwtWithJtiMatcherUser < ApplicationRecord 2 | include Devise::JWT::RevocationStrategies::JTIMatcher 3 | 4 | devise :database_authenticatable, 5 | :registerable, 6 | :jwt_authenticatable, 7 | jwt_revocation_strategy: self 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/app/models/jwt_with_null_user.rb: -------------------------------------------------------------------------------- 1 | class JwtWithNullUser < ApplicationRecord 2 | devise :database_authenticatable, 3 | :registerable, 4 | :jwt_authenticatable, 5 | jwt_revocation_strategy: Devise::JWT::RevocationStrategies::Null 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/app/models/no_jwt_user.rb: -------------------------------------------------------------------------------- 1 | class NoJwtUser < ApplicationRecord 2 | # Include default devise modules. Others available are: 3 | # :confirmable, :lockable, :timeoutable and :omniauthable 4 | devise :database_authenticatable, :registerable 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | # require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | # require "action_cable/engine" 12 | # require "sprockets/railtie" 13 | require "rails/test_unit/railtie" 14 | 15 | # Require the gems listed in Gemfile, including any gems 16 | # you've limited to :test, :development, or :production. 17 | Bundler.require(*Rails.groups) 18 | 19 | module RailsApp 20 | class Application < Rails::Application 21 | # Settings in config/environments/* take precedence over those specified here. 22 | # Application configuration should go into files in config/initializers 23 | # -- all .rb files in that directory are automatically loaded. 24 | 25 | config.load_defaults 8.0 26 | 27 | # Only loads a smaller set of middleware suitable for API only apps. 28 | # Middleware like session, flash, cookies can be added back manually. 29 | # Skip views, helpers and assets when generating a new resource. 30 | config.api_only = true 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/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 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/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. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=172800' 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Print deprecation notices to the Rails logger. 30 | config.active_support.deprecation = :log 31 | 32 | # Raise an error on page load if there are pending migrations. 33 | config.active_record.migration_error = :page_load 34 | # Raises error for missing translations 35 | # config.action_view.raise_on_missing_translations = true 36 | 37 | # Use an evented file watcher to asynchronously detect changes in source code, 38 | # routes, locales, etc. This feature depends on the listen gem. 39 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker 40 | end 41 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/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 | # Disable serving static files from the `/public` folder by default since 18 | # Apache or NGINX already handles this. 19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 20 | 21 | 22 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 23 | # config.action_controller.asset_host = 'http://assets.example.com' 24 | 25 | # Specifies the header that your server uses for sending files. 26 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 27 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 28 | 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # Use the lowest log level to ensure availability of diagnostic information 34 | # when problems arise. 35 | config.log_level = :debug 36 | 37 | # Prepend all log lines with the following tags. 38 | config.log_tags = [ :request_id ] 39 | 40 | # Use a different cache store in production. 41 | # config.cache_store = :mem_cache_store 42 | 43 | # Use a real queuing backend for Active Job (and separate queues per environment) 44 | # config.active_job.queue_adapter = :resque 45 | # config.active_job.queue_name_prefix = "rails_app_#{Rails.env}" 46 | 47 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 48 | # the I18n.default_locale when a translation cannot be found). 49 | config.i18n.fallbacks = true 50 | 51 | # Send deprecation notices to registered listeners. 52 | config.active_support.deprecation = :notify 53 | 54 | # Use default logging formatter so that PID and timestamp are not suppressed. 55 | config.log_formatter = ::Logger::Formatter.new 56 | 57 | # Use a different logger for distributed setups. 58 | # require 'syslog/logger' 59 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 60 | 61 | if ENV["RAILS_LOG_TO_STDOUT"].present? 62 | logger = ActiveSupport::Logger.new(STDOUT) 63 | logger.formatter = config.log_formatter 64 | config.logger = ActiveSupport::TaggedLogging.new(logger) 65 | end 66 | 67 | # Do not dump schema after migrations. 68 | config.active_record.dump_schema_after_migration = false 69 | end 70 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/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 public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => 'public, max-age=3600' 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Print deprecation notices to the stderr. 32 | config.active_support.deprecation = :stderr 33 | 34 | # Raises error for missing translations 35 | # config.action_view.raise_on_missing_translations = true 36 | end 37 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/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 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Avoid CORS issues when API is called from the frontend app. 4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 5 | 6 | # Read more: https://github.com/cyu/rack-cors 7 | 8 | # Rails.application.config.middleware.insert_before 0, Rack::Cors do 9 | # allow do 10 | # origins 'example.com' 11 | # 12 | # resource '*', 13 | # headers: :any, 14 | # methods: [:get, :post, :put, :patch, :delete, :options, :head] 15 | # end 16 | # end 17 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | # Use this hook to configure devise mailer, warden hooks and so forth. 2 | # Many of these configuration options can be set straight in your model. 3 | Devise.setup do |config| 4 | # The secret key used by Devise. Devise uses this key to generate 5 | # random tokens. Changing this key will render invalid all existing 6 | # confirmation, reset password and unlock tokens in the database. 7 | # Devise will use the `secret_key_base` as its `secret_key` 8 | # by default. You can change it below and use your own secret key. 9 | # config.secret_key = 'bfcb68a0d38cb917461c9b8a87b61014f49d02fb07f9b086219c40911240bab30d18d188541a8dfcfc8a27a06e09b244b3b71d70b8c34ec184f4da01bf35d17a' 10 | 11 | # ==> Mailer Configuration 12 | # Configure the e-mail address which will be shown in Devise::Mailer, 13 | # note that it will be overwritten if you use your own mailer class 14 | # with default "from" parameter. 15 | config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' 16 | 17 | # Configure the class responsible to send e-mails. 18 | # config.mailer = 'Devise::Mailer' 19 | 20 | # Configure the parent class responsible to send e-mails. 21 | # config.parent_mailer = 'ActionMailer::Base' 22 | 23 | # ==> ORM configuration 24 | # Load and configure the ORM. Supports :active_record (default) and 25 | # :mongoid (bson_ext recommended) by default. Other ORMs may be 26 | # available as additional gems. 27 | require 'devise/orm/active_record' 28 | 29 | # ==> Configuration for any authentication mechanism 30 | # Configure which keys are used when authenticating a user. The default is 31 | # just :email. You can configure it to use [:username, :subdomain], so for 32 | # authenticating a user, both parameters are required. Remember that those 33 | # parameters are used only when authenticating and not when retrieving from 34 | # session. If you need permissions, you should implement that in a before filter. 35 | # You can also supply a hash where the value is a boolean determining whether 36 | # or not authentication should be aborted when the value is not present. 37 | # config.authentication_keys = [:email] 38 | 39 | # Configure parameters from the request object used for authentication. Each entry 40 | # given should be a request method and it will automatically be passed to the 41 | # find_for_authentication method and considered in your model lookup. For instance, 42 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. 43 | # The same considerations mentioned for authentication_keys also apply to request_keys. 44 | # config.request_keys = [] 45 | 46 | # Configure which authentication keys should be case-insensitive. 47 | # These keys will be downcased upon creating or modifying a user and when used 48 | # to authenticate or find a user. Default is :email. 49 | config.case_insensitive_keys = [:email] 50 | 51 | # Configure which authentication keys should have whitespace stripped. 52 | # These keys will have whitespace before and after removed upon creating or 53 | # modifying a user and when used to authenticate or find a user. Default is :email. 54 | config.strip_whitespace_keys = [:email] 55 | 56 | # Tell if authentication through request.params is enabled. True by default. 57 | # It can be set to an array that will enable params authentication only for the 58 | # given strategies, for example, `config.params_authenticatable = [:database]` will 59 | # enable it only for database (email + password) authentication. 60 | # config.params_authenticatable = true 61 | 62 | # Tell if authentication through HTTP Auth is enabled. False by default. 63 | # It can be set to an array that will enable http authentication only for the 64 | # given strategies, for example, `config.http_authenticatable = [:database]` will 65 | # enable it only for database authentication. The supported strategies are: 66 | # :database = Support basic authentication with authentication key + password 67 | # config.http_authenticatable = false 68 | 69 | # If 401 status code should be returned for AJAX requests. True by default. 70 | # config.http_authenticatable_on_xhr = true 71 | 72 | # The realm used in Http Basic Authentication. 'Application' by default. 73 | # config.http_authentication_realm = 'Application' 74 | 75 | # It will change confirmation, password recovery and other workflows 76 | # to behave the same regardless if the e-mail provided was right or wrong. 77 | # Does not affect registerable. 78 | # config.paranoid = true 79 | 80 | # By default Devise will store the user in session. You can skip storage for 81 | # particular strategies by setting this option. 82 | # Notice that if you are skipping storage for all authentication paths, you 83 | # may want to disable generating routes to Devise's sessions controller by 84 | # passing skip: :sessions to `devise_for` in your config/routes.rb 85 | config.skip_session_storage = [:http_auth] 86 | 87 | # By default, Devise cleans up the CSRF token on authentication to 88 | # avoid CSRF token fixation attacks. This means that, when using AJAX 89 | # requests for sign in and sign up, you need to get a new CSRF token 90 | # from the server. You can disable this option at your own risk. 91 | # config.clean_up_csrf_token_on_authentication = true 92 | 93 | # When false, Devise will not attempt to reload routes on eager load. 94 | # This can reduce the time taken to boot the app but if your application 95 | # requires the Devise mappings to be loaded during boot time the application 96 | # won't boot properly. 97 | # config.reload_routes = true 98 | 99 | # ==> Configuration for :database_authenticatable 100 | # For bcrypt, this is the cost for hashing the password and defaults to 11. If 101 | # using other algorithms, it sets how many times you want the password to be hashed. 102 | # 103 | # Limiting the stretches to just one in testing will increase the performance of 104 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use 105 | # a value less than 10 in other environments. Note that, for bcrypt (the default 106 | # algorithm), the cost increases exponentially with the number of stretches (e.g. 107 | # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). 108 | config.stretches = Rails.env.test? ? 1 : 11 109 | 110 | # Set up a pepper to generate the hashed password. 111 | # config.pepper = 'aa4f4ef8585fc41a95d55ee0fa18bdeb53552014e0bcf06add4c0b3e564a93ddee2df86302c01b5bbd5ae3ce531feedee738732dbe30f7f83995c6c5eb47ad7a' 112 | 113 | # Send a notification email when the user's password is changed 114 | # config.send_password_change_notification = false 115 | 116 | # ==> Configuration for :confirmable 117 | # A period that the user is allowed to access the website even without 118 | # confirming their account. For instance, if set to 2.days, the user will be 119 | # able to access the website for two days without confirming their account, 120 | # access will be blocked just in the third day. Default is 0.days, meaning 121 | # the user cannot access the website without confirming their account. 122 | # config.allow_unconfirmed_access_for = 2.days 123 | 124 | # A period that the user is allowed to confirm their account before their 125 | # token becomes invalid. For example, if set to 3.days, the user can confirm 126 | # their account within 3 days after the mail was sent, but on the fourth day 127 | # their account can't be confirmed with the token any more. 128 | # Default is nil, meaning there is no restriction on how long a user can take 129 | # before confirming their account. 130 | # config.confirm_within = 3.days 131 | 132 | # If true, requires any email changes to be confirmed (exactly the same way as 133 | # initial account confirmation) to be applied. Requires additional unconfirmed_email 134 | # db field (see migrations). Until confirmed, new email is stored in 135 | # unconfirmed_email column, and copied to email column on successful confirmation. 136 | config.reconfirmable = true 137 | 138 | # Defines which key will be used when confirming an account 139 | # config.confirmation_keys = [:email] 140 | 141 | # ==> Configuration for :rememberable 142 | # The time the user will be remembered without asking for credentials again. 143 | # config.remember_for = 2.weeks 144 | 145 | # Invalidates all the remember me tokens when the user signs out. 146 | config.expire_all_remember_me_on_sign_out = true 147 | 148 | # If true, extends the user's remember period when remembered via cookie. 149 | # config.extend_remember_period = false 150 | 151 | # Options to be passed to the created cookie. For instance, you can set 152 | # secure: true in order to force SSL only cookies. 153 | # config.rememberable_options = {} 154 | 155 | # ==> Configuration for :validatable 156 | # Range for password length. 157 | config.password_length = 6..128 158 | 159 | # Email regex used to validate email formats. It simply asserts that 160 | # one (and only one) @ exists in the given string. This is mainly 161 | # to give user feedback and not to assert the e-mail validity. 162 | config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ 163 | 164 | # ==> Configuration for :timeoutable 165 | # The time you want to timeout the user session without activity. After this 166 | # time the user will be asked for credentials again. Default is 30 minutes. 167 | # config.timeout_in = 30.minutes 168 | 169 | # ==> Configuration for :lockable 170 | # Defines which strategy will be used to lock an account. 171 | # :failed_attempts = Locks an account after a number of failed attempts to sign in. 172 | # :none = No lock strategy. You should handle locking by yourself. 173 | # config.lock_strategy = :failed_attempts 174 | 175 | # Defines which key will be used when locking and unlocking an account 176 | # config.unlock_keys = [:email] 177 | 178 | # Defines which strategy will be used to unlock an account. 179 | # :email = Sends an unlock link to the user email 180 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) 181 | # :both = Enables both strategies 182 | # :none = No unlock strategy. You should handle unlocking by yourself. 183 | # config.unlock_strategy = :both 184 | 185 | # Number of authentication tries before locking an account if lock_strategy 186 | # is failed attempts. 187 | # config.maximum_attempts = 20 188 | 189 | # Time interval to unlock the account if :time is enabled as unlock_strategy. 190 | # config.unlock_in = 1.hour 191 | 192 | # Warn on the last attempt before the account is locked. 193 | # config.last_attempt_warning = true 194 | 195 | # ==> Configuration for :recoverable 196 | # 197 | # Defines which key will be used when recovering the password for an account 198 | # config.reset_password_keys = [:email] 199 | 200 | # Time interval you can reset your password with a reset password key. 201 | # Don't put a too small interval or your users won't have the time to 202 | # change their passwords. 203 | config.reset_password_within = 6.hours 204 | 205 | # When set to false, does not sign a user in automatically after their password is 206 | # reset. Defaults to true, so a user is signed in automatically after a reset. 207 | # config.sign_in_after_reset_password = true 208 | 209 | # ==> Configuration for :encryptable 210 | # Allow you to use another hashing or encryption algorithm besides bcrypt (default). 211 | # You can use :sha1, :sha512 or algorithms from others authentication tools as 212 | # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 213 | # for default behavior) and :restful_authentication_sha1 (then you should set 214 | # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). 215 | # 216 | # Require the `devise-encryptable` gem when using anything other than bcrypt 217 | # config.encryptor = :sha512 218 | 219 | # ==> Scopes configuration 220 | # Turn scoped views on. Before rendering "sessions/new", it will first check for 221 | # "users/sessions/new". It's turned off by default because it's slower if you 222 | # are using only default views. 223 | # config.scoped_views = false 224 | 225 | # Configure the default scope given to Warden. By default it's the first 226 | # devise role declared in your routes (usually :user). 227 | # config.default_scope = :user 228 | 229 | # Set this configuration to false if you want /users/sign_out to sign out 230 | # only the current scope. By default, Devise signs out all scopes. 231 | # config.sign_out_all_scopes = true 232 | 233 | # ==> Navigation configuration 234 | # Lists the formats that should be treated as navigational. Formats like 235 | # :html, should redirect to the sign in page when the user does not have 236 | # access, but formats like :xml or :json, should return 401. 237 | # 238 | # If you have any extra navigational formats, like :iphone or :mobile, you 239 | # should add them to the navigational formats lists. 240 | # 241 | # The "*/*" below is required to match Internet Explorer requests. 242 | # config.navigational_formats = ['*/*', :html] 243 | 244 | # The default HTTP method used to sign out a resource. Default is :delete. 245 | config.sign_out_via = :delete 246 | 247 | # ==> OmniAuth 248 | # Add a new OmniAuth provider. Check the wiki for more information on setting 249 | # up on your models and hooks. 250 | # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' 251 | 252 | # ==> Warden configuration 253 | # If you want to use other strategies, that are not supported by Devise, or 254 | # change the failure app, you can configure them inside the config.warden block. 255 | # 256 | # config.warden do |manager| 257 | # manager.intercept_401 = false 258 | # manager.default_strategies(scope: :user).unshift :some_external_strategy 259 | # end 260 | 261 | # ==> Mountable engine configurations 262 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine 263 | # is mountable, there are some extra configurations to be taken into account. 264 | # The following options are available, assuming the engine is mounted as: 265 | # 266 | # mount MyEngine, at: '/my_engine' 267 | # 268 | # The router that invoked `devise_for`, in the example above, would be: 269 | # config.router_name = :my_engine 270 | # 271 | # When using OmniAuth, Devise cannot automatically set OmniAuth path, 272 | # so you need to do it manually. For the users scope, it would be: 273 | # config.omniauth_path_prefix = '/my_engine/users/auth' 274 | # 275 | # JWT configuration 276 | config.jwt do |jwt| 277 | jwt.secret = 'dwdwdwdwdwdwedwedwedwddw' 278 | jwt.dispatch_requests = [ 279 | ['GET', %r{^/foo_path$}], 280 | ] 281 | jwt.revocation_requests = [['GET', %r{^/bar_path$}]] 282 | jwt.request_formats = { 283 | jwt_with_denylist_user: %i[json xml] 284 | } 285 | end 286 | end 287 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/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 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/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 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/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 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/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] 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 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n 2 | 3 | en: 4 | devise: 5 | confirmations: 6 | confirmed: "Your email address has been successfully confirmed." 7 | send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." 8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." 9 | failure: 10 | already_authenticated: "You are already signed in." 11 | inactive: "Your account is not activated yet." 12 | invalid: "Invalid %{authentication_keys} or password." 13 | locked: "Your account is locked." 14 | last_attempt: "You have one more attempt before your account is locked." 15 | not_found_in_database: "Invalid %{authentication_keys} or password." 16 | timeout: "Your session expired. Please sign in again to continue." 17 | unauthenticated: "You need to sign in or sign up before continuing." 18 | unconfirmed: "You have to confirm your email address before continuing." 19 | mailer: 20 | confirmation_instructions: 21 | subject: "Confirmation instructions" 22 | reset_password_instructions: 23 | subject: "Reset password instructions" 24 | unlock_instructions: 25 | subject: "Unlock instructions" 26 | password_change: 27 | subject: "Password Changed" 28 | omniauth_callbacks: 29 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"." 30 | success: "Successfully authenticated from %{kind} account." 31 | passwords: 32 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." 33 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." 34 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." 35 | updated: "Your password has been changed successfully. You are now signed in." 36 | updated_not_active: "Your password has been changed successfully." 37 | registrations: 38 | destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." 39 | signed_up: "Welcome! You have signed up successfully." 40 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." 41 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." 42 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." 43 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." 44 | updated: "Your account has been updated successfully." 45 | sessions: 46 | signed_in: "Signed in successfully." 47 | signed_out: "Signed out successfully." 48 | already_signed_out: "Signed out successfully." 49 | unlocks: 50 | send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." 51 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." 52 | unlocked: "Your account has been unlocked successfully. Please sign in to continue." 53 | errors: 54 | messages: 55 | already_confirmed: "was already confirmed, please try signing in" 56 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 57 | expired: "has expired, please request a new one" 58 | not_found: "not found" 59 | not_locked: "was not locked" 60 | not_saved: 61 | one: "1 error prohibited this %{resource} from being saved:" 62 | other: "%{count} errors prohibited this %{resource} from being saved:" 63 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/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 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum, this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | # on_worker_boot do 43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | # end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | get '/jwt_with_jti_matcher_user_auth_action', 3 | to: 'application#jwt_with_jti_matcher_user_auth_action', 4 | defauts: { format: :json } 5 | 6 | get '/jwt_with_denylist_user_auth_action', 7 | to: 'application#jwt_with_denylist_user_auth_action', 8 | defauts: { format: :json } 9 | 10 | get '/jwt_with_allowlist_user_auth_action', 11 | to: 'application#jwt_with_allowlist_user_auth_action', 12 | defauts: { format: :json } 13 | 14 | get '/jwt_with_null_user_auth_action', 15 | to: 'application#jwt_with_null_user_auth_action', 16 | defauts: { format: :json } 17 | 18 | devise_for :no_jwt_users, 19 | controllers: { registrations: :registrations }, 20 | defaults: { format: :json } 21 | 22 | devise_for :jwt_with_jti_matcher_users, 23 | controllers: { registrations: :registrations }, 24 | defaults: { format: :json } 25 | 26 | devise_for :jwt_with_denylist_users, 27 | defaults: { format: :json }, 28 | controllers: { registrations: :registrations }, 29 | sign_out_via: :post 30 | 31 | devise_for :jwt_with_allowlist_users, 32 | defaults: { format: :json }, 33 | controllers: { registrations: :registrations }, 34 | sign_out_via: [:get, :delete] 35 | 36 | scope 'a/scope' do 37 | devise_for :jwt_with_null_users, 38 | controllers: { registrations: :registrations }, 39 | defaults: { format: :json } 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/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 `rails 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: 02ff27dbd8973900b13bdb13ad102412490f1013fd907bba5ab7ee67b40058b6704e61631d08f43ee026a8d74288c6843e043ba41eccac6a43e6abe688adf921 15 | 16 | test: 17 | secret_key_base: b9768a847f7a90b78d521e4e75ec8b0b2fe64b77ead085934f9242beeb03d00aa6c8dc4dc75602f6ca5082126093ec202d84ecb8f41898c74b70de269701acb3 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 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20170113091441_devise_create_jwt_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateJwtUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :jwt_users do |t| 4 | ## Database authenticatable 5 | t.string :email, null: false, default: "" 6 | t.string :encrypted_password, null: false, default: "" 7 | 8 | ## Recoverable 9 | t.string :reset_password_token 10 | t.datetime :reset_password_sent_at 11 | 12 | ## Rememberable 13 | t.datetime :remember_created_at 14 | 15 | ## Trackable 16 | t.integer :sign_in_count, default: 0, null: false 17 | t.datetime :current_sign_in_at 18 | t.datetime :last_sign_in_at 19 | t.string :current_sign_in_ip 20 | t.string :last_sign_in_ip 21 | 22 | ## Confirmable 23 | # t.string :confirmation_token 24 | # t.datetime :confirmed_at 25 | # t.datetime :confirmation_sent_at 26 | # t.string :unconfirmed_email # Only if using reconfirmable 27 | 28 | ## Lockable 29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 30 | # t.string :unlock_token # Only if unlock strategy is :email or :both 31 | # t.datetime :locked_at 32 | 33 | 34 | t.timestamps null: false 35 | end 36 | 37 | add_index :jwt_users, :email, unique: true 38 | add_index :jwt_users, :reset_password_token, unique: true 39 | # add_index :jwt_users, :confirmation_token, unique: true 40 | # add_index :jwt_users, :unlock_token, unique: true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20170113150139_devise_create_no_jwt_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateNoJwtUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :no_jwt_users do |t| 4 | ## Database authenticatable 5 | t.string :email, null: false, default: "" 6 | t.string :encrypted_password, null: false, default: "" 7 | 8 | ## Recoverable 9 | t.string :reset_password_token 10 | t.datetime :reset_password_sent_at 11 | 12 | ## Rememberable 13 | t.datetime :remember_created_at 14 | 15 | ## Trackable 16 | t.integer :sign_in_count, default: 0, null: false 17 | t.datetime :current_sign_in_at 18 | t.datetime :last_sign_in_at 19 | t.string :current_sign_in_ip 20 | t.string :last_sign_in_ip 21 | 22 | ## Confirmable 23 | # t.string :confirmation_token 24 | # t.datetime :confirmed_at 25 | # t.datetime :confirmation_sent_at 26 | # t.string :unconfirmed_email # Only if using reconfirmable 27 | 28 | ## Lockable 29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 30 | # t.string :unlock_token # Only if unlock strategy is :email or :both 31 | # t.datetime :locked_at 32 | 33 | 34 | t.timestamps null: false 35 | end 36 | 37 | add_index :no_jwt_users, :email, unique: true 38 | add_index :no_jwt_users, :reset_password_token, unique: true 39 | # add_index :no_jwt_users, :confirmation_token, unique: true 40 | # add_index :no_jwt_users, :unlock_token, unique: true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20170119120658_add_jti_to_jwt_users.rb: -------------------------------------------------------------------------------- 1 | class AddJtiToJwtUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :jwt_users, :jti, :string 4 | change_column_null :jwt_users, :jti, false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20170119143719_rename_jwt_user_to_jwt_with_jti_matcher_users.rb: -------------------------------------------------------------------------------- 1 | class RenameJwtUserToJwtWithJtiMatcherUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | rename_table :jwt_users, :jwt_with_jti_matcher_users 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20170119145401_create_blacklist.rb: -------------------------------------------------------------------------------- 1 | class CreateBlacklist < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :blacklist do |t| 4 | t.string :jti 5 | end 6 | change_column_null :blacklist, :jti, false 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20170120092719_create_jwt_with_blacklist_users.rb: -------------------------------------------------------------------------------- 1 | class CreateJwtWithBlacklistUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :jwt_with_blacklist_users do |t| 4 | ## Database authenticatable 5 | t.string :email, null: false, default: "" 6 | t.string :encrypted_password, null: false, default: "" 7 | 8 | ## Recoverable 9 | t.string :reset_password_token 10 | t.datetime :reset_password_sent_at 11 | 12 | ## Rememberable 13 | t.datetime :remember_created_at 14 | 15 | ## Trackable 16 | t.integer :sign_in_count, default: 0, null: false 17 | t.datetime :current_sign_in_at 18 | t.datetime :last_sign_in_at 19 | t.string :current_sign_in_ip 20 | t.string :last_sign_in_ip 21 | 22 | ## Confirmable 23 | # t.string :confirmation_token 24 | # t.datetime :confirmed_at 25 | # t.datetime :confirmation_sent_at 26 | # t.string :unconfirmed_email # Only if using reconfirmable 27 | 28 | ## Lockable 29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 30 | # t.string :unlock_token # Only if unlock strategy is :email or :both 31 | # t.datetime :locked_at 32 | 33 | 34 | t.timestamps null: false 35 | end 36 | 37 | add_index :jwt_with_blacklist_users, :email, unique: true 38 | add_index :jwt_with_blacklist_users, :reset_password_token, unique: true 39 | # add_index :jwt_with_blacklist_users, :confirmation_token, unique: true 40 | # add_index :jwt_with_blacklist_users, :unlock_token, unique: true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20170120101213_devise_create_jwt_with_null_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateJwtWithNullUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :jwt_with_null_users do |t| 4 | ## Database authenticatable 5 | t.string :email, null: false, default: "" 6 | t.string :encrypted_password, null: false, default: "" 7 | 8 | ## Recoverable 9 | t.string :reset_password_token 10 | t.datetime :reset_password_sent_at 11 | 12 | ## Rememberable 13 | t.datetime :remember_created_at 14 | 15 | ## Trackable 16 | t.integer :sign_in_count, default: 0, null: false 17 | t.datetime :current_sign_in_at 18 | t.datetime :last_sign_in_at 19 | t.string :current_sign_in_ip 20 | t.string :last_sign_in_ip 21 | 22 | ## Confirmable 23 | # t.string :confirmation_token 24 | # t.datetime :confirmed_at 25 | # t.datetime :confirmation_sent_at 26 | # t.string :unconfirmed_email # Only if using reconfirmable 27 | 28 | ## Lockable 29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 30 | # t.string :unlock_token # Only if unlock strategy is :email or :both 31 | # t.datetime :locked_at 32 | 33 | 34 | t.timestamps null: false 35 | end 36 | 37 | add_index :jwt_with_null_users, :email, unique: true 38 | add_index :jwt_with_null_users, :reset_password_token, unique: true 39 | # add_index :jwt_with_null_users, :confirmation_token, unique: true 40 | # add_index :jwt_with_null_users, :unlock_token, unique: true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20170802000822_add_expiration_time_to_blacklist.rb: -------------------------------------------------------------------------------- 1 | class AddExpirationTimeToBlacklist < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :blacklist, :exp, :datetime, null: false, default: Time.now 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20170804032446_rename_blacklist_table_to_jwt_blacklist.rb: -------------------------------------------------------------------------------- 1 | class RenameBlacklistTableToJwtBlacklist < ActiveRecord::Migration[5.0] 2 | def change 3 | rename_table :blacklist, :jwt_blacklist 4 | end 5 | end -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20171209162000_devise_create_jwt_with_whitelist_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateJwtWithWhitelistUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :jwt_with_whitelist_users do |t| 4 | ## Database authenticatable 5 | t.string :email, null: false, default: "" 6 | t.string :encrypted_password, null: false, default: "" 7 | 8 | ## Recoverable 9 | t.string :reset_password_token 10 | t.datetime :reset_password_sent_at 11 | 12 | ## Rememberable 13 | t.datetime :remember_created_at 14 | 15 | ## Trackable 16 | t.integer :sign_in_count, default: 0, null: false 17 | t.datetime :current_sign_in_at 18 | t.datetime :last_sign_in_at 19 | t.string :current_sign_in_ip 20 | t.string :last_sign_in_ip 21 | 22 | ## Confirmable 23 | # t.string :confirmation_token 24 | # t.datetime :confirmed_at 25 | # t.datetime :confirmation_sent_at 26 | # t.string :unconfirmed_email # Only if using reconfirmable 27 | 28 | ## Lockable 29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 30 | # t.string :unlock_token # Only if unlock strategy is :email or :both 31 | # t.datetime :locked_at 32 | 33 | 34 | t.timestamps null: false 35 | end 36 | 37 | add_index :jwt_with_whitelist_users, :email, unique: true 38 | add_index :jwt_with_whitelist_users, :reset_password_token, unique: true 39 | # add_index :jwt_with_whitelist_users, :confirmation_token, unique: true 40 | # add_index :jwt_with_whitelist_users, :unlock_token, unique: true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20171209162500_create_whitelisted_jwts.rb: -------------------------------------------------------------------------------- 1 | class CreateWhitelistedJwts < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :whitelisted_jwts do |t| 4 | t.string :jti, null: false, index: { unique: true } 5 | t.string :aud 6 | t.references :jwt_with_whitelist_user, foreign_key: true 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20171211114759_add_exp_to_whitelisted_jwts.rb: -------------------------------------------------------------------------------- 1 | class AddExpToWhitelistedJwts < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :whitelisted_jwts, :exp, :datetime, null: false, default: Time.now 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20171230182243_add_unique_index_to_blacklist_jti.rb: -------------------------------------------------------------------------------- 1 | class AddUniqueIndexToBlacklistJti < ActiveRecord::Migration[5.1] 2 | def change 3 | add_index :jwt_blacklist, :jti, unique: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20200603051918_rename_blacklist_to_denylist.rb: -------------------------------------------------------------------------------- 1 | class RenameBlacklistToDenylist < ActiveRecord::Migration[6.0] 2 | def change 3 | rename_table :jwt_blacklist, :jwt_denylist 4 | rename_table :jwt_with_blacklist_users, :jwt_with_denylist_users 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/migrate/20200603053415_rename_whitelist_to_allowlist.rb: -------------------------------------------------------------------------------- 1 | class RenameWhitelistToAllowlist < ActiveRecord::Migration[6.0] 2 | def change 3 | rename_table :whitelisted_jwts, :allowlisted_jwts 4 | rename_table :jwt_with_whitelist_users, :jwt_with_allowlist_users 5 | rename_column :allowlisted_jwts, :jwt_with_whitelist_user_id, :jwt_with_allowlist_user_id 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/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 | # This file is the source Rails uses to define your schema when running `rails 6 | # db:schema:load`. When creating a new database, `rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2020_06_03_053415) do 14 | 15 | create_table "allowlisted_jwts", force: :cascade do |t| 16 | t.string "jti", null: false 17 | t.string "aud" 18 | t.integer "jwt_with_allowlist_user_id" 19 | t.datetime "exp", default: "2017-12-11 11:49:00", null: false 20 | t.index ["jti"], name: "index_allowlisted_jwts_on_jti", unique: true 21 | t.index ["jwt_with_allowlist_user_id"], name: "index_allowlisted_jwts_on_jwt_with_allowlist_user_id" 22 | end 23 | 24 | create_table "jwt_denylist", force: :cascade do |t| 25 | t.string "jti", null: false 26 | t.datetime "exp", default: "2017-12-11 09:46:10", null: false 27 | t.index ["jti"], name: "index_jwt_denylist_on_jti", unique: true 28 | end 29 | 30 | create_table "jwt_with_allowlist_users", force: :cascade do |t| 31 | t.string "email", default: "", null: false 32 | t.string "encrypted_password", default: "", null: false 33 | t.string "reset_password_token" 34 | t.datetime "reset_password_sent_at" 35 | t.datetime "remember_created_at" 36 | t.integer "sign_in_count", default: 0, null: false 37 | t.datetime "current_sign_in_at" 38 | t.datetime "last_sign_in_at" 39 | t.string "current_sign_in_ip" 40 | t.string "last_sign_in_ip" 41 | t.datetime "created_at", null: false 42 | t.datetime "updated_at", null: false 43 | t.index ["email"], name: "index_jwt_with_allowlist_users_on_email", unique: true 44 | t.index ["reset_password_token"], name: "index_jwt_with_allowlist_users_on_reset_password_token", unique: true 45 | end 46 | 47 | create_table "jwt_with_denylist_users", force: :cascade do |t| 48 | t.string "email", default: "", null: false 49 | t.string "encrypted_password", default: "", null: false 50 | t.string "reset_password_token" 51 | t.datetime "reset_password_sent_at" 52 | t.datetime "remember_created_at" 53 | t.integer "sign_in_count", default: 0, null: false 54 | t.datetime "current_sign_in_at" 55 | t.datetime "last_sign_in_at" 56 | t.string "current_sign_in_ip" 57 | t.string "last_sign_in_ip" 58 | t.datetime "created_at", null: false 59 | t.datetime "updated_at", null: false 60 | t.index ["email"], name: "index_jwt_with_denylist_users_on_email", unique: true 61 | t.index ["reset_password_token"], name: "index_jwt_with_denylist_users_on_reset_password_token", unique: true 62 | end 63 | 64 | create_table "jwt_with_jti_matcher_users", force: :cascade do |t| 65 | t.string "email", default: "", null: false 66 | t.string "encrypted_password", default: "", null: false 67 | t.string "reset_password_token" 68 | t.datetime "reset_password_sent_at" 69 | t.datetime "remember_created_at" 70 | t.integer "sign_in_count", default: 0, null: false 71 | t.datetime "current_sign_in_at" 72 | t.datetime "last_sign_in_at" 73 | t.string "current_sign_in_ip" 74 | t.string "last_sign_in_ip" 75 | t.datetime "created_at", null: false 76 | t.datetime "updated_at", null: false 77 | t.string "jti", null: false 78 | t.index ["email"], name: "index_jwt_with_jti_matcher_users_on_email", unique: true 79 | t.index ["reset_password_token"], name: "index_jwt_with_jti_matcher_users_on_reset_password_token", unique: true 80 | end 81 | 82 | create_table "jwt_with_null_users", force: :cascade do |t| 83 | t.string "email", default: "", null: false 84 | t.string "encrypted_password", default: "", null: false 85 | t.string "reset_password_token" 86 | t.datetime "reset_password_sent_at" 87 | t.datetime "remember_created_at" 88 | t.integer "sign_in_count", default: 0, null: false 89 | t.datetime "current_sign_in_at" 90 | t.datetime "last_sign_in_at" 91 | t.string "current_sign_in_ip" 92 | t.string "last_sign_in_ip" 93 | t.datetime "created_at", null: false 94 | t.datetime "updated_at", null: false 95 | t.index ["email"], name: "index_jwt_with_null_users_on_email", unique: true 96 | t.index ["reset_password_token"], name: "index_jwt_with_null_users_on_reset_password_token", unique: true 97 | end 98 | 99 | create_table "no_jwt_users", force: :cascade do |t| 100 | t.string "email", default: "", null: false 101 | t.string "encrypted_password", default: "", null: false 102 | t.string "reset_password_token" 103 | t.datetime "reset_password_sent_at" 104 | t.datetime "remember_created_at" 105 | t.integer "sign_in_count", default: 0, null: false 106 | t.datetime "current_sign_in_at" 107 | t.datetime "last_sign_in_at" 108 | t.string "current_sign_in_ip" 109 | t.string "last_sign_in_ip" 110 | t.datetime "created_at", null: false 111 | t.datetime "updated_at", null: false 112 | t.index ["email"], name: "index_no_jwt_users_on_email", unique: true 113 | t.index ["reset_password_token"], name: "index_no_jwt_users_on_reset_password_token", unique: true 114 | end 115 | 116 | add_foreign_key "allowlisted_jwts", "jwt_with_allowlist_users" 117 | end 118 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/test/fixtures/jwt_users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/test/fixtures/no_jwt_users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/test/models/jwt_with_jti_matcher_user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class JwtWithJtiMatcherUserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/test/models/no_jwt_user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class NoJwtUserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/rails_app/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | end 11 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 4 | require 'devise/jwt' 5 | require 'rails' 6 | # Pick the frameworks you want: 7 | require 'active_model/railtie' 8 | # require 'active_job/railtie' 9 | require 'active_record/railtie' 10 | require 'action_controller/railtie' 11 | # require 'action_mailer/railtie' 12 | require 'action_view/railtie' 13 | # require 'action_cable/engine' 14 | # require 'sprockets/railtie' 15 | require 'rails/test_unit/railtie' 16 | 17 | require 'rspec/rails' 18 | require 'simplecov' 19 | SimpleCov.start 20 | 21 | ENV['RAILS_ENV'] ||= 'test' 22 | require File.expand_path( 23 | 'fixtures/rails_app/config/environment', __dir__ 24 | ) 25 | 26 | SPEC_ROOT = Pathname(__FILE__).dirname 27 | Dir[SPEC_ROOT.join('support/**/*.rb')].sort.each do |file| 28 | require file 29 | end 30 | 31 | RSpec.configure do |config| 32 | config.use_transactional_fixtures = true 33 | end 34 | -------------------------------------------------------------------------------- /spec/support/shared_contexts/feature.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_context 'feature' do 4 | def auth_headers(auth) 5 | { 6 | 'Authorization' => auth, 7 | 'Accept' => 'application/json' 8 | } 9 | end 10 | 11 | def sign_in(session_path, params, format: nil) 12 | path = format ? session_path + ".#{format}" : session_path 13 | post(path, params: params) 14 | response.headers['Authorization'] 15 | end 16 | 17 | def sign_up(registration_path, params, format: nil) 18 | path = format ? registration_path + ".#{format}" : registration_path 19 | post(path, params: params) 20 | end 21 | 22 | def sign_out(destroy_session_path, auth, method = :delete, format: nil) 23 | path = format ? destroy_session_path + ".#{format}" : destroy_session_path 24 | send(method, path, headers: auth_headers(auth)) 25 | end 26 | 27 | def get_with_auth(path, auth) 28 | get(path, 29 | headers: auth_headers(auth)) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/support/shared_contexts/fixtures.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # rubocop:disable RSpec/MultipleMemoizedHelpers 4 | shared_context 'fixtures' do 5 | let(:jwt_with_jti_matcher_model) { JwtWithJtiMatcherUser } 6 | let(:jwt_with_jti_matcher_user) do 7 | jwt_with_jti_matcher_model.create( 8 | email: 'an@user.com', 9 | password: 'password' 10 | ) 11 | end 12 | 13 | let(:jwt_with_denylist_model) { JwtWithDenylistUser } 14 | let(:jwt_with_denylist_user) do 15 | jwt_with_denylist_model.create( 16 | email: 'an@user.com', 17 | password: 'password' 18 | ) 19 | end 20 | 21 | let(:jwt_with_allowlist_model) { JwtWithAllowlistUser } 22 | let(:jwt_with_allowlist_user) do 23 | jwt_with_allowlist_model.create( 24 | email: 'an@user.com', 25 | password: 'password' 26 | ) 27 | end 28 | 29 | let(:jwt_with_null_model) { JwtWithNullUser } 30 | let(:jwt_with_null_user) do 31 | jwt_with_null_model.create( 32 | email: 'an@user.com', 33 | password: 'password' 34 | ) 35 | end 36 | 37 | let(:no_jwt_model) { NoJwtUser } 38 | let(:no_jwt_user) do 39 | no_jwt_model.create( 40 | email: 'an@user.com', 41 | password: 'password' 42 | ) 43 | end 44 | end 45 | # rubocop:enable RSpec/MultipleMemoizedHelpers 46 | --------------------------------------------------------------------------------