[SalesForce] How to further limit OAuth API scope access

We have a connected app configured and working.

We have a web server oauth flow setup and have configured "api" and "refresh token / offline access" as the oauth scope.

We also created a separate user and profile and limited down the access of that profile to just those objects that we wanted to expose.

However, we noticed that granting the "api" oauth scope grants access to a bit too much then we would like. Things like chatter, content, custom settings etc are also visible. Potentially also other parts of the REST api that we rather would not like to expose.

Is there a way to further limit which parts of the api our connected app can use?

Or (maybe more suitable), is there a way to create (for example) a custom apexrest service and only expose that one and not the full REST api?

Best Answer

This might not be what you're looking for, but I felt like I should float the idea out there anyways.

One good restrictive OAuth Scope is: Provide access to custom applications (visualforce). With this scope, all regular api calls will fail, and you can restrict access to just the few visualforce pages that you want that user/profile to have access to. Then you just have to turn your Visualforce into an api. For queries it is pretty straightforward:

Controller

public with sharing class VisualforceQueryService{

    public String QueryResult {get;set;}

    public VisualforceQueryService(){
        String QueryString = ApexPages.currentPage().getParameters().get('q');
        try {
            list<sObject> solist = Database.query(QueryString);
            QueryResult = JSON.serialize(solist);
        } catch (Exception e) {
            QueryResult = '{"error":"Line ' + e.getLineNumber() + ', ' + String.valueOf(e) + '"}';
        }
    }

}

Visualforce page

<apex:page controller="VisualforceQueryService" 
    docType="" 
    contentType="application/json" 
    applyHtmlTag="false" 
    applyBodyTag="false" 
    showHeader="false" 
    sidebar="false" 
    standardStyleSheets="false">
{!QueryResult}
</apex:page>

A query could be made through this page as so:

curl -H 'Authorization: Bearer <session-id>' https://analysis-dev-ed--analysis.ap1.visual.force.com/apex/VisualforceQueryService?q=SELECT+Id,Name,CreatedDate,OwnerId+FROM+Account

For other actions though (insert, update, delete, undelete), things are much harder. You can't do DML operations, future calls, queueable classes in a visualforce page constructor, and because the api is blocked for this user profile, you can't reach out to the api through an HttpRequest. There should be a way to use javascript remoting endpoints to perform those actions, but you would at least need to request the page to scrape up the csrf tokens before you can perform the remote action.


Okay, so for the sake of completeness, I got the DML operations to start working too.

Visualforce page

<apex:page controller="VisualforceDMLService" 
    showHeader="false" 
    sidebar="false" 
    standardStyleSheets="false">
</apex:page>

Controller

public with sharing class VisualforceDMLService{

    @RemoteAction
    public static sObject performDML(String Method, sObject so){
             if (Method == 'Insert') insert so;
        else if (Method == 'Update') update so;
        else if (Method == 'Delete') delete so;
        else if (Method == 'Undelete') undelete so;
        return so;
    }

}

To access the remote action here, I did two separate calls. The first one here returns the page with the CSRF token:

curl -H 'Authorization: Bearer <authorization-token>' https://analysis-dev-ed--analysis.ap1.visual.force.com/apex/VisualForceDMLService > res.txt ; node scrape.js res.txt

I used this to isolate the value:

var fs = require('fs');
fs.readFile(process.argv[2],'utf-8',
  function(err,data){
    var s = data.split('"');
    for ( var i = 0 , n = s.length ; i < n ; i++){ 
      if (s[i] === 'csrf') console.log(s[i+2]);
    }
  }
);

Then I could make the request to the Remote Action:

curl -H 'Authorization: Bearer <authorization-token>' -H 'Content-Type: application/json' https://analysis-dev-ed--analysis.ap1.visual.force.com/apexremote -d '{"action":"analysis.VisualforceDMLService", "method":"performDML", "data":["insert",{"Name":"new account","sObjectType":"Account"}], "type":"rpc", "tid":2, "ctx":{   "csrf":"<csrf-token>",   "vid":"06690000005c6hg",   "ns":"analysis",   "ver":39 }}' -H 'Referer: https://analysis-dev-ed--analysis.ap1.visual.force.com/apex/VisualForceDMLService' 

Ultimately, it's not that simple to implement, but if you really want to lock down access, it does give you that control.