restforce / restforce

A Ruby client for the Salesforce REST API.
MIT License
802 stars 359 forks source link

JWT Auth Flow Throwing OpenSSL::PKey::RSAErrors (Neither PUB key nor PRIV key: nested asn1 error) and (Neither PUB key nor PRIV key: not enough data) #702

Open blakelthaus opened 2 years ago

blakelthaus commented 2 years ago

Have spent the last couple of days trying to get the restforce client to work to no avail. Included my code below, if I dump out my JWT Token and make an auth request to salesforce with postman with the exact same parameters I get a successful response from the salesforce API with an access_token. Have tried multiple different API versions and variations of passing the token/other params but when calling SalesforceService.new.test from irb I always end up with the OpenSSL::PKey::RSAError (Neither PUB key nor PRIV key: nested asn1 error)

Additionally, I have tried just passing a string of the key private key file name to the jwt_key param thinking maybe restforce handles the signing but with that I receive a similar error OpenSSL::PKey::RSAError (Neither PUB key nor PRIV key: not enough data)

Any advice here would be greatly appreciated, at this point thinking I might just have to do a manual integration but would really like to use this client as it makes things much simpler!

class SalesforceService

  def initialize

    jwt = generate_jwt()

    Restforce.configure do |config|
      config.instance_url = INSTANCE_URL
      config.username = USERNAME
      config.client_id = CONSUMER_KEY
      config.jwt_key = jwt
      config.api_version = '53.0'
    end
  end

  def generate_jwt

    key = File.read 'private_key.pem'
    certificate = File.read 'certificate.pem'
    cert = OpenSSL::X509::Certificate.new(certificate)
    private_key = OpenSSL::PKey::RSA.new(key)
    cert.sign private_key, OpenSSL::Digest::SHA256.new

    payload = {
      iss: CONSUMER_KEY,
      prn: USERNAME,
      aud: AUDIENCE,
      exp: (Time.now + 2.minutes).to_i
    }

    token = JWT.encode payload, private_key, 'RS256'
    # ap token

    return token
  end

  def test
    client = Restforce.new
    # ap client
    test = client.describe("Account")
  end
end
januszm commented 2 years ago

@blakelthaus you don't need to generate jwt on your own, Restforce already provides a JWT middleware for you that generates proper jwt, see: https://github.com/restforce/restforce/blob/main/lib/restforce/middleware/authentication/jwt_bearer.rb

If I understand correctly, the first step is to create the certificate and the private key:

# valid for 10 years, 1024 or 4096 for rsa
openssl req -x509 -newkey rsa:1024 -nodes -keyout sf_key.pem -out sf_cert.pem -sha256 -days 3650

Follow the README at https://github.com/restforce/restforce#jwt-bearer-token , upload the certificate, in your code load the private key:

class SalesforceClient
  def jwt_key
    @jwt_key ||= open('sf_key.pem') { _1.read.strip }
  end

  def client
    # make sure SALESFORCE_USERNAME, SALESFORCE_CLIENT_ID, SALESFORCE_CLIENT_SECRET variables are set
    # in ENV so you can skip them below, restforce will read them
    @client ||= Restforce.new(
      jwt_key: jwt_key,
      instance_url: "FIXME"
    )
  end
end

README does not describe it in too many details though. Apparently you may need to authorize the app first by opening this URL in the browser:

https://login.salesforce.com/services/oauth2/authorize?response_type=code&client_id=<your_client_id>&redirect_uri=https://localhost:3000&scope=api+refresh_token+offline_access or https://login.salesforce.com/services/oauth2/authorize?response_type=token&client_id=<your_client_id>&redirect_uri=<your_redirect_uri>

to avoid this error:

Restforce::AuthenticationError: invalid_grant: user hasn't approved this consumer (400)

See: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&type=5

This flow uses a certificate to sign the JWT request and doesn’t require explicit user interaction. However, this flow does require prior approval of the client app.

What is absolutely critical for this to work is setting up correct OAuth scopes in Salesforce (Edit Connected App):

Selected OAuth Scopes
Manage user data via APIs (api)
Perform requests at any time (refresh_token, offline_access)
MECU commented 4 months ago

An alternative to the error

Restforce::AuthenticationError: invalid_grant: user hasn't approved this consumer (400)

Is to

BVerma-CRM commented 2 months ago

PR #886 might be having fix for the root cause of it.