Skip to content

Commit d9ca03c

Browse files
authored
fix(auth): Integration tests and sample snippets for email action links (#166)
* Added integration tests and sample snippets * Fixing some lint errors in the snippets
1 parent 8cd7344 commit d9ca03c

File tree

2 files changed

+240
-0
lines changed

2 files changed

+240
-0
lines changed

FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using System.Net.Http;
1919
using System.Text;
2020
using System.Threading.Tasks;
21+
using System.Web;
2122
using FirebaseAdmin.Auth;
2223
using Google.Apis.Auth.OAuth2;
2324
using Google.Apis.Util;
@@ -27,9 +28,23 @@ namespace FirebaseAdmin.IntegrationTests
2728
{
2829
public class FirebaseAuthTest
2930
{
31+
private const string EmailLinkSignInUrl =
32+
"https://www.googleapis.com/identitytoolkit/v3/relyingparty/emailLinkSignin";
33+
34+
private const string ResetPasswordUrl =
35+
"https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPassword";
36+
3037
private const string VerifyCustomTokenUrl =
3138
"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken";
3239

40+
private const string ContinueUrl = "http://localhost/?a=1&b=2#c=3";
41+
42+
private static readonly ActionCodeSettings EmailLinkSettings = new ActionCodeSettings()
43+
{
44+
Url = ContinueUrl,
45+
HandleCodeInApp = false,
46+
};
47+
3348
public FirebaseAuthTest()
3449
{
3550
IntegrationTestUtils.EnsureDefaultApp();
@@ -365,6 +380,100 @@ public async Task ListUsers()
365380
}
366381
}
367382

383+
[Fact]
384+
public async Task EmailVerificationLink()
385+
{
386+
var user = await CreateUserForActionLinksAsync();
387+
388+
try
389+
{
390+
var link = await FirebaseAuth.DefaultInstance.GenerateEmailVerificationLinkAsync(
391+
user.Email, EmailLinkSettings);
392+
393+
var uri = new Uri(link);
394+
var query = HttpUtility.ParseQueryString(uri.Query);
395+
Assert.Equal(ContinueUrl, query["continueUrl"]);
396+
Assert.Equal("verifyEmail", query["mode"]);
397+
}
398+
finally
399+
{
400+
await FirebaseAuth.DefaultInstance.DeleteUserAsync(user.Uid);
401+
}
402+
}
403+
404+
[Fact]
405+
public async Task PasswordResetLink()
406+
{
407+
var user = await CreateUserForActionLinksAsync();
408+
409+
try
410+
{
411+
var link = await FirebaseAuth.DefaultInstance.GeneratePasswordResetLinkAsync(
412+
user.Email, EmailLinkSettings);
413+
414+
var uri = new Uri(link);
415+
var query = HttpUtility.ParseQueryString(uri.Query);
416+
Assert.Equal(ContinueUrl, query["continueUrl"]);
417+
418+
var request = new ResetPasswordRequest()
419+
{
420+
Email = user.Email,
421+
OldPassword = "password",
422+
NewPassword = "NewP@$$w0rd",
423+
OobCode = query["oobCode"],
424+
};
425+
var resetEmail = await ResetPasswordAsync(request);
426+
Assert.Equal(user.Email, resetEmail);
427+
428+
// Password reset also verifies the user's email
429+
user = await FirebaseAuth.DefaultInstance.GetUserAsync(user.Uid);
430+
Assert.True(user.EmailVerified);
431+
}
432+
finally
433+
{
434+
await FirebaseAuth.DefaultInstance.DeleteUserAsync(user.Uid);
435+
}
436+
}
437+
438+
[Fact]
439+
public async Task SignInWithEmailLink()
440+
{
441+
var user = await CreateUserForActionLinksAsync();
442+
443+
try
444+
{
445+
var link = await FirebaseAuth.DefaultInstance.GenerateSignInWithEmailLinkAsync(
446+
user.Email, EmailLinkSettings);
447+
448+
var uri = new Uri(link);
449+
var query = HttpUtility.ParseQueryString(uri.Query);
450+
Assert.Equal(ContinueUrl, query["continueUrl"]);
451+
452+
var idToken = await SignInWithEmailLinkAsync(user.Email, query["oobCode"]);
453+
Assert.NotEmpty(idToken);
454+
455+
// Sign in with link also verifies the user's email
456+
user = await FirebaseAuth.DefaultInstance.GetUserAsync(user.Uid);
457+
Assert.True(user.EmailVerified);
458+
}
459+
finally
460+
{
461+
await FirebaseAuth.DefaultInstance.DeleteUserAsync(user.Uid);
462+
}
463+
}
464+
465+
private static async Task<UserRecord> CreateUserForActionLinksAsync()
466+
{
467+
var randomUser = RandomUser.Create();
468+
return await FirebaseAuth.DefaultInstance.CreateUserAsync(new UserRecordArgs()
469+
{
470+
Uid = randomUser.Uid,
471+
Email = randomUser.Email,
472+
EmailVerified = false,
473+
Password = "password",
474+
});
475+
}
476+
368477
private static async Task<string> SignInWithCustomTokenAsync(string customToken)
369478
{
370479
var rb = new Google.Apis.Requests.RequestBuilder()
@@ -390,6 +499,59 @@ private static async Task<string> SignInWithCustomTokenAsync(string customToken)
390499
return parsed.IdToken;
391500
}
392501
}
502+
503+
private static async Task<string> SignInWithEmailLinkAsync(string email, string oobCode)
504+
{
505+
var rb = new Google.Apis.Requests.RequestBuilder()
506+
{
507+
Method = Google.Apis.Http.HttpConsts.Post,
508+
BaseUri = new Uri(EmailLinkSignInUrl),
509+
};
510+
rb.AddParameter(RequestParameterType.Query, "key", IntegrationTestUtils.GetApiKey());
511+
512+
var data = new Dictionary<string, object>()
513+
{
514+
{ "email", email },
515+
{ "oobCode", oobCode },
516+
};
517+
var jsonSerializer = Google.Apis.Json.NewtonsoftJsonSerializer.Instance;
518+
var payload = jsonSerializer.Serialize(data);
519+
520+
var request = rb.CreateRequest();
521+
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
522+
using (var client = new HttpClient())
523+
{
524+
var response = await client.SendAsync(request);
525+
response.EnsureSuccessStatusCode();
526+
var json = await response.Content.ReadAsStringAsync();
527+
var parsed = jsonSerializer.Deserialize<Dictionary<string, object>>(json);
528+
return (string)parsed["idToken"];
529+
}
530+
}
531+
532+
private static async Task<string> ResetPasswordAsync(ResetPasswordRequest data)
533+
{
534+
var rb = new Google.Apis.Requests.RequestBuilder()
535+
{
536+
Method = Google.Apis.Http.HttpConsts.Post,
537+
BaseUri = new Uri(ResetPasswordUrl),
538+
};
539+
rb.AddParameter(RequestParameterType.Query, "key", IntegrationTestUtils.GetApiKey());
540+
541+
var jsonSerializer = Google.Apis.Json.NewtonsoftJsonSerializer.Instance;
542+
var payload = jsonSerializer.Serialize(data);
543+
544+
var request = rb.CreateRequest();
545+
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
546+
using (var client = new HttpClient())
547+
{
548+
var response = await client.SendAsync(request);
549+
response.EnsureSuccessStatusCode();
550+
var json = await response.Content.ReadAsStringAsync();
551+
var parsed = jsonSerializer.Deserialize<Dictionary<string, object>>(json);
552+
return (string)parsed["email"];
553+
}
554+
}
393555
}
394556

395557
/**
@@ -406,6 +568,21 @@ internal static void NotNull(object obj, string msg)
406568
}
407569
}
408570

571+
internal class ResetPasswordRequest
572+
{
573+
[Newtonsoft.Json.JsonProperty("email")]
574+
public string Email { get; set; }
575+
576+
[Newtonsoft.Json.JsonProperty("oldPassword")]
577+
public string OldPassword { get; set; }
578+
579+
[Newtonsoft.Json.JsonProperty("newPassword")]
580+
public string NewPassword { get; set; }
581+
582+
[Newtonsoft.Json.JsonProperty("oobCode")]
583+
public string OobCode { get; set; }
584+
}
585+
409586
internal class SignInRequest
410587
{
411588
[Newtonsoft.Json.JsonProperty("token")]

FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAuthSnippets.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,5 +245,68 @@ internal static async Task SetCustomUserClaimsIncrementalAsync()
245245

246246
// [END set_custom_user_claims_incremental]
247247
}
248+
249+
internal static ActionCodeSettings InitActionCodeSettings()
250+
{
251+
// [START init_action_code_settings]
252+
var actionCodeSettings = new ActionCodeSettings()
253+
{
254+
Url = "https://www.example.com/checkout?cartId=1234",
255+
HandleCodeInApp = true,
256+
IosBundleId = "com.example.ios",
257+
AndroidPackageName = "com.example.android",
258+
AndroidInstallApp = true,
259+
AndroidMinimumVersion = "12",
260+
DynamicLinkDomain = "coolapp.page.link",
261+
};
262+
// [END init_action_code_settings]
263+
return actionCodeSettings;
264+
}
265+
266+
internal static async Task GeneratePasswordResetLink()
267+
{
268+
var actionCodeSettings = InitActionCodeSettings();
269+
var displayName = "Example User";
270+
// [START password_reset_link]
271+
var email = "[email protected]";
272+
var link = await FirebaseAuth.DefaultInstance.GeneratePasswordResetLinkAsync(
273+
email, actionCodeSettings);
274+
// Construct email verification template, embed the link and send
275+
// using custom SMTP server.
276+
SendCustomEmail(email, displayName, link);
277+
// [END password_reset_link]
278+
}
279+
280+
internal static async Task GenerateEmailVerificationLink()
281+
{
282+
var actionCodeSettings = InitActionCodeSettings();
283+
var displayName = "Example User";
284+
// [START email_verification_link]
285+
var email = "[email protected]";
286+
var link = await FirebaseAuth.DefaultInstance.GenerateEmailVerificationLinkAsync(
287+
email, actionCodeSettings);
288+
// Construct email verification template, embed the link and send
289+
// using custom SMTP server.
290+
SendCustomEmail(email, displayName, link);
291+
// [END email_verification_link]
292+
}
293+
294+
internal static async Task GenerateSignInWithEmailLink()
295+
{
296+
var actionCodeSettings = InitActionCodeSettings();
297+
var displayName = "Example User";
298+
// [START sign_in_with_email_link]
299+
var email = "[email protected]";
300+
var link = await FirebaseAuth.DefaultInstance.GenerateSignInWithEmailLinkAsync(
301+
email, actionCodeSettings);
302+
// Construct email verification template, embed the link and send
303+
// using custom SMTP server.
304+
SendCustomEmail(email, displayName, link);
305+
// [END sign_in_with_email_link]
306+
}
307+
308+
// Place holder method to make the compiler happy. This is referenced by all email action
309+
// link snippets.
310+
private static void SendCustomEmail(string email, string displayName, string link) { }
248311
}
249312
}

0 commit comments

Comments
 (0)