diff --git a/README.md b/README.md index 9334a3ac..e5aef4f4 100644 --- a/README.md +++ b/README.md @@ -482,6 +482,14 @@ rescue JWT::InvalidIssuerError end ``` +### Required Claims + +You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing +```ruby +# Will raise a JWT::ExpiredSignature error if the 'exp' claim is absent +JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' } +``` + ### JSON Web Key (JWK) JWK is a JSON structure representing a cryptographic key. Currently only supports RSA public keys. diff --git a/lib/jwt/decode.rb b/lib/jwt/decode.rb index 3d97baad..4677056e 100644 --- a/lib/jwt/decode.rb +++ b/lib/jwt/decode.rb @@ -71,6 +71,7 @@ def find_key(&keyfinder) def verify_claims Verify.verify_claims(payload, @options) + Verify.verify_required_claims(payload, @options) end def validate_segment_count! diff --git a/lib/jwt/default_options.rb b/lib/jwt/default_options.rb index 0cf0e3de..fc02c70f 100644 --- a/lib/jwt/default_options.rb +++ b/lib/jwt/default_options.rb @@ -9,7 +9,8 @@ module DefaultOptions verify_aud: false, verify_sub: false, leeway: 0, - algorithms: ['HS256'] + algorithms: ['HS256'], + required_claims: [] }.freeze end end diff --git a/lib/jwt/error.rb b/lib/jwt/error.rb index ab573a0e..c0bf2f69 100644 --- a/lib/jwt/error.rb +++ b/lib/jwt/error.rb @@ -15,6 +15,7 @@ class InvalidAudError < DecodeError; end class InvalidSubError < DecodeError; end class InvalidJtiError < DecodeError; end class InvalidPayload < DecodeError; end + class MissingRequiredClaim < DecodeError; end class JWKError < DecodeError; end end diff --git a/lib/jwt/verify.rb b/lib/jwt/verify.rb index 7c699352..cea4f89b 100644 --- a/lib/jwt/verify.rb +++ b/lib/jwt/verify.rb @@ -10,7 +10,7 @@ class Verify }.freeze class << self - %w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub].each do |method_name| + %w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub verify_required_claims].each do |method_name| define_method method_name do |payload, options| new(payload, options).send(method_name) end @@ -81,6 +81,13 @@ def verify_sub raise(JWT::InvalidSubError, "Invalid subject. Expected #{options_sub}, received #{sub || ''}") unless sub.to_s == options_sub.to_s end + def verify_required_claims + return unless (options_required_claims = @options[:required_claims]) + options_required_claims.each do |required_claim| + raise(JWT::MissingRequiredClaim, "Missing required claim #{required_claim}") unless @payload.include?(required_claim) + end + end + private def global_leeway diff --git a/spec/integration/readme_examples_spec.rb b/spec/integration/readme_examples_spec.rb index e424a0d4..98e0a3dd 100644 --- a/spec/integration/readme_examples_spec.rb +++ b/spec/integration/readme_examples_spec.rb @@ -226,6 +226,20 @@ end.not_to raise_error end + it 'required_claims' do + payload = { data: 'test' } + + token = JWT.encode payload, hmac_secret, 'HS256' + + expect do + JWT.decode token, hmac_secret, true, required_claims: ['exp'], algorithm: 'HS256' + end.to raise_error(JWT::MissingRequiredClaim) + + expect do + JWT.decode token, hmac_secret, true, required_claims: ['data'], algorithm: 'HS256' + end.not_to raise_error + end + it 'find_key' do issuers = %w[My_Awesome_Company1 My_Awesome_Company2] iss_payload = { data: 'data', iss: issuers.first } diff --git a/spec/jwt/verify_spec.rb b/spec/jwt/verify_spec.rb index d4757758..0bcea214 100644 --- a/spec/jwt/verify_spec.rb +++ b/spec/jwt/verify_spec.rb @@ -248,4 +248,17 @@ end end end + + context '.verify_required_claims(payload, options)' do + it 'must raise JWT::MissingRequiredClaim if a required claim is absent' do + expect do + described_class.verify_required_claims(base_payload, options.merge(required_claims: ['exp'])) + end.to raise_error JWT::MissingRequiredClaim + end + + it 'must verify the claims if all required claims are present' do + payload = base_payload.merge('exp' => (Time.now.to_i + 5), 'custom_claim' => true) + described_class.verify_required_claims(payload, options.merge(required_claims: ['exp', 'custom_claim'])) + end + end end