doorkeeper-jwt 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ce5cf8224951ed71ef293afd6c863ad606300ec2082f4f9c821228890ae384c
4
- data.tar.gz: 92f97889cb72c6a6d5fc17cabeeb54fa934f30c2f23296ca24f1e5fe4490a574
3
+ metadata.gz: 51843843df19db6ed75ac9500ba810ab9e5ccde882386ced00d0746933ccd4c7
4
+ data.tar.gz: 982167d24d8e64f959fde2a1b89ffa11a513a02136a4147dd64ecacca2536ffb
5
5
  SHA512:
6
- metadata.gz: 27d14827f9e9a5c9b5dcacf85f7a30c3d4714c95017dade01e85b27c8232883cf2f9a79dd9975b94db2ba904d3097307f5b05167eb94d7961cb61dc0ed93ea6e
7
- data.tar.gz: 6847396d8b1a00f7c5abd9f4955e978bc148d559dc7de15a438f1df25790237b5e200a67bee6429eb5f25a1b986379f27557d692f34161797ec7515d08c73abc
6
+ metadata.gz: 2dad7626a80e34c5cea0cfd69c1b48d94db47a2748eccefa9d4e3f462a054865e28e518af1da1836008d7cecbaa16ebf2c122d934f3d75f7762879300fad3e40
7
+ data.tar.gz: d191d7577852ae158b2c54fb4cfe530e3e0f383dea8b1bde0ccdf528dc2489b9458811709ed2e09a3e818eaeaf5ec773d192e50296d88918e9e43bb2dfb0e506
@@ -5,3 +5,8 @@ updates:
5
5
  schedule:
6
6
  interval: daily
7
7
  open-pull-requests-limit: 10
8
+ - package-ecosystem: github-actions
9
+ directory: "/"
10
+ schedule:
11
+ interval: weekly
12
+ open-pull-requests-limit: 10
@@ -16,10 +16,10 @@ jobs:
16
16
  changelog:
17
17
  runs-on: ubuntu-latest
18
18
  steps:
19
- - uses: actions/checkout@v7
19
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
20
20
  - name: Check changed paths
21
21
  id: changes
22
- uses: dorny/paths-filter@v4
22
+ uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
23
23
  with:
24
24
  filters: |
25
25
  gem_changes:
@@ -34,7 +34,7 @@ jobs:
34
34
  - name: Enforce changelog
35
35
  if: |
36
36
  steps.changes.outputs.gem_changes == 'true'
37
- uses: dangoslen/changelog-enforcer@v3
37
+ uses: dangoslen/changelog-enforcer@8b5e9dc3121363bb7c0115f8533404d92af382de # v3.7.0
38
38
  with:
39
39
  changeLogPath: CHANGELOG.md
40
40
  skipLabels: "dependencies"
@@ -23,8 +23,10 @@ jobs:
23
23
  experimental: [false]
24
24
  os: [ ubuntu-latest ]
25
25
  ruby:
26
- - '3.0'
27
- - '3.1'
26
+ - '3.2'
27
+ - '3.3'
28
+ - '3.4'
29
+ - '4.0'
28
30
  # The minimum declared in the gemspec and the latest release. 5.4 is the oldest
29
31
  # version whose options DSL resolves against our own config builder.
30
32
  doorkeeper:
@@ -32,10 +34,10 @@ jobs:
32
34
  - 'latest'
33
35
  steps:
34
36
  - name: Repo checkout
35
- uses: actions/checkout@v2
37
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
36
38
 
37
39
  - name: Setup Ruby
38
- uses: ruby/setup-ruby@v1
40
+ uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
39
41
  with:
40
42
  ruby-version: ${{ matrix.ruby }}
41
43
  bundler-cache: true
@@ -45,3 +47,22 @@ jobs:
45
47
  - name: Run tests
46
48
  timeout-minutes: 10
47
49
  run: bundle exec rake test
50
+
51
+ - name: Upload coverage to Coveralls
52
+ uses: coverallsapp/github-action@8d6379e14d29928660c4ba802d8e85393440b329 # v2.3.8
53
+ with:
54
+ file: coverage/.resultset.json
55
+ format: simplecov
56
+ flag-name: ruby-${{ matrix.ruby }}-doorkeeper-${{ matrix.doorkeeper }}
57
+ parallel: true
58
+
59
+ coverage:
60
+ name: Coveralls Finished
61
+ needs: build
62
+ if: always()
63
+ runs-on: ubuntu-latest
64
+ steps:
65
+ - name: Coveralls Finished
66
+ uses: coverallsapp/github-action@8d6379e14d29928660c4ba802d8e85393440b329 # v2.3.8
67
+ with:
68
+ parallel-finished: true
data/.rubocop.yml CHANGED
@@ -6,7 +6,7 @@ AllCops:
6
6
  DisplayCopNames: true
7
7
  DisplayStyleGuide: false
8
8
  ExtraDetails: true
9
- TargetRubyVersion: 2.5
9
+ TargetRubyVersion: 3.2
10
10
 
11
11
  Metrics/LineLength:
12
12
  Exclude:
data/CHANGELOG.md CHANGED
@@ -8,6 +8,13 @@ project adheres to [Semantic Versioning](http://semver.org/).
8
8
 
9
9
  - Add your entry here
10
10
 
11
+ # [0.4.5] - 2026-09-16
12
+
13
+ - **Security**: Raise `Doorkeeper::JWT::SigningMethodMissing` instead of issuing an unsigned (`alg: none`) JWT when a signing key is configured but `signing_method` is not. Issuing unsigned tokens with no signing key at all now warns and will become an error in a future release
14
+ - Add `required_ruby_version >= 3.2` to the gemspec and update the CI matrix to 3.2, 3.3, 3.4 and 4.0, aligning with doorkeeper core
15
+ - Allow `signing_method`, `secret_key` and `secret_key_path` to be configured with a block evaluated per token [#66](https://github.com/doorkeeper-gem/doorkeeper-jwt/pull/66)
16
+ - Fix broken Coveralls tests coverage [#67](https://github.com/doorkeeper-gem/doorkeeper-jwt/pull/67)
17
+
11
18
  # [0.4.4] - 2026-08-21
12
19
 
13
20
  - Fix `[DOORKEEPER] Option ... already defined and will be overridden` warnings on gem load [#61](https://github.com/doorkeeper-gem/doorkeeper-jwt/pull/61)
data/Gemfile CHANGED
@@ -11,6 +11,6 @@ gemspec
11
11
  doorkeeper_version = ENV.fetch("DOORKEEPER_VERSION", "latest")
12
12
  gem "doorkeeper", doorkeeper_version unless doorkeeper_version == "latest"
13
13
 
14
- gem "coveralls_reborn", require: false
14
+ gem "simplecov", "~> 1.1", require: false
15
15
  gem "rubocop", "~> 1.8", require: false
16
16
  gem "rubocop-rspec", "~> 3.0", require: false
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  [![Gem Version](https://badge.fury.io/rb/doorkeeper-jwt.svg)](https://rubygems.org/gems/doorkeeper-jwt)
2
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
3
  [![CI](https://github.com/doorkeeper-gem/doorkeeper-jwt/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/doorkeeper-gem/doorkeeper-jwt/actions/workflows/ci.yml)
4
- [![Maintainability](https://api.codeclimate.com/v1/badges/ca4d81b49acabda27e0c/maintainability)](https://codeclimate.com/github/doorkeeper-gem/doorkeeper-jwt/maintainability)
4
+ [![Maintainability](https://qlty.sh/gh/doorkeeper-gem/projects/doorkeeper-jwt/maintainability.svg)](https://qlty.sh/gh/doorkeeper-gem/projects/doorkeeper-jwt)
5
5
 
6
6
  # Doorkeeper::JWT
7
7
 
@@ -103,6 +103,87 @@ Doorkeeper::JWT.configure do
103
103
  end
104
104
  ```
105
105
 
106
+ ### Authenticating more than one model
107
+
108
+ `opts[:resource_owner_id]` alone does not tell the payload block which model the token belongs to, so an application
109
+ with several resource owner models (say, `Driver` and `Client`) cannot look the owner up from the id.
110
+
111
+ Doorkeeper 5.4 and newer can hand the owner record itself to the generator. Run the generator, which enables
112
+ `use_polymorphic_resource_owner` in the `Doorkeeper.configure` block and creates the migration that adds the
113
+ `resource_owner_type` columns, then apply the migration:
114
+
115
+ $ rails generate doorkeeper:enable_polymorphic_resource_owner
116
+ $ rails db:migrate
117
+
118
+ The payload block then receives the record as `opts[:resource_owner]`, so the payload can branch on its class:
119
+
120
+ ```ruby
121
+ Doorkeeper::JWT.configure do
122
+ token_payload do |opts|
123
+ owner = opts[:resource_owner]
124
+
125
+ # client credentials tokens have no resource owner
126
+ next { sub: opts[:application]&.uid } unless owner
127
+
128
+ # ids are only unique per model, so include the model name in `sub`
129
+ {
130
+ sub: "#{owner.class.name.underscore}:#{owner.id}",
131
+ owner_type: owner.class.name,
132
+ email: owner.email
133
+ }
134
+ end
135
+ end
136
+ ```
137
+
138
+ Tokens issued by the client credentials flow have no resource owner, so `opts[:resource_owner]` is `nil` there.
139
+
140
+ ### Using more than one signing method
141
+
142
+ `signing_method`, `secret_key` and `secret_key_path` also accept a block. The block receives the same
143
+ options hash as `token_payload` (`resource_owner_id`, `application`, `scopes`, ...) and is evaluated
144
+ whenever the option is needed for the token being generated, so you can pick the algorithm and the key
145
+ per request — for example an RSA-signed token for clients with an `admin` scope and an HMAC-signed token
146
+ for everyone else:
147
+
148
+ ```ruby
149
+ Doorkeeper::JWT.configure do
150
+ signing_method do |opts|
151
+ opts[:scopes].exists?('admin') ? :rs512 : :hs256
152
+ end
153
+
154
+ secret_key do |opts|
155
+ opts[:scopes].exists?('admin') ? ENV['JWT_RSA_PRIVATE_KEY'] : ENV['JWT_HMAC_SECRET']
156
+ end
157
+ end
158
+ ```
159
+
160
+ Doorkeeper hands `opts[:scopes]` over as a `Doorkeeper::OAuth::Scopes`, so match a single scope with
161
+ `exists?` rather than with a substring check: `opts[:scopes].to_s.include?('admin')` would also be true for
162
+ an unrelated `superadmin` or `admin_readonly` scope, and picking a signing key that way is easy to get wrong.
163
+
164
+ The blocks are independent of each other, so make sure `secret_key` (or `secret_key_path`) returns a key that
165
+ matches the algorithm returned by `signing_method` for the same options.
166
+
167
+ Note that the key options are only resolved when they are actually used: `secret_key_path` is read for
168
+ `RS*` and `ES*` algorithms only, and both `secret_key` and `secret_key_path` are skipped entirely when
169
+ `use_application_secret` is enabled. When a path is configured for an asymmetric algorithm it takes
170
+ precedence, and the `secret_key` block is not evaluated at all.
171
+
172
+ Whoever verifies these tokens now has more than one key to choose from, so tell them which one was used:
173
+ `token_headers` receives the same options hash and is evaluated per token as well, which lets you emit a
174
+ matching `kid` header.
175
+
176
+ ```ruby
177
+ Doorkeeper::JWT.configure do
178
+ token_headers do |opts|
179
+ { kid: opts[:scopes].exists?('admin') ? 'rsa-2026' : 'hmac-2026' }
180
+ end
181
+ end
182
+ ```
183
+
184
+ See the `token_headers` entry in the configuration above for deriving the `kid` of an `RS*` key with
185
+ `JWT::JWK` instead of labelling it by hand.
186
+
106
187
  ## Development
107
188
 
108
189
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
13
13
 
14
14
  spec.summary = "JWT token generator for Doorkeeper"
15
15
  spec.description = "JWT token generator extension for Doorkeeper"
16
- spec.homepage = "https://github.com/chriswarren/doorkeeper-jwt"
16
+ spec.homepage = "https://github.com/doorkeeper-gem/doorkeeper-jwt"
17
17
  spec.license = "MIT"
18
18
 
19
19
  spec.bindir = "exe"
@@ -30,6 +30,8 @@ Gem::Specification.new do |spec|
30
30
  "funding_uri" => "https://opencollective.com/doorkeeper-gem",
31
31
  }
32
32
 
33
+ spec.required_ruby_version = ">= 3.2"
34
+
33
35
  # Loaded at require time by lib/doorkeeper/jwt/config.rb. The options DSL it
34
36
  # relies on only resolves against our own builder since Doorkeeper 5.4.
35
37
  spec.add_dependency "doorkeeper", ">= 5.4"
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "doorkeeper/config/option"
4
+ require "doorkeeper/jwt/errors"
4
5
 
5
6
  module Doorkeeper
6
7
  module JWT
7
- class MissingConfiguration < StandardError
8
+ class MissingConfiguration < Error
8
9
  def initialize
9
10
  super("Configuration for doorkeeper-jwt missing.")
10
11
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module JWT
5
+ # Base class for every error raised by doorkeeper-jwt.
6
+ class Error < StandardError; end
7
+
8
+ # Raised when a signing key is configured but `signing_method` is not, so
9
+ # the only token that could be issued would be an unsigned (alg: none) one.
10
+ class SigningMethodMissing < Error; end
11
+ end
12
+ end
@@ -10,7 +10,7 @@ module Doorkeeper
10
10
  # Semantic versioning
11
11
  MAJOR = 0
12
12
  MINOR = 4
13
- TINY = 4
13
+ TINY = 5
14
14
  PRE = nil
15
15
 
16
16
  # Full version number
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "doorkeeper/jwt/version"
4
+ require "doorkeeper/jwt/errors"
4
5
  require "doorkeeper/jwt/config"
5
6
  require "jwt"
6
7
 
@@ -8,14 +9,22 @@ module Doorkeeper
8
9
  module JWT
9
10
  class << self
10
11
  def generate(opts = {})
12
+ algorithm = signing_method(opts)
13
+
11
14
  ::JWT.encode(
12
15
  token_payload(opts),
13
- secret_key(opts),
14
- signing_method,
16
+ secret_key(opts, algorithm),
17
+ algorithm,
15
18
  token_headers(opts)
16
19
  )
17
20
  end
18
21
 
22
+ def signing_key_configured?
23
+ use_application_secret? ||
24
+ !Doorkeeper::JWT.configuration.secret_key.nil? ||
25
+ !Doorkeeper::JWT.configuration.secret_key_path.nil?
26
+ end
27
+
19
28
  private
20
29
 
21
30
  def token_payload(opts = {})
@@ -26,27 +35,61 @@ module Doorkeeper
26
35
  Doorkeeper::JWT.configuration.token_headers.call(opts)
27
36
  end
28
37
 
29
- def secret_key(opts)
30
- opts = { application: {} }.merge(opts)
31
-
38
+ def secret_key(opts, algorithm)
32
39
  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
40
 
37
- Doorkeeper::JWT.configuration.secret_key
41
+ case algorithm
42
+ when /RS\d{3}/ then OpenSSL::PKey::RSA.new(asymmetric_key(opts))
43
+ when /ES\d{3}/ then OpenSSL::PKey::EC.new(asymmetric_key(opts))
44
+ else configured_secret_key(opts)
45
+ end
46
+ end
47
+
48
+ # A configured key file wins over `secret_key`, which is only resolved
49
+ # when no path is configured for this token.
50
+ def asymmetric_key(opts)
51
+ secret_key_file(opts) || configured_secret_key(opts)
52
+ end
53
+
54
+ def configured_secret_key(opts)
55
+ resolve_option(Doorkeeper::JWT.configuration.secret_key, opts)
38
56
  end
39
57
 
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?
58
+ def secret_key_file(opts)
59
+ path = resolve_option(Doorkeeper::JWT.configuration.secret_key_path, opts)
60
+ File.read(path) if path
44
61
  end
45
62
 
46
- def signing_method
47
- return "none" unless Doorkeeper::JWT.configuration.signing_method
63
+ def signing_method(opts)
64
+ method = resolve_option(Doorkeeper::JWT.configuration.signing_method, opts)
65
+ return method.to_s.upcase unless method.nil?
66
+
67
+ raise_missing_signing_method if signing_key_configured?
48
68
 
49
- Doorkeeper::JWT.configuration.signing_method.to_s.upcase
69
+ warn_unsigned_token
70
+ "none"
71
+ end
72
+
73
+ def raise_missing_signing_method
74
+ raise(
75
+ SigningMethodMissing,
76
+ "JWT `signing_method` is not configured, but a signing key is." \
77
+ " Refusing to issue an unsigned (alg: none) token. Set" \
78
+ " `signing_method` explicitly, e.g. `signing_method :hs512`."
79
+ )
80
+ end
81
+
82
+ def warn_unsigned_token
83
+ Kernel.warn(
84
+ "[DOORKEEPER-JWT]: No `signing_method` configured; issuing UNSIGNED" \
85
+ " tokens (alg: none). This will become an error in a future release."
86
+ )
87
+ end
88
+
89
+ # Options can be configured either with a static value or with a block
90
+ # that receives the token generation options and returns the value.
91
+ def resolve_option(value, opts)
92
+ value.respond_to?(:call) ? value.call(opts) : value
50
93
  end
51
94
 
52
95
  def use_application_secret?
@@ -54,6 +97,8 @@ module Doorkeeper
54
97
  end
55
98
 
56
99
  def application_secret(opts)
100
+ opts = { application: {} }.merge(opts)
101
+
57
102
  if opts[:application].nil?
58
103
  raise(
59
104
  "JWT `use_application_secret` is enabled, but application is nil." \
@@ -82,34 +127,6 @@ module Doorkeeper
82
127
 
83
128
  secret
84
129
  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
130
  end
114
131
  end
115
132
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doorkeeper-jwt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.4
4
+ version: 0.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Warren
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2026-08-21 00:00:00.000000000 Z
12
+ date: 2026-09-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: doorkeeper
@@ -117,8 +117,9 @@ files:
117
117
  - doorkeeper-jwt.gemspec
118
118
  - lib/doorkeeper/jwt.rb
119
119
  - lib/doorkeeper/jwt/config.rb
120
+ - lib/doorkeeper/jwt/errors.rb
120
121
  - lib/doorkeeper/jwt/version.rb
121
- homepage: https://github.com/chriswarren/doorkeeper-jwt
122
+ homepage: https://github.com/doorkeeper-gem/doorkeeper-jwt
122
123
  licenses:
123
124
  - MIT
124
125
  metadata:
@@ -135,7 +136,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
135
136
  requirements:
136
137
  - - ">="
137
138
  - !ruby/object:Gem::Version
138
- version: '0'
139
+ version: '3.2'
139
140
  required_rubygems_version: !ruby/object:Gem::Requirement
140
141
  requirements:
141
142
  - - ">="