Skip to content

Commit 228e33c

Browse files
NhienLamXiaoshouzi-ghsrushtisvncooke3
authored
[Auth] Add ActionCodeSettings.linkDomain and deprecate ActionCodeSettings.dynamicLinkDomain (#14384)
Co-authored-by: Liubin Jiang <[email protected]> Co-authored-by: Srushti Vaidya <[email protected]> Co-authored-by: Liubin Jiang <[email protected]> Co-authored-by: Nick Cooke <[email protected]>
1 parent ebd7425 commit 228e33c

File tree

15 files changed

+173
-3
lines changed

15 files changed

+173
-3
lines changed

FirebaseAuth/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 11.8.0
2+
- [added] Added `ActionCodeSettings.linkDomain` to customize the Firebase Hosting link domain
3+
that is used in out-of-band email action flows.
4+
- [deprecated] Deprecated `ActionCodeSettings.dynamicLinkDomain`.
5+
16
# 11.7.0
27
- [fixed] Fix Multi-factor session crash on second Firebase app. (#14238)
38
- [fixed] Updated most decoders to be consistent with Firebase 10's behavior

FirebaseAuth/Sources/Swift/ActionCode/ActionCodeSettings.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,18 @@ import Foundation
4040
@objc open var androidInstallIfNotAvailable: Bool = false
4141

4242
/// The Firebase Dynamic Link domain used for out of band code flow.
43+
#if !FIREBASE_CI
44+
@available(
45+
*,
46+
deprecated,
47+
message: "Firebase Dynamic Links is deprecated. Migrate to use Firebase Hosting link and use `linkDomain` to set a custom domain instead."
48+
)
49+
#endif // !FIREBASE_CI
4350
@objc open var dynamicLinkDomain: String?
4451

52+
/// The out of band custom domain for handling code in app.
53+
@objc public var linkDomain: String?
54+
4555
/// Sets the iOS bundle ID.
4656
@objc override public init() {
4757
iOSBundleID = Bundle.main.bundleIdentifier

FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,8 @@ final class AuthBackend: AuthBackendProtocol {
385385
.missingAppCredential(message: serverDetailErrorMessage)
386386
case "INVALID_CODE": return AuthErrorUtils
387387
.invalidVerificationCodeError(message: serverDetailErrorMessage)
388+
case "INVALID_HOSTING_LINK_DOMAIN": return AuthErrorUtils
389+
.invalidHostingLinkDomainError(message: serverDetailErrorMessage)
388390
case "INVALID_SESSION_INFO": return AuthErrorUtils
389391
.invalidVerificationIDError(message: serverDetailErrorMessage)
390392
case "SESSION_EXPIRED": return AuthErrorUtils

FirebaseAuth/Sources/Swift/Backend/RPC/GetOOBConfirmationCodeRequest.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ private let kCanHandleCodeInAppKey = "canHandleCodeInApp"
7878
/// The key for the "dynamic link domain" value in the request.
7979
private let kDynamicLinkDomainKey = "dynamicLinkDomain"
8080

81+
/// The key for the "link domain" value in the request.
82+
private let kLinkDomainKey = "linkDomain"
83+
8184
/// The value for the "PASSWORD_RESET" request type.
8285
private let kPasswordResetRequestTypeValue = "PASSWORD_RESET"
8386

@@ -140,6 +143,9 @@ class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest {
140143
/// The Firebase Dynamic Link domain used for out of band code flow.
141144
private let dynamicLinkDomain: String?
142145

146+
/// The Firebase Hosting domain used for out of band code flow.
147+
private(set) var linkDomain: String?
148+
143149
/// Response to the captcha.
144150
var captchaResponse: String?
145151

@@ -172,6 +178,7 @@ class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest {
172178
androidInstallApp = actionCodeSettings?.androidInstallIfNotAvailable ?? false
173179
handleCodeInApp = actionCodeSettings?.handleCodeInApp ?? false
174180
dynamicLinkDomain = actionCodeSettings?.dynamicLinkDomain
181+
linkDomain = actionCodeSettings?.linkDomain
175182

176183
super.init(
177184
endpoint: kGetOobConfirmationCodeEndpoint,
@@ -274,6 +281,9 @@ class GetOOBConfirmationCodeRequest: IdentityToolkitRequest, AuthRPCRequest {
274281
if let dynamicLinkDomain {
275282
body[kDynamicLinkDomainKey] = dynamicLinkDomain
276283
}
284+
if let linkDomain {
285+
body[kLinkDomainKey] = linkDomain
286+
}
277287
if let captchaResponse {
278288
body[kCaptchaResponseKey] = captchaResponse
279289
}

FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,10 @@ class AuthErrorUtils {
370370
error(code: .invalidDynamicLinkDomain, message: message)
371371
}
372372

373+
static func invalidHostingLinkDomainError(message: String?) -> Error {
374+
error(code: .invalidHostingLinkDomain, message: message)
375+
}
376+
373377
static func missingOrInvalidNonceError(message: String?) -> Error {
374378
error(code: .missingOrInvalidNonce, message: message)
375379
}

FirebaseAuth/Sources/Swift/Utilities/AuthErrors.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@ import Foundation
258258
/// unauthorized for the current project.
259259
case invalidDynamicLinkDomain = 17074
260260

261+
/// Indicates that the provided Firebase Hosting Link domain is not owned by the current project.
262+
case invalidHostingLinkDomain = 17214
263+
261264
/// Indicates that the credential is rejected because it's malformed or mismatching.
262265
case rejectedCredential = 17075
263266

@@ -468,6 +471,8 @@ import Foundation
468471
return kErrorInvalidProviderID
469472
case .invalidDynamicLinkDomain:
470473
return kErrorInvalidDynamicLinkDomain
474+
case .invalidHostingLinkDomain:
475+
return kErrorInvalidHostingLinkDomain
471476
case .webInternalError:
472477
return kErrorWebInternalError
473478
case .webSignInUserInteractionFailure:
@@ -661,6 +666,8 @@ import Foundation
661666
return "ERROR_INVALID_PROVIDER_ID"
662667
case .invalidDynamicLinkDomain:
663668
return "ERROR_INVALID_DYNAMIC_LINK_DOMAIN"
669+
case .invalidHostingLinkDomain:
670+
return "ERROR_INVALID_HOSTING_LINK_DOMAIN"
664671
case .webInternalError:
665672
return "ERROR_WEB_INTERNAL_ERROR"
666673
case .webSignInUserInteractionFailure:
@@ -905,6 +912,9 @@ private let kErrorInvalidProviderID =
905912
private let kErrorInvalidDynamicLinkDomain =
906913
"The Firebase Dynamic Link domain used is either not configured or is unauthorized for the current project."
907914

915+
private let kErrorInvalidHostingLinkDomain =
916+
"The provided hosting link domain is not configured in Firebase Hosting or is not owned by the current project."
917+
908918
private let kErrorInternalError =
909919
"An internal error has occurred, print and inspect the error details for more information."
910920

FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ enum AuthMenu: String {
4343
case deleteApp
4444
case actionType
4545
case continueURL
46+
case linkDomain
4647
case requestVerifyEmail
4748
case requestPasswordReset
4849
case resetPassword
@@ -117,6 +118,8 @@ enum AuthMenu: String {
117118
return "Action Type"
118119
case .continueURL:
119120
return "Continue URL"
121+
case .linkDomain:
122+
return "Link Domain"
120123
case .requestVerifyEmail:
121124
return "Request Verify Email"
122125
case .requestPasswordReset:
@@ -197,6 +200,8 @@ enum AuthMenu: String {
197200
self = .actionType
198201
case "Continue URL":
199202
self = .continueURL
203+
case "Link Domain":
204+
self = .linkDomain
200205
case "Request Verify Email":
201206
self = .requestVerifyEmail
202207
case "Request Password Reset":
@@ -328,6 +333,7 @@ class AuthMenuData: DataSourceProvidable {
328333
let items: [Item] = [
329334
Item(title: AuthMenu.actionType.name, detailTitle: ActionCodeRequestType.inApp.name),
330335
Item(title: AuthMenu.continueURL.name, detailTitle: "--", isEditable: true),
336+
Item(title: AuthMenu.linkDomain.name, detailTitle: "--", isEditable: true),
331337
Item(title: AuthMenu.requestVerifyEmail.name),
332338
Item(title: AuthMenu.requestPasswordReset.name),
333339
Item(title: AuthMenu.resetPassword.name),

FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AccountLinkingViewController.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,9 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate
354354
/// Similar to in `PasswordlessViewController`, enter the authorized domain.
355355
/// Please refer to this Quickstart's README for more information.
356356
private let authorizedDomain: String = "ENTER AUTHORIZED DOMAIN"
357+
358+
/// This is the replacement for customized dynamic link domain.
359+
private let customDomain: String = "ENTER AUTHORIZED HOSTING DOMAIN"
357360
/// Maintain a reference to the email entered for linking user to Passwordless.
358361
private var email: String?
359362

@@ -380,6 +383,7 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate
380383
// The sign-in operation must be completed in the app.
381384
actionCodeSettings.handleCodeInApp = true
382385
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
386+
actionCodeSettings.linkDomain = customDomain
383387

384388
AppManager.shared.auth()
385389
.sendSignInLink(toEmail: email, actionCodeSettings: actionCodeSettings) { error in

FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
4343
var authStateDidChangeListeners: [AuthStateDidChangeListenerHandle] = []
4444
var IDTokenDidChangeListeners: [IDTokenDidChangeListenerHandle] = []
4545
var actionCodeContinueURL: URL?
46+
var actionCodeLinkDomain: String?
4647
var actionCodeRequestType: ActionCodeRequestType = .inApp
4748

4849
let spinner = UIActivityIndicatorView(style: .medium)
@@ -73,6 +74,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
7374
let settings = ActionCodeSettings()
7475
settings.url = actionCodeContinueURL
7576
settings.handleCodeInApp = (actionCodeRequestType == .inApp)
77+
settings.linkDomain = actionCodeLinkDomain
7678
return settings
7779
}
7880

@@ -160,6 +162,9 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
160162
case .continueURL:
161163
changeActionCodeContinueURL(at: indexPath)
162164

165+
case .linkDomain:
166+
changeActionCodeLinkDomain(at: indexPath)
167+
163168
case .requestVerifyEmail:
164169
requestVerifyEmail()
165170

@@ -561,7 +566,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
561566
private func changeActionCodeContinueURL(at indexPath: IndexPath) {
562567
showTextInputPrompt(with: "Continue URL:", completion: { newContinueURL in
563568
self.actionCodeContinueURL = URL(string: newContinueURL)
564-
print("Successfully set Continue URL to: \(newContinueURL)")
569+
print("Successfully set Continue URL to: \(newContinueURL)")
565570
self.dataSourceProvider.updateItem(
566571
at: indexPath,
567572
item: Item(
@@ -574,6 +579,22 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
574579
})
575580
}
576581

582+
private func changeActionCodeLinkDomain(at indexPath: IndexPath) {
583+
showTextInputPrompt(with: "Link Domain:", completion: { newLinkDomain in
584+
self.actionCodeLinkDomain = newLinkDomain
585+
print("Successfully set Link Domain to: \(newLinkDomain)")
586+
self.dataSourceProvider.updateItem(
587+
at: indexPath,
588+
item: Item(
589+
title: AuthMenu.linkDomain.name,
590+
detailTitle: self.actionCodeLinkDomain,
591+
isEditable: true
592+
)
593+
)
594+
self.tableView.reloadData()
595+
})
596+
}
597+
577598
private func requestVerifyEmail() {
578599
showSpinner()
579600
let completionHandler: ((any Error)?) -> Void = { [weak self] error in

FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/OtherAuthMethodControllers/PasswordlessViewController.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ class PasswordlessViewController: OtherAuthViewController {
3131

3232
// MARK: - Firebase 🔥
3333

34-
private let authorizedDomain: String = "ENTER AUTHORIZED DOMAIN"
34+
private let authorizedDomain: String =
35+
"fir-ios-auth-sample.firebaseapp.com" // Enter AUTHORIZED_DOMAIN
36+
private let customDomain: String =
37+
"firebaseiosauthsample.testdomaindonotuse.com" // Enter AUTHORIZED_HOSTING_DOMAIN
3538

3639
private func sendSignInLink(to email: String) {
3740
let actionCodeSettings = ActionCodeSettings()
@@ -42,6 +45,7 @@ class PasswordlessViewController: OtherAuthViewController {
4245
// The sign-in operation must be completed in the app.
4346
actionCodeSettings.handleCodeInApp = true
4447
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
48+
actionCodeSettings.linkDomain = customDomain
4549

4650
AppManager.shared.auth()
4751
.sendSignInLink(toEmail: email, actionCodeSettings: actionCodeSettings) { error in

FirebaseAuth/Tests/SampleSwift/AuthenticationExampleUITests/AuthenticationExampleUITests.swift

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,89 @@ class AuthenticationExampleUITests: XCTestCase {
226226
removeUIInterruptionMonitor(interruptionMonitor)
227227
}
228228

229+
func testEmailLinkSentSuccessfully() {
230+
app.staticTexts["Email Link/Passwordless"].tap()
231+
232+
let testEmail = "[email protected]"
233+
app.textFields["Enter Authentication Email"].tap()
234+
app.textFields["Enter Authentication Email"].typeText(testEmail)
235+
app.buttons["return"].tap() // Dismiss keyboard
236+
app.buttons["Send Sign In Link"].tap()
237+
238+
// Wait for the error message to appear (if there is an error)
239+
let errorAlert = app.alerts.staticTexts["Error"]
240+
let errorExists = errorAlert.waitForExistence(timeout: 5.0)
241+
242+
app.swipeDown(velocity: .fast)
243+
244+
// Assert that there is no error message (success case)
245+
// The email sign in link is sent successfully if no error message appears
246+
XCTAssertFalse(errorExists, "Error")
247+
248+
// Go back and check that there is no user that is signed in
249+
app.tabBars.firstMatch.buttons.element(boundBy: 1).tap()
250+
wait(forElement: app.navigationBars["User"], timeout: 5.0)
251+
XCTAssertEqual(
252+
app.cells.count,
253+
0,
254+
"The user shouldn't be signed in and the user view should have no cells."
255+
)
256+
}
257+
258+
func testResetPasswordLinkCustomDomain() {
259+
// assuming action type is in-app + continue URL everytime the app launches
260+
261+
// set Authorized Domain as Continue URL
262+
let testContinueURL = "fir-ios-auth-sample.firebaseapp.com"
263+
app.staticTexts["Continue URL"].tap()
264+
app.alerts.textFields.element.typeText(testContinueURL)
265+
app.buttons["Save"].tap()
266+
267+
// set Custom Hosting Domain as Link Domain
268+
let testLinkDomain = "http://firebaseiosauthsample.testdomaindonotuse.com"
269+
app.staticTexts["Link Domain"].tap()
270+
app.alerts.textFields.element.typeText(testLinkDomain)
271+
app.buttons["Save"].tap()
272+
273+
app.staticTexts["Request Password Reset"].tap()
274+
let testEmail = "[email protected]"
275+
app.alerts.textFields.element.typeText(testEmail)
276+
app.buttons["Save"].tap()
277+
278+
// Go back and check that there is no user that is signed in
279+
app.tabBars.firstMatch.buttons.element(boundBy: 1).tap()
280+
wait(forElement: app.navigationBars["User"], timeout: 5.0)
281+
XCTAssertEqual(
282+
app.cells.count,
283+
0,
284+
"The user shouldn't be signed in and the user view should have no cells."
285+
)
286+
}
287+
288+
func testResetPasswordLinkDefaultDomain() {
289+
// assuming action type is in-app + continue URL everytime the app launches
290+
291+
// set Authorized Domain as Continue URL
292+
let testContinueURL = "fir-ios-auth-sample.firebaseapp.com"
293+
app.staticTexts["Continue URL"].tap()
294+
app.alerts.textFields.element.typeText(testContinueURL)
295+
app.buttons["Save"].tap()
296+
297+
app.staticTexts["Request Password Reset"].tap()
298+
let testEmail = "[email protected]"
299+
app.alerts.textFields.element.typeText(testEmail)
300+
app.buttons["Save"].tap()
301+
302+
// Go back and check that there is no user that is signed in
303+
app.tabBars.firstMatch.buttons.element(boundBy: 1).tap()
304+
wait(forElement: app.navigationBars["User"], timeout: 5.0)
305+
XCTAssertEqual(
306+
app.cells.count,
307+
0,
308+
"The user shouldn't be signed in and the user view should have no cells."
309+
)
310+
}
311+
229312
// MARK: - Private Helpers
230313

231314
private func signOut() {

FirebaseAuth/Tests/Unit/GetOOBConfirmationCodeTests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class GetOOBConfirmationCodeTests: RPCBaseTests {
3434
private let kAndroidMinimumVersionKey = "androidMinimumVersion"
3535
private let kCanHandleCodeInAppKey = "canHandleCodeInApp"
3636
private let kDynamicLinkDomainKey = "dynamicLinkDomain"
37+
private let kLinkDomainKey = "linkDomain"
3738
private let kExpectedAPIURL =
3839
"https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode?key=APIKey"
3940
private let kOOBCodeKey = "oobCode"
@@ -66,6 +67,7 @@ class GetOOBConfirmationCodeTests: RPCBaseTests {
6667
XCTAssertEqual(decodedRequest[kAndroidInstallAppKey] as? Bool, true)
6768
XCTAssertEqual(decodedRequest[kCanHandleCodeInAppKey] as? Bool, true)
6869
XCTAssertEqual(decodedRequest[kDynamicLinkDomainKey] as? String, kDynamicLinkDomain)
70+
XCTAssertEqual(decodedRequest[kLinkDomainKey] as? String, kLinkDomain)
6971
}
7072
}
7173

@@ -110,6 +112,7 @@ class GetOOBConfirmationCodeTests: RPCBaseTests {
110112
XCTAssertEqual(decodedRequest[kAndroidInstallAppKey] as? Bool, true)
111113
XCTAssertEqual(decodedRequest[kCanHandleCodeInAppKey] as? Bool, true)
112114
XCTAssertEqual(decodedRequest[kDynamicLinkDomainKey] as? String, kDynamicLinkDomain)
115+
XCTAssertEqual(decodedRequest[kLinkDomainKey] as? String, kLinkDomain)
113116
XCTAssertEqual(decodedRequest[kCaptchaResponseKey] as? String, kTestCaptchaResponse)
114117
XCTAssertEqual(decodedRequest[kClientTypeKey] as? String, kTestClientType)
115118
XCTAssertEqual(decodedRequest[kRecaptchaVersionKey] as? String, kTestRecaptchaVersion)

FirebaseAuth/Tests/Unit/ObjCAPITests.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ - (void)FIRActionCodeSettings_h {
6565
s = [codeSettings androidPackageName];
6666
s = [codeSettings androidMinimumVersion];
6767
s = [codeSettings dynamicLinkDomain];
68+
s = [codeSettings linkDomain];
6869
}
6970

7071
- (void)FIRAuthAdditionalUserInfo_h:(FIRAdditionalUserInfo *)additionalUserInfo {
@@ -280,6 +281,7 @@ - (void)FIRAuthErrors_h {
280281
c = FIRAuthErrorCodeTenantIDMismatch;
281282
c = FIRAuthErrorCodeUnsupportedTenantOperation;
282283
c = FIRAuthErrorCodeInvalidDynamicLinkDomain;
284+
c = FIRAuthErrorCodeInvalidHostingLinkDomain;
283285
c = FIRAuthErrorCodeRejectedCredential;
284286
c = FIRAuthErrorCodeGameKitNotLinked;
285287
c = FIRAuthErrorCodeSecondFactorRequired;

FirebaseAuth/Tests/Unit/RPCBaseTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class RPCBaseTests: XCTestCase {
3838
let kAndroidPackageName = "androidpackagename"
3939
let kAndroidMinimumVersion = "3.0"
4040
let kDynamicLinkDomain = "test.page.link"
41+
let kLinkDomain = "link.firebaseapp.com"
4142
let kTestPhotoURL = "https://host.domain/image"
4243
let kCreationDateTimeIntervalInSeconds = 1_505_858_500.0
4344
let kLastSignInDateTimeIntervalInSeconds = 1_505_858_583.0
@@ -304,6 +305,7 @@ class RPCBaseTests: XCTestCase {
304305
settings.handleCodeInApp = true
305306
settings.url = URL(string: kContinueURL)
306307
settings.dynamicLinkDomain = kDynamicLinkDomain
308+
settings.linkDomain = kLinkDomain
307309
return settings
308310
}
309311

0 commit comments

Comments
 (0)