Skip to content

beforeEmailSent is triggered by Admin SDK generatePasswordResetLink despite returnOobLink #1909

Description

@boraberkcetintas

Description

beforeEmailSent is invoked for password reset links generated by the Firebase Admin SDK via generatePasswordResetLink(), even though no Firebase-managed email is sent.

This makes it impossible to block Firebase-managed password reset emails while still using Admin SDK-generated password reset links for a custom email delivery flow.

Why this seems unexpected

The Admin SDK documentation recommends generatePasswordResetLink() for custom password reset emails:

https://firebase.google.com/docs/auth/admin/email-action-links#generate_password_reset_email_link

In the Node Admin SDK, generatePasswordResetLink() calls the Identity Toolkit accounts:sendOobCode endpoint with returnOobLink: true.

The Identity Platform REST docs describe returnOobLink as returning the OOB link instead of sending an email:

https://cloud.google.com/identity-platform/docs/reference/rest/v1/accounts/sendOobCode

Whether the confirmation link containing the OOB code should be returned in the response (no email is sent).

Because beforeEmailSent is documented and named as a trigger that runs before an email is sent, I would not expect it to block OOB link generation when returnOobLink: true.

Minimal reproduction

  1. Deploy a beforeEmailSent blocking function:
import {
  beforeEmailSent,
  HttpsError,
} from "firebase-functions/v2/identity";

export const beforeAuthEmailSent = beforeEmailSent((event) => {
  if (event.emailType === "PASSWORD_RESET") {
    throw new HttpsError(
      "permission-denied",
      "Firebase-managed password reset email disabled.",
    );
  }
});
  1. From a trusted backend using the Admin SDK, generate a custom password reset link:
import { getAuth } from "firebase-admin/auth";

const link = await getAuth().generatePasswordResetLink("user@example.com", {
  url: "https://example.com/reset",
  handleCodeInApp: false,
});
  1. The link generation fails because the beforeEmailSent function is invoked and blocks the operation.

Expected behavior

Either:

  1. beforeEmailSent should not be invoked when sendOobCode is called with returnOobLink: true, because no Firebase-managed email is being sent.

Or:

  1. If this behavior is intentional, the AuthBlockingEvent exposed to beforeEmailSent should include enough context to distinguish a Firebase-managed email send from Admin/backend OOB link generation.

Useful context could include one or more of:

  • whether returnOobLink was true
  • whether the request was made with OAuth/service-account credentials
  • whether the request came from an API-key client SDK flow vs a trusted backend/Admin SDK flow
  • a request source/caller type

Actual behavior

The function receives only event.emailType === "PASSWORD_RESET" for this case, so the handler cannot safely distinguish:

  • a public client/API-key password reset email send, which I want to block
  • a trusted backend/Admin SDK link generation request, which I want to allow and send through my own email provider

Impact

For applications that need custom password reset email delivery and stronger anti-abuse controls, this creates a conflict:

  • Allowing PASSWORD_RESET in beforeEmailSent leaves Firebase-managed password reset email sending available through the public Firebase Auth API.
  • Blocking PASSWORD_RESET also breaks Admin SDK custom password reset link generation.

As a result, the only reliable workaround appears to be implementing a fully custom password reset token flow and using admin.auth().updateUser(uid, { password }), instead of using Firebase OOB password reset links.

Notes

This may require an Identity Platform backend change, not only a change in this SDK. However, this repository is where the beforeEmailSent API and AuthBlockingEvent developer surface are exposed, so I am reporting it here first.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions