├── .rspec ├── .hound.yml ├── bin ├── setup └── console ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── Rakefile ├── spec ├── spec_helper.rb ├── support │ ├── 512key_pub.pem │ ├── 512key.pem │ └── 1024key.pem └── doorkeeper │ ├── jwt │ └── configuration_spec.rb │ └── jwt_spec.rb ├── Gemfile ├── lib └── doorkeeper │ ├── jwt │ ├── version.rb │ └── config.rb │ └── jwt.rb ├── LICENSE.txt ├── doorkeeper-jwt.gemspec ├── .rubocop.yml ├── CHANGELOG.md └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | rubocop: 2 | config_file: .rubocop.yml 3 | version: 0.72.0 4 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here. 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | /coverage/ 3 | /pkg/ 4 | /spec/reports/ 5 | /tmp/ 6 | 7 | /.yardoc/ 8 | /_yardoc/ 9 | /doc/ 10 | 11 | /.bundle/ 12 | 13 | Gemfile.lock 14 | 15 | .idea/ 16 | *.iml 17 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new 7 | 8 | task default: :spec 9 | task test: :spec 10 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "coveralls" 4 | Coveralls.wear! 5 | 6 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 7 | 8 | require "doorkeeper/jwt" 9 | require "pry" 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in doorkeeper-jwt.gemspec 6 | gemspec 7 | 8 | gem "coveralls_reborn", require: false 9 | gem "rubocop", "~> 1.8", require: false 10 | gem "rubocop-rspec", "~> 3.0", require: false 11 | -------------------------------------------------------------------------------- /spec/support/512key_pub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA6bVV/pApsBVaPJPddfre8YoMvKb7 3 | E8oS1VUg96k+e99xPOc5FqHtpHO99Yj5kzZUUKJanfVPUmQRWqa56UebtFcAvoPE 4 | +jInHUMriZpEAVS4xZhOhUyc9m7eLie5RuLiljEy6lOismJV6+8CaFKYGYlvRbnd 5 | JHmcRpOJ7P0I6FusOj8= 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /spec/support/512key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIHcAgEBBEIB7L7adlDAMlMFSl2uuwyVDUTp22CZonTEkHsaBr0MYEQiPIQPhFuf 3 | Bw4nzzkTHhlPEouhRe0wo9gzkZ+eYDuTVBugBwYFK4EEACOhgYkDgYYABADptVX+ 4 | kCmwFVo8k911+t7xigy8pvsTyhLVVSD3qT5733E85zkWoe2kc731iPmTNlRQolqd 5 | 9U9SZBFaprnpR5u0VwC+g8T6MicdQyuJmkQBVLjFmE6FTJz2bt4uJ7lG4uKWMTLq 6 | U6KyYlXr7wJoUpgZiW9Fud0keZxGk4ns/QjoW6w6Pw== 7 | -----END EC PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "doorkeeper-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 | # 13 | # Pry.start 14 | 15 | require "irb" 16 | 17 | IRB.start 18 | -------------------------------------------------------------------------------- /lib/doorkeeper/jwt/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module JWT 5 | def self.gem_version 6 | Gem::Version.new VERSION::STRING 7 | end 8 | 9 | module VERSION 10 | # Semantic versioning 11 | MAJOR = 0 12 | MINOR = 4 13 | TINY = 2 14 | PRE = nil 15 | 16 | # Full version number 17 | STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/support/1024key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQC88+2fugQuVeKSjJbUsA1shHNk6QZ56ELj6Ez2EDiWa0C35OJ1 3 | OmakJ4z9DaEkaQ5dbd29BxvFScImJ0qzsIIFwJENR+ug9SxqPwDuZtfmsGt9vGil 4 | 03V472Z8eBleTm0eCa2B3+2krElIGnLoMmG0j4FX4jx14/6Uhq/oUmGFwwIDAQAB 5 | AoGABMBdiVa0JDYkrGq8oDyNGd16yV7Sut4MUI7W4Jkn0yB/UCCBUVFWo2obMYHW 6 | O4QoyFCq8sXq/NsN9semKT6XBmLxCMaRpa/blJ10Sjc0jSz0MLOKdW1+KhAceqLV 7 | wfx8ipb7Eo5miHc5ioIbAhMvRhLme2lqjqcvPSNBwWHClIECQQDgTqGDfB3ATfbR 8 | Uy5bykE2IqUMqrsIK+g0C2fkxI2MW5ADS5uiSOI4dMrRdXgPezIcwdHTUFBuOsn7 9 | d1kDq8G1AkEA16aA9IZ2i6mP2aeWfZMt8V+UYPWwZNOTfDQhG/ga+PoWLUUT/hiC 10 | Le0TFmyiIDSa9wLkhUvTK4d4IwEo28O0lwJAYWMCUPoEWMgAz2VUDVpE8eIc0uEV 11 | jziw+lexZevIoRXn8uZSziTiwyxCGqrr05zjZwTnut117kXsCBgLN8LMuQJBAMyh 12 | ZsShw2OLpsC2Ugse2f2s4LOQ76z9R3oYTUKpD48qYFjcr7fxsbW6vN4Of1loEZRK 13 | 3mXOKbeoeMteklLXRkkCQBu7t1gnLbjo5bhpDVrDhk3f7BeeuAroJYwo4C1LkeJ1 14 | ExGnLLofLK/0nOHfNTtMvbbfzwVIbHl/7QLvI1WnYYA= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: >- 8 | Ruby ${{ matrix.ruby }} 9 | env: 10 | CI: true 11 | runs-on: ${{ matrix.os }} 12 | continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' || matrix.experimental }} 13 | if: | 14 | !( contains(github.event.pull_request.title, '[ci skip]') 15 | || contains(github.event.pull_request.title, '[skip ci]')) 16 | strategy: 17 | fail-fast: true 18 | matrix: 19 | experimental: [false] 20 | os: [ ubuntu-latest ] 21 | ruby: 22 | - '3.0' 23 | - '3.1' 24 | steps: 25 | - name: Repo checkout 26 | uses: actions/checkout@v2 27 | 28 | - name: Setup Ruby 29 | uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{ matrix.ruby }} 32 | bundler-cache: true 33 | 34 | - name: Run tests 35 | timeout-minutes: 10 36 | run: bundle exec rake test 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Christopher Warren 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 | -------------------------------------------------------------------------------- /doorkeeper-jwt.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("lib", __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | require "doorkeeper/jwt/version" 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = "doorkeeper-jwt" 10 | spec.version = Doorkeeper::JWT.gem_version 11 | spec.authors = ["Chris Warren", "Nikita Bulai"] 12 | spec.email = ["chris@expectless.com"] 13 | 14 | spec.summary = "JWT token generator for Doorkeeper" 15 | spec.description = "JWT token generator extension for Doorkeeper" 16 | spec.homepage = "https://github.com/chriswarren/doorkeeper-jwt" 17 | spec.license = "MIT" 18 | 19 | spec.bindir = "exe" 20 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 22 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 23 | spec.require_paths = ["lib"] 24 | 25 | spec.add_dependency "jwt", ">= 2.1" 26 | 27 | spec.add_development_dependency "bundler", ">= 1.16", "< 3" 28 | spec.add_development_dependency "pry", "~> 0" 29 | spec.add_development_dependency "rake", "~> 13.0" 30 | spec.add_development_dependency "rspec", "~> 3.8" 31 | end 32 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | require: rubocop-rspec 3 | 4 | AllCops: 5 | DefaultFormatter: progress 6 | DisplayCopNames: true 7 | DisplayStyleGuide: false 8 | ExtraDetails: true 9 | TargetRubyVersion: 2.5 10 | 11 | Metrics/LineLength: 12 | Exclude: 13 | - spec/**/* 14 | Max: 100 15 | 16 | Metrics/BlockLength: 17 | Exclude: 18 | - spec/**/* 19 | - doorkeeper-jwt.gemspec 20 | 21 | Style/StringLiterals: 22 | EnforcedStyle: double_quotes 23 | Style/StringLiteralsInInterpolation: 24 | EnforcedStyle: double_quotes 25 | 26 | Style/FrozenStringLiteralComment: 27 | Enabled: true 28 | 29 | Style/TrailingCommaInHashLiteral: 30 | EnforcedStyleForMultiline: consistent_comma 31 | Style/TrailingCommaInArrayLiteral: 32 | EnforcedStyleForMultiline: consistent_comma 33 | 34 | Style/SymbolArray: 35 | MinSize: 3 36 | Style/WordArray: 37 | MinSize: 3 38 | 39 | Style/ClassAndModuleChildren: 40 | Exclude: 41 | - spec/**/* 42 | Style/NumericPredicate: 43 | Enabled: false 44 | Style/DoubleNegation: 45 | Enabled: false 46 | 47 | Layout/MultilineMethodCallIndentation: 48 | EnforcedStyle: indented 49 | Layout/TrailingBlankLines: 50 | Enabled: true 51 | Layout/DotPosition: 52 | EnforcedStyle: leading 53 | 54 | Naming/FileName: 55 | Exclude: 56 | - lib/doorkeeper-jwt.rb 57 | 58 | RSpec/ExampleLength: 59 | Enabled: false 60 | 61 | RSpec/MultipleExpectations: 62 | Enabled: false 63 | -------------------------------------------------------------------------------- /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/) and this 5 | project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## master 8 | 9 | Add here 10 | 11 | ## [0.4.2] - 2024-08-12 12 | 13 | - Rename encryption_method to signing_method [#53](https://github.com/doorkeeper-gem/doorkeeper-jwt/pull/53) 14 | - Fix default token generation [#56](https://github.com/doorkeeper-gem/doorkeeper-jwt/pull/56) 15 | 16 | ### Fixed 17 | 18 | - Fixed default token generation to return a random hex value [#56](https://github.com/doorkeeper-gem/doorkeeper-jwt/pull/56) 19 | 20 | ## [0.4.1] - 2022-02-23 21 | 22 | - JWT gem requirement relaxed to use any version >= 2.1 23 | 24 | ### Changed 25 | 26 | ## [0.4.0] - 2019-10-02 27 | 28 | - Restructured library files to follow naming conventions. (https://guides.rubygems.org/name-your-gem/). 29 | - Add support of new doorkeeper with encryption [#30](https://github.com/doorkeeper-gem/doorkeeper-jwt/pull/30) 30 | 31 | ## [0.3.0] - 2018-10-01 32 | 33 | ### Added 34 | 35 | - Bump JWT gem version. Via [#27](https://github.com/doorkeeper-gem/doorkeeper-jwt/pull/27) by [@pacop](https://github.com/pacop/). 36 | 37 | ## [0.2.1] - 2017-06-07 38 | 39 | ### Fixed 40 | 41 | - The `token_headers` proc now passes `opts` like the other config methods. Fixed via #19 by @travisofthenorth. 42 | 43 | ## [0.2.0] - 2017-05-25 44 | 45 | ### Added 46 | 47 | - Added support for ["kid" (Key ID) Header Parameter](https://tools.ietf.org/html/rfc7515#section-4.1.4) 48 | @travisofthenorth. Allows custom token headers. 49 | -------------------------------------------------------------------------------- /spec/doorkeeper/jwt/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe(Doorkeeper::JWT, "#configuration") do 6 | subject(:configuration) { described_class.configuration } 7 | 8 | describe "token_payload" do 9 | it "is nil by default" do 10 | described_class.configure {} 11 | 12 | expect(configuration.token_payload).to be_a(Proc) 13 | end 14 | 15 | it "sets the block that is accessible via authenticate_admin" do 16 | block = proc {} 17 | 18 | described_class.configure do 19 | token_payload(&block) 20 | end 21 | 22 | expect(configuration.token_payload).to eq(block) 23 | end 24 | end 25 | 26 | describe "token_headers" do 27 | it "is nil by default" do 28 | described_class.configure {} 29 | 30 | expect(configuration.token_headers).to be_a(Proc) 31 | end 32 | 33 | it "sets the block that is accessible via authenticate_admin" do 34 | block = proc {} 35 | 36 | described_class.configure do 37 | token_headers(&block) 38 | end 39 | 40 | expect(configuration.token_headers).to eq(block) 41 | end 42 | end 43 | 44 | describe "signing_method" do 45 | it "defaults to nil" do 46 | described_class.configure {} 47 | 48 | expect(configuration.signing_method).to be_nil 49 | end 50 | 51 | it "can change the value" do 52 | described_class.configure do 53 | signing_method :rs512 54 | end 55 | 56 | expect(configuration.signing_method).to eq :rs512 57 | end 58 | end 59 | 60 | describe "use_application_secret" do 61 | it "defaults to false" do 62 | described_class.configure {} 63 | 64 | expect(configuration.use_application_secret).to be false 65 | end 66 | 67 | it "changes the value of secret_key to the application's secret" do 68 | described_class.configure do 69 | use_application_secret true 70 | end 71 | 72 | expect(configuration.use_application_secret).to be true 73 | end 74 | end 75 | 76 | describe "secret_key" do 77 | it "defaults to nil" do 78 | described_class.configure {} 79 | 80 | expect(configuration.secret_key).to be_nil 81 | end 82 | 83 | it "can change the value" do 84 | described_class.configure do 85 | secret_key "foo" 86 | end 87 | 88 | expect(configuration.secret_key).to eq "foo" 89 | end 90 | end 91 | 92 | describe "secret_key_path" do 93 | it "defaults to nil" do 94 | described_class.configure {} 95 | 96 | expect(configuration.secret_key_path).to be_nil 97 | end 98 | 99 | it "can change the value" do 100 | described_class.configure do 101 | secret_key_path File.join("..", "support", "1024key.pem") 102 | end 103 | 104 | expect(configuration.secret_key_path).to eq "../support/1024key.pem" 105 | end 106 | end 107 | 108 | it "raises an exception when configuration is not set" do 109 | expect { described_class.configuration } 110 | .to raise_error Doorkeeper::JWT::MissingConfiguration 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/doorkeeper/jwt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "doorkeeper/jwt/version" 4 | require "doorkeeper/jwt/config" 5 | require "jwt" 6 | 7 | module Doorkeeper 8 | module JWT 9 | class << self 10 | def generate(opts = {}) 11 | ::JWT.encode( 12 | token_payload(opts), 13 | secret_key(opts), 14 | signing_method, 15 | token_headers(opts) 16 | ) 17 | end 18 | 19 | private 20 | 21 | def token_payload(opts = {}) 22 | Doorkeeper::JWT.configuration.token_payload.call(opts) 23 | end 24 | 25 | def token_headers(opts = {}) 26 | Doorkeeper::JWT.configuration.token_headers.call(opts) 27 | end 28 | 29 | def secret_key(opts) 30 | opts = { application: {} }.merge(opts) 31 | 32 | return application_secret(opts) if use_application_secret? 33 | return secret_key_file unless secret_key_file.nil? 34 | return rsa_key if rsa_signing? 35 | return ecdsa_key if ecdsa_signing? 36 | 37 | Doorkeeper::JWT.configuration.secret_key 38 | end 39 | 40 | def secret_key_file 41 | return nil if Doorkeeper::JWT.configuration.secret_key_path.nil? 42 | return rsa_key_file if rsa_signing? 43 | return ecdsa_key_file if ecdsa_signing? 44 | end 45 | 46 | def signing_method 47 | return "none" unless Doorkeeper::JWT.configuration.signing_method 48 | 49 | Doorkeeper::JWT.configuration.signing_method.to_s.upcase 50 | end 51 | 52 | def use_application_secret? 53 | Doorkeeper::JWT.configuration.use_application_secret 54 | end 55 | 56 | def application_secret(opts) 57 | if opts[:application].nil? 58 | raise( 59 | "JWT `use_application_secret` is enabled, but application is nil." \ 60 | " This can happen if `client_id` was absent in the request params." 61 | ) 62 | end 63 | 64 | secret = if opts[:application].respond_to?(:plaintext_secret) 65 | unless opts[:application].secret_strategy.allows_restoring_secrets? 66 | raise( 67 | "JWT `use_application_secret` is enabled, but secret strategy " \ 68 | "doesn't allow plaintext secret restoring" 69 | ) 70 | end 71 | opts[:application].plaintext_secret 72 | else 73 | opts[:application][:secret] 74 | end 75 | 76 | if secret.nil? 77 | raise( 78 | "JWT `use_application_secret` is enabled, but the application" \ 79 | " secret is nil." 80 | ) 81 | end 82 | 83 | secret 84 | end 85 | 86 | def rsa_signing? 87 | /RS\d{3}/ =~ signing_method 88 | end 89 | 90 | def ecdsa_signing? 91 | /ES\d{3}/ =~ signing_method 92 | end 93 | 94 | def rsa_key 95 | OpenSSL::PKey::RSA.new(Doorkeeper::JWT.configuration.secret_key) 96 | end 97 | 98 | def ecdsa_key 99 | OpenSSL::PKey::EC.new(Doorkeeper::JWT.configuration.secret_key) 100 | end 101 | 102 | def rsa_key_file 103 | secret_key_file_open { |f| OpenSSL::PKey::RSA.new(f) } 104 | end 105 | 106 | def ecdsa_key_file 107 | secret_key_file_open { |f| OpenSSL::PKey::EC.new(f) } 108 | end 109 | 110 | def secret_key_file_open(&block) 111 | File.open(Doorkeeper::JWT.configuration.secret_key_path, &block) 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/doorkeeper-jwt.svg)](https://rubygems.org/gems/doorkeeper-jwt) 2 | [![Coverage Status](https://coveralls.io/repos/github/doorkeeper-gem/doorkeeper-jwt/badge.svg?branch=master)](https://coveralls.io/github/doorkeeper-gem/doorkeeper-jwt?branch=master) 3 | [![Build Status](https://travis-ci.org/doorkeeper-gem/doorkeeper-jwt.svg?branch=master)](https://travis-ci.org/doorkeeper-gem/doorkeeper-jwt) 4 | [![Maintainability](https://api.codeclimate.com/v1/badges/ca4d81b49acabda27e0c/maintainability)](https://codeclimate.com/github/doorkeeper-gem/doorkeeper-jwt/maintainability) 5 | 6 | # Doorkeeper::JWT 7 | 8 | Doorkeeper JWT adds JWT token support to the Doorkeeper OAuth library. Confirmed to work with Doorkeeper 2.2.x - 4.x. 9 | Untested with later versions of Doorkeeper. 10 | 11 | ```ruby 12 | gem 'doorkeeper' 13 | ``` 14 | 15 | ## Installation 16 | 17 | Add this line to your application's Gemfile: 18 | 19 | ```ruby 20 | gem 'doorkeeper-jwt' 21 | ``` 22 | 23 | And then execute: 24 | 25 | $ bundle 26 | 27 | Or install it yourself as: 28 | 29 | $ gem install doorkeeper-jwt 30 | 31 | ## Usage 32 | 33 | In your `doorkeeper.rb` initializer add the follow to the `Doorkeeper.configure` block: 34 | 35 | ```ruby 36 | access_token_generator '::Doorkeeper::JWT' 37 | ``` 38 | 39 | Then add a `Doorkeeper::JWT.configure` block below the `Doorkeeper.configure` block to set your JWT preferences. 40 | 41 | ```ruby 42 | Doorkeeper::JWT.configure do 43 | # Set the payload for the JWT token. This should contain unique information 44 | # about the user. Defaults to a randomly generated token in a hash: 45 | # { token: "RANDOM-TOKEN" } 46 | token_payload do |opts| 47 | user = User.find(opts[:resource_owner_id]) 48 | 49 | { 50 | iss: 'My App', 51 | iat: Time.current.utc.to_i, 52 | aud: opts[:application][:uid], 53 | 54 | # @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7 55 | jti: SecureRandom.uuid, 56 | sub: user.id, 57 | 58 | user: { 59 | id: user.id, 60 | email: user.email 61 | } 62 | } 63 | end 64 | 65 | # Optionally set additional headers for the JWT. See 66 | # https://tools.ietf.org/html/rfc7515#section-4.1 67 | # JWK can be used to automatically verify RS* tokens client-side if token's kid matches a public kid in /oauth/discovery/keys 68 | # token_headers do |_opts| 69 | # key = OpenSSL::PKey::RSA.new(File.read(File.join('path', 'to', 'file.pem'))) 70 | # { kid: JWT::JWK.new(key)[:kid] } 71 | # end 72 | 73 | # Use the application secret specified in the access grant token. Defaults to 74 | # `false`. If you specify `use_application_secret true`, both `secret_key` and 75 | # `secret_key_path` will be ignored. 76 | use_application_secret false 77 | 78 | # Set the signing secret. This would be shared with any other applications 79 | # that should be able to verify the authenticity of the token. Defaults to "secret". 80 | secret_key ENV['JWT_SECRET'] 81 | 82 | # If you want to use RS* algorithms specify the path to the RSA key to use for 83 | # signing. If you specify a `secret_key_path` it will be used instead of 84 | # `secret_key`. 85 | secret_key_path File.join('path', 'to', 'file.pem') 86 | 87 | # Specify cryptographic signing algorithm type (https://github.com/progrium/ruby-jwt). Defaults to 88 | # `nil`. 89 | signing_method :hs512 90 | end 91 | ``` 92 | 93 | ## Development 94 | 95 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt 96 | that will allow you to experiment. 97 | 98 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the 99 | version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git 100 | commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 101 | 102 | ## Contributing 103 | 104 | 1. Fork it (https://github.com/[my-github-username]/doorkeeper-jwt/fork) 105 | 2. Create your feature branch (`git checkout -b my-new-feature`) 106 | 3. Commit your changes (`git commit -am 'Add some feature'`) 107 | 4. Push to the branch (`git push origin my-new-feature`) 108 | 5. Create a new Pull Request 109 | -------------------------------------------------------------------------------- /lib/doorkeeper/jwt/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module JWT 5 | class MissingConfiguration < StandardError 6 | def initialize 7 | super("Configuration for doorkeeper-jwt missing.") 8 | end 9 | end 10 | 11 | def self.configure(&block) 12 | @config = Config::Builder.new(&block).build 13 | end 14 | 15 | def self.configuration 16 | @config || raise(MissingConfiguration) 17 | end 18 | 19 | class Config 20 | class Builder 21 | def initialize(&block) 22 | @config = Config.new 23 | instance_eval(&block) 24 | end 25 | 26 | def build 27 | @config 28 | end 29 | 30 | def use_application_secret(value) 31 | @config.instance_variable_set("@use_application_secret", value) 32 | end 33 | 34 | def secret_key(value) 35 | @config.instance_variable_set("@secret_key", value) 36 | end 37 | 38 | def secret_key_path(value) 39 | @config.instance_variable_set("@secret_key_path", value) 40 | end 41 | 42 | # For backward compatibility. This library does not support encryption. 43 | def encryption_method(value) 44 | @config.instance_variable_set("@signing_method", value) 45 | Kernel.warn("[DOORKEEPER-JWT]: Please use signing_method instead, this option is deprecated and will be removed soon") 46 | end 47 | 48 | def signing_method(value) 49 | @config.instance_variable_set("@signing_method", value) 50 | end 51 | end 52 | 53 | module Option 54 | # Defines configuration options. 55 | # 56 | # When you call option, it defines two methods. One method will take 57 | # place in the +Config+ class and the other method will take place in 58 | # the +Builder+ class. 59 | # 60 | # The +name+ parameter will set both builder method and config 61 | # attribute. If the +:as+ option is defined, the builder method will be 62 | # the specified option while the config attribute will be the +name+ 63 | # parameter. 64 | # 65 | # If you want to introduce another level of config DSL you can define 66 | # +builder_class+ parameter. Builder should take a block as the 67 | # initializer parameter and respond to function +build+ that returns the 68 | # value of the config attribute. 69 | # 70 | # ==== Options 71 | # 72 | # * [+:as+] Set the builder method that goes inside +configure+ block. 73 | # * [+:default+] The default value in case no option was set. 74 | # 75 | # ==== Examples 76 | # 77 | # option :name 78 | # option :name, as: :set_name 79 | # option :name, default: 'My Name' 80 | # option :scopes, builder_class: ScopesBuilder 81 | def option(name, options = {}) 82 | attribute = options[:as] || name 83 | attribute_builder = options[:builder_class] 84 | attribute_symbol = :"@#{attribute}" 85 | 86 | Builder.instance_eval do 87 | define_method name do |*args, &block| 88 | # TODO: is builder_class option being used? 89 | value = 90 | if attribute_builder 91 | attribute_builder.new(&block).build 92 | else 93 | block || args.first 94 | end 95 | 96 | @config.instance_variable_set(attribute_symbol, value) 97 | end 98 | end 99 | 100 | define_method attribute do |*| 101 | if instance_variable_defined?(attribute_symbol) 102 | instance_variable_get(attribute_symbol) 103 | else 104 | options[:default] 105 | end 106 | end 107 | 108 | public attribute 109 | end 110 | 111 | def extended(base) 112 | base.send(:private, :option) 113 | end 114 | end 115 | 116 | extend Option 117 | 118 | option( 119 | :token_payload, 120 | default: proc { { token: SecureRandom.hex } }, 121 | ) 122 | 123 | option :token_headers, default: proc { {} } 124 | option :use_application_secret, default: false 125 | option :secret_key, default: nil 126 | option :secret_key_path, default: nil 127 | option :signing_method, default: nil 128 | 129 | def use_application_secret 130 | @use_application_secret ||= false 131 | end 132 | 133 | def secret_key 134 | @secret_key ||= nil 135 | end 136 | 137 | def secret_key_path 138 | @secret_key_path ||= nil 139 | end 140 | 141 | def signing_method 142 | @signing_method ||= nil 143 | end 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /spec/doorkeeper/jwt_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe Doorkeeper::JWT do 6 | it "has a version number" do 7 | expect(Doorkeeper::JWT::VERSION).not_to be nil 8 | end 9 | 10 | describe ".generate" do 11 | it "creates a JWT token" do 12 | described_class.configure {} 13 | 14 | token = described_class.generate({}) 15 | decoded_token = ::JWT.decode(token, nil, false) 16 | 17 | expect(decoded_token[0]).to be_a(Hash) 18 | expect(decoded_token[0]["token"]).to match(/^\h{32}$/) 19 | expect(decoded_token[1]).to be_a(Hash) 20 | expect(decoded_token[1]["alg"]).to eq "none" 21 | end 22 | 23 | it "creates a JWT token with a custom payload" do 24 | described_class.configure do 25 | token_payload do 26 | { foo: "bar" } 27 | end 28 | end 29 | 30 | token = described_class.generate({}) 31 | decoded_token = ::JWT.decode(token, nil, false) 32 | 33 | expect(decoded_token[0]).to be_a(Hash) 34 | expect(decoded_token[0]["foo"]).to eq "bar" 35 | expect(decoded_token[1]).to be_a(Hash) 36 | expect(decoded_token[1]["alg"]).to eq "none" 37 | end 38 | 39 | it "creates a JWT token with custom dynamic headers" do 40 | described_class.configure do 41 | token_headers do |opts| 42 | { kid: opts[:application][:uid] } 43 | end 44 | end 45 | 46 | token = described_class.generate(application: { uid: "foo" }) 47 | decoded_token = ::JWT.decode(token, nil, false) 48 | 49 | expect(decoded_token[1]).to be_a(Hash) 50 | expect(decoded_token[1]["alg"]).to eq "none" 51 | expect(decoded_token[1]["kid"]).to eq "foo" 52 | end 53 | 54 | it "creates a signed JWT token" do 55 | described_class.configure do 56 | secret_key "super secret" 57 | end 58 | 59 | token = described_class.generate({}) 60 | decoded_token = ::JWT.decode(token, "super secret", false) 61 | 62 | expect(decoded_token[0]).to be_a(Hash) 63 | expect(decoded_token[0]["token"]).to be_a(String) 64 | expect(decoded_token[1]).to be_a(Hash) 65 | expect(decoded_token[1]["alg"]).to eq "none" 66 | end 67 | 68 | it "creates a signed JWT token using hs256" do 69 | described_class.configure do 70 | secret_key "super secret" 71 | signing_method :hs256 72 | end 73 | 74 | token = described_class.generate({}) 75 | algorithm = { algorithm: "HS256" } 76 | decoded_token = ::JWT.decode(token, "super secret", true, algorithm) 77 | 78 | expect(decoded_token[0]).to be_a(Hash) 79 | expect(decoded_token[0]["token"]).to be_a(String) 80 | expect(decoded_token[1]).to be_a(Hash) 81 | expect(decoded_token[1]["alg"]).to eq "HS256" 82 | end 83 | 84 | it "creates a signed JWT token with a custom payload" do 85 | described_class.configure do 86 | token_payload do 87 | { foo: "bar" } 88 | end 89 | 90 | secret_key "super secret" 91 | signing_method :hs256 92 | end 93 | 94 | token = described_class.generate({}) 95 | algorithm = { algorithm: "HS256" } 96 | decoded_token = ::JWT.decode(token, "super secret", true, algorithm) 97 | 98 | expect(decoded_token[0]).to be_a(Hash) 99 | expect(decoded_token[0]["foo"]).to eq "bar" 100 | expect(decoded_token[1]).to be_a(Hash) 101 | expect(decoded_token[1]["alg"]).to eq "HS256" 102 | end 103 | 104 | it "creates a signed JWT token using the deprecated signing_method" do 105 | described_class.configure do 106 | token_payload do 107 | { foo: "bar" } 108 | end 109 | 110 | secret_key "super secret" 111 | signing_method :hs256 112 | end 113 | 114 | token = described_class.generate({}) 115 | algorithm = { algorithm: "HS256" } 116 | decoded_token = ::JWT.decode(token, "super secret", true, algorithm) 117 | 118 | expect(decoded_token[0]).to be_a(Hash) 119 | expect(decoded_token[0]["foo"]).to eq "bar" 120 | expect(decoded_token[1]).to be_a(Hash) 121 | expect(decoded_token[1]["alg"]).to eq "HS256" 122 | end 123 | 124 | it "creates a signed JWT token with a custom dynamic payload" do 125 | described_class.configure do 126 | token_payload do |opts| 127 | { foo: "bar_#{opts[:resource_owner_id]}" } 128 | end 129 | 130 | secret_key "super secret" 131 | signing_method :hs256 132 | end 133 | 134 | token = described_class.generate(resource_owner_id: 1) 135 | algorithm = { algorithm: "HS256" } 136 | decoded_token = ::JWT.decode(token, "super secret", true, algorithm) 137 | 138 | expect(decoded_token[0]).to be_a(Hash) 139 | expect(decoded_token[0]["foo"]).to eq "bar_1" 140 | expect(decoded_token[1]).to be_a(Hash) 141 | expect(decoded_token[1]["alg"]).to eq "HS256" 142 | end 143 | 144 | it "creates a signed JWT token with an RSA key from a file" do 145 | described_class.configure do 146 | token_payload do 147 | { foo: "bar" } 148 | end 149 | 150 | secret_key_path "spec/support/1024key.pem" 151 | signing_method :rs512 152 | end 153 | 154 | token = described_class.generate({}) 155 | secret_key = OpenSSL::PKey::RSA.new File.read("spec/support/1024key.pem") 156 | decoded_token = ::JWT.decode(token, secret_key, true, algorithm: "RS512") 157 | 158 | expect(decoded_token[0]).to be_a(Hash) 159 | expect(decoded_token[0]["foo"]).to eq "bar" 160 | expect(decoded_token[1]).to be_a(Hash) 161 | expect(decoded_token[1]["alg"]).to eq "RS512" 162 | end 163 | 164 | it "creates a signed JWT token with an RSA key from a string" do 165 | secret_key = OpenSSL::PKey::RSA.new(1024) 166 | 167 | described_class.configure do 168 | token_payload do 169 | { foo: "bar" } 170 | end 171 | 172 | secret_key secret_key.to_s 173 | signing_method :rs512 174 | end 175 | 176 | token = described_class.generate({}) 177 | decoded_token = ::JWT.decode(token, secret_key, true, algorithm: "RS512") 178 | 179 | expect(decoded_token[0]).to be_a(Hash) 180 | expect(decoded_token[0]["foo"]).to eq "bar" 181 | expect(decoded_token[1]).to be_a(Hash) 182 | expect(decoded_token[1]["alg"]).to eq "RS512" 183 | end 184 | 185 | it "creates a signed JWT token with an ECDSA key from a file" do 186 | described_class.configure do 187 | token_payload do 188 | { foo: "bar" } 189 | end 190 | 191 | secret_key_path "spec/support/512key.pem" 192 | signing_method :es512 193 | end 194 | 195 | token = described_class.generate({}) 196 | key_file = File.read("spec/support/512key_pub.pem") 197 | secret_key = OpenSSL::PKey::EC.new key_file 198 | decoded_token = ::JWT.decode(token, secret_key, true, algorithm: "ES512") 199 | 200 | expect(decoded_token[0]).to be_a(Hash) 201 | expect(decoded_token[0]["foo"]).to eq "bar" 202 | expect(decoded_token[1]).to be_a(Hash) 203 | expect(decoded_token[1]["alg"]).to eq "ES512" 204 | end 205 | 206 | it "creates a signed JWT token with an ECDSA key from a string" do 207 | secret_key = OpenSSL::PKey::EC.generate("secp521r1") 208 | 209 | public_key = OpenSSL::PKey::EC.new(secret_key) 210 | public_key.private_key = nil 211 | 212 | described_class.configure do 213 | token_payload do 214 | { foo: "bar" } 215 | end 216 | 217 | secret_key secret_key 218 | signing_method :es512 219 | end 220 | 221 | token = described_class.generate({}) 222 | decoded_token = ::JWT.decode(token, public_key, true, algorithm: "ES512") 223 | 224 | expect(decoded_token[0]).to be_a(Hash) 225 | expect(decoded_token[0]["foo"]).to eq "bar" 226 | expect(decoded_token[1]).to be_a(Hash) 227 | expect(decoded_token[1]["alg"]).to eq "ES512" 228 | end 229 | 230 | context "when use_application_secret used" do 231 | let(:secret_key) do 232 | OpenSSL::PKey::RSA.new(1024) 233 | end 234 | 235 | let(:application) do 236 | instance_double("Doorkeeper::Application", 237 | secret: Digest::SHA256.digest(secret_key.to_s), 238 | plaintext_secret: secret_key, 239 | secret_strategy: class_double("Doorkeeper::SecretStoring::Sha256Hash", 240 | allows_restoring_secrets?: true)) 241 | end 242 | 243 | before do 244 | described_class.configure do 245 | use_application_secret true 246 | 247 | token_payload do 248 | { foo: "bar" } 249 | end 250 | 251 | signing_method :rs512 252 | end 253 | end 254 | 255 | it "creates a signed JWT token with an app secret", :aggregate_failures do 256 | token = described_class.generate(application: application) 257 | decoded_token = ::JWT.decode(token, secret_key, true, algorithm: "RS512") 258 | 259 | expect(decoded_token[0]).to be_a(Hash) 260 | expect(decoded_token[0]["foo"]).to eq "bar" 261 | expect(decoded_token[1]).to be_a(Hash) 262 | expect(decoded_token[1]["alg"]).to eq "RS512" 263 | end 264 | end 265 | 266 | context "when use_application_secret used and Doorkeeper version < 5.1.0" do 267 | let(:secret_key) do 268 | OpenSSL::PKey::RSA.new(1024) 269 | end 270 | 271 | let(:application) { { secret: secret_key } } 272 | 273 | before do 274 | described_class.configure do 275 | use_application_secret true 276 | 277 | token_payload do 278 | { foo: "bar" } 279 | end 280 | 281 | signing_method :rs512 282 | end 283 | end 284 | 285 | it "creates a signed JWT token with an app secret", :aggregate_failures do 286 | token = described_class.generate(application: application) 287 | decoded_token = ::JWT.decode(token, secret_key, true, algorithm: "RS512") 288 | 289 | expect(decoded_token[0]).to be_a(Hash) 290 | expect(decoded_token[0]["foo"]).to eq "bar" 291 | expect(decoded_token[1]).to be_a(Hash) 292 | expect(decoded_token[1]["alg"]).to eq "RS512" 293 | end 294 | end 295 | 296 | context "when use_application_secret used" do 297 | let(:secret_key) do 298 | OpenSSL::PKey::RSA.new(1024) 299 | end 300 | 301 | let(:application) do 302 | instance_double("Doorkeeper::Application", 303 | secret: Digest::SHA256.digest(secret_key.to_s), 304 | plaintext_secret: secret_key, 305 | secret_strategy: class_double("Doorkeeper::SecretStoring::Sha256Hash", 306 | allows_restoring_secrets?: false)) 307 | end 308 | 309 | before do 310 | described_class.configure do 311 | use_application_secret true 312 | 313 | token_payload do 314 | { foo: "bar" } 315 | end 316 | 317 | signing_method :rs512 318 | end 319 | end 320 | 321 | it "creates a signed JWT token with an app secret", :aggregate_failures do 322 | expect { described_class.generate(application: application) }.to( 323 | raise_error.with_message(/secret strategy doesn't/) 324 | ) 325 | end 326 | end 327 | end 328 | end 329 | --------------------------------------------------------------------------------