Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
4F7EB4AC1BFFCF0F00768720 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F7EB4A01BFFCEF600768720 /* main.m */; };
4F7EB4AD1BFFCF2300768720 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F7EB4A41BFFCEF600768720 /* ViewController.m */; };
4F8A3B012CEC202F00ECDC76 /* JwtAccessToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8A3B002CEC202F00ECDC76 /* JwtAccessToken.swift */; };
1A31073F5F374B9EB1162F2E /* SFOAuthCoordinatorLightningURLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399A11508BCB47F490DFB724 /* SFOAuthCoordinatorLightningURLTests.swift */; };
4F9E05322DD6A08000548985 /* SFSDKOAuthTokenEndpointResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F9E052C2DD6A06F00548985 /* SFSDKOAuthTokenEndpointResponseTests.m */; };
4F9E05342DD7BE1500548985 /* SFOAuthCredentialsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F9E05332DD7BE0A00548985 /* SFOAuthCredentialsTests.m */; };
4FA1B2C32F0E000000000001 /* LoginForAdminTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA1B2C32F0E000000000002 /* LoginForAdminTests.swift */; };
Expand Down Expand Up @@ -691,6 +692,7 @@
4F96FD471BFD32140022F021 /* SFSDKResourceUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SFSDKResourceUtils.m; sourceTree = "<group>"; };
4F96FD481BFD32140022F021 /* SFSDKWebUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SFSDKWebUtils.h; sourceTree = "<group>"; };
4F96FD491BFD32140022F021 /* SFSDKWebUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SFSDKWebUtils.m; sourceTree = "<group>"; };
399A11508BCB47F490DFB724 /* SFOAuthCoordinatorLightningURLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SFOAuthCoordinatorLightningURLTests.swift; path = ../SalesforceSDKCoreTests/SFOAuthCoordinatorLightningURLTests.swift; sourceTree = "<group>"; };
4F9E052C2DD6A06F00548985 /* SFSDKOAuthTokenEndpointResponseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SFSDKOAuthTokenEndpointResponseTests.m; path = ../SalesforceSDKCoreTests/SFSDKOAuthTokenEndpointResponseTests.m; sourceTree = "<group>"; };
4F9E05332DD7BE0A00548985 /* SFOAuthCredentialsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SFOAuthCredentialsTests.m; path = ../SalesforceSDKCoreTests/SFOAuthCredentialsTests.m; sourceTree = "<group>"; };
4FA1B2C32F0E000000000002 /* LoginForAdminTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginForAdminTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1107,6 +1109,7 @@
E1DDC1431CAEEB34002F51DD /* SFSDKLoginHostTests.m */,
B7355248228E84AF001C7759 /* SFSDKLogoutBlocker.h */,
B7A901BD228E4DFA0036D749 /* SFSDKLogoutBlocker.m */,
399A11508BCB47F490DFB724 /* SFOAuthCoordinatorLightningURLTests.swift */,
4F9E052C2DD6A06F00548985 /* SFSDKOAuthTokenEndpointResponseTests.m */,
69848CBB2364063E00893E57 /* SFSDKPushNotificationDataProvider.h */,
69848CBC2364063E00893E57 /* SFSDKPushNotificationDataProvider.m */,
Expand Down Expand Up @@ -2256,6 +2259,7 @@
69848CB82364035300893E57 /* SFSDKEncryptedPushNotificationTests.m in Sources */,
4F3ECD8A2EBBD150005020A6 /* SFOAuthCoordinatorTests.m in Sources */,
4FA1B2C32F0E000000000001 /* LoginForAdminTests.swift in Sources */,
1A31073F5F374B9EB1162F2E /* SFOAuthCoordinatorLightningURLTests.swift in Sources */,
4F9E05322DD6A08000548985 /* SFSDKOAuthTokenEndpointResponseTests.m in Sources */,
4F06AF8D1C49A18E00F70798 /* SalesforceSDKManagerTests.m in Sources */,
237C186C2E44FCAE0008015C /* EncryptStreamTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
#import <SalesforceSDKCommon/SalesforceSDKCommon-Swift.h>
#import <SalesforceSDKCommon/SFSDKDatasharingHelper.h>
#import <LocalAuthentication/LocalAuthentication.h>
#import "SFSDKResourceUtils.h"
@interface SFOAuthCoordinator()

@property (nonatomic) NSString *networkIdentifier;
Expand Down Expand Up @@ -620,7 +621,7 @@ - (void)beginTokenEndpointFlow {
request.refreshToken = self.credentials.refreshToken;
request.redirectURI = self.credentials.redirectUri;
request.serverURL = [self.credentials overrideDomainIfNeeded];

__weak typeof (self) weakSelf = self;
if (self.approvalCode) {
[SFSDKCoreLogger i:[self class] format:@"%@: Initiating authorization code flow.", NSStringFromSelector(_cmd)];
Expand Down Expand Up @@ -669,7 +670,20 @@ - (void)handleResponse:(SFSDKOAuthTokenEndpointResponse *)response {
[SFSDKCoreLogger d:[self class] format:@"Refresh attempt timed out after %f seconds.", self.timeout];
[self stopAuthentication];
}
[self notifyDelegateOfFailure:response.error.error authInfo:self.authInfo];
BOOL isUnsupportedGrantType = [response.error.tokenEndpointErrorCode isEqualToString:kSFOAuthErrorTypeUnsupportedGrantType];
BOOL isLightningURL = [self.credentials.domain containsString:@".lightning."];
if (isUnsupportedGrantType && isLightningURL) {
[SFSDKCoreLogger e:[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];
NSString *localizedMessage = [SFSDKResourceUtils localizedString:@"lightningUrlCodeExchangeError"];
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:response.error.error.userInfo ?: @{}];
userInfo[NSLocalizedDescriptionKey] = localizedMessage;
NSError *diagnosticError = [NSError errorWithDomain:response.error.error.domain
code:response.error.error.code
userInfo:userInfo];
[self notifyDelegateOfFailure:diagnosticError authInfo:self.authInfo];
} else {
[self notifyDelegateOfFailure:response.error.error authInfo:self.authInfo];
}
self.responseData = [NSMutableData dataWithCapacity:kSFOAuthReponseBufferLength];
}
}
Expand Down Expand Up @@ -877,10 +891,10 @@ - (void)handleCustomDomainUpdateWithLoginHint:(NSString *)loginHint myDomain:(NS

#pragma mark - WKNavigationDelegate (User-Agent Token Flow)
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {

NSURL *url = navigationAction.request.URL;
NSString *requestUrl = [url absoluteString];

// Determine if presence of discovery domain, then handle if present.
SFDomainDiscoveryResult *discoveryResult = [self.domainDiscoveryCoordinator handleWithWebAction:navigationAction];
if (discoveryResult) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ static NSString * const kSFOAuthErrorTypeInactiveUser = @"inactive
static NSString * const kSFOAuthErrorTypeInactiveOrg = @"inactive_org";
static NSString * const kSFOAuthErrorTypeRateLimitExceeded = @"rate_limit_exceeded";
static NSString * const kSFOAuthErrorTypeUnsupportedResponseType = @"unsupported_response_type";
static NSString * const kSFOAuthErrorTypeUnsupportedGrantType = @"unsupported_grant_type";
static NSString * const kSFOAuthErrorTypeTimeout = @"auth_timeout";
static NSString * const kSFOAuthErrorTypeWrongVersion = @"wrong_version"; // credentials do not match current Connected App version in the org
static NSString * const kSFOAuthErrorTypeBrowserLaunchFailed = @"browser_launch_failed";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
SFOAuthCoordinatorLightningURLTests.swift
SalesforceSDKCoreTests

Copyright (c) 2026-present, salesforce.com, inc. All rights reserved.

Redistribution and use of this software in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions
and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of
conditions and the following disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of salesforce.com, inc. nor the names of its contributors may be used to
endorse or promote products derived from this software without specific prior written
permission of salesforce.com, inc.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

import XCTest
@testable import SalesforceSDKCore

final class SFOAuthCoordinatorLightningURLTests: XCTestCase, SFOAuthCoordinatorDelegate {

private var coordinator: SFOAuthCoordinator!
private var lastFailureError: NSError?
private var delegateExpectation: XCTestExpectation?

override class func setUp() {
SFSDKLogoutBlocker.block()
super.setUp()
}

override func setUp() {
super.setUp()
lastFailureError = nil
}

override func tearDown() {
coordinator = nil
lastFailureError = nil
delegateExpectation = nil
super.tearDown()
}

// MARK: - Constants

private let lightningDomain = "myorg.lightning.force.com"
private let lightningSubdomain = "myorg.lightning.pc-rnd.force.com"
private let myDomain = "myorg.my.salesforce.com"
private let unsupportedGrantType = "unsupported_grant_type"
private let invalidGrant = "invalid_grant"
private let lightningMessage = SFSDKResourceUtils.localizedString("lightningUrlCodeExchangeError")

// MARK: - Helpers

private func handleTokenResponse(domain: String, errorCode: String) throws {
let credentials = try XCTUnwrap(OAuthCredentials(identifier: "com.salesforce.ios.oauth.lightningtest", clientId: "TestClientId", encrypted: false))
credentials.domain = domain
credentials.redirectUri = "testapp://callback"
coordinator = SFOAuthCoordinator(credentials: credentials)
coordinator.delegate = self
delegateExpectation = expectation(description: "Delegate called")

let params = ["error": errorCode, "error_description": "\(errorCode): grant type not supported"]
let response = SFSDKOAuthTokenEndpointResponse(dictionary: params, parseAdditionalFields: nil)
coordinator.handle(response)

waitForExpectations(timeout: 2.0)
XCTAssertNotNil(lastFailureError, "Delegate should have received a failure error")
}

// MARK: - SC-1: Warning triggers when BOTH conditions met

func test_givenLightningURLAndUnsupportedGrantType_whenHandleResponse_thenDelegateReceivesLocalizedError() throws {
try handleTokenResponse(domain: lightningDomain, errorCode: unsupportedGrantType)
XCTAssertEqual(lastFailureError?.localizedDescription, lightningMessage)
}

func test_givenLightningSubdomainAndUnsupportedGrantType_whenHandleResponse_thenDelegateReceivesLocalizedError() throws {
try handleTokenResponse(domain: lightningSubdomain, errorCode: unsupportedGrantType)
XCTAssertEqual(lastFailureError?.localizedDescription, lightningMessage)
}

// MARK: - SC-2: Warning does NOT appear unless both conditions met

func test_givenNonLightningURLAndUnsupportedGrantType_whenHandleResponse_thenDelegateReceivesGenericError() throws {
try handleTokenResponse(domain: myDomain, errorCode: unsupportedGrantType)
XCTAssertNotEqual(lastFailureError?.localizedDescription, lightningMessage)
}

func test_givenLightningURLAndDifferentError_whenHandleResponse_thenDelegateReceivesGenericError() throws {
try handleTokenResponse(domain: lightningDomain, errorCode: invalidGrant)
XCTAssertNotEqual(lastFailureError?.localizedDescription, lightningMessage)
}

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

func test_givenLightningURLError_whenHandleResponse_thenErrorDescriptionMatchesLocalizedString() throws {
try handleTokenResponse(domain: lightningDomain, errorCode: unsupportedGrantType)
XCTAssertEqual(lastFailureError?.localizedDescription, lightningMessage)
}

// MARK: - SFOAuthCoordinatorDelegate

func oauthCoordinator(_ coordinator: SFOAuthCoordinator, didFailWithError error: Error, authInfo: AuthInfo?) {
lastFailureError = error as NSError
delegateExpectation?.fulfill()
}

func oauthCoordinatorDidAuthenticate(_ coordinator: SFOAuthCoordinator, authInfo: AuthInfo) {}
func oauthCoordinator(_ coordinator: SFOAuthCoordinator, willBeginAuthenticationWith view: WKWebView) {}
func oauthCoordinator(_ coordinator: SFOAuthCoordinator, didBeginAuthenticationWith view: WKWebView) {}
func oauthCoordinator(_ coordinator: SFOAuthCoordinator, didBeginAuthenticationWith session: ASWebAuthenticationSession) {}
func oauthCoordinatorDidCancelBrowserAuthentication(_ coordinator: SFOAuthCoordinator) {}
func oauthCoordinatorDidBeginNativeAuthentication(_ coordinator: SFOAuthCoordinator) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@
#import "SFOAuthCoordinator+Internal.h"
#import "SFUserAccountManager+Internal.h"
#import "SFOAuthCredentials+Internal.h"
#import "SFSDKOAuth2.h"

@interface SFOAuthCoordinator (LightningURLTesting)
- (void)handleResponse:(SFSDKOAuthTokenEndpointResponse *)response;
@end

@interface SFSDKOAuthTokenEndpointResponse (Testing)
- (instancetype)initWithDictionary:(NSDictionary *)nvPairs parseAdditionalFields:(NSArray<NSString *> *)additionalOAuthParameterKeys;
@end
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"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" = "Lightning URLs (.lightning.) are not supported for OAuth code exchange. Use your My Domain (.my.) URL instead.";

// LoginViewController
"TITLE_LOGIN" = "Log In";
Expand Down
Loading