40

For years I have used Salesforce APIs from within Apex. Mostly to enhance my apps with Setup automation. So I used the Tooling and Metadata API from code snippets that used UserInfo.getSessionId() and looked like that:

private HTTPResponse createSObject(String fragment) {
    HttpRequest request = new HttpRequest();

    request.setEndpoint(URL.getOrgDomainUrl().toExternalForm() + '/services/Soap/u/53.0');
    request.setMethod('POST');
    request.setHeader('Content-Type', 'text/xml; charset=UTF-8');
    request.setHeader('SOAPAction', 'ignored');
    request.setBody('<?xml version="1.0" encoding="UTF-8"?>.... 
                     <sessionId>' + UserInfo.getSessionId() + '</sessionId>');

    return new Http().send(request);
}

Also, many other well-known, widely used OSS libraries use this approach. The best example is the Apex Metadata API from Andy Fawcett.

Now a former customer and apparently many other long-time ISV partners, failed a Security Review with similar code:

Unauthorized use of SessionID

Please use OAuth, (try JWT flows and look into admin pre-authorization). Do not touch user credentials in a managed package.

I have no clue what they want. Add a Connected App, Named Credential, and a ton of other metadata for a simple API loopback.

Please advise how to do it right.

11
  • 6
    This surfaced over on the Partner Forums a few months ago. JWT flow OAuth is available with Connected Apps. We handle our Connected App by creating it on our PBO - I recommend against packaging the Connected App since that causes every install to have unique values and requires manual post-install activities to make the necessary parameters available for your JWT flow invocation. That said, you already know that ;).
    – Phil W
    Commented Nov 3, 2022 at 17:14
  • 8
    (And, yes, I think this is a crazy policy. Salesforce Security going too far.) Half the time you wouldn't need to use such nasty loopbacks if the platform provided Apex-callable APIs in the first place. The Salesforce API landscape, as broad and significant as it is, is too siloed and too fragmented. You really should be able to invoke all the APIs through Apex proxies IMHO without any actual loopback needed.
    – Phil W
    Commented Nov 3, 2022 at 17:20
  • 1
    Agree on the Apex proxies but disagree on the policy with respect to 'external' APIs. Using session ID is completely out of date with respect to modern security best practices. Yes, SF has lots of gaps with respect to "easy to use" metric but compromising on security isn't a solution.
    – identigral
    Commented Nov 3, 2022 at 18:34
  • 7
    My problems here: 1. documentation still recommends this approach. 2. Salesforce Security can see, during the review, how the session ID is being used and could easily allow for benign usages such as this. The point is that the session ID isn't being exposed outside the org; it's just being used to loopback to APIs that SHOULD be available without resorting to REST calls.
    – Phil W
    Commented Nov 3, 2022 at 19:34
  • 3
    @identigral would you be able to describe what an ISV partner should do in detail to replace such a flaky solution with a proper one? I mean what should be packaged, and what needs to be configured after installation? Which parts can be automated, etcs. Please share your insights. Commented Nov 3, 2022 at 20:20

6 Answers 6

9

The OAuth flow we used for our AppExchange app is Web Server Flow for Web App Integration. There are various types of flows that can be used as per your requirements; we used Web Server Flow for Web App Integration.

  1. Once the subscriber installs the application in Salesforce, it lands on a custom app's screen where they are asked to authorize themselves; they click on a button that redirects the user to the login page, and the OAuth flow kicks in.
  2. After providing login details, the user sees the app permission screen. enter image description here
  3. Users can review these permissions and allow or deny them.
  4. Once they allow, Salesforce OAuth flow grants an authorization code in the callback URL
  5. Based on this authorization code, we can request an access token
  6. Now, the app's logic can use this access token to make various API calls to Salesforce services like tooling API, metadata API, etc
  7. This access token has a scope per the permissions selected on connected app settings.
6
  • 1
    Thank you for sharing Chirag. A couple of related questions: (1) Does every user of the app need to go over the oAuth flow or just an admin ? (2) Where do you keep the refresh_token and access_token, in Salesforce ?
    – Fernando
    Commented Nov 7, 2022 at 19:27
  • @Fernando 1 - Yes. 2 - Store them securely within Salesforce. Commented Nov 8, 2022 at 6:25
  • I'd prefer the JWT Bearer Flow described in another answer because it does not require implementation of an interactive OAuth authorisation flow. Commented Nov 8, 2022 at 9:57
  • @Fernando - If you're storing data that is unique to the org, for example a refresh token in this case, then you should use a Protected Custom Setting. If you're storing data that is the same across orgs such as the JWT signing key for the JWT flow then you should use a Protected Custom Metadata record. Best you use one Metadata Type for all of this stuff as Metadata Types have severe limits - 200 per org across all managed an unmanaged code. Commented Nov 8, 2022 at 10:01
  • 1
    @Chirag and Richard thank you both for your replies. This is very helpful.
    – Fernando
    Commented Nov 8, 2022 at 11:02
5

This solution passed Security Review

TL;DR

Our final decision was:

  • Connected App created on client's Partner Business Org (PBO)
  • Make it Pre-authorized by Admins + JWT Token flow
  • Custom Metadata + UI to store secret key for JWT

Intro / Problem description

Our team had to analyze potential solutions which will be:

  • secure & controlled by Admins of the Orgs,
  • possibly invisible to the end users.

The solution is a Connected App, but what exactly does it mean?

Connected App allows us to control connections from External App to our Salesforce Org resources. It uses scopes, OAuth policies, IP ranges and other parameters to limit unwanted traffic and operations. Admins can verify and give/revoke/monitor access at any time.

Following question - which OAuth Flow to use?

  • Web Server Flow

    • each user needs to authenticate via browser,
    • authorization code needs to be retrieved (callback URI),
    • refresh token needs to be stored safely per user,
  • JWT Bearer Token Flow

    • no user interaction is needed,
    • admin needs to authorize the App.

The assumption was to make it invisible to the users, therefore, we chose JWT Bearer Token, although it's designed to handle Server-to-Server integration.

But there was another question to be answered - should we use a central Connected App created on our Partner PBO or create a new Connected App for each customer using this package?

Packaged Connected App:

  • Connected App is installed with the package,

  • manuals steps - client id, client secret and certificate private key are created by customer's Admin,

  • OAuth policies can be set by the customer and adjusted in the first go,

  • new UI/instructions must be provided to store the client id, secret and private key in Custom Metadata to be used in the code.

Central Connected App:

  • Admin can install the Connected App on their org to use it and to manage the OAuth policies (self-authorization vs. pre-authorization; assigning profiles, permission sets etc. for pre-authorization),

  • client id, client secret and certificate private key are distributed in the app

  • a limited number of manual steps - required one is Admin approval

Create a Connected App for each customer:

  • customer Admin needs to create a new Connected App based on provided instructions,

  • client id, client secret and certificate private key are created per customer (with Connected App),

  • OAuth policies are set by the customer and adjusted in the first go,

  • new UI/instructions must be provided to store the client id, secret and private key in Custom Metadata to be used in the code.

We finally decided to move on with the last option as it seems more secure and we had problems with packaging the Connected App. It passed Security Review. However, after some research we prefer the Packaged Connected App, as this requires less manual steps for Admins.

4

Solutions

We have been working towards a few solutions here:

A. Named Credentials & Auth. Provider Setup per Org

Auth. Provider specifies a url, and a named credential specifies the URL of a callout endpoint and authentication parameters for a user in one definition. This will take a customer about 10 minutes to setup and one account to operate as a sort of "System Account" to perform all interactions with the REST API. Note that if this account is deactivated, then user will be blocked from using the functionality that relies upon it.

B. Oauth Consent Flow per Org

Alternative route that involves creating a connected app in each customer environment. In this connected app, we provide a callback url to the VisualForce page. In this example, the user must actively participate in an Oauth Consent flow.

Additional Comments

This has been impacting us significantly as well. We have 2 packages which have faced this issue either by UserInfo.getSessionId() or !$Api.Session_ID.

We have built up a lot of great functionality that leverages REST API, Tooling API, UI API, Metadata API, etc.

After a lot of experimentation from our team, we have found that when our 150+ customers upgrade our package, they will have to create some form of metadata involving connected apps, named credentials, auth. providers, etc.

This setup is something that will be error prone, and frankly hard to explain to customers (as evidenced by the discussion in this thread). If it's not setup correctly, mission critical functionality they have been relying upon for 6+ years will stop working with no workaround in the name of security.

Also, throwing open an Oauth flow in our application after our users already authenticated could be negatively perceived by our user base and will likely raise more security concern. "I just authenticated, as i've always done, why do I need to do this again? Is this legit or am I being hacked?"

Alternatively, with a named credential approach, we will lose some audit tracking on the named user (fixed one security problem but created a data integrity one instead that is significantly more important to our customers).

These mitigations are too disruptive in nature.

Salesforce has made great strides in REST API usage over the past few years (adjusting REST API daily limits, UI API, Composite API, Composite Graph API, GraphQL support, etc.). We have embraced these new capabilities and delivered value to our customers with it, which has allowed us to mutually grow with Salesforce. This new enforcement feels like the rug being pulled out from beneath us with, from my point of view, no equivalent alternative and an under-appreciated impact to the user experience.

In the short term, I believe this should be reconsidered as an enforcement, in the long term, we need some alternative without such technical lift.

Many fantastic new features are first accessible via REST API. It feels like a significant gap that we need to leverage Session Id in the first place to access these resources for code that is already running in the instance.

It would be great to if Salesforce can prioritize solutions of how all of these resources could be exposed for us to call from within Salesforce (In Apex or even LWC), where the authentication is taken out of our hands (similar to how it works with Salesforce Functions).

I don't want to add architectural complexities to our package so that I can use UserInfo.getSessionId, I want to not have to use it at all...

5
  • Is this an answer to the question being asked? This reads more like feedback to Salesforce than anything.
    – Derek F
    Commented Dec 14, 2022 at 22:43
  • 1
    @DerekF This comment was made in the Partner forum, and Robert asked them to put it here for external reference
    – Jeff Kranz
    Commented Dec 15, 2022 at 7:49
  • 1
    @DerekF I wanted to have that beautiful advocacy outside the partner forum. I will ask sfreco to add also how hw solved it in some cases. Commented Dec 15, 2022 at 8:02
  • This has been flagged but will leave it for now so that @streco can make it more of an answer, failing that we could add it to your question Robert. It is important that these POVs are out there because the solutions that do pass security review (and make for a valid SE answer) aren't really fit for purpose.
    – Matt Lacey
    Commented Dec 15, 2022 at 11:29
  • 1
    @DerekF, Understand the concern. I've updated this to include 2 solutions we have been pursuing. Thanks!
    – streco
    Commented Dec 15, 2022 at 13:05
2

This is part of the JWT Token Client code from my PoC.

This code uses a Named Certificate. This is great for testing in unmanaged code. I'd need to find another way of providing the certificate from Protected Custom Metadata. The certificate would be company proprietary and must be kept secure.

This idea (Package Certificate Idea) suggests that what using Managed Certificates is not possible. No certificate, no use of the Auth.JWS class. I can manually build a JWT and manually create the signature using the Crypto classes, but it's messier to do.

This code could be usable in a managed package if each Org created their own Connected App and provided the Certificate and Client ID through settings. That sounds horrific though.

    // This may be test.salesforce.com
    String tokenEndpoint = 'https://login.salesforce.com/services/oauth2/token';

    Auth.JWT claims = new Auth.JWT();
    claims.setAud(tokenAudience);
    claims.setIss(settings.OAuthClientId__c);
    claims.setSub(UserInfo.getUserName());
    claims.setAdditionalClaims(new Map<String, Object>{ 'scope' => 'api' });

    Auth.JWS token = new Auth.JWS(claims, settings.OAuthCertificateName__c);
    String tokenString = token.getCompactSerialization();

    Auth.JWTBearerTokenExchange bearer = new Auth.JWTBearerTokenExchange(tokenEndpoint, token);

    return bearer.getAccessToken();

Further to Robert's answer I have tried to create a Web Flow.

Connected App Configuration for Web Flow

I have manually invoked this

https://builder-sagittarius-3050-dev-ed.scratch.my.salesforce.com/services/oauth2/authorize?response_type=code&client_id=REDACTED&redirect_uri=https%3A%2F%2Ftest.salesforce.com%2Fapex%2FRCTest

The OAuth grant was fine, but I am (as requested) redirected to the login page and asked to log in again. I enter my username and password (which is ugly as part of an oAuth flow) and am directed to the VF Page with a code parameter.

Second time in my oAuth grant was automatic and I appeared straight at the login page. So even if the app is granted then the user has to log in every time a token is requested.

1
  • 1
    I've been experimenting with a Legacy Named Credential. No luck so far. I can get the call to happen with no error, but I get an empty body and HTTP 302 Found. I also have to place my custom domain in the named credential. I'd need a generic domain if this is to be packaged. Commented Nov 11, 2022 at 15:11
1

IMHO You need to create a connected App inside the managed package; better if you use a certificate and enable the Oauth flows. then implement the JWT bearer flow to get a new access token previous (you can use the platform cache to store it for a while) and call the metadata API

Why do you need to do this?

You have created a managed package that is doing metadata changes on the user's behalf (that is because you are using the SessionId). There is no way to track that changes were done by a 3rd party app(your package) with the user's approval. That is the purpose of creating a connected App

6
  • 1
    Why must the Connected App be created and packaged in the Managed Package. Can't you just create them in the Partner Business org and use them inside your package? I am referring to this discussion and Best practice to only package the App under certain circumstances as it is less flexible salesforce.stackexchange.com/questions/60784 Commented Nov 7, 2022 at 9:13
  • 1
    Creating the Connected App in the PBO is an old practice and a bad practice because the admin doesn't have control over the connected APP, only the ISV.
    – Torsen
    Commented Nov 7, 2022 at 14:22
  • 1
    I've prototyped this using the JWT Bearer Flow, but not yet put it into a managed package. I didn't cache the session key but got a new one every time, but then my access is infrequent. It was easy enough, but I do have concerns managing the secret key which becomes a whole new massive threat vector. Especially given I can sign the JWT as any user so switching user context! Though I have reason to suspect interesting things may happen if I tried. Commented Nov 8, 2022 at 9:55
  • 1
    The reason I investigated this is because of the Assured Session setting, which prevents asynchronous code using UserInfo.getSessionId() to call into Salesforce. The Connected App was set to never require an assured session, so was able to operate even when the user had Assured Sessions configured. Commented Nov 8, 2022 at 9:58
  • 2
    @Torsen I've looked back over my proof of concept code. I use the Auth.JWS constructor to make my token. This needs a Named Certificate. I don't believe it's possible to package named certificates. How did you work around this? Did you manually build your JWT using the Crypto APIs to sign it? Commented Nov 8, 2022 at 10:36
1

Since first looking at this I have been developing approaches that we can use to solve this problem. Two such processes have been JWT and Web Flow.

Salesforce suggested a JWT based solution using a single master key which is stored in the developer's packaging org. I have shied away from this because, no matter how good our internal security is and no matter how much we limit who can access our packaging orgs, the thought of a Single Master Key that can access all customer orgs from anywhere on the Internet and be used to act as an administrator on those Orgs is just too scary to contemplate.

My JWT solution uses a local key installed and controlled by the administrator. This takes the key out of our control, and therefore removes the non-repudiation aspect of using keys, but it gives a simple to implement means of continuing to access the auth while satisfying the requirement of using a Connected App. It brings the cost of an extra post-install admin task. So this is the solution for "We have to get this in fast with minimal code change"

The other solution is to use a Web Flow. My web flow uses a very simple server sitting somewhere external. My sample code (linked below) is an Express server, but a production system would of course look at more production ready solutions. The sample is designed to be easy to deploy and explore.

The Web Flow brings the advantage that it is far more user friendly and it offers the Salesforce admin the control they desire. The sample explores a couple of different questions, and does not really reflect our final solution.

  • Do we use Refresh Tokens to allow access at any time? The sample can but a final solution may choose not to.
  • What happens if we need to store lots of user tokens? A solution is provided for discussion. I have found that we don't need this in our situation. I believe this solution is secure, but have not taken it to Security Review.

The code samples are here:

https://github.com/financialforcedev/mdapi-oauth-demo

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .