Skip to content

Support OAuth 2.0 Client Credentials Flow for non-interactive Salesforce authentication #138

Description

@Mike-Hanna

Support OAuth 2.0 Client Credentials Flow for non-interactive Salesforce authentication

Feature request

Thank you for maintaining salesforcer. We rely on this package for production R scripts that connect to Salesforce, and it has been very useful.

Salesforce is moving away from username/password-based API authentication patterns, and we are trying to migrate our R automation to a non-interactive OAuth-based flow.

In Python, we use the simple_salesforce package and were able to update our scripts away from username/password/security-token authentication:

from simple_salesforce import Salesforce

sf = Salesforce(
    username="...",
    password="...",
    security_token="...",
    domain="..."
)

to connected-app based authentication using:

from simple_salesforce import Salesforce

sf = Salesforce(
    consumer_key="...",
    consumer_secret="...",
    domain="..."
)

This lets scheduled jobs and server-side automation authenticate without storing Salesforce passwords or relying on browser-based OAuth.

Current limitation in salesforcer

From what I can tell, salesforcer::sf_auth() currently supports:

  1. Username/password/security-token authentication.
  2. Browser/cached OAuth 2.0 using consumer_key, consumer_secret, and callback_url.
  3. Reusing a previously saved Token2.0 object or .rds token file.

The package accepts consumer_key and consumer_secret, but those appear to be used for the existing interactive/cached OAuth flow via httr::oauth2.0_token(), not for a fully non-interactive server-to-server flow such as OAuth 2.0 Client Credentials.

For automated scripts, especially scheduled production jobs, the browser/cached-token approach is difficult to operationalize. The old username/password/security-token approach is also something we are trying to remove due to Salesforce authentication changes and security best practices.

Requested functionality

Would you consider adding first-class support for OAuth 2.0 Client Credentials Flow?

A possible interface could look like:

salesforcer::sf_auth(
  auth_type = "client_credentials",
  consumer_key = Sys.getenv("SALESFORCE_CONSUMER_KEY"),
  consumer_secret = Sys.getenv("SALESFORCE_CONSUMER_SECRET"),
  login_url = "https://login.salesforce.com"
)

or perhaps:

salesforcer::sf_auth_client_credentials(
  consumer_key = Sys.getenv("SALESFORCE_CONSUMER_KEY"),
  consumer_secret = Sys.getenv("SALESFORCE_CONSUMER_SECRET"),
  login_url = "https://login.salesforce.com"
)

Internally, this would call:

POST /services/oauth2/token

with a form body like:

grant_type=client_credentials
client_id=<consumer_key>
client_secret=<consumer_secret>

and then store the returned access_token and instance_url in the package auth state so existing functions like sf_query(), Bulk API helpers, REST calls, etc. continue to work.

Why this matters

This would help teams migrate off username/password/security-token auth while still using salesforcer for production R jobs.

The main use case is:

  • non-interactive scheduled scripts
  • no browser login
  • no Salesforce password stored in scripts or environment variables
  • connected-app based authentication using consumer_key, consumer_secret, and domain / login_url
  • compatibility with Salesforce orgs enforcing newer authentication and security requirements

Possible workaround today

It looks like a workaround may be to manually request an OAuth access token in R, construct an httr::Token2.0 object, and then pass that token object directly to salesforcer::sf_auth(token = ...).

That seems preferable to directly modifying salesforcer's internal state, since sf_auth() already supports receiving a token object.

Conceptually, something like this may work:

sf_auth_client_credentials <- function(
  consumer_key = Sys.getenv("SALESFORCE_CONSUMER_KEY"),
  consumer_secret = Sys.getenv("SALESFORCE_CONSUMER_SECRET"),
  domain = Sys.getenv("SALESFORCE_DOMAIN")
) {
  login_url <- paste0("https://", domain)
  token_url <- paste0(login_url, "/services/oauth2/token")

  response <- httr::POST(
    url = token_url,
    body = list(
      grant_type = "client_credentials",
      client_id = consumer_key,
      client_secret = consumer_secret
    ),
    encode = "form"
  )

  httr::stop_for_status(response)

  credentials <- httr::content(
    response,
    as = "parsed",
    type = "application/json"
  )

  # `salesforcer::sf_auth_check()` expects OAuth tokens to have an `issued_at`
  # value when checking whether a token may need refresh.
  credentials$issued_at <- as.character(as.numeric(Sys.time()) * 1000)

  endpoint <- httr::oauth_endpoint(
    request = NULL,
    base_url = paste0(login_url, "/services/oauth2"),
    authorize = "authorize",
    access = "token",
    revoke = "revoke"
  )

  app <- httr::oauth_app(
    appname = "salesforce",
    key = consumer_key,
    secret = consumer_secret
  )

  token_object <- httr::Token2.0$new(
    endpoint = endpoint,
    app = app,
    credentials = credentials
  )

  salesforcer::sf_auth(token = token_object)

  invisible(token_object)
}

Then a script could do:

sf_auth_client_credentials()

salesforcer::sf_query("SELECT Id, Name FROM Account LIMIT 10")

The concern with this workaround is that client-credentials tokens generally do not behave like normal browser OAuth tokens with refresh tokens. salesforcer may attempt to refresh OAuth tokens after they expire, so long-running jobs might need to request a new client-credentials token instead of calling the normal token refresh path.

Because of that, it would be helpful if salesforcer had native support for this flow and handled token acquisition and re-authentication in a package-supported way.

Questions

  1. Is support for OAuth 2.0 Client Credentials Flow already planned?
  2. If not, would you be open to adding it?
  3. Is the Token2.0 workaround above compatible with how salesforcer expects OAuth tokens to behave?
  4. Is there a preferred approach for contributing this via PR?

Thanks again for maintaining the package.

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions