diff --git a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAuthSnippets.cs b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAuthSnippets.cs index 1b316fd6..091b981e 100644 --- a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAuthSnippets.cs +++ b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAuthSnippets.cs @@ -643,7 +643,7 @@ internal static ActionCodeSettings InitActionCodeSettings() AndroidPackageName = "com.example.android", AndroidInstallApp = true, AndroidMinimumVersion = "12", - DynamicLinkDomain = "coolapp.page.link", + LinkDomain = "coolapp.page.link", }; // [END init_action_code_settings] return actionCodeSettings; diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/AuthErrorHandlerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/AuthErrorHandlerTest.cs index 94de1b86..87f792ca 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/AuthErrorHandlerTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/AuthErrorHandlerTest.cs @@ -63,6 +63,12 @@ public class AuthErrorHandlerTest ErrorCode.NotFound, AuthErrorCode.EmailNotFound, }, + new object[] + { + "INVALID_HOSTING_LINK_DOMAIN", + ErrorCode.InvalidArgument, + AuthErrorCode.InvalidHostingLinkDomain, + }, }; [Theory] diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Users/EmailActionRequestTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Users/EmailActionRequestTest.cs index 97ec2038..db7dd84a 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Users/EmailActionRequestTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Users/EmailActionRequestTest.cs @@ -47,7 +47,17 @@ public class EmailActionRequestTest new ActionCodeSettings() { Url = "https://example.dynamic.link", +#pragma warning disable CS0618 DynamicLinkDomain = string.Empty, +#pragma warning restore CS0618 + }, + }, + new object[] + { + new ActionCodeSettings() + { + Url = "https://example.dynamic.link", + LinkDomain = string.Empty, }, }, new object[] @@ -84,7 +94,10 @@ public class EmailActionRequestTest { Url = "https://example.dynamic.link", HandleCodeInApp = true, +#pragma warning disable CS0618 DynamicLinkDomain = "custom.page.link", +#pragma warning restore CS0618 + LinkDomain = "custom.page.link", IosBundleId = "com.example.ios", AndroidPackageName = "com.example.android", AndroidMinimumVersion = "6", @@ -163,14 +176,17 @@ public async Task EmailVerificationLinkWithSettings() var request = NewtonsoftJsonSerializer.Instance.Deserialize>( handler.LastRequestBody); - Assert.Equal(10, request.Count); + Assert.Equal(11, request.Count); Assert.Equal("user@example.com", request["email"]); Assert.Equal("VERIFY_EMAIL", request["requestType"]); Assert.True((bool)request["returnOobLink"]); Assert.Equal(ActionCodeSettings.Url, request["continueUrl"]); Assert.True((bool)request["canHandleCodeInApp"]); +#pragma warning disable CS0618 Assert.Equal(ActionCodeSettings.DynamicLinkDomain, request["dynamicLinkDomain"]); +#pragma warning restore CS0618 + Assert.Equal(ActionCodeSettings.LinkDomain, request["linkDomain"]); Assert.Equal(ActionCodeSettings.IosBundleId, request["iOSBundleId"]); Assert.Equal(ActionCodeSettings.AndroidPackageName, request["androidPackageName"]); Assert.Equal( @@ -229,14 +245,17 @@ public async Task PasswordResetLinkWithSettings() var request = NewtonsoftJsonSerializer.Instance.Deserialize>( handler.LastRequestBody); - Assert.Equal(10, request.Count); + Assert.Equal(11, request.Count); Assert.Equal("user@example.com", request["email"]); Assert.Equal("PASSWORD_RESET", request["requestType"]); Assert.True((bool)request["returnOobLink"]); Assert.Equal(ActionCodeSettings.Url, request["continueUrl"]); Assert.True((bool)request["canHandleCodeInApp"]); +#pragma warning disable CS0618 Assert.Equal(ActionCodeSettings.DynamicLinkDomain, request["dynamicLinkDomain"]); +#pragma warning restore CS0618 + Assert.Equal(ActionCodeSettings.LinkDomain, request["linkDomain"]); Assert.Equal(ActionCodeSettings.IosBundleId, request["iOSBundleId"]); Assert.Equal(ActionCodeSettings.AndroidPackageName, request["androidPackageName"]); Assert.Equal( @@ -287,14 +306,17 @@ public async Task SignInWithEmailLink() var request = NewtonsoftJsonSerializer.Instance.Deserialize>( handler.LastRequestBody); - Assert.Equal(10, request.Count); + Assert.Equal(11, request.Count); Assert.Equal("user@example.com", request["email"]); Assert.Equal("EMAIL_SIGNIN", request["requestType"]); Assert.True((bool)request["returnOobLink"]); Assert.Equal(ActionCodeSettings.Url, request["continueUrl"]); Assert.True((bool)request["canHandleCodeInApp"]); +#pragma warning disable CS0618 Assert.Equal(ActionCodeSettings.DynamicLinkDomain, request["dynamicLinkDomain"]); +#pragma warning restore CS0618 + Assert.Equal(ActionCodeSettings.LinkDomain, request["linkDomain"]); Assert.Equal(ActionCodeSettings.IosBundleId, request["iOSBundleId"]); Assert.Equal(ActionCodeSettings.AndroidPackageName, request["androidPackageName"]); Assert.Equal( @@ -351,6 +373,39 @@ public async Task InvalidDynamicLinkDomain() Assert.Null(exception.InnerException); } + [Fact] + public async Task InvalidHostingLinkDomain() + { + var json = $@"{{ + ""error"": {{ + ""message"": ""INVALID_HOSTING_LINK_DOMAIN"", + }} + }}"; + var handler = new MockMessageHandler() + { + StatusCode = HttpStatusCode.InternalServerError, + Response = json, + }; + var auth = this.CreateFirebaseAuth(handler); + var settings = new ActionCodeSettings() + { + Url = "https://example.dynamic.link", + LinkDomain = "custom.page.link", + }; + + var exception = await Assert.ThrowsAsync( + async () => await auth.GenerateSignInWithEmailLinkAsync( + "user@example.com", settings)); + + Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode); + Assert.Equal(AuthErrorCode.InvalidHostingLinkDomain, exception.AuthErrorCode); + Assert.Equal( + "The provided hosting link domain is not configured in Firebase Hosting or is not owned by the current project (INVALID_HOSTING_LINK_DOMAIN).", + exception.Message); + Assert.NotNull(exception.HttpResponse); + Assert.Null(exception.InnerException); + } + private FirebaseAuth CreateFirebaseAuth(HttpMessageHandler handler) { var userManager = new FirebaseUserManager(new FirebaseUserManager.Args diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/ActionCodeSettings.cs b/FirebaseAdmin/FirebaseAdmin/Auth/ActionCodeSettings.cs index fac620e2..f763a3e1 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/ActionCodeSettings.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/ActionCodeSettings.cs @@ -49,12 +49,21 @@ public sealed class ActionCodeSettings /// public bool HandleCodeInApp { get; set; } + /// + /// Gets or sets the Hosting Link domain to use for the current link if it is to be opened + /// using handleCodeInApp, as multiple hosting link domains can be configured per + /// project. This setting provides the ability to explicitly choose one. If none is provided, + /// the default hosting domain is used. + /// + public string LinkDomain { get; set; } + /// /// Gets or sets the dynamic link domain to use for the current link if it is to be opened /// using Firebase Dynamic Links, as multiple dynamic link domains can be configured per /// project. This setting provides the ability to explicitly choose one. If none is provided, /// the oldest domain is used by default. /// + [System.Obsolete("DynamicLinkDomain is deprecated, use LinkDomain instead")] public string DynamicLinkDomain { get; set; } /// diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/AuthErrorCode.cs b/FirebaseAdmin/FirebaseAdmin/Auth/AuthErrorCode.cs index c7098830..fe6fc949 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/AuthErrorCode.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/AuthErrorCode.cs @@ -64,6 +64,11 @@ public enum AuthErrorCode /// InvalidDynamicLinkDomain, + /// + /// The provided hosting link domain is not configured in Firebase Hosting or is not owned by the current project. + /// + InvalidHostingLinkDomain, + /// /// The specified ID token has been revoked. /// diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/AuthErrorHandler.cs b/FirebaseAdmin/FirebaseAdmin/Auth/AuthErrorHandler.cs index f9554dfe..a40ebaac 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/AuthErrorHandler.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/AuthErrorHandler.cs @@ -77,6 +77,13 @@ internal sealed class AuthErrorHandler AuthErrorCode.InvalidDynamicLinkDomain, "Dynamic link domain specified in ActionCodeSettings is not authorized") }, + { + "INVALID_HOSTING_LINK_DOMAIN", + new ErrorInfo( + ErrorCode.InvalidArgument, + AuthErrorCode.InvalidHostingLinkDomain, + "The provided hosting link domain is not configured in Firebase Hosting or is not owned by the current project") + }, { "PHONE_NUMBER_EXISTS", new ErrorInfo( diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/Users/EmailActionLinkRequest.cs b/FirebaseAdmin/FirebaseAdmin/Auth/Users/EmailActionLinkRequest.cs index 315dedc6..b1b83860 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/Users/EmailActionLinkRequest.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/Users/EmailActionLinkRequest.cs @@ -42,7 +42,10 @@ private EmailActionLinkRequest(string type, string email, ActionCodeSettings set { this.Url = settings.Url; this.HandleCodeInApp = settings.HandleCodeInApp; +#pragma warning disable CS0618 this.DynamicLinkDomain = settings.DynamicLinkDomain; +#pragma warning restore CS0618 + this.LinkDomain = settings.LinkDomain; this.IosBundleId = settings.IosBundleId; this.AndroidPackageName = settings.AndroidPackageName; this.AndroidMinimumVersion = settings.AndroidMinimumVersion; @@ -70,6 +73,9 @@ private EmailActionLinkRequest(string type, string email, ActionCodeSettings set [JsonProperty("dynamicLinkDomain")] internal string DynamicLinkDomain { get; } + [JsonProperty("linkDomain")] + internal string LinkDomain { get; } + [JsonProperty("iOSBundleId")] internal string IosBundleId { get; } @@ -125,6 +131,11 @@ private void ValidateSettings() throw new ArgumentException("DynamicLinkDomain must not be empty"); } + if (this.LinkDomain == string.Empty) + { + throw new ArgumentException("LinkDomain must not be empty"); + } + if (this.IosBundleId == string.Empty) { throw new ArgumentException("IosBundleId must not be empty");