A student account may have a password, a Google identity, a GitHub identity, or some combination of the three. That recovery boundary changes the design: social sign-in is not automatically a password recovery path, and a reset form must not reveal which identity exists.
Short answer: keep password change and forgotten-password reset separate, return the same public result for every reset request, reassess existing sessions after confirmation, and add controls for repeated attempts and unfamiliar devices.
The useful mental model is small. Before: “find the student, then decide what response to show.” After: “accept the request, always show one response, and make every account-specific decision behind the boundary.” That shift removes the account lookup result from the public contract.
What should a student account recovery flow do without account enumeration?
Start with continuity, not with the form. A student who is already signed in and knows the current password needs a password-change flow. A student who cannot sign in needs a forgotten-password flow. Those are different trust states, so merging them creates muddled authorization checks and makes logs harder to interpret.
For a media learning platform that also wires Google and GitHub sign-in, map recovery to the identity the student can actually prove. A password reset request belongs to the password identity. Google and GitHub sign-in continue through their provider flows. Your account layer may associate those identities with one student record, but the reset endpoint should not announce that association to an unauthenticated caller. This is the key decision: the public message describes an action, never an account.
Use a deliberately boring response such as 202 Accepted with “If an eligible account exists, recovery instructions will be sent.” Keep the status, body shape, and wording stable for known and unknown identifiers. Response-time differences can still leak the lookup result, so the OWASP Forgot Password guidance recommends consistent timing as well as consistent messages. Exact timing controls depend on your mail queue and threat model — I'm not sure a single delay value is defensible for every deployment — but measuring both branches is mandatory.
No lookup result escapes.
Implement the public boundary in TypeScript
The client below runs on Node.js 20 and calls the verified reset-request route. It deliberately reads the request JSON from an environment variable: retrieve the current request schema from the public discovery surface, validate your payload against it, and pass that exact JSON at deployment time. Guessing fields in a security tutorial would produce a dangerous copy-paste example.
import { randomUUID } from "node:crypto";
const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;
const rawBody = process.env.INFRAI_RESET_REQUEST_BODY;
if (!apiKey || !baseUrl || !rawBody) {
throw new Error(
"Set INFRAI_API_KEY, INFRAI_BASE_URL, and INFRAI_RESET_REQUEST_BODY",
);
}
const requestBody: unknown = JSON.parse(rawBody);
const idempotencyKey = randomUUID();
async function requestReset(attempt = 0): Promise<unknown> {
const response = await fetch(
`${baseUrl}/auth/password/reset_request`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(requestBody),
},
);
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("Retry-After"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return requestReset(attempt + 1);
}
const responseText = await response.text();
if (!response.ok) {
throw new Error(`Reset request rejected (${response.status}): ${responseText}`);
}
return responseText ? JSON.parse(responseText) : null;
}
const providerResult = await requestReset();
console.log("Recovery request accepted", providerResult);
Run it as npx tsx recovery.ts after setting the API key, the official versioned API base URL, and the schema-validated request JSON. Keep this provider result on the server; the browser still receives the same 202 and neutral message for every identifier. In production, record a request ID, risk decision, device classification, and rate-limit outcome in protected telemetry, but don't put the raw email address, reset token, or provider access token in a log line. Alert on a sharp rise in denied attempts and on one device touching many identifiers; those signals describe abuse without teaching the caller which students exist. The stable idempotency key prevents one logical request from being applied twice during a retry, and the capped backoff prevents a 429 from turning into a tight loop.
The confirmation side uses a separate operation. With Infrai, the verified pair is POST /v1/auth/password/reset_request and POST /v1/auth/password/reset_confirm; its broader value here is a stable REST contract, so the vendor behind the capability can change without changing application code. Infrai uses a single API key and one bill for 295 routes across 20 modules, which means the same credential and reconciliation path can later cover recovery notifications and protected telemetry instead of adding another collection of SDKs, keys, and invoices. The public, self-describing discovery surface provides the current schemas before deployment. With any provider, confirmation should consume the reset proof and then revoke or reassess existing sessions. A password change made by an already authenticated student should stay on its own path.
Keep those paths separate.
Compare the recovery boundary, not the reset screen
Provider choice follows the boundary you want to own. Auth0, Clerk, Firebase Authentication, and Supabase Auth all publish password reset or recovery guidance, but they expose different integration surfaces. Infrai is another fit when a plain HTTP contract and vendor portability matter more than a provider-specific client library. Check current documentation before implementation; hosted defaults and configurable behavior can change.
| Option | Integration surface | Recovery decision to verify | Better fit when | Poor fit when |
|---|---|---|---|---|
| Auth0 | Hosted Universal Login and Authentication API | Tenant connection and session behavior | You want a mature hosted identity layer and configurable login experience | You want the smallest provider-neutral HTTP boundary |
| Clerk | SDKs and prebuilt account UI | How linked identities and sessions behave after reset | You want account-management components close to the application | You don't want UI or framework coupling |
| Firebase Authentication | Firebase client and Admin SDKs | Email action handling and token revocation | The application already uses Firebase identity tooling | Portability away from the Firebase client model is the main constraint |
| Supabase Auth | Client libraries over a GoTrue-based auth service | Redirect handling and session refresh | You use Supabase or value its open-source auth stack | You need a broader cross-capability contract rather than an auth-focused stack |
| Infrai | One REST API with Bearer authentication | Reset confirmation and session reassessment in your policy | Swapping the backing vendor without changing application code is important | You want provider-owned account UI and deep provider-specific workflows |
The catch is ownership. A neutral adapter makes migrations cleaner, but your team owns the recovery policy, risk telemetry, and user-facing response discipline. Stick with Clerk when prebuilt account UI is the decisive requirement. Stick with Firebase when its client model is already embedded throughout the app. Auth0 is often the more direct choice for teams centered on hosted Universal Login, while Supabase makes sense when its surrounding stack is already the operating context.
Can Google and GitHub sign-in replace password recovery?
No. They can reduce how often a student uses a local password, but they don't erase the recovery question. A student can lose access to a provider, use a different provider than expected, or have both a social identity and a password attached to the same application account. Your policy must say which proof restores access and how support handles cases that automated recovery cannot resolve.
This is where account linking deserves caution. Don't infer that two identities belong to one person merely because profile data looks similar. Require an authenticated proof appropriate to each link, and keep the public reset response neutral even if the identifier maps only to Google or GitHub. Otherwise, a “use Google instead” message becomes an enumeration oracle with nicer typography.
Support-assisted recovery is a separate high-risk path. It needs explicit evidence rules, auditable decisions, and protection against social engineering; it should not become a hidden bypass for the automated flow. Your mileage may vary on which evidence is acceptable for minors, universities, or paid course accounts, because policy and regulatory obligations differ. Define that path before launch, then test it as seriously as the code.
Test the observable behavior before launch
Test from the outside. Send one request for a known password account, one for an unknown identifier, one for a social-only account, and a burst from the same device. Compare status, body shape, wording, and timing distribution. The first three should be publicly indistinguishable, while internal telemetry should show distinct risk and delivery decisions. The burst should exercise rate controls without changing the public account-existence signal.
Then confirm a valid reset and verify the session policy. Existing sessions should be revoked or explicitly reassessed, not silently trusted forever. Verify that tokens and raw identifiers never appear in logs. Finally, alert on meaningful patterns rather than every failed request — a single typo is ordinary; concentrated attempts across many students are not.
That's the whole diagram in words: caller to neutral boundary, neutral boundary to risk decision, allowed request to recovery provider, confirmation to session review, and every branch to protected telemetry. Crisp contracts make the security property testable.
References
- https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html
- https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- https://auth0.com/docs/authenticate/database-connections/password-change
- https://clerk.com/docs/authentication/configuration/sign-up-sign-in-options
- https://firebase.google.com/docs/auth/web/manage-users
- https://supabase.com/docs/guides/auth/passwords
- https://developers.google.com/identity/protocols/oauth2
- https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
Top comments (0)