diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index 44fe9b7d2..c6d9f0a2a 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -1189,6 +1189,81 @@ public ApiFuture generateEmailVerificationLinkAsync( .callAsync(firebaseApp); } + /** + * Generates the out-of-band email action link for verify and change email flows for the specified + * user. + * + * @param email The email address of the user to be verified. + * @param newEmail The email address to update the user's account to. + * @return A verify and change email link. + * @throws IllegalArgumentException If either email address is null or empty. + * @throws FirebaseAuthException If an error occurs while generating the link. + */ + public String generateVerifyAndChangeEmailLink(@NonNull String email, @NonNull String newEmail) + throws FirebaseAuthException { + return generateVerifyAndChangeEmailLink(email, newEmail, null); + } + + + + /** + * Generates the out-of-band email action link for the verify and change email flow. + * + * @param email The user's current email. + * @param newEmail The user's new email. + * @param settings The action code settings. + * @return A verify and change email link. + * @throws IllegalArgumentException If email or newEmail are null or empty. + * @throws FirebaseAuthException If an error occurs while generating the link. + */ + public String generateVerifyAndChangeEmailLink( + @NonNull String email, @NonNull String newEmail, @Nullable ActionCodeSettings settings) + throws FirebaseAuthException { + return generateVerifyAndChangeEmailLinkOp(email, newEmail, settings).call(); + } + + /** + * Asynchronously generates the out-of-band email action link for the verify and change email + * flow. + * + * @param email The user's current email. + * @param newEmail The user's new email. + * @return An {@code ApiFuture} which will complete with the generated link. + * @throws IllegalArgumentException If email or newEmail are null or empty. + */ + public ApiFuture generateVerifyAndChangeEmailLinkAsync( + @NonNull String email, @NonNull String newEmail) { + return generateVerifyAndChangeEmailLinkAsync(email, newEmail, null); + } + + /** + * Asynchronously generates the out-of-band email action link for the verify and change email + * flow. + * + * @param email The user's current email. + * @param newEmail The user's new email. + * @param settings The action code settings. + * @return An {@code ApiFuture} which will complete with the generated link. + * @throws IllegalArgumentException If email or newEmail are null or empty. + */ + public ApiFuture generateVerifyAndChangeEmailLinkAsync( + @NonNull String email, @NonNull String newEmail, @Nullable ActionCodeSettings settings) { + return generateVerifyAndChangeEmailLinkOp(email, newEmail, settings).callAsync(firebaseApp); + } + + private CallableOperation generateVerifyAndChangeEmailLinkOp( + final String email, final String newEmail, final ActionCodeSettings settings) { + checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); + checkArgument(!Strings.isNullOrEmpty(newEmail), "newEmail must not be null or empty"); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected String execute() throws FirebaseAuthException { + return userManager.getEmailActionLink( + EmailLinkType.VERIFY_AND_CHANGE_EMAIL, email, newEmail, settings); + } + }; + } /** * Generates the out-of-band email action link for email link sign-in flows, using the action code * settings provided. @@ -1226,15 +1301,24 @@ public ApiFuture generateSignInWithEmailLinkAsync( private CallableOperation generateEmailActionLinkOp( final EmailLinkType type, final String email, final ActionCodeSettings settings) { + return generateEmailActionLinkOp(type, email, null, settings); + } + + private CallableOperation generateEmailActionLinkOp( + final EmailLinkType type, final String email, @Nullable final String newEmail, + final ActionCodeSettings settings) { checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty"); if (type == EmailLinkType.EMAIL_SIGNIN) { checkNotNull(settings, "ActionCodeSettings must not be null when generating sign-in links"); } + if (type == EmailLinkType.VERIFY_AND_CHANGE_EMAIL) { + checkArgument(!Strings.isNullOrEmpty(newEmail), "newEmail must not be null or empty"); + } final FirebaseUserManager userManager = getUserManager(); return new CallableOperation() { @Override protected String execute() throws FirebaseAuthException { - return userManager.getEmailActionLink(type, email, settings); + return userManager.getEmailActionLink(type, email, newEmail, settings); } }; } diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 0647084c8..7c2587e7e 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -219,10 +219,20 @@ String createSessionCookie(String idToken, String getEmailActionLink(EmailLinkType type, String email, @Nullable ActionCodeSettings settings) throws FirebaseAuthException { + return getEmailActionLink(type, email, null, settings); + } + + String getEmailActionLink(EmailLinkType type, String email, @Nullable String newEmail, + @Nullable ActionCodeSettings settings) throws FirebaseAuthException { ImmutableMap.Builder payload = ImmutableMap.builder() - .put("requestType", type.name()) - .put("email", email) - .put("returnOobLink", true); + .put("requestType", type.name()) + .put("email", email) + .put("returnOobLink", true); + + if (newEmail != null) { + payload.put("newEmail", newEmail); + } + if (settings != null) { payload.putAll(settings.getProperties()); } @@ -389,6 +399,7 @@ enum EmailLinkType { VERIFY_EMAIL, EMAIL_SIGNIN, PASSWORD_RESET, + VERIFY_AND_CHANGE_EMAIL, } static FirebaseUserManager createUserManager(FirebaseApp app, String tenantId) { diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 057754087..489414591 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -47,6 +47,7 @@ import com.google.firebase.auth.multitenancy.TenantAwareFirebaseAuth; import com.google.firebase.auth.multitenancy.TenantManager; import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseProcessEnvironment; import com.google.firebase.internal.SdkUtils; import com.google.firebase.testing.MultiRequestMockHttpTransport; @@ -1511,6 +1512,52 @@ public void testGenerateEmailVerificationLink() throws Exception { assertTrue((Boolean) parsed.get("returnOobLink")); } + @Test + public void testGenerateVerifyAndChangeEmailLink() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("generateEmailLink.json")); + String link = FirebaseAuth.getInstance() + .generateVerifyAndChangeEmailLinkAsync("test@example.com", "new@example.com").get(); + assertEquals("https://mock-oob-link.for.auth.tests", link); + checkRequestHeaders(interceptor); + + GenericJson parsed = parseRequestContent(interceptor); + assertEquals(4, parsed.size()); + assertEquals("test@example.com", parsed.get("email")); + assertEquals("new@example.com", parsed.get("newEmail")); + assertEquals("VERIFY_AND_CHANGE_EMAIL", parsed.get("requestType")); + assertTrue((Boolean) parsed.get("returnOobLink")); + } + + @Test + public void testGenerateVerifyAndChangeEmailLinkWithSettings() throws Exception { + TestResponseInterceptor interceptor = initializeAppForUserManagement( + TestUtils.loadResource("generateEmailLink.json")); + + // Create custom settings with a continue URL + ActionCodeSettings settings = ActionCodeSettings.builder() + .setUrl("https://example.com/continue") + .setHandleCodeInApp(true) + .build(); + + String link = FirebaseAuth.getInstance() + .generateVerifyAndChangeEmailLinkAsync("test@example.com", + "new@example.com", settings).get(); + + assertEquals("https://mock-oob-link.for.auth.tests", link); + checkRequestHeaders(interceptor); + + GenericJson parsed = parseRequestContent(interceptor); + // We expect 6 fields now because of the newEmail and ActionCodeSettings properties. + assertEquals(6, parsed.size()); + assertEquals("test@example.com", parsed.get("email")); + assertEquals("new@example.com", parsed.get("newEmail")); + assertEquals("VERIFY_AND_CHANGE_EMAIL", parsed.get("requestType")); + assertTrue((Boolean) parsed.get("returnOobLink")); + assertEquals("https://example.com/continue", parsed.get("continueUrl")); + assertTrue((Boolean) parsed.get("canHandleCodeInApp")); + } + @Test public void testGenerateESignInWithEmailLinkNoEmail() throws Exception { initializeAppForUserManagement(); @@ -1529,6 +1576,7 @@ public void testGenerateESignInWithEmailLinkNoEmail() throws Exception { } } + @Test public void testGenerateESignInWithEmailLinkNullSettings() throws Exception { initializeAppForUserManagement(); @@ -2864,6 +2912,7 @@ public void testTenantAwareDeleteSamlProviderConfig() throws Exception { checkUrl(interceptor, "DELETE", expectedUrl); } + @Test public void testCreateOidcProviderFromEmulatorAuth() throws Exception { FirebaseProcessEnvironment.setenv("FIREBASE_AUTH_EMULATOR_HOST", AUTH_EMULATOR);