diff --git a/auth/email_action_links.go b/auth/email_action_links.go index 6b649254..30b66af5 100644 --- a/auth/email_action_links.go +++ b/auth/email_action_links.go @@ -64,9 +64,10 @@ func (settings *ActionCodeSettings) toMap() (map[string]interface{}, error) { type linkType string const ( - emailLinkSignIn linkType = "EMAIL_SIGNIN" - emailVerification linkType = "VERIFY_EMAIL" - passwordReset linkType = "PASSWORD_RESET" + emailLinkSignIn linkType = "EMAIL_SIGNIN" + emailVerification linkType = "VERIFY_EMAIL" + passwordReset linkType = "PASSWORD_RESET" + verifyAndChangeEmail linkType = "VERIFY_AND_CHANGE_EMAIL" ) // EmailVerificationLink generates the out-of-band email action link for email verification flows for the specified @@ -79,7 +80,7 @@ func (c *baseClient) EmailVerificationLink(ctx context.Context, email string) (s // specified email address, using the action code settings provided. func (c *baseClient) EmailVerificationLinkWithSettings( ctx context.Context, email string, settings *ActionCodeSettings) (string, error) { - return c.generateEmailActionLink(ctx, emailVerification, email, settings) + return c.generateEmailActionLink(ctx, emailVerification, email, settings, nil) } // PasswordResetLink generates the out-of-band email action link for password reset flows for the specified email @@ -92,18 +93,31 @@ func (c *baseClient) PasswordResetLink(ctx context.Context, email string) (strin // specified email address, using the action code settings provided. func (c *baseClient) PasswordResetLinkWithSettings( ctx context.Context, email string, settings *ActionCodeSettings) (string, error) { - return c.generateEmailActionLink(ctx, passwordReset, email, settings) + return c.generateEmailActionLink(ctx, passwordReset, email, settings, nil) } // EmailSignInLink generates the out-of-band email action link for email link sign-in flows, using the action // code settings provided. func (c *baseClient) EmailSignInLink( ctx context.Context, email string, settings *ActionCodeSettings) (string, error) { - return c.generateEmailActionLink(ctx, emailLinkSignIn, email, settings) + return c.generateEmailActionLink(ctx, emailLinkSignIn, email, settings, nil) +} + +// VerifyAndChangeEmailLink generates the out-of-band email action link for email verification and change flows for the +// specified email address. +func (c *baseClient) VerifyAndChangeEmailLink(ctx context.Context, email string, newEmail string) (string, error) { + return c.VerifyAndChangeEmailLinkWithSettings(ctx, email, newEmail, nil) +} + +// VerifyAndChangeEmailLinkWithSettings generates the out-of-band email action link for email verification and change +// flows for the specified email address, using the action code settings provided. +func (c *baseClient) VerifyAndChangeEmailLinkWithSettings( + ctx context.Context, email string, newEmail string, settings *ActionCodeSettings) (string, error) { + return c.generateEmailActionLink(ctx, verifyAndChangeEmail, email, settings, &newEmail) } func (c *baseClient) generateEmailActionLink( - ctx context.Context, linkType linkType, email string, settings *ActionCodeSettings) (string, error) { + ctx context.Context, linkType linkType, email string, settings *ActionCodeSettings, newEmail *string) (string, error) { if email == "" { return "", errors.New("email must not be empty") @@ -118,6 +132,14 @@ func (c *baseClient) generateEmailActionLink( "email": email, "returnOobLink": true, } + + if linkType == verifyAndChangeEmail { + if newEmail == nil { + return "", errors.New("newEmail must not be nil when linkType is verifyAndChangeEmail") + } + payload["newEmail"] = *newEmail + } + if settings != nil { settingsMap, err := settings.toMap() if err != nil { diff --git a/auth/email_action_links_test.go b/auth/email_action_links_test.go index 876f3300..90651875 100644 --- a/auth/email_action_links_test.go +++ b/auth/email_action_links_test.go @@ -29,6 +29,7 @@ const ( testActionLink = "https://test.link" testActionLinkFormat = `{"oobLink": %q}` testEmail = "user@domain.com" + testNewEmail = "user-new@domain.com" ) var testActionLinkResponse = []byte(fmt.Sprintf(testActionLinkFormat, testActionLink)) @@ -309,6 +310,55 @@ func TestEmailVerificationLinkError(t *testing.T) { } } +func TestVerifyAndChangeEmailLink(t *testing.T) { + s := echoServer(testActionLinkResponse, t) + defer s.Close() + + link, err := s.Client.VerifyAndChangeEmailLink(context.Background(), testEmail, testNewEmail) + if err != nil { + t.Fatal(err) + } + if link != testActionLink { + t.Errorf("TestVerifyAndChangeEmailLink() = %q; want = %q", link, testActionLink) + } + + want := map[string]interface{}{ + "requestType": "VERIFY_AND_CHANGE_EMAIL", + "email": testEmail, + "returnOobLink": true, + "newEmail": testNewEmail, + } + if err := checkActionLinkRequest(want, s); err != nil { + t.Fatalf("TestVerifyAndChangeEmailLink() %v", err) + } +} + +func TestVerifyAndChangeEmailLinkWithSettings(t *testing.T) { + s := echoServer(testActionLinkResponse, t) + defer s.Close() + + link, err := s.Client.VerifyAndChangeEmailLinkWithSettings(context.Background(), testEmail, testNewEmail, testActionCodeSettings) + if err != nil { + t.Fatal(err) + } + if link != testActionLink { + t.Errorf("VerifyAndChangeEmailLinkWithSettings() = %q; want = %q", link, testActionLink) + } + + want := map[string]interface{}{ + "requestType": "VERIFY_AND_CHANGE_EMAIL", + "email": testEmail, + "returnOobLink": true, + "newEmail": testNewEmail, + } + for k, v := range testActionCodeSettingsMap { + want[k] = v + } + if err := checkActionLinkRequest(want, s); err != nil { + t.Fatalf("checkActionLinkRequest() = %v", err) + } +} + func checkActionLinkRequest(want map[string]interface{}, s *mockAuthServer) error { wantURL := "/projects/mock-project-id/accounts:sendOobCode" return checkActionLinkRequestWithURL(want, wantURL, s) diff --git a/auth/tenant_mgt_test.go b/auth/tenant_mgt_test.go index 1e9e2b91..dc0cdaaa 100644 --- a/auth/tenant_mgt_test.go +++ b/auth/tenant_mgt_test.go @@ -569,6 +569,34 @@ func TestTenantEmailSignInLink(t *testing.T) { } } +func TestTenantVerifyAndChangeEmail(t *testing.T) { + s := echoServer(testActionLinkResponse, t) + defer s.Close() + + client, err := s.Client.TenantManager.AuthForTenant("tenantID") + if err != nil { + t.Fatalf("AuthForTenant() = %v", err) + } + + link, err := client.VerifyAndChangeEmailLink(context.Background(), testEmail, testNewEmail) + if err != nil { + t.Fatal(err) + } + if link != testActionLink { + t.Errorf("VerifyAndChangeEmailLink() = %q; want = %q", link, testActionLink) + } + + want := map[string]interface{}{ + "requestType": "VERIFY_AND_CHANGE_EMAIL", + "email": testEmail, + "returnOobLink": true, + "newEmail": testNewEmail, + } + if err := checkActionLinkRequestWithURL(want, wantEmailActionURL, s); err != nil { + t.Fatalf("checkActionLinkRequestWithURL() = %v", err) + } +} + func TestTenantOIDCProviderConfig(t *testing.T) { s := echoServer([]byte(oidcConfigResponse), t) defer s.Close() diff --git a/integration/auth/tenant_mgt_test.go b/integration/auth/tenant_mgt_test.go index 190a29f4..1a67180d 100644 --- a/integration/auth/tenant_mgt_test.go +++ b/integration/auth/tenant_mgt_test.go @@ -348,6 +348,23 @@ func testTenantAwareUserManagement(t *testing.T, id string) { } }) + t.Run("VerifyAndChangeEmailLink()", func(t *testing.T) { + newEmail := "new-" + want.Email + link, err := tenantClient.VerifyAndChangeEmailLink(context.Background(), want.Email, newEmail) + if err != nil { + t.Fatalf("VerifyAndChangeEmailLink() = %v", err) + } + + tenant, err := extractTenantID(link) + if err != nil { + t.Fatalf("extractTenantID(%s) = %v", link, err) + } + + if id != tenant { + t.Fatalf("VerifyAndChangeEmailLink() TenantID = %q; want = %q", tenant, id) + } + }) + t.Run("RevokeRefreshTokens()", func(t *testing.T) { validSinceMillis := time.Now().Unix() * 1000 time.Sleep(1 * time.Second)