CVE-2020-15240: Improper Verification of Cryptographic Signature (CWE-347)
value: omniauth-auth0 (rubygems) versions >= 2.3.0 and < 2.4.1 improperly validate the JWT token signature when using the `jwt_validator.verify` method. Improper validation of the JWT token signature can allow an attacker to bypass authentication and authorization. You are affected by this vulnerability if all of the following conditions apply: 1. You are using `omniauth-auth0`. 2. You are using `JWTValidator.verify` method directly OR you are not authenticating using the SDK’s default Authorization Code Flow. The issue is patched in version 2.4.1.
CWE description: The product does not verify, or incorrectly verifies, the cryptographic signature for data.
CVSS v3 base score: 9.1
Published: 2020-10-21T18:15Z
Repository: https://github.com/auth0/omniauth-auth0
Language: Ruby
Files changed: lib/omniauth/auth0/jwt_validator.rb, spec/omniauth/auth0/jwt_validator_spec.rb
Commit date: 2020-10-08 15:12:47 -0500
Commit message: Verify the JWT signature

Diff: diff --git a/lib/omniauth/auth0/jwt_validator.rb b/lib/omniauth/auth0/jwt_validator.rb
index 6fc1948..134ff43 100644
--- a/lib/omniauth/auth0/jwt_validator.rb
+++ b/lib/omniauth/auth0/jwt_validator.rb
@@ -26,21 +26,28 @@ module OmniAuth
 
         @client_id = options.client_id
         @client_secret = options.client_secret
       end
 
+      # Verify a token's signature. Only tokens signed with the RS256 or HS256 signatures are supported.
+      # @return array - The token's key and signing algorithm
       def verify_signature(jwt)
         head = token_head(jwt)
 
         # Make sure the algorithm is supported and get the decode key.
         if head[:alg] == 'RS256'
-          [rs256_decode_key(head[:kid]), head[:alg]]
+          key, alg = [rs256_decode_key(head[:kid]), head[:alg]]
         elsif head[:alg] == 'HS256'
-          [@client_secret, head[:alg]]
+          key, alg = [@client_secret, head[:alg]]
         else
           raise OmniAuth::Auth0::TokenValidationError.new("Signature algorithm of #{head[:alg]} is not supported. Expected the ID token to be signed with RS256 or HS256")
         end
+
+        # Call decode to verify the signature
+        JWT.decode(jwt, key, true, decode_opts(alg))
+
+        return key, alg
       end
 
       # Verify a JWT.
       # @param jwt string - JWT to verify.
       # @param authorize_params hash - Authorization params to verify on the JWT
@@ -91,15 +98,31 @@ module OmniAuth
         matching_jwk = jwks[:keys].find { |jwk| jwk[:kid] == kid }
         matching_jwk[key] if matching_jwk
       end
 
       private
+      # Get the JWT decode options. We disable the claim checks since we perform our claim validation logic
+      # Docs: https://github.com/jwt/ruby-jwt
+      # @return hash
+      def decode_opts(alg)
+        {
+          algorithm: alg,
+          verify_expiration: false,
+          verify_iat: false,
+          verify_iss: false,
+          verify_aud: false,
+          verify_jti: false,
+          verify_subj: false,
+          verify_not_before: false
+        }
+      end
+
       def rs256_decode_key(kid)
         jwks_x5c = jwks_key(:x5c, kid)
 
         if jwks_x5c.nil?
-          raise OmniAuth::Auth0::TokenValidationError.new("Could not find a public key for Key ID (kid) '#{kid}''")
+          raise OmniAuth::Auth0::TokenValidationError.new("Could not find a public key for Key ID (kid) '#{kid}'")
         end
 
         jwks_public_cert(jwks_x5c.first)
       end
 
diff --git a/spec/omniauth/auth0/jwt_validator_spec.rb b/spec/omniauth/auth0/jwt_validator_spec.rb
index 21d65a4..3d9280a 100644
--- a/spec/omniauth/auth0/jwt_validator_spec.rb
+++ b/spec/omniauth/auth0/jwt_validator_spec.rb
@@ -10,21 +10,21 @@ describe OmniAuth::Auth0::JWTValidator do
   let(:client_id) { 'CLIENT_ID' }
   let(:client_secret) { 'CLIENT_SECRET' }
   let(:domain) { 'samples.auth0.com' }
   let(:future_timecode) { 32_503_680_000 }
   let(:past_timecode) { 303_912_000 }
-  let(:jwks_kid) { 'NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg' }
+  let(:valid_jwks_kid) { 'NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg' }
 
   let(:rsa_private_key) do
     OpenSSL::PKey::RSA.generate 2048
   end
 
-  let(:rsa_token_jwks) do
+  let(:valid_jwks) do
     {
       keys: [
         {
-          kid: jwks_kid,
+          kid: valid_jwks_kid,
           x5c: [Base64.encode64(make_cert(rsa_private_key).to_der)]
         }
       ]
     }.to_json
   end
@@ -89,33 +89,33 @@ describe OmniAuth::Auth0::JWTValidator do
         jwt_validator.jwks_public_cert('QXV0aDA=')
       end.to raise_error(OpenSSL::X509::CertificateError)
     end
   end
 
-  describe 'JWT verifier jwks_key' do
+  describe 'JWT verifier jwks key parsing' do
     let(:jwt_validator) do
       make_jwt_validator
     end
 
     before do
-      stub_jwks
+      stub_complete_jwks
     end
 
     it 'should return a key' do
-      expect(jwt_validator.jwks_key(:alg, jwks_kid)).to eq('RS256')
+      expect(jwt_validator.jwks_key(:alg, valid_jwks_kid)).to eq('RS256')
     end
 
     it 'should return an x5c key' do
-      expect(jwt_validator.jwks_key(:x5c, jwks_kid).length).to eq(1)
+      expect(jwt_validator.jwks_key(:x5c, valid_jwks_kid).length).to eq(1)
     end
 
     it 'should return nil if there is not key' do
-      expect(jwt_validator.jwks_key(:auth0, jwks_kid)).to eq(nil)
+      expect(jwt_validator.jwks_key(:auth0, valid_jwks_kid)).to eq(nil)
     end
 
     it 'should return nil if the key ID is invalid' do
-      expect(jwt_validator.jwks_key(:alg, "#{jwks_kid}_invalid")).to eq(nil)
+      expect(jwt_validator.jwks_key(:alg, "#{valid_jwks_kid}_invalid")).to eq(nil)
     end
   end
 
   describe 'JWT verifier custom issuer' do
     context 'same as domain' do
@@ -151,12 +151,28 @@ describe OmniAuth::Auth0::JWTValidator do
     let(:jwt_validator) do
       make_jwt_validator
     end
 
     before do
-      stub_jwks
-      stub_dummy_jwks
+      stub_complete_jwks
+      stub_expected_jwks
+    end
+
+    it 'should fail when JWT is nil' do
+      expect do
+        jwt_validator.verify(nil)
+      end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
+        message: "ID token is required but missing"
+      }))
+    end
+
+    it 'should fail when JWT is not well-formed' do
+      expect do
+        jwt_validator.verify('abc.123')
+      end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
+        message: "ID token could not be decoded"
+      }))
     end
 
     it 'should fail with missing issuer' do
       expect do
         jwt_validator.verify(make_hs256_token)
@@ -246,10 +262,43 @@ describe OmniAuth::Auth0::JWTValidator do
       end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
         message: "Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(past_timecode + 60)})"
       }))
     end
 
+    it 'should pass when past expiration but within default leeway' do
+      exp = Time.now.to_i - 59
+      payload = {
+        iss: "https://#{domain}/",
+        sub: 'sub',
+        aud: client_id,
+        exp: exp,
+        iat: past_timecode
+      }
+
+      token = make_hs256_token(payload)
+      id_token = jwt_validator.verify(token)
+      expect(id_token['exp']).to eq(exp)
+    end
+
+    it 'should fail when past expiration and outside default leeway' do
+      exp = Time.now.to_i - 61
+      payload = {
+        iss: "https://#{domain}/",
+        sub: 'sub',
+        aud: client_id,
+        exp: exp,
+        iat: past_timecode
+      }
+
+      token = make_hs256_token(payload)
+      expect do
+        jwt_validator.verify(token)
+      end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
+        message: "Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(exp + 60)})"
+      }))
+    end
+
     it 'should fail when missing iat' do
       payload = {
         iss: "https://#{domain}/",
         sub: 'sub',
         aud: client_id,
@@ -375,10 +424,118 @@ describe OmniAuth::Auth0::JWTValidator do
       end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
         message: "Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time (#{Time.now}) is after last auth time (#{Time.at(past_timecode + 60 + 60)})"
       }))
     end
 
+    it 'should fail when “max_age” sent on the authentication request and this claim added the “max_age” value doesn’t represent a date in the future, outside the default leeway' do
+      now = Time.now.to_i
+      auth_time = now - 121
+      max_age = 60
+      payload = {
+        iss: "https://#{domain}/",
+        sub: 'sub',
+        aud: client_id,
+        exp: future_timecode,
+        iat: past_timecode,
+        auth_time: auth_time
+      }
+
+      token = make_hs256_token(payload)
+      expect do
+        jwt_validator.verify(token, { max_age: max_age })
+        # Time.at(auth_time + max_age + leeway
+      end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
+        message: "Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time (#{Time.now}) is after last auth time (#{Time.at(auth_time + max_age + 60)})"
+      }))
+    end
+
+    it 'should verify when “max_age” sent on the authentication request and this claim added the “max_age” value doesn’t represent a date in the future, outside the default leeway' do
+      now = Time.now.to_i
+      auth_time = now - 119
+      max_age = 60
+      payload = {
+        iss: "https://#{domain}/",
+        sub: 'sub',
+        aud: client_id,
+        exp: future_timecode,
+        iat: past_timecode,
+        auth_time: auth_time
+      }
+
+      token = make_hs256_token(payload)
+      id_token = jwt_validator.verify(token, { max_age: max_age })
+      expect(id_token['auth_time']).to eq(auth_time)
+    end
+
+    it 'should fail for RS256 token when kid is incorrect' do
+      domain = 'example.org'
+      sub = 'abc123'
+      payload = {
+        sub: sub,
+        exp: future_timecode,
+        iss: "https://#{domain}/",
+        iat: past_timecode,
+        aud: client_id
+      }
+      invalid_kid = 'invalid-kid'
+      token = make_rs256_token(payload, invalid_kid)
+      expect do
+        verified_token = make_jwt_validator(opt_domain: domain).verify(token)
+      end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
+        message: "Could not find a public key for Key ID (kid) 'invalid-kid'"
+      }))
+    end
+
+    it 'should fail when RS256 token has invalid signature' do
+      domain = 'example.org'
+      sub = 'abc123'
+      payload = {
+        sub: sub,
+        exp: future_timecode,
+        iss: "https://#{domain}/",
+        iat: past_timecode,
+        aud: client_id
+      }
+      token = make_rs256_token(payload) + 'bad'
+      expect do
+        verified_token = make_jwt_validator(opt_domain: domain).verify(token)
+      end.to raise_error(an_instance_of(JWT::VerificationError).and having_attributes({
+        message: "Signature verification raised"
+      }))
+    end
+
+    it 'should fail when algorithm is not RS256 or HS256' do
+      payload = {
+        iss: "https://#{domain}/",
+        sub: 'abc123',
+        aud: client_id,
+        exp: future_timecode,
+        iat: past_timecode
+      }
+      token = JWT.encode payload, 'secret', 'HS384'
+      expect do
+        jwt_validator.verify(token)
+      end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
+        message: "Signature algorithm of HS384 is not supported. Expected the ID token to be signed with RS256 or HS256"
+      }))
+    end
+
+    it 'should fail when HS256 token has invalid signature' do
+      payload = {
+        iss: "https://#{domain}/",
+        sub: 'abc123',
+        aud: client_id,
+        exp: future_timecode,
+        iat: past_timecode
+      }
+      token = make_hs256_token(payload, 'bad_secret')
+      expect do
+        # validator is configured to use "CLIENT_SECRET" by default
+        jwt_validator.verify(token)
+      end.to raise_error(an_instance_of(JWT::VerificationError))
+    end
+
     it 'should verify a valid HS256 token with multiple audiences' do
       audience = [
         client_id,
         "https://#{domain}/userinfo"
       ]
@@ -415,17 +572,48 @@ describe OmniAuth::Auth0::JWTValidator do
       payload = {
         sub: sub,
         exp: future_timecode,
         iss: "https://#{domain}/",
         iat: past_timecode,
-        aud: client_id,
-        kid: jwks_kid
+        aud: client_id
       }
       token = make_rs256_token(payload)
       verified_token = make_jwt_validator(opt_domain: domain).verify(token)
       expect(verified_token['sub']).to eq(sub)
     end
+
+    it 'should verify a HS256 JWT signature when calling verify signature directly' do
+      sub = 'abc123'
+        payload = {
+          iss: "https://#{domain}/",
+          sub: sub,
+          aud: client_id,
+          exp: future_timecode,
+          iat: past_timecode
+        }
+        token = make_hs256_token(payload)
+        verified_token_signature = jwt_validator.verify_signature(token)
+        expect(verified_token_signature[0]).to eq('CLIENT_SECRET')
+        expect(verified_token_signature[1]).to eq('HS256')
+    end
+
+    it 'should verify a RS256 JWT signature verify signature directly' do
+      domain = 'example.org'
+      sub = 'abc123'
+      payload = {
+        sub: sub,
+        exp: future_timecode,
+        iss: "https://#{domain}/",
+        iat: past_timecode,
+        aud: client_id
+      }
+      token = make_rs256_token(payload)
+      verified_token_signature = make_jwt_validator(opt_domain: domain).verify_signature(token)
+      expect(verified_token_signature.length).to be(2)
+      expect(verified_token_signature[0]).to be_a(OpenSSL::PKey::RSA)
+      expect(verified_token_signature[1]).to eq('RS256')
+    end
   end
 
   private
 
   def make_jwt_validator(opt_domain: domain, opt_issuer: nil)
@@ -437,18 +625,20 @@ describe OmniAuth::Auth0::JWTValidator do
     opts[:issuer] = opt_issuer unless opt_issuer.nil?
 
     OmniAuth::Auth0::JWTValidator.new(opts)
   end
 
-  def make_hs256_token(payload = nil)
+  def make_hs256_token(payload = nil, secret = nil)
     payload = { sub: 'abc123' } if payload.nil?
-    JWT.encode payload, client_secret, 'HS256'
+    secret = client_secret if secret.nil?
+    JWT.encode payload, secret, 'HS256'
   end
 
-  def make_rs256_token(payload = nil)
+  def make_rs256_token(payload = nil, kid = nil)
     payload = { sub: 'abc123' } if payload.nil?
-    JWT.encode payload, rsa_private_key, 'RS256', kid: jwks_kid
+    kid = valid_jwks_kid if kid.nil?
+    JWT.encode payload, rsa_private_key, 'RS256', kid: kid
   end
 
   def make_cert(private_key)
     cert = OpenSSL::X509::Certificate.new
     cert.issuer = OpenSSL::X509::Name.parse('/C=BE/O=Auth0/OU=Auth0/CN=Auth0')
@@ -472,30 +662,23 @@ describe OmniAuth::Auth0::JWTValidator do
     )
 
     cert.sign private_key, OpenSSL::Digest::SHA1.new
   end
 
-  def stub_jwks
+  def stub_complete_jwks
     stub_request(:get, 'https://samples.auth0.com/.well-known/jwks.json')
       .to_return(
         headers: { 'Content-Type' => 'application/json' },
         body: jwks.to_json,
         status: 200
       )
   end
 
-  def stub_bad_jwks
-    stub_request(:get, 'https://samples.auth0.com/.well-known/jwks-bad.json')
-      .to_return(
-        status: 404
-      )
-  end
-
-  def stub_dummy_jwks
+  def stub_expected_jwks
     stub_request(:get, 'https://example.org/.well-known/jwks.json')
       .to_return(
         headers: { 'Content-Type' => 'application/json' },
-        body: rsa_token_jwks,
+        body: valid_jwks,
         status: 200
       )
   end
 end

Vulnerable code:           [rs256_decode_key(head[:kid]), head[:alg]]
          [@client_secret, head[:alg]]
          raise OmniAuth::Auth0::TokenValidationError.new("Could not find a public key for Key ID (kid) '#{kid}''")
  let(:jwks_kid) { 'NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg' }
  let(:rsa_token_jwks) do
          kid: jwks_kid,
  describe 'JWT verifier jwks_key' do
      stub_jwks
      expect(jwt_validator.jwks_key(:alg, jwks_kid)).to eq('RS256')
      expect(jwt_validator.jwks_key(:x5c, jwks_kid).length).to eq(1)
      expect(jwt_validator.jwks_key(:auth0, jwks_kid)).to eq(nil)
      expect(jwt_validator.jwks_key(:alg, "#{jwks_kid}_invalid")).to eq(nil)
      stub_jwks
      stub_dummy_jwks
        aud: client_id,
        kid: jwks_kid
  def make_hs256_token(payload = nil)
    JWT.encode payload, client_secret, 'HS256'
  def make_rs256_token(payload = nil)
    JWT.encode payload, rsa_private_key, 'RS256', kid: jwks_kid
  def stub_jwks
  def stub_bad_jwks
    stub_request(:get, 'https://samples.auth0.com/.well-known/jwks-bad.json')
      .to_return(
        status: 404
      )
  end

  def stub_dummy_jwks
        body: rsa_token_jwks,
Fixed code:       # Verify a token's signature. Only tokens signed with the RS256 or HS256 signatures are supported.
      # @return array - The token's key and signing algorithm
          key, alg = [rs256_decode_key(head[:kid]), head[:alg]]
          key, alg = [@client_secret, head[:alg]]

        # Call decode to verify the signature
        JWT.decode(jwt, key, true, decode_opts(alg))

        return key, alg
      # Get the JWT decode options. We disable the claim checks since we perform our claim validation logic
      # Docs: https://github.com/jwt/ruby-jwt
      # @return hash
      def decode_opts(alg)
        {
          algorithm: alg,
          verify_expiration: false,
          verify_iat: false,
          verify_iss: false,
          verify_aud: false,
          verify_jti: false,
          verify_subj: false,
          verify_not_before: false
        }
      end

          raise OmniAuth::Auth0::TokenValidationError.new("Could not find a public key for Key ID (kid) '#{kid}'")
  let(:valid_jwks_kid) { 'NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg' }
  let(:valid_jwks) do
          kid: valid_jwks_kid,
  describe 'JWT verifier jwks key parsing' do
      stub_complete_jwks
      expect(jwt_validator.jwks_key(:alg, valid_jwks_kid)).to eq('RS256')
      expect(jwt_validator.jwks_key(:x5c, valid_jwks_kid).length).to eq(1)
      expect(jwt_validator.jwks_key(:auth0, valid_jwks_kid)).to eq(nil)
      expect(jwt_validator.jwks_key(:alg, "#{valid_jwks_kid}_invalid")).to eq(nil)
      stub_complete_jwks
      stub_expected_jwks
    end

    it 'should fail when JWT is nil' do
      expect do
        jwt_validator.verify(nil)
      end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
        message: "ID token is required but missing"
      }))
    end

    it 'should fail when JWT is not well-formed' do
      expect do
        jwt_validator.verify('abc.123')
      end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
        message: "ID token could not be decoded"
      }))
    it 'should pass when past expiration but within default leeway' do
      exp = Time.now.to_i - 59
      payload = {
        iss: "https://#{domain}/",
        sub: 'sub',
        aud: client_id,
        exp: exp,
        iat: past_timecode
      }

      token = make_hs256_token(payload)
      id_token = jwt_validator.verify(token)
      expect(id_token['exp']).to eq(exp)
    end

    it 'should fail when past expiration and outside default leeway' do
      exp = Time.now.to_i - 61
      payload = {
        iss: "https://#{domain}/",
        sub: 'sub',
        aud: client_id,
        exp: exp,
        iat: past_timecode
      }

      token = make_hs256_token(payload)
      expect do
        jwt_validator.verify(token)
      end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
        message: "Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(exp + 60)})"
      }))
    end

    it 'should fail when “max_age” sent on the authentication request and this claim added the “max_age” value doesn’t represent a date in the future, outside the default leeway' do
      now = Time.now.to_i
      auth_time = now - 121
      max_age = 60
      payload = {
        iss: "https://#{domain}/",
        sub: 'sub',
        aud: client_id,
        exp: future_timecode,
        iat: past_timecode,
        auth_time: auth_time
      }

      token = make_hs256_token(payload)
      expect do
        jwt_validator.verify(token, { max_age: max_age })
        # Time.at(auth_time + max_age + leeway
      end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
        message: "Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time (#{Time.now}) is after last auth time (#{Time.at(auth_time + max_age + 60)})"
      }))
    end

    it 'should verify when “max_age” sent on the authentication request and this claim added the “max_age” value doesn’t represent a date in the future, outside the default leeway' do
      now = Time.now.to_i
      auth_time = now - 119
      max_age = 60
      payload = {
        iss: "https://#{domain}/",
        sub: 'sub',
        aud: client_id,
        exp: future_timecode,
        iat: past_timecode,
        auth_time: auth_time
      }

      token = make_hs256_token(payload)
      id_token = jwt_validator.verify(token, { max_age: max_age })
      expect(id_token['auth_time']).to eq(auth_time)
    end

    it 'should fail for RS256 token when kid is incorrect' do
      domain = 'example.org'
      sub = 'abc123'
      payload = {
        sub: sub,
        exp: future_timecode,
        iss: "https://#{domain}/",
        iat: past_timecode,
        aud: client_id
      }
      invalid_kid = 'invalid-kid'
      token = make_rs256_token(payload, invalid_kid)
      expect do
        verified_token = make_jwt_validator(opt_domain: domain).verify(token)
      end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
        message: "Could not find a public key for Key ID (kid) 'invalid-kid'"
      }))
    end

    it 'should fail when RS256 token has invalid signature' do
      domain = 'example.org'
      sub = 'abc123'
      payload = {
        sub: sub,
        exp: future_timecode,
        iss: "https://#{domain}/",
        iat: past_timecode,
        aud: client_id
      }
      token = make_rs256_token(payload) + 'bad'
      expect do
        verified_token = make_jwt_validator(opt_domain: domain).verify(token)
      end.to raise_error(an_instance_of(JWT::VerificationError).and having_attributes({
        message: "Signature verification raised"
      }))
    end

    it 'should fail when algorithm is not RS256 or HS256' do
      payload = {
        iss: "https://#{domain}/",
        sub: 'abc123',
        aud: client_id,
        exp: future_timecode,
        iat: past_timecode
      }
      token = JWT.encode payload, 'secret', 'HS384'
      expect do
        jwt_validator.verify(token)
      end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({
        message: "Signature algorithm of HS384 is not supported. Expected the ID token to be signed with RS256 or HS256"
      }))
    end

    it 'should fail when HS256 token has invalid signature' do
      payload = {
        iss: "https://#{domain}/",
        sub: 'abc123',
        aud: client_id,
        exp: future_timecode,
        iat: past_timecode
      }
      token = make_hs256_token(payload, 'bad_secret')
      expect do
        # validator is configured to use "CLIENT_SECRET" by default
        jwt_validator.verify(token)
      end.to raise_error(an_instance_of(JWT::VerificationError))
    end

        aud: client_id

    it 'should verify a HS256 JWT signature when calling verify signature directly' do
      sub = 'abc123'
        payload = {
          iss: "https://#{domain}/",
          sub: sub,
          aud: client_id,
          exp: future_timecode,
          iat: past_timecode
        }
        token = make_hs256_token(payload)
        verified_token_signature = jwt_validator.verify_signature(token)
        expect(verified_token_signature[0]).to eq('CLIENT_SECRET')
        expect(verified_token_signature[1]).to eq('HS256')
    end

    it 'should verify a RS256 JWT signature verify signature directly' do
      domain = 'example.org'
      sub = 'abc123'
      payload = {
        sub: sub,
        exp: future_timecode,
        iss: "https://#{domain}/",
        iat: past_timecode,
        aud: client_id
      }
      token = make_rs256_token(payload)
      verified_token_signature = make_jwt_validator(opt_domain: domain).verify_signature(token)
      expect(verified_token_signature.length).to be(2)
      expect(verified_token_signature[0]).to be_a(OpenSSL::PKey::RSA)
      expect(verified_token_signature[1]).to eq('RS256')
    end
  def make_hs256_token(payload = nil, secret = nil)
    secret = client_secret if secret.nil?
    JWT.encode payload, secret, 'HS256'
  def make_rs256_token(payload = nil, kid = nil)
    kid = valid_jwks_kid if kid.nil?
    JWT.encode payload, rsa_private_key, 'RS256', kid: kid
  def stub_complete_jwks
  def stub_expected_jwks
        body: valid_jwks,