CVE-2020-15240 (Ruby, published 2020-10-21T18:15Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 9.1)
Weakness: CWE-347 \u2014 Improper Verification of Cryptographic Signature
The product does not verify, or incorrectly verifies, the cryptographic signature for data.
Repository: https://github.com/auth0/omniauth-auth0
Files changed:
lib/omniauth/auth0/jwt_validator.rb
spec/omniauth/auth0/jwt_validator_spec.rb
Commit message: Verify the JWT signature


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,