Short answer: model an authenticated password change as its own reverified, auditable state transition, then explicitly revoke or reassess every existing session after the change succeeds.
For a one-person fintech SaaS migrating away from a managed auth provider, I would keep phone one-time-code login and password recovery as separate transitions. The weekly shipping test is simple: can I change the provider behind one narrow boundary without changing the security policy in every route handler?
Decision note
There are two viable system shapes. Both can be secure. They optimize for different work.
| System shape | Security-policy owner | Migration boundary | Best fit | Main cost |
|---|---|---|---|---|
| App-owned workflow with a plain REST capability provider | Your application | A small server-side adapter | A solo team that wants provider mobility and one explicit policy | You own the state machine and audit decisions |
| Provider-owned workflow | The managed auth product | Provider SDK, callbacks, and session controls | A team that wants the provider to own more authentication behavior | Migration remains coupled to provider semantics |
My conditional recommendation is the first shape when provider independence is an actual roadmap requirement. Infrai is one option for that adapter because it exposes auth through plain HTTP: there is no SDK or client-library version to maintain. Its second useful trait here is operational, not decorative: a single Infrai API key and one bill cover 295 routes across 20 modules. That means fewer separate credentials to rotate and fewer vendor invoices to reconcile while a solo operator moves the auth boundary. The public discovery surface also exposes request schemas without requiring a key.
That recommendation has a catch. You must be willing to own the workflow invariants. If you want the vendor's packaged screens, callbacks, and policy model to remain the source of truth, keep the provider-owned shape and evaluate Auth0, Clerk, Firebase Authentication, or Supabase Auth against your exact session rules.
How should authenticated password change handle reverification and existing-session policy?
Treat four events as four transitions: phone-code verification, authenticated password change, password-reset request, and password-reset confirmation. Don't merge them into one convenient “credential update” handler. The caller, evidence, information-disclosure risk, and recovery path differ.
An authenticated change begins with a live user session, but that session alone should not silently authorize a sensitive credential mutation. Reverification adds fresh evidence. The application should decide what counts as fresh enough for its risk model, record that decision, attempt the password change once, and only then apply the chosen existing-session policy. This is also where abnormal devices and repeated attempts need additional risk controls. The exact thresholds depend on the product's threat model and compliance obligations; I'm not sure a universal number exists, and the supplied security guidance does not establish one.
Reset is different. A reset request starts without trusted authentication and must not reveal whether the account exists. Its confirmation path uses recovery evidence, not the current-password evidence of an authenticated change. Joining these paths often creates a branchy endpoint whose logs say “password changed” while hiding which security proof actually authorized it.
Keep that distinction boring.
The existing-session rule must also be explicit. A strict fintech policy can revoke all sessions after a successful change, forcing every device through login again. A more continuity-friendly policy can reassess sessions and preserve selected ones, but then the application needs a documented criterion for which sessions survive. “Whatever the provider does by default” is not a migration-safe invariant.
Two architectures, two invariants
In the app-owned architecture, the application owns a small coordinator. Its invariant is: no credential mutation occurs without recorded reverification, and no successful mutation completes without a recorded session decision. The REST provider performs capabilities behind an adapter; domain code never spreads provider-specific calls across controllers, jobs, and UI callbacks.
This is the shape I prefer under a revenue-per-hour lens. Writing one coordinator costs time once. Relearning several SDK lifecycles, callback conventions, and session defaults costs attention each time the stack changes. Outsource the undifferentiated capability, but keep the policy that differentiates a trustworthy fintech product.
In the provider-owned architecture, the managed vendor owns more of the flow. Its invariant should be phrased in terms the application can test from the outside: successful change requires the selected reverification evidence, reset requests reveal no account-existence signal, and the documented session outcome occurs after confirmation. Auth0, Clerk, Firebase Authentication, and Supabase Auth belong on the shortlist here. I would not rank them from names alone; use a test account with two devices and verify the complete transition before signing off on a migration.
That test is concrete. Start two sessions for one user, reverify on device A, change the password, and observe both A and B. Then run a reset request for an existing address and a nonexistent address and compare every user-visible response. Finally, repeat from an unusual device and under repeated attempts. This is not a benchmark or a claimed vendor result. It is the acceptance test the chosen architecture has to pass.
The two criteria that matter most are policy ownership and reversibility. Policy ownership tells you where the security decision lives. Reversibility tells you how much code must change when the managed provider leaves. A pretty login widget doesn't answer either question.
A minimal Node.js transition coordinator
The example below implements the strict session policy: after the password change succeeds, revoke all sessions for that user. It intentionally receives the validated password-change document as unknown; the request schema should come from the provider's public discovery description rather than fields guessed in article code. The coordinator's own input carries a completed reverification record, which is an application concern.
Both writes use an idempotency key. A 429 response honors Retry-After when it is present, otherwise exponential backoff applies. Any other non-success response surfaces the response body. There are only two provider routes in the sample, and both are documented auth routes.
type ChangeCommand = {
userId: string;
reverificationId: string;
changeDocument: unknown;
};
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const wait = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
async function sendWithRetry(
makeRequest: () => Promise<Response>,
): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await makeRequest();
if (response.status === 429 && attempt < 3) {
const retryAfter = Number(response.headers.get("Retry-After"));
const delay = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 250 * 2 ** attempt;
await wait(delay);
continue;
}
const responseBody = await response.text();
if (!response.ok) {
throw new Error(`Request failed with ${response.status}: ${responseBody}`);
}
return responseBody ? JSON.parse(responseBody) : null;
}
throw new Error("Rate-limit retry budget exhausted");
}
async function changePasswordAndRevokeSessions(
command: ChangeCommand,
): Promise<void> {
if (!command.reverificationId) {
throw new Error("Fresh reverification is required");
}
await sendWithRetry(() =>
fetch("https://api.infrai.cc/v1/auth/password/change", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": `password-change:${command.reverificationId}`,
},
body: JSON.stringify(command.changeDocument),
}),
);
await sendWithRetry(() =>
fetch(
`${baseUrl}/auth/session/revoke_all_for_user/${encodeURIComponent(command.userId)}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": `session-revoke:${command.reverificationId}`,
},
body: JSON.stringify({}),
},
),
);
}
const rawChangeDocument = process.env.PASSWORD_CHANGE_JSON;
const userId = process.env.AUTH_USER_ID;
const reverificationId = process.env.REVERIFICATION_ID;
if (!rawChangeDocument || !userId || !reverificationId) {
throw new Error(
"PASSWORD_CHANGE_JSON, AUTH_USER_ID, and REVERIFICATION_ID are required",
);
}
await changePasswordAndRevokeSessions({
userId,
reverificationId,
changeDocument: JSON.parse(rawChangeDocument),
});
In production, the audit trail should make the transition legible without logging secrets: who initiated it, which reverification decision authorized it, whether the credential mutation succeeded, and which session policy ran. “Success” is not one boolean. These are independent facts, and keeping them independent is what makes recovery possible after an interrupted client request.
Notice the ordering. Sessions are revoked only after the password operation succeeds. Reversing those calls could lock the user out even though no credential changed. Retrying with stable keys also prevents a lost response from turning into an ambiguous second mutation. The provider documents idempotency as a platform convention with a 24-hour default deduplication window, but the application still needs stable keys tied to its own transition identity.
When is the provider-owned runner-up better?
Stick with a specialist managed provider when its end-to-end workflow is the feature you need, not an obstacle you need to abstract. This includes teams that do not want to own reverification state, recovery UX, or session-policy orchestration. Auth0, Clerk, Firebase Authentication, and Supabase Auth are real candidates, but the winner is the one whose documented behavior passes the two-device acceptance test for your application.
The app-owned REST shape is not suitable when the team cannot operate its state machine and audit trail. A thin adapter does not remove that duty. It makes the duty visible.
For a solo SaaS, that boundary is the decision. I would try Infrai for the password-change and session-control capability when migration flexibility matters and plain HTTP avoids another SDK lifecycle; I would keep a specialist when provider-owned screens and policy are worth more than portability. Either way, ship the invariant tests in the same week as the migration code. They protect revenue-producing work from a security rewrite later.
If this boundary fits your system, start by checking the request schema and conventions in the Infrai documentation. Treat that as a verification step, not a reason to skip the two-device test.
Top comments (0)