json-jwt: verify throwing an error in JSON::JWT.decode

In https://github.com/onli/sinatra-browserid/blob/88ae06ebf8248a277759f9917eb65249cf6711c2/lib/sinatra/browserid.rb#L35 I’m calling decode on string which is generated in https://github.com/callahad/authbackend/blob/e14f16899fc7a3dadf84651b897eac2eccc1d74d/server.rb#L381, by JWT.encode. If not skipping verification, this is leading to an error:

NoMethodError - undefined method `verify' for #<String:0x00000003e86478>:
    /home/onli/Dropbox-decrypted/ursprung.git/vendor/bundle/ruby/2.2.0/gems/json-jwt-1.5.2/lib/json/jws.rb:94:in `valid?'
    /home/onli/Dropbox-decrypted/ursprung.git/vendor/bundle/ruby/2.2.0/gems/json-jwt-1.5.2/lib/json/jws.rb:25:in `verify!'
    /home/onli/Dropbox-decrypted/ursprung.git/vendor/bundle/ruby/2.2.0/gems/json-jwt-1.5.2/lib/json/jws.rb:162:in `decode_compact_serialized'
    /home/onli/Dropbox-decrypted/ursprung.git/vendor/bundle/ruby/2.2.0/gems/json-jwt-1.5.2/lib/json/jwt.rb:77:in `decode_compact_serialized'
    /home/onli/Dropbox-decrypted/ursprung.git/vendor/bundle/ruby/2.2.0/gems/json-jwt-1.5.2/lib/json/jose.rb:39:in `decode'

I’m assuming I have to wrap the jwt-token-string in something. But this is not covered by the docu in https://github.com/nov/json-jwt/wiki/JWS#verifying, where that function is called with a normal string. Thus I assume this is a bug in docu or implementation.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 2
  • Comments: 20 (7 by maintainers)

Most upvoted comments

Ah, JWKS (JSON::JWK::Set, an array of JWKs) is different from JWK (JSON::JWK). Try this.

jwks_json = {
  keys: [{
    kty: 'RSA',
    alg: 'RS256',
    use: 'sig',
    n: 'a',
    e: 'b'
  }]
}
jwks = JSON::JWK::Set.new jwks_json
JSON::JWT.decode params[:id_token], jwks

I assume your public_key on JSON::JWT.decode params[:id_token], public_key call is a String, and your JWT header includes alg=RS256.

In that case, you need to put a OpenSSL::PKey::RSA or JSON::JWK as the public_key.

like JSON::JWT.decode params[:id_token], OpenSSL::PKey::RSA.new(public_key)

Ran into same issue, better error message/general fix on the JSON JWT side for handling this case would be very helpful

Thanks for the answer, and sorry for the delay.

That is sadly leading to a different error message:

 /home/onli/Dropbox-decrypted/ursprung.git/vendor/bundle/ruby/2.2.0/gems/json-jwt-1.6.2/lib/json/jose.rb in with_jwk_support
        end.try(:to_key) or raise JWK::Set::KidNotFound
/home/onli/Dropbox-decrypted/ursprung.git/vendor/bundle/ruby/2.2.0/gems/json-jwt-1.6.2/lib/json/jws.rb in valid?
      public_key_or_secret = with_jwk_support public_key_or_secret
/home/onli/Dropbox-decrypted/ursprung.git/vendor/bundle/ruby/2.2.0/gems/json-jwt-1.6.2/lib/json/jws.rb in verify!
        public_key_or_secret && valid?(public_key_or_secret) or
/home/onli/Dropbox-decrypted/ursprung.git/vendor/bundle/ruby/2.2.0/gems/json-jwt-1.6.2/lib/json/jws.rb in decode_compact_serialized
        jws.verify! public_key_or_secret unless public_key_or_secret == :skip_verification
/home/onli/Dropbox-decrypted/ursprung.git/vendor/bundle/ruby/2.2.0/gems/json-jwt-1.6.2/lib/json/jwt.rb in decode_compact_serialized
          JWS.decode_compact_serialized jwt_string, key_or_secret
/home/onli/Dropbox-decrypted/ursprung.git/vendor/bundle/ruby/2.2.0/gems/json-jwt-1.6.2/lib/json/jose.rb in decode
          decode_compact_serialized input, key_or_secret
/home/onli/Dropbox-decrypted/sinatra-browserid.git/lib/sinatra/browserid.rb in block in registered
                  id_token = JSON::JWT.decode params[:id_token], public_key

I debugged this as far as I could. In with_jwk_support in jose.rb, https://github.com/nov/json-jwt/blob/master/lib/json/jose.rb#L28, this is how key looks like:

{"k"=>"{\"keys\":[{\"kty\":\"RSA\",\"alg\":\"RS256\",\"use\":\"sig\",\"kid\":\"5b6eab5fafe3a0c681c4dab6847242264889b885\",\"n\":\"rcgjgnPWAO7ANqwqkmfLSGzUp_BA_bx2uaF1QnYA8-gfRRLVi5l6RNbAddK_NzqfFOqXPGF1-dbLrtwC_3L8iS6nP71w-FcOmaho0S62puPy_zlaXtraznN8emLDvOJn55AMcoXeTeWofdQ45Rt9H44-9OAZeVcvozP2jkD8VTH5hlxcp0WBXIFbapIRGG3yd_Bnfcd60oCsz2Y3egr6tg1SmhiO1vTT1fCYMIwrYNoGmNk0mMF0HMPRGr6C998RuoX-joRnnq2eacxSr6h-zf9hkg9-hR5eJGfspSJGiH1LA_WpKhkxk4NOLFL7biRXRojUXjlg2vDwte5Ud9Ozlw\",\"e\":\"AQAB\"}]}", "kty"=>:oct, "kid"=>"6Y3ITVdZL5w5zFYr96kH4vjoY3Lcxr_SuNiVvDznmDw"
}

Please notice the kid below k, and the other kid above. The kid at the top level is coming from the jwk constructor in jwk.rb:

self[:kid] ||= thumbprint rescue nil #ignore

thumbprint is what ends up in key. I read up the code of that, it seems to be the base64 encoded sha256 hash of parts of the jwk, e kty and n. But my backend is defining the kid as such:

set :kid, Digest::SHA1.hexdigest(settings.pubkey.to_s)

As far as I know the spec, how kid defined is unspecified. But to change the backend to work with one specific jwk library seems wrong. May I suggest changing that code to work with the inner kid? Assuming I’m not on the wrong track here.

Hi @nov, thanks for your answer!

My JWT header indeed includes alg=RS256 I’m actually not sure whether the header is properly set. But public_key is not a string, it is already a JSON::JWK. params[:id_token] is a String, but this is the whole idea of the decode function, isn’t it? This is the relevant code on my side:

public_key_jwks = URI.parse(URI.escape(settings.browserid_url + '/jwks.json')).read
public_key = JSON::JWK.new(public_key_jwks)
id_token = JSON::JWT.decode params[:id_token], public_key

The jwks.json on the server is (linked above) defined like this:

json ({
  keys: [{
    kty: 'RSA',
    alg: 'RS256',
    use: 'sig',
    kid: settings.kid,
    n: Base64.urlsafe_encode64([settings.pubkey.params['n'].to_s(16)].pack('H*')).gsub(/=*$/, ''),
    e: Base64.urlsafe_encode64([settings.pubkey.params['e'].to_s(16)].pack('H*')).gsub(/=*$/, '')
  }]
})

I think it is something in the internals of JSON::JWT.decode that makes the verify try to work on a string.

I did not highlight that properly above, but the JWT.encode that encodes params[:id_token] for transport is coming from the ‘jwt’ gem, not ‘json/jwt’. That’s the call:

headers = {}
headers[:kid] = settings.kid if settings.kid
JWT.encode payload, settings.privkey, 'RS256', headers

An incompatibility?

I’m not sure I follow. Could you point out more directly what the solution would be? Instead of JSON::JWT.decode params[:id_token], public_key for decoding the encoded string, I would use a different call? Or would the server have to change something?

Thank you for putting this doc here, I was having problems in Rails with this, and couldn’t spin my head around. I would suggest to check the algorithm you are using, and should be HMAC for strings! The document you provided actually states it, and ended up fixing it for me:

`Supported key representations are

String (for shared key) OpenSSL::PKey::RSA OpenSSL::PKey::EC JSON::JWK JSON::JWK::Set skip_verification # NOTE: magic word for skipping signature verification For HMAC keys, String, JSON::JWK and JSON::JWK::Set instances are available.

For RSA/ECDSA keys, OpenSSL::PKey::RSA, OpenSSL::PKey::EC, JSON::JWK and JSON::JWK::Set > instances are available.`