Skip to content

@W-22917500 Add diagnostic warning when OAuth code exchange fails against Lightning URL#4059

Merged
JohnsonEricAtSalesforce merged 5 commits into
forcedotcom:devfrom
JohnsonEricAtSalesforce:W-22917500
Jun 11, 2026
Merged

@W-22917500 Add diagnostic warning when OAuth code exchange fails against Lightning URL#4059
JohnsonEricAtSalesforce merged 5 commits into
forcedotcom:devfrom
JohnsonEricAtSalesforce:W-22917500

Conversation

@JohnsonEricAtSalesforce

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add diagnostic warning when OAuth token endpoint returns unsupported_grant_type and login server URL contains .lightning.
  • Log a detailed developer-facing warning with the offending URL and explanation
  • Surface a localized user-facing alert instead of the generic error message

Details

Work Link: W-22917500

Port of Android PR: forcedotcom/SalesforceMobileSDK-Android#2921

Spec: SalesforceMobileSDK-Workspace PR 20 (approved and merged)

Changes

File Change
SFOAuthCoordinator.m +1 import, +13 lines detection logic in handleResponse:
Localizable.strings +1 localized string key
SFOAuthCoordinatorLightningURLTests.swift New test file (5 tests)
SalesforceSDKCoreTests-Bridging-Header.h Expose handleResponse: and initWithDictionary: for testing
project.pbxproj Register new test file in Xcode project

Test Plan

  • Unit tests pass (5 new tests covering all acceptance criteria)
  • Full OAuth-related test suite passes (13 tests, 0 regressions)
  • Build verified for all schemes (Everything, RestAPIExplorer, MobileSyncExplorer, AuthFlowTester)
  • Manual verification via simulated error injection (screenshot available)
  • CI passes

Verification Checklist

  • SC-1: Warning triggers only when BOTH conditions met (error == "unsupported_grant_type" AND URL contains ".lightning.")
  • SC-2: Warning does NOT appear unless both conditions are met simultaneously
  • SC-3: User-facing alert string is localized (Localizable.strings)

Deviations from Plan

None — implementation followed plan exactly.

Requirements

  • Unit tests added or updated
  • Test cases have been updated

Testing Methodology

This diagnostic cannot be triggered through the standard iOS auth flow because iOS NSURLSession automatically follows the HTTP 302 redirect that Lightning URL token endpoints return. The redirect transparently routes to the My Domain token endpoint, so the unsupported_grant_type error never reaches the SDK.

To verify the diagnostic end-to-end, we injected a synthetic unsupported_grant_type response in SFSDKOAuth2.m when the token endpoint URL contains .lightning., simulating what Android's OkHttpClient receives (OkHttp does not follow 302 on POST per HTTP spec). The resulting alert was confirmed visually on an iPhone 17 Pro simulator (screenshot below).

Code Review Notes

The following refinements were applied during development review:

  • Test file rewritten from Objective-C to Swift (project standard: no new ObjC files)
  • Replaced force unwraps with XCTUnwrap + throws for graceful test failure
  • Extracted repeated data strings to constants (DRY)
  • Consolidated setup/act/wait pattern into a shared handleTokenResponse(domain:errorCode:) helper
  • Removed redundant type annotations (type inference)
  • Removed one redundant test case (neither-condition-met covered by individual negative tests)
  • Log statement formatting adjusted to match file conventions
  • User-facing string updated to final approved wording

…inst Lightning URL

Detect when the token endpoint returns unsupported_grant_type and the
login server URL contains .lightning. — log a detailed developer warning
and surface a localized user-facing alert instead of the generic error.

Port of Android PR forcedotcom/SalesforceMobileSDK-Android/pull/2921.
@JohnsonEricAtSalesforce

Copy link
Copy Markdown
Contributor Author

Testing Details

The Lightning URL diagnostic fires when both conditions are met:

  1. Token endpoint returns unsupported_grant_type
  2. credentials.domain contains .lightning.

Why this can't be triggered in the standard iOS flow:

During testing against orgfarm-da3f74a054.test1.lightning.pc-rnd.force.com, we discovered that the Lightning URL's /services/oauth2/token endpoint returns HTTP 302 to the My Domain's token endpoint. iOS NSURLSession follows this redirect transparently (preserving the POST method and body), so the code exchange succeeds without the SDK ever seeing an error.

This 302 behavior is undocumented and not guaranteed by Salesforce (see KB articles 000386369 and 000385751). Android's OkHttpClient does not follow 302 on POST (per RFC 9110 Section 15.4.3), which is why Android sees the failure.

How we verified the implementation:

We temporarily injected a synthetic {"error":"unsupported_grant_type","error_description":"grant type not supported"} response when the token endpoint URL contains .lightning.. This simulates the response Android receives. The diagnostic correctly fired — the user-facing alert displayed the localized message.

This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce.

@github-actions

Copy link
Copy Markdown
1 Warning
⚠️ Static Analysis found an issue with one or more files you modified. Please fix the issue(s).

Clang Static Analysis Issues

File Type Category Description Line Col
SFOAuthCoordinator Nullability Memory error nil assigned to a pointer which is expected to have non-null value 120 19
SFOAuthCoordinator Nullability Memory error nil assigned to a pointer which is expected to have non-null value 244 15

Generated by 🚫 Danger

@JohnsonEricAtSalesforce

Copy link
Copy Markdown
Contributor Author

Here's a screenshot of the new alert, which wraps the CX provided text in the usual context used by other alerts in this flow.
Simulator Screenshot - iPhone 17 Pro - 2026-06-11 at 11 36 38

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
TestsPassed ☑️SkippedFailed ❌️
SalesforceSDKCore iOS ^18 Test Results652 ran651 ✅1 ❌
TestResult
SalesforceSDKCore iOS ^18 Test Results
testMalformedCallbackURL()❌ failure

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.47%. Comparing base (4ed3b2d) to head (cef3528).
⚠️ Report is 5 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #4059      +/-   ##
==========================================
- Coverage   71.00%   68.47%   -2.53%     
==========================================
  Files         246      246              
  Lines       21477    21489      +12     
==========================================
- Hits        15250    14715     -535     
- Misses       6227     6774     +547     
Components Coverage Δ
Analytics 70.78% <ø> (ø)
Common 70.79% <ø> (-0.19%) ⬇️
Core 62.00% <100.00%> (-3.91%) ⬇️
SmartStore 73.60% <ø> (ø)
MobileSync 89.06% <ø> (ø)
Files with missing lines Coverage Δ
...lesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m 53.92% <100.00%> (-10.03%) ⬇️

... and 27 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
TestsPassedSkippedFailed ❌️
AuthFlowTester UI Test Results all1 ran1 ❌
TestResult
AuthFlowTester UI Test Results all
AuthFlowTesterUITests.xctest
LegacyLoginTests.testCAOpaque_DefaultScopes_WebServerFlow()❌ failure

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
TestsPassed ☑️SkippedFailed ❌️
SalesforceSDKCore iOS ^26 Test Results652 ran545 ✅107 ❌
TestResult
SalesforceSDKCore iOS ^26 Test Results
testMalformedCallbackURL()❌ failure
SalesforceRestAPITests.testBatchRequest❌ failure
SalesforceRestAPITests.testBatchWithBatchRequest❌ failure
SalesforceRestAPITests.testBatchWithBatchRequestResponse❌ failure
SalesforceRestAPITests.testBlocks❌ failure
SalesforceRestAPITests.testBlocksCancel❌ failure
SalesforceRestAPITests.testBlocksTimeout❌ failure
SalesforceRestAPITests.testBlockUpdate❌ failure
SalesforceRestAPITests.testCollectionCreate❌ failure
SalesforceRestAPITests.testCollectionCreateWithBadRecordAndAllOrNoneFalse❌ failure
SalesforceRestAPITests.testCollectionCreateWithBadRecordAndAllOrNoneTrue❌ failure
SalesforceRestAPITests.testCollectionDelete❌ failure
SalesforceRestAPITests.testCollectionRetrieve❌ failure
SalesforceRestAPITests.testCollectionUpdate❌ failure
SalesforceRestAPITests.testCollectionUpsertExistingRecords❌ failure
SalesforceRestAPITests.testCollectionUpsertNewRecords❌ failure
SalesforceRestAPITests.testCompositeRequest❌ failure
SalesforceRestAPITests.testCreateBogusContact❌ failure
SalesforceRestAPITests.testCreateQuerySearchDelete❌ failure
SalesforceRestAPITests.testCreateUpdateQuerySearchDelete❌ failure
SalesforceRestAPITests.testCustomBaseURLRequest❌ failure
SalesforceRestAPITests.testCustomBaseURLRequestPOST❌ failure
SalesforceRestAPITests.testCustomSalesforceEndpoint❌ failure
SalesforceRestAPITests.testEscapingWithSOQLQuery❌ failure
SalesforceRestAPITests.testFailedRequestRemovedFromQueue❌ failure
SalesforceRestAPITests.testFileSharesWithUserCommunity❌ failure
SalesforceRestAPITests.testFilesInUsersGroups❌ failure
SalesforceRestAPITests.testFilesSharedWithUser❌ failure
SalesforceRestAPITests.testFullRequestPath❌ failure
SalesforceRestAPITests.testGetDescribeGlobal❌ failure
SalesforceRestAPITests.testGetDescribeGlobal_Cancel❌ failure
SalesforceRestAPITests.testGetDescribeGlobal_Timeout❌ failure
SalesforceRestAPITests.testGetDescribeWithObjectType❌ failure
SalesforceRestAPITests.testGetLayoutWithObjectAPINameWithFormFactor❌ failure
SalesforceRestAPITests.testGetLayoutWithObjectAPINameWithLayoutType❌ failure
SalesforceRestAPITests.testGetLayoutWithObjectAPINameWithMode❌ failure
SalesforceRestAPITests.testGetLayoutWithObjectAPINameWithoutFormFactor❌ failure
SalesforceRestAPITests.testGetLayoutWithObjectAPINameWithoutLayoutType❌ failure
SalesforceRestAPITests.testGetLayoutWithObjectAPINameWithoutMode❌ failure
SalesforceRestAPITests.testGetLayoutWithObjectAPINameWithoutRecordTypeId❌ failure
SalesforceRestAPITests.testGetLimits❌ failure
SalesforceRestAPITests.testGetMetadataWithObjectType❌ failure
SalesforceRestAPITests.testGetNotificationRequestPath❌ failure
SalesforceRestAPITests.testGetNotifications❌ failure
SalesforceRestAPITests.testGetPrimingRecords❌ failure
SalesforceRestAPITests.testGetResources❌ failure
SalesforceRestAPITests.testGetSearchResultLayout❌ failure
SalesforceRestAPITests.testGetSearchScopeAndOrder❌ failure
SalesforceRestAPITests.testGetSingleAccess❌ failure
SalesforceRestAPITests.testGetUserInfo❌ failure
SalesforceRestAPITests.testGetVersion_SetDelegate❌ failure
SalesforceRestAPITests.testGetVersions❌ failure
SalesforceRestAPITests.testInvalidAccessAndRefreshToken❌ failure
SalesforceRestAPITests.testInvalidAccessAndRefreshToken_MultipleRequests❌ failure
SalesforceRestAPITests.testInvalidAccessTokenWithInvalidRequest❌ failure
SalesforceRestAPITests.testInvalidAccessTokenWithValidGetRequest❌ failure
SalesforceRestAPITests.testInvalidAccessTokenWithValidPostRequest❌ failure
SalesforceRestAPITests.testNotificationsStatus❌ failure
SalesforceRestAPITests.testNoTrailingQuestionMarkForEmptyParams❌ failure
SalesforceRestAPITests.testOwnedFilesList❌ failure
SalesforceRestAPITests.testOwnedFilesListWithCommunityWithHeaders❌ failure
SalesforceRestAPITests.testParsePrimingRecordsResponse❌ failure
SalesforceRestAPITests.testParsePrimingRecordsResponseFromServer❌ failure
SalesforceRestAPITests.testPublicApiCalls❌ failure
SalesforceRestAPITests.testReallyLongSOQL❌ failure
SalesforceRestAPITests.testRedirect❌ failure
SalesforceRestAPITests.testRefreshNotificationWithValidGetRequest❌ failure
SalesforceRestAPITests.testRequestForInvokeNotificationAction❌ failure
SalesforceRestAPITests.testRequestForInvokeNotificationActionWithVersion❌ failure
SalesforceRestAPITests.testRequestForNotificationTypes❌ failure
SalesforceRestAPITests.testRequestForNotificationTypesWithVersion❌ failure
SalesforceRestAPITests.testRequestUserAgent❌ failure
SalesforceRestAPITests.testRequestUserAgentWithOverride❌ failure
SalesforceRestAPITests.testRequestWithCompositeRequest❌ failure
SalesforceRestAPITests.testRequestWithCompositeRequestResponse❌ failure
SalesforceRestAPITests.testRestApiGlobalInstance❌ failure
SalesforceRestAPITests.testRestUrlForBaseUrl❌ failure
SalesforceRestAPITests.testRestUrlForCommunityUrl❌ failure
SalesforceRestAPITests.testRestUrlForInstanceServiceHost❌ failure
SalesforceRestAPITests.testRestUrlForLoginServiceHost❌ failure
SalesforceRestAPITests.testRestUrlForNetworkServiceType❌ failure
SalesforceRestAPITests.testRetrieveError❌ failure
SalesforceRestAPITests.testSalesforceFullUrlPath❌ failure
SalesforceRestAPITests.testSObjectTreeRequest❌ failure
SalesforceRestAPITests.testSOQL❌ failure
SalesforceRestAPITests.testSOQLError❌ failure
SalesforceRestAPITests.testSOQLQueryWithBatchSize❌ failure
SalesforceRestAPITests.testSOQLWithNewLine❌ failure
SalesforceRestAPITests.testSOSL❌ failure
SalesforceRestAPITests.testUpdateNotificationRequestPath❌ failure
SalesforceRestAPITests.testUpdateNotificationsRequestContent❌ failure
SalesforceRestAPITests.testUpdateReadNotifications❌ failure
SalesforceRestAPITests.testUpdateSeenNotifications❌ failure
SalesforceRestAPITests.testUpdateWithIfUnmodifiedSince❌ failure
SalesforceRestAPITests.testUploadBatchDetailsDeleteFiles❌ failure
SalesforceRestAPITests.testUploadBatchDetailsDeleteFilesCommunity❌ failure
SalesforceRestAPITests.testUploadDetailsDeleteFile❌ failure
SalesforceRestAPITests.testUploadDetailsDeleteFileWithCommunity❌ failure
SalesforceRestAPITests.testUploadDownloadDeleteFile❌ failure
SalesforceRestAPITests.testUploadDownloadDeleteFileWithCommunity❌ failure
SalesforceRestAPITests.testUploadOwnedFilesDelete❌ failure
SalesforceRestAPITests.testUploadProfilePhoto❌ failure
SalesforceRestAPITests.testUploadProfilePhotoCommunity❌ failure
SalesforceRestAPITests.testUploadShareFileSharesSharedFilesUnshareDelete❌ failure
SalesforceRestAPITests.testUpsert❌ failure
SalesforceRestAPITests.testUpsertWithBogusExternalIdField❌ failure
SalesforceRestAPITests.testUserDefinedEndpoint❌ failure

@JohnsonEricAtSalesforce

Copy link
Copy Markdown
Contributor Author

CI Test Failure Analysis

The single test failure (LoginOptionsViewControllerTests.testBootConfigPickerViewRendered()) is unrelated to this PR's changes. It tests SwiftUI view rendering of LoginOptionsView with a BootConfig — no connection to OAuth token exchange or SFOAuthCoordinator.

The test uses a 0.2-second DispatchQueue.main.asyncAfter delay before asserting, making it timing-sensitive under CI load. It passes 10/10 runs locally in isolation.

This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce.

@JohnsonEricAtSalesforce

Copy link
Copy Markdown
Contributor Author

Danger Static Analysis Response

The two Clang nullability warnings are pre-existing issues at lines 120 and 244 of SFOAuthCoordinator.m — both are nil assignments in cleanup/teardown methods, far from this PR's changes (which are at line ~675 in handleResponse:).

These warnings exist on the dev branch today and are not introduced by this PR.

This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce.

[self stopAuthentication];
}
[self notifyDelegateOfFailure:response.error.error authInfo:self.authInfo];
BOOL isUnsupportedGrantType = [response.error.tokenEndpointErrorCode isEqualToString:@"unsupported_grant_type"];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have an existing const for this, kSFOAuthErrorTypeUnsupportedResponseType

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. The existing kSFOAuthErrorTypeUnsupportedResponseType is actually unsupported_response_type (not the same value), so I added a new kSFOAuthErrorTypeUnsupportedGrantType constant to SFSDKOAuthConstants.h following the same pattern and reference it here.

See commit: 2b8ced275

This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce.

BOOL isUnsupportedGrantType = [response.error.tokenEndpointErrorCode isEqualToString:@"unsupported_grant_type"];
BOOL isLightningURL = [self.credentials.domain containsString:@".lightning."];
if (isUnsupportedGrantType && isLightningURL) {
[SFSDKCoreLogger w:[self class] format:@"Code exchange failed with unsupported_grant_type against Lightning URL: %@. Lightning URLs do not support authorization_code grant type. Use a My Domain login server URL instead.", self.credentials.domain];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about error instead of warning?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — this is a misconfiguration that will always fail, not a transient condition. Changed to error level.

See commit: b0a096a83

This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce.

"authAlertVersionMismatchError" = "Your app has been updated, and you will need to log in again to continue using the app.";
"authAlertBrowserFlowTitle" = "Log In";
"authAlertFrontdoorLoginUrlConsumerKeyMismatch" = "Cannot use another app's login QR Code. Please log in to this app.";
"lightningUrlCodeExchangeError" = "Lighting URLs (.lightning.) are not supported for OAuth code exchange. Use your My Domain (.my.) URL instead.";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "Lighting" -> "Lightning"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good eye — fixed.

See commit: 3b7c14a6b

This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce.

// MARK: - SC-3: User-facing alert string is localized

func test_givenLightningURLError_whenHandleResponse_thenErrorDescriptionMatchesLocalizedString() throws {
XCTAssertNotNil(lightningMessage, "Localized string key 'lightningUrlCodeExchangeError' must be defined")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this will ever fail because the util will return the key even if the message isn't defined

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — SFSDKResourceUtils.localizedString: returns the key itself when the string isn't defined, so those assertions will always pass. The XCTAssertEqual on the next line is the real guard. Removed the redundant checks.

See commit: cef352839

This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce.

@JohnsonEricAtSalesforce

Copy link
Copy Markdown
Contributor Author

Additional CI Test Failures

Two more pre-existing failures reported by CI, both unrelated to this PR:

iOS ^26: DomainDiscoveryCoordinatorTests.testMissingLoginHint()

  • Tests that DomainDiscoveryCoordinator returns nil when a callback URL is missing login_hint
  • No connection to OAuth token exchange or SFOAuthCoordinator
  • Fails only on iOS 26, not iOS 18 — likely a platform-specific URLComponents behavior change

AuthFlowTester UI: LegacyLoginTests.testCAOpaque_DefaultScopes_WebServerFlow()

  • UI test for legacy Connected App login flow
  • No connection to the Lightning URL diagnostic
  • UI tests are environment-sensitive (network, org availability)

None of these tests touch handleResponse:, Localizable.strings, or the Lightning URL detection logic introduced in this PR.

This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce.

…ls against Lightning URL (Use constant for unsupported_grant_type)
…ls against Lightning URL (Use error log level instead of warning)
…ls against Lightning URL (Fix typo: Lighting -> Lightning)
…ls against Lightning URL (Remove redundant assertions on localized string)
@JohnsonEricAtSalesforce JohnsonEricAtSalesforce merged commit bdeba0b into forcedotcom:dev Jun 11, 2026
22 of 26 checks passed
@JohnsonEricAtSalesforce JohnsonEricAtSalesforce deleted the W-22917500 branch June 11, 2026 23:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants