I wrote this fix. I want to explain what happened and why the solution looks the way it does, because the behaviour that caused it is not immediately obvious. It will affect anyone integrating Salesforce with Nexus, and likely a few other providers as well.
The Error
When a developer tries to connect a Salesforce workspace through Nexus, the OAuth flow breaks before the user ever sees a consent screen. Salesforce returns an error response to the authorization URL redirect. The error message is not helpful. It does not say "scope parameter rejected." It just fails.
The developer checks their client ID and secret. Those are correct. They check their redirect URI. That is correct too. They check Nexus logs and the request looks valid. It is a valid OAuth2 authorization request. The problem is that Salesforce does not accept it.
What RFC 6749 Says and What Salesforce Does
RFC 6749 section 3.3 specifies that the scope parameter is optional on the authorization endpoint. If included, it indicates the requested access scope. Most OAuth2 providers, including Google, GitHub, Microsoft, and Stripe, accept it on the authorization URL without complaint.
Salesforce rejects it. If your authorization URL includes a scope query parameter, Salesforce returns an error before displaying the consent screen. This is a documented non-conformance with the specification, and it is not going to change.
Before Nexus 0.1.4, the Broker unconditionally added scope to every authorization URL it constructed:
q.Set("scope", strings.Join(scopes, " "))
For Salesforce, that single line was enough to break the entire consent flow.
The Fix: skip_scope_on_auth
The fix in Nexus 0.1.4 is a boolean field on the provider profile called skip_scope_on_auth. When set to true, the Broker omits the scope parameter from the authorization URL. Scope is still sent and enforced at the token exchange step. Only the authorization redirect is affected.
In the Broker's consent handler, the flag is read from the provider's params JSON blob:
skipScopeOnAuth := false
if providerParams != nil {
var paramsMap map[string]interface{}
if err := json.Unmarshal(*providerParams, ¶msMap); err == nil {
if skip, ok := paramsMap["skip_scope_on_auth"].(bool); ok {
skipScopeOnAuth = skip
}
}
}
if !skipScopeOnAuth {
if len(scopes) > 0 {
q.Set("scope", strings.Join(scopes, " "))
}
}
This is a focused change: 21 lines in nexus-broker/pkg/handlers/consent.go. Nothing else in the flow changes. The scope validation at token exchange is untouched.
To use it, set the flag in your provider manifest:
providers:
- name: salesforce
auth_type: oauth2
client_id: "${SALESFORCE_CLIENT_ID}"
client_secret: "${SALESFORCE_CLIENT_SECRET}"
auth_url: "https://login.salesforce.com/services/oauth2/authorize"
token_url: "https://login.salesforce.com/services/oauth2/token"
scopes:
- api
- refresh_token
skip_scope_on_auth: true
Without skip_scope_on_auth: true, Salesforce OAuth flows fail at the redirect step. With it, they complete normally. Scopes are still requested and validated at the token exchange step. The user simply never sees them appended to the authorization URL.
Why Scope at Authorization Exists at All
The reason most providers want scope on the authorization URL is user experience: it lets them show the user exactly what permissions are being requested on the consent screen before any token is issued. Salesforce handles this differently. It determines the displayed permissions from the connected app configuration in Salesforce itself, not from the URL parameters. From Salesforce's perspective, the scope parameter on the authorization URL is redundant at best and an error condition at worst.
This is worth understanding because it explains why the flag is safe. Scope on authorization is a hint to the server. Scope on exchange is where tokens are actually scoped. Salesforce enforces scope at exchange. It simply does not want to see it at authorization. Nexus respects that distinction.
Other Providers That Need This
Salesforce is the most common case. If you are integrating a provider that follows the OAuth2 specification in all other respects but returns an error specifically on the authorization URL redirect, skip_scope_on_auth: true is the first thing to try. The flag is intentionally generic.
Upgrade by pulling the v0.1.4 tag from GitHub. No database migrations are required.