Skip to content

Commit 8cd7344

Browse files
authored
feat(auth): Added APIs for generating email sign-in links (#165)
1 parent 431fc85 commit 8cd7344

File tree

3 files changed

+118
-0
lines changed

3 files changed

+118
-0
lines changed

FirebaseAdmin/FirebaseAdmin.Tests/Auth/EmailActionRequestTest.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ public void NoEmail()
105105
async () => await auth.GeneratePasswordResetLinkAsync(null));
106106
Assert.ThrowsAsync<ArgumentException>(
107107
async () => await auth.GeneratePasswordResetLinkAsync(string.Empty));
108+
109+
Assert.ThrowsAsync<ArgumentException>(
110+
async () => await auth.GenerateSignInWithEmailLinkAsync(null, ActionCodeSettings));
111+
Assert.ThrowsAsync<ArgumentException>(
112+
async () => await auth.GenerateSignInWithEmailLinkAsync(string.Empty, ActionCodeSettings));
108113
}
109114

110115
[Theory]
@@ -120,6 +125,9 @@ public void InvalidActionCodeSettings(ActionCodeSettings settings)
120125

121126
Assert.ThrowsAsync<ArgumentException>(
122127
async () => await auth.GeneratePasswordResetLinkAsync(email, settings));
128+
129+
Assert.ThrowsAsync<ArgumentException>(
130+
async () => await auth.GenerateSignInWithEmailLinkAsync(email, settings));
123131
}
124132

125133
[Fact]
@@ -254,6 +262,65 @@ public async Task PasswordResetLinkUnexpectedResponse()
254262
Assert.Null(exception.InnerException);
255263
}
256264

265+
[Fact]
266+
public void SignInWithEmailLinkNoSettings()
267+
{
268+
var handler = new MockMessageHandler() { Response = GenerateEmailLinkResponse };
269+
var auth = this.CreateFirebaseAuth(handler);
270+
var email = "[email protected]";
271+
272+
Assert.ThrowsAsync<ArgumentException>(
273+
async () => await auth.GenerateSignInWithEmailLinkAsync(email, null));
274+
}
275+
276+
[Fact]
277+
public async Task SignInWithEmailLink()
278+
{
279+
var handler = new MockMessageHandler() { Response = GenerateEmailLinkResponse };
280+
var auth = this.CreateFirebaseAuth(handler);
281+
282+
var link = await auth.GenerateSignInWithEmailLinkAsync(
283+
"[email protected]", ActionCodeSettings);
284+
285+
Assert.Equal("https://mock-oob-link.for.auth.tests", link);
286+
287+
var request = NewtonsoftJsonSerializer.Instance.Deserialize<Dictionary<string, object>>(
288+
handler.LastRequestBody);
289+
Assert.Equal(10, request.Count);
290+
Assert.Equal("[email protected]", request["email"]);
291+
Assert.Equal("EMAIL_SIGNIN", request["requestType"]);
292+
Assert.True((bool)request["returnOobLink"]);
293+
294+
Assert.Equal(ActionCodeSettings.Url, request["continueUrl"]);
295+
Assert.True((bool)request["canHandleCodeInApp"]);
296+
Assert.Equal(ActionCodeSettings.DynamicLinkDomain, request["dynamicLinkDomain"]);
297+
Assert.Equal(ActionCodeSettings.IosBundleId, request["iOSBundleId"]);
298+
Assert.Equal(ActionCodeSettings.AndroidPackageName, request["androidPackageName"]);
299+
Assert.Equal(
300+
ActionCodeSettings.AndroidMinimumVersion, request["androidMinimumVersion"]);
301+
Assert.True((bool)request["androidInstallApp"]);
302+
this.AssertRequest(handler.Requests[0]);
303+
}
304+
305+
[Fact]
306+
public async Task SignInWithEmailLinkUnexpectedResponse()
307+
{
308+
var handler = new MockMessageHandler() { Response = "{}" };
309+
var auth = this.CreateFirebaseAuth(handler);
310+
311+
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
312+
async () => await auth.GenerateSignInWithEmailLinkAsync(
313+
"[email protected]", ActionCodeSettings));
314+
315+
Assert.Equal(ErrorCode.Unknown, exception.ErrorCode);
316+
Assert.Equal(AuthErrorCode.UnexpectedResponse, exception.AuthErrorCode);
317+
Assert.Equal(
318+
$"Failed to generate email action link for: [email protected]",
319+
exception.Message);
320+
Assert.NotNull(exception.HttpResponse);
321+
Assert.Null(exception.InnerException);
322+
}
323+
257324
private FirebaseAuth CreateFirebaseAuth(HttpMessageHandler handler)
258325
{
259326
var userManager = new FirebaseUserManager(new FirebaseUserManager.Args

FirebaseAdmin/FirebaseAdmin/Auth/EmailActionLinkRequest.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ internal sealed class EmailActionLinkRequest
2121
{
2222
private const string VerifyEmail = "VERIFY_EMAIL";
2323
private const string PasswordReset = "PASSWORD_RESET";
24+
private const string EmailSignIn = "EMAIL_SIGNIN";
2425

2526
private EmailActionLinkRequest(string type, string email, ActionCodeSettings settings)
2627
{
@@ -29,6 +30,12 @@ private EmailActionLinkRequest(string type, string email, ActionCodeSettings set
2930
throw new ArgumentException("Email cannot be null or empty.");
3031
}
3132

33+
if (type == EmailSignIn && settings == null)
34+
{
35+
throw new ArgumentNullException(
36+
"ActionCodeSettings must not be null when generating sign in links");
37+
}
38+
3239
this.RequestType = type;
3340
this.Email = email;
3441
if (settings != null)
@@ -87,6 +94,12 @@ internal static EmailActionLinkRequest PasswordResetLinkRequest(
8794
return new EmailActionLinkRequest(PasswordReset, email, settings);
8895
}
8996

97+
internal static EmailActionLinkRequest EmailSignInLinkRequest(
98+
string email, ActionCodeSettings settings)
99+
{
100+
return new EmailActionLinkRequest(EmailSignIn, email, settings);
101+
}
102+
90103
private void ValidateSettings()
91104
{
92105
if (string.IsNullOrEmpty(this.Url))

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,44 @@ public async Task<string> GeneratePasswordResetLinkAsync(
654654
.ConfigureAwait(false);
655655
}
656656

657+
/// <summary>
658+
/// Generates the out-of-band email action link for email link sign-in flows for the
659+
/// specified email address.
660+
/// </summary>
661+
/// <exception cref="FirebaseAuthException">If an error occurs while generating the link.</exception>
662+
/// <param name="email">The email of the user signing in.</param>
663+
/// <param name="settings">The action code settings object that defines whether
664+
/// the link is to be handled by a mobile app and the additional state information to be
665+
/// passed in the deep link.</param>
666+
/// <returns>A task that completes with the email sign in link.</returns>
667+
public async Task<string> GenerateSignInWithEmailLinkAsync(
668+
string email, ActionCodeSettings settings)
669+
{
670+
return await this.GenerateSignInWithEmailLinkAsync(email, settings, default(CancellationToken))
671+
.ConfigureAwait(false);
672+
}
673+
674+
/// <summary>
675+
/// Generates the out-of-band email action link for email link sign-in flows for the
676+
/// specified email address.
677+
/// </summary>
678+
/// <exception cref="FirebaseAuthException">If an error occurs while generating the link.</exception>
679+
/// <param name="email">The email of the user signing in.</param>
680+
/// <param name="settings">The action code settings object that defines whether
681+
/// the link is to be handled by a mobile app and the additional state information to be
682+
/// passed in the deep link.</param>
683+
/// <param name="cancellationToken">A cancellation token to monitor the asynchronous
684+
/// operation.</param>
685+
/// <returns>A task that completes with the email sign in link.</returns>
686+
public async Task<string> GenerateSignInWithEmailLinkAsync(
687+
string email, ActionCodeSettings settings, CancellationToken cancellationToken)
688+
{
689+
var userManager = this.IfNotDeleted(() => this.userManager.Value);
690+
var request = EmailActionLinkRequest.EmailSignInLinkRequest(email, settings);
691+
return await userManager.GenerateEmailActionLinkAsync(request, cancellationToken)
692+
.ConfigureAwait(false);
693+
}
694+
657695
/// <summary>
658696
/// Deletes this <see cref="FirebaseAuth"/> service instance.
659697
/// </summary>

0 commit comments

Comments
 (0)