1

I am following this document as I try to implement the Salesforce OAuth 2.0 JWT Bearer Token Flow. There are a number of steps involved and I have tried them over and over again but I keep getting {"error"=>"invalid_grant", "error_description"=>"invalid assertion"}.

I hope someone can point out to me what I may have missed. Here is my code:

require "httparty"
class Salesforce
  def initialize
    @cert_file = File.join(File.dirname(__FILE__), *%w[private_key.pem])
    @base_url = "https://xxxxx--yyyy.zzzz.my.salesforce.com"
    @auth_endpoint = "/services/oauth2/authorize"
    @token_request_endpoint = "/services/oauth2/token"
    @token_revoke_endpoint = "/services/oauth2/revoke"
    @username = "user@mail.com"
    @client_id = "client_id"
    @private_key = OpenSSL::PKey::RSA.new(File.read(@cert_file))
  end

  # Construct a JWT header with this format: {"alg":"RS256"}.
  # Base64url encode the JWT header

  def encoded_jwt_header
    Base64.strict_encode64({"alg":"RS256"}.to_s)
  end

  # Construct a JSON Claims Set for the JWT with iss, sub, aud, and exp.

  def claim_set
    {
      iss: @client_id, 
      sub: @username, 
      aud: @base_url,
      exp: (Time.now + 3.minutes).to_i.to_s
    }
  end

  # Base64url encode the JWT Claims Set without any line breaks.

  def encoded_claim_set
    Base64.strict_encode64(claim_set.to_s)
  end

  # Create a string for the encoded JWT Header and the encoded JWT Claims Set

  def claim_set_header_combo
    "#{encoded_jwt_header}.#{encoded_claim_set}"
  end

  # Sign the resulting string using SHA256 with RSA

  def signed_claim_set_header_combo
    # From the docs, it is not very clear if I should sign with the private key or not
    # Digest::SHA256.base64digest(claim_set_header_combo) 
    JWT.encode(claim_set_header_combo, @private_key, 'RS256')
  end

  def jwt_bearer_token
    "#{claim_set_header_combo}.#{Base64.strict_encode64(signed_claim_set_header_combo)}"
  end

  def request_auth
    HTTParty.post("#{@base_url}#{@token_request_endpoint}", body: {grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: jwt_bearer_token}, debug_output: $stdout)
  end
end

EDIT

I have revised the code to make use of the ruby-jwt gem:

require "httparty"
class Salesforce
  def initialize
    @cert_file = File.join(File.dirname(__FILE__), *%w[private_key.pem])
    @base_url = "https://test.salesforce.com"
    @auth_endpoint = "/services/oauth2/authorize"
    @token_request_endpoint = "/services/oauth2/token"
    @token_revoke_endpoint = "/services/oauth2/revoke"
    @username = "hsisis-xzc5@force.com"
    @client_id = "client_id" 
    @private_key = OpenSSL::PKey::RSA.new(File.read(@cert_file))
  end

  def claim_set
    {
      iss: @client_id, 
      sub: @username, 
      aud: @base_url,
      exp: (Time.now + 3.minutes).to_i.to_s
    }
  end

  def jwt_bearer_token
    JWT.encode(claim_set.to_s, @private_key, 'RS256')
  end

  def request_auth
    HTTParty.post("#{@base_url}#{@token_request_endpoint}", body: {grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: jwt_bearer_token}, debug_output: $stdout)
  end
end

Am not familiar with Salesforce and am developing this for a client so am not sure if I maybe missing something. Any ideas that will point me in the right direction will be appreciated.

Thanks.

0
  1. Don't reinvent the wheel - there are plenty of Ruby gems (libraries) available for construction of JWT, ruby-jwt being the most popular.

  2. The aud(ience) claim AND the endpoint (base_url in your code) need to be https://login.salesforce.com (Production), https://test.salesforce.com (Sandbox) or a community URL. This is covered in the docs.

To test on your own, you can sign up for your own free Developer Edition org, create a connected app (see docs above + tutorial) and go from there.

  • Thanks for the suggestions. I have created a dev account, created an app and made the changes you mentioned. Am still getting invalid assertion. My certs are .pem files? Does that matter? – Muaad Aug 25 at 19:23
  • .pem is fine. If the cert format was an issue, it'd complain. Post your revised code. – identigral Aug 25 at 21:43
  • I have updated my question with the revised code. Please take a look. – Muaad Aug 26 at 10:02
0

I've only a passing familiarity with Ruby, but...

def signed_claim_set_header_combo
  # From the docs, it is not very clear if I should sign with the private key or not
  # Digest::SHA256.base64digest(claim_set_header_combo) 
  JWT.encode(claim_set_header_combo, @private_key, 'RS256')
end

Yep, you need to sign the JWT with your private key. You also need to make sure that the certificate itself is being used by the Connected App that you set up on the Salesforce end of things. I forget if you do that from editing the Connected App itself, or through managing the policies for the Connected App, but dig around for a bit and you should find a Use Digital Signature checkbox. That needs to be checked, and that'll allow you to upload the .crt file for your cert.

The other bit that I see is wrong is your Base64 encoding. Since the signed JWT is sent in the url query string for your auth request, it needs to be url safe. There is a variant of Base64 called Base64URL, which replaces 2 url-unsafe characters with safe replacements (+ and / are replaced with - and _, respectively).

Use Base64.urlsafe_encode64 instead of Base64.strict_encode64

If you're still running into issues after that, then perhaps try changing the base url that you're sending the auth request to to "https://login.salesforce.com" (for production) or "https://test.salesforce.com" (for a sandbox).

  • Thanks for chiming in on this. I was given the base URL in this format: https://xxxxx--yyyy.zzzz.my.salesforce.com. Can this work? I saw "login.salesforce.com" and "test.salesforce.com" in the docs too but wasn't sure. – Muaad Aug 25 at 13:07
  • Also, my certs are .pem files. Does that make a difference? – Muaad Aug 25 at 13:12
  • @Muaad Re Q1: Perhaps, but I'm not certain. The docs mention being able to use a community url, but the one you were given is a "myDomain" url. The safe approach would be to use the login or test endpoint. When you get a successful response, it'll contain an instance_url (which might match your given x-y-z.my.salesforce.com url) that you can rely on for making subesquent requests to Salesforce. Re Q2: Yes, I believe the Connected App on the Salesforce side only accepts .crt files – Derek F Aug 25 at 13:33
  • OK. This is still failing. I don't have access to the connected app so I will have to ask them these questions tomorrow. I will update once I get more info. Thanks for the help. – Muaad Aug 25 at 13:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.