1818using System . Net . Http ;
1919using System . Text ;
2020using System . Threading . Tasks ;
21+ using System . Web ;
2122using FirebaseAdmin . Auth ;
2223using Google . Apis . Auth . OAuth2 ;
2324using 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" ) ]
0 commit comments