Skip to content

Commit cf166e6

Browse files
committed
Raise on blank REQUIRED ID Token claims instead of dropping them
OIDC Core 1.0 §2 lists iss/sub/aud/exp/iat as REQUIRED. IdToken#as_json rejected nil/empty values uniformly, so a blank REQUIRED claim was silently omitted, producing a non-conformant ID Token. Raise the new Errors::MissingRequiredClaim for those claims; OPTIONAL claims (nonce, auth_time, custom) are still dropped when blank.
1 parent 75d1613 commit cf166e6

4 files changed

Lines changed: 63 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- [#304] allow handle auth_time per grant
55
- [#305] Document the `auth_time_from_access_token` config option in the README (per-grant `auth_time`), clarifying that it only affects the ID Token `auth_time` claim and not `max_age` enforcement
66
- [#307] Fix `bundle exec rake server` for the test application
7+
- [#312] Raise `Errors::MissingRequiredClaim` instead of silently dropping a blank REQUIRED ID Token claim (`iss`/`sub`/`aud`/`exp`/`iat`) in `IdToken#as_json`, which previously could emit a non-conformant ID Token (OIDC Core 1.0 §2). OPTIONAL claims such as `nonce`/`auth_time` are still omitted when blank
78

89
## v1.10.1 (2026-06-03)
910

lib/doorkeeper/openid_connect/errors.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ def type
1212
# internal errors
1313
class InvalidConfiguration < OpenidConnectError; end
1414

15+
# Raised when a REQUIRED ID Token claim (OIDC Core §2: iss/sub/aud/exp/iat)
16+
# resolves to a blank value, which would otherwise be silently dropped and
17+
# produce a non-conformant ID Token.
18+
class MissingRequiredClaim < OpenidConnectError
19+
attr_reader :claim
20+
21+
def initialize(claim)
22+
@claim = claim
23+
super("Required ID Token claim `#{claim}` is missing or blank")
24+
end
25+
end
26+
1527
class MissingConfiguration < OpenidConnectError
1628
def initialize
1729
super("Configuration for Doorkeeper OpenID Connect missing. Do you have doorkeeper_openid_connect initializer?")

lib/doorkeeper/openid_connect/id_token.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ module OpenidConnect
55
class IdToken
66
include ActiveModel::Validations
77

8+
# OIDC Core 1.0 §2 — these claims are REQUIRED in every ID Token, so they
9+
# must never be silently dropped when blank.
10+
REQUIRED_CLAIMS = %i[iss sub aud exp iat].freeze
11+
812
attr_reader :nonce
913

1014
def initialize(access_token, nonce = nil, expires_in = Doorkeeper::OpenidConnect.configuration.expiration)
@@ -31,7 +35,19 @@ def claims
3135
end
3236

3337
def as_json(*_)
34-
claims.reject { |_, value| value.nil? || value == "" }
38+
claims.each_with_object({}) do |(key, value), result|
39+
blank = value.nil? || value == ""
40+
41+
if blank
42+
# A REQUIRED claim must never be silently omitted; surface the
43+
# misconfiguration instead of issuing a non-conformant ID Token.
44+
raise Errors::MissingRequiredClaim, key if REQUIRED_CLAIMS.include?(key)
45+
46+
next
47+
end
48+
49+
result[key] = value
50+
end
3551
end
3652

3753
def as_jws_token

spec/lib/id_token_spec.rb

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -191,14 +191,42 @@
191191
end
192192

193193
describe "#as_json" do
194-
it "returns claims with nil values and empty strings removed" do
195-
allow(subject).to receive_messages(issuer: nil, subject: "", audience: " ")
194+
let(:valid_claims) do
195+
{
196+
iss: "dummy",
197+
sub: user.id.to_s,
198+
aud: access_token.application.uid,
199+
exp: 180,
200+
iat: 60,
201+
nonce: "123456",
202+
}
203+
end
204+
205+
it "removes OPTIONAL claims with nil or empty values" do
206+
# nonce / auth_time and custom claims are OPTIONAL, so blanks are dropped
207+
# while the REQUIRED claims remain present.
208+
allow(subject).to receive(:claims).and_return(
209+
valid_claims.merge(nonce: nil, auth_time: "", custom: " "),
210+
)
196211

197212
json = subject.as_json
198213

199-
expect(json).not_to include :iss
200-
expect(json).not_to include :sub
201-
expect(json).to include :aud
214+
expect(json).not_to include :nonce
215+
expect(json).not_to include :auth_time
216+
expect(json).to include :iss, :sub, :aud, :exp, :iat, :custom
217+
end
218+
219+
Doorkeeper::OpenidConnect::IdToken::REQUIRED_CLAIMS.each do |claim|
220+
[nil, ""].each do |blank|
221+
it "raises MissingRequiredClaim when the REQUIRED #{claim} claim is #{blank.inspect}" do
222+
allow(subject).to receive(:claims).and_return(valid_claims.merge(claim => blank))
223+
224+
expect { subject.as_json }
225+
.to raise_error(Doorkeeper::OpenidConnect::Errors::MissingRequiredClaim) do |error|
226+
expect(error.claim).to eq(claim)
227+
end
228+
end
229+
end
202230
end
203231
end
204232

0 commit comments

Comments
 (0)