Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ff26d73
Basic methods to override consumer key / revert to bootconfig
wmathurin Oct 21, 2025
7356c32
Showing buttons before login to pick between static bootconfig or som…
wmathurin Oct 21, 2025
825f2e8
Revert "Showing buttons before login to pick between static bootconfi…
wmathurin Oct 21, 2025
c595468
New sample app AuthFlowTester
wmathurin Oct 21, 2025
4aa7a09
AuthFlowTester now showing session details
wmathurin Oct 22, 2025
cf2033d
Enriching AuthFlowTester application
wmathurin Oct 23, 2025
d8971ab
Removing duplicate in RestAPIExplorer project
wmathurin Oct 23, 2025
414d432
Added revoke access token button
wmathurin Oct 23, 2025
abb0366
Merge from dev
wmathurin Oct 23, 2025
5fae385
Instead of passing a consumer key, apps pass in a block that will be …
wmathurin Oct 25, 2025
3fa16e5
Using SalesforceManager.shared.bootConfigRuntimeSelector
wmathurin Oct 25, 2025
2565980
New unit tests
wmathurin Oct 25, 2025
9ec1996
(Incomplete) Tests for AuthFlowTester
wmathurin Oct 25, 2025
ed39a21
Building new sample app and running new UI tests in CI
wmathurin Oct 25, 2025
d22f67f
Masking client id in JWT payload details
wmathurin Oct 25, 2025
0fc755c
Added explaining comment in SalesforceSDKManager
wmathurin Oct 27, 2025
5ff8ac4
Runtime app config (aka boot config) selection can now be asynchronous
wmathurin Oct 27, 2025
2cdd53a
Taking out one of the new tests - for now
wmathurin Oct 28, 2025
485997a
Removing keychain group
wmathurin Oct 30, 2025
90df65a
Removing UI tests for the new test app AuthFlowTester
wmathurin Nov 3, 2025
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
1 change: 1 addition & 0 deletions .github/DangerFiles/TestOrchestrator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
SCHEMES = ['SalesforceSDKCommon', 'SalesforceAnalytics', 'SalesforceSDKCore', 'SmartStore', 'MobileSync']

modifed_libs = Set[]

for file in (git.modified_files + git.added_files);
scheme = file.split("libs/").last.split("/").first
if SCHEMES.include?(scheme)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/nightly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
strategy:
fail-fast: false
matrix:
app: [RestAPIExplorer, MobileSyncExplorer]
app: [RestAPIExplorer, MobileSyncExplorer, AuthFlowTester]
ios: [^26, ^18, ^17]
include:
- ios: ^26
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ jobs:
strategy:
fail-fast: false
matrix:
app: [RestAPIExplorer, MobileSyncExplorer]
app: [RestAPIExplorer, MobileSyncExplorer, AuthFlowTester]
ios: [^26, ^18]
include:
- ios: ^26
Expand Down
3 changes: 3 additions & 0 deletions SalesforceMobileSDK.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B716A3E3218F6EEA009D407F"
BlueprintIdentifier = "F27137B928405BB8003B3D69"
BuildableName = "SalesforceSDKCommonTestApp.app"
BlueprintName = "SalesforceSDKCommonTestApp"
ReferencedContainer = "container:libs/SalesforceSDKCommon/SalesforceSDKCommon.xcodeproj">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B716A3E3218F6EEA009D407F"
BlueprintIdentifier = "F27137B928405BB8003B3D69"
BuildableName = "SalesforceSDKCommonTestApp.app"
BlueprintName = "SalesforceSDKCommonTestApp"
ReferencedContainer = "container:libs/SalesforceSDKCommon/SalesforceSDKCommon.xcodeproj">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,19 @@ NS_SWIFT_NAME(SalesforceManager)
*/
@property (nonatomic, copy) SFSDKUserAgentCreationBlock userAgentString NS_SWIFT_NAME(userAgentGenerator);

/**
Block to dynamically select the app config at runtime based on login host.

NB: SFUserAccountManager stores the consumer key, callback URL, etc. in its shared
instance, backed by shared prefs and initialized from the static boot config.
Previously, the app always used these shared instance values for login.
Now, the app can inject alternate values instead — in that case, the shared
instance and prefs are left untouched (not read or overwritten).
The consumer key and related values used for login are saved in the user
account credentials (as before) and therefore used later for token refresh.
*/
@property (nonatomic, copy, nullable) SFSDKAppConfigRuntimeSelectorBlock appConfigRuntimeSelectorBlock NS_SWIFT_NAME(bootConfigRuntimeSelector);

/** Use this flag to indicate if the APP will be an identity provider. When enabled this flag allows this application to perform authentication on behalf of another app.
*/
@property (nonatomic,assign) BOOL isIdentityProvider NS_SWIFT_NAME(isIdentityProvider);
Expand Down Expand Up @@ -306,6 +319,17 @@ NS_SWIFT_NAME(SalesforceManager)
*/
- (id <SFBiometricAuthenticationManager>)biometricAuthenticationManager;

/**
* Asynchronously retrieves the app config (aka boot config) for the specified login host.
*
* If an appConfigRuntimeSelectorBlock is set, it will be invoked to select the appropriate config.
* If the block is not set or returns nil, the default appConfig will be returned.
*
* @param loginHost The selected login host
* @param callback The callback invoked with the selected app config
*/
- (void)appConfigForLoginHost:(nullable NSString *)loginHost callback:(nonnull void (^)(SFSDKAppConfig * _Nullable))callback NS_SWIFT_NAME(bootConfig(forLoginHost:callback:));

/**
* Creates the NativeLoginManager instance.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,19 @@ - (void)biometricAuthenticationFlowDidComplete:(NSNotification *)notification {
return [SFScreenLockManagerInternal shared];
}

#pragma mark - Runtime App Config (aka Bootconfig) Override

- (void) appConfigForLoginHost:(nullable NSString *)loginHost callback:(nonnull void (^)(SFSDKAppConfig * _Nullable))callback {
if (self.appConfigRuntimeSelectorBlock) {
self.appConfigRuntimeSelectorBlock(loginHost, ^(SFSDKAppConfig *config) {
// Fall back to default appConfig if the selector block returns nil
callback(config ?: self.appConfig);
});
} else {
callback(self.appConfig);
}
}

#pragma mark - Native Login

- (id <SFNativeLoginManager>)useNativeLoginWithConsumerKey:(nonnull NSString *)consumerKey
Expand Down Expand Up @@ -964,7 +977,7 @@ - (void)biometricAuthenticationFlowDidComplete:(NSNotification *)notification {

return nativeLogin;
}

@end

NSString *SFAppTypeGetDescription(SFAppType appType){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ import Foundation

/// Struct representing a JWT Header
public struct JwtHeader: Codable {
let algorithm: String?
let type: String?
let keyId: String?
let tokenType: String?
let tenantKey: String?
let version: String?
public let algorithm: String?
public let type: String?
public let keyId: String?
public let tokenType: String?
public let tenantKey: String?
public let version: String?

enum CodingKeys: String, CodingKey {
case algorithm = "alg"
Expand All @@ -49,13 +49,13 @@ public struct JwtHeader: Codable {

/// Struct representing a JWT Payload
public struct JwtPayload: Codable {
let audience: [String]?
let expirationTime: Int?
let issuer: String?
let notBeforeTime: Int?
let subject: String?
let scopes: String?
let clientId: String?
public let audience: [String]?
public let expirationTime: Int?
public let issuer: String?
public let notBeforeTime: Int?
public let subject: String?
public let scopes: String?
public let clientId: String?

enum CodingKeys: String, CodingKey {
case audience = "aud"
Expand All @@ -71,9 +71,9 @@ public struct JwtPayload: Codable {
/// Class representing a JWT Access Token
@objc(SFSDKJwtAccessToken)
public class JwtAccessToken : NSObject {
let rawJwt: String
let header: JwtHeader
let payload: JwtPayload
public let rawJwt: String
public let header: JwtHeader
public let payload: JwtPayload

/// Initializer to parse and decode the JWT string
@objc public init(jwt: String) throws {
Expand Down Expand Up @@ -116,7 +116,7 @@ public class JwtAccessToken : NSObject {
}

/// Helper method to decode Base64 URL-encoded strings
private static func decodeBase64Url(_ string: String) throws -> String {
public static func decodeBase64Url(_ string: String) throws -> String {
var base64 = string
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
Expand All @@ -133,7 +133,7 @@ public class JwtAccessToken : NSObject {
}

/// Custom errors for JWT decoding
enum JwtError: Error {
public enum JwtError: Error {
case invalidFormat
case invalidBase64
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) NSString *redirectUri;
@property (nonatomic, readonly) NSString *loginHost;
@property (nonatomic, readonly) NSString *communityUrl;
@property (nonatomic, readonly) NSString *username;
@property (nonatomic, readonly) NSString *displayName;

@end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,14 @@ - (NSString *)communityUrl
return _credentialsDict[@"community_url"];
}

- (NSString *)username
{
return _credentialsDict[@"username"];
}

- (NSString *)displayName
{
return _credentialsDict[@"display_name"];
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ Set this block to handle presentation of the Authentication View Controller.

- (SFSDKAuthRequest *)defaultAuthRequest;

- (SFSDKAuthRequest *)defaultAuthRequestWithLoginHost:(nullable NSString *)loginHost;
- (SFSDKAuthRequest *)authRequestWithLoginHost:(nullable NSString *)loginHost appConfig:(nullable SFSDKAppConfig*)appConfig;


- (BOOL)loginWithCompletion:(nullable SFUserAccountManagerSuccessCallbackBlock)completionBlock
failure:(nullable SFUserAccountManagerFailureCallbackBlock)failureBlock
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@
}
@end

@implementation SFUserAccountManager

Check warning on line 158 in libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m

View workflow job for this annotation

GitHub Actions / native-samples-pr (RestAPIExplorer, ^18) / test-ios

method definition for 'authRequestWithLoginHost:appConfig:' not found [-Wincomplete-implementation]

Check warning on line 158 in libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m

View workflow job for this annotation

GitHub Actions / native-samples-pr (RestAPIExplorer, ^26) / test-ios

method definition for 'authRequestWithLoginHost:appConfig:' not found [-Wincomplete-implementation]

Check warning on line 158 in libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m

View workflow job for this annotation

GitHub Actions / native-samples-pr (MobileSyncExplorer, ^18) / test-ios

method definition for 'authRequestWithLoginHost:appConfig:' not found [-Wincomplete-implementation]

Check warning on line 158 in libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m

View workflow job for this annotation

GitHub Actions / native-samples-pr (MobileSyncExplorer, ^26) / test-ios

method definition for 'authRequestWithLoginHost:appConfig:' not found [-Wincomplete-implementation]

Check warning on line 158 in libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m

View workflow job for this annotation

GitHub Actions / ios-pr (SalesforceSDKCore, ^18) / test-ios

method definition for 'authRequestWithLoginHost:appConfig:' not found [-Wincomplete-implementation]

Check warning on line 158 in libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m

View workflow job for this annotation

GitHub Actions / ios-pr (SalesforceSDKCore, ^26) / test-ios

method definition for 'authRequestWithLoginHost:appConfig:' not found [-Wincomplete-implementation]

@synthesize currentUser = _currentUser;
@synthesize userAccountMap = _userAccountMap;
Expand Down Expand Up @@ -613,9 +613,16 @@

dispatch_async(dispatch_get_main_queue(), ^{
[SFSDKWebViewStateManager removeSessionForcefullyWithCompletionHandler:^{
[authSession.oauthCoordinator authenticateWithCredentials:authSession.credentials];
// Get app config for the login host. If appConfigRuntimeSelectorBlock is set,
// it will be invoked to select the appropriate config. Otherwise, returns the default appConfig.
[[SalesforceSDKManager sharedManager] appConfigForLoginHost:request.loginHost callback:^(SFSDKAppConfig* appConfig) {
authSession.credentials.clientId = appConfig.remoteAccessConsumerKey;
authSession.credentials.redirectUri = appConfig.oauthRedirectURI;
authSession.credentials.scopes = [appConfig.oauthScopes allObjects];
[authSession.oauthCoordinator authenticateWithCredentials:authSession.credentials];
}];
}];

});
return self.authSessions[sceneId].isAuthenticating;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@

#import <Foundation/Foundation.h>
@class SFUserAccount;
@class SFSDKAppConfig;
@class UIViewController;
@class SFSDKAppConfig;
@protocol SFSDKLoginFlowSelectionView;
@protocol SFSDKUserSelectionView;

Expand Down Expand Up @@ -53,4 +55,10 @@ typedef UIViewController<SFSDKLoginFlowSelectionView>*_Nonnull (^SFIDPLoginFlowS
*/
typedef UIViewController<SFSDKUserSelectionView>*_Nonnull (^SFIDPUserSelectionBlock)(void) NS_SWIFT_NAME(IDPUserSelectionBlock);

/**
Block to select an app config at runtime based on the login host.
The block takes a login host and a callback. The callback should be invoked with the selected app config.
*/
typedef void (^SFSDKAppConfigRuntimeSelectorBlock)(NSString * _Nonnull loginHost, void (^_Nonnull callback)(SFSDKAppConfig * _Nullable)) NS_SWIFT_NAME(BootConfigRuntimeSelector);

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -485,4 +485,118 @@ - (void)compareAppNames:(NSString *)expectedAppName
XCTAssertTrue([userAgent containsString:expectedAppName], @"App names should match");
}

#pragma mark - Runtime Selected App Config Tests

- (void)verifyAppConfigForLoginHost:(NSString *)loginHost
description:(NSString *)description
assertions:(void (^)(SFSDKAppConfig *config))assertions {
XCTestExpectation *expectation = [self expectationWithDescription:description];
[[SalesforceSDKManager sharedManager] appConfigForLoginHost:loginHost callback:^(SFSDKAppConfig *config) {
assertions(config);
[expectation fulfill];
}];
[self waitForExpectations:@[expectation] timeout:1.0];
}

- (void)testAppConfigForLoginHostReturnsDefaultWhenBlockNotSet {
// Clear any existing block
[SalesforceSDKManager sharedManager].appConfigRuntimeSelectorBlock = nil;

// Get the default app config for comparison
SFSDKAppConfig *defaultConfig = [SalesforceSDKManager sharedManager].appConfig;

// Test with nil loginHost - should return default config
[self verifyAppConfigForLoginHost:nil
description:@"Callback should be called with default config"
assertions:^(SFSDKAppConfig *config) {
XCTAssertEqual(config, defaultConfig, @"Should return default appConfig when no selector block is set");
}];

// Test with a loginHost - should still return default config
[self verifyAppConfigForLoginHost:@"https://test.salesforce.com"
description:@"Callback should be called with default config"
assertions:^(SFSDKAppConfig *config) {
XCTAssertEqual(config, defaultConfig, @"Should return default appConfig when no selector block is set, regardless of loginHost");
}];
}

- (void)testAppConfigForLoginHostWithDifferentLoginHosts {
NSString *loginHost1 = @"https://login.salesforce.com";
NSString *loginHost2 = @"https://test.salesforce.com";

NSDictionary *config1Dict = @{
@"remoteAccessConsumerKey": @"clientId1",
@"oauthRedirectURI": @"app1://oauth/done",
@"shouldAuthenticate": @YES
};
SFSDKAppConfig *config1 = [[SFSDKAppConfig alloc] initWithDict:config1Dict];

NSDictionary *config2Dict = @{
@"remoteAccessConsumerKey": @"clientId2",
@"oauthRedirectURI": @"app2://oauth/done",
@"shouldAuthenticate": @YES
};
SFSDKAppConfig *config2 = [[SFSDKAppConfig alloc] initWithDict:config2Dict];

// Get the default app config for comparison
SFSDKAppConfig *defaultConfig = [SalesforceSDKManager sharedManager].appConfig;

// Set the selector block to return different configs based on loginHost
[SalesforceSDKManager sharedManager].appConfigRuntimeSelectorBlock = ^(NSString *loginHost, void (^callback)(SFSDKAppConfig *)) {
if ([loginHost isEqualToString:loginHost1]) {
callback(config1);
} else if ([loginHost isEqualToString:loginHost2]) {
callback(config2);
} else {
callback(nil);
}
};

// Test first loginHost
[self verifyAppConfigForLoginHost:loginHost1
description:@"First callback should be called"
assertions:^(SFSDKAppConfig *result1) {
XCTAssertNotNil(result1, @"Should return config for loginHost1");
XCTAssertEqual(result1, config1, @"Should return config1 for loginHost1");
XCTAssertEqualObjects(result1.remoteAccessConsumerKey, @"clientId1", @"Should have correct client ID for config1");
}];

// Test second loginHost
[self verifyAppConfigForLoginHost:loginHost2
description:@"Second callback should be called"
assertions:^(SFSDKAppConfig *result2) {
XCTAssertNotNil(result2, @"Should return config for loginHost2");
XCTAssertEqual(result2, config2, @"Should return config2 for loginHost2");
XCTAssertEqualObjects(result2.remoteAccessConsumerKey, @"clientId2", @"Should have correct client ID for config2");
}];

// Test with nil loginHost - should return default config
[self verifyAppConfigForLoginHost:nil
description:@"Callback should be called with default config"
assertions:^(SFSDKAppConfig *config) {
XCTAssertEqual(config, defaultConfig, @"Should return default appConfig when nil loginHost is passed");
}];
}

- (void)testAppConfigForLoginHostReturnsDefaultWhenBlockReturnsNil {
__block BOOL blockWasCalled = NO;

// Get the default app config for comparison
SFSDKAppConfig *defaultConfig = [SalesforceSDKManager sharedManager].appConfig;

// Set the selector block to return nil via callback
[SalesforceSDKManager sharedManager].appConfigRuntimeSelectorBlock = ^(NSString *loginHost, void (^callback)(SFSDKAppConfig *)) {
blockWasCalled = YES;
callback(nil);
};

// Call the method - should fall back to default config even though block returns nil
[self verifyAppConfigForLoginHost:@"https://test.salesforce.com"
description:@"Callback should be called"
assertions:^(SFSDKAppConfig *config) {
XCTAssertTrue(blockWasCalled, @"Block should have been called");
XCTAssertEqual(config, defaultConfig, @"Should return default appConfig when block returns nil");
}];
}

@end
Loading
Loading