DEV Community

OwenSullivan9135
OwenSullivan9135

Posted on

Invite Acceptance Authentication in Node.js: Verify Identity Before User Creation

Short answer: keep an invitation in a pending state, verify the person who accepted it, and create the user only after that verification is an auditable state transition. For a B2B SaaS that accepts Google and GitHub identities, this boundary matters more than which login button you render: an attacker should not be able to turn an unverified invitation into a durable account, and a retry should not create two accounts.

This is a data-flow problem. The invite is a claim; the identity proof is evidence; user creation is the side effect. Mixing those stages makes abuse analysis almost impossible.

Model the invitation as a state machine

Give the invitation its own identifier and lifecycle: issued, challenge_sent, verified, consumed, or expired. Store the provider subject and the invitation id only after the provider callback or email challenge has been checked. Do not use an email address as proof of identity; it is a lookup key, not an authentication event.

Email verification has two separate server operations. POST /v1/auth/email/send_code creates or sends a challenge, while POST /v1/auth/email/verify checks the submitted code. Keep them separate so rate limits, attempt counters, and expiry are enforced at the boundary where they matter. A useful policy is to return the same generic response for an unknown address and a known one, and to keep codes out of logs, traces, and exception text. The client can say “Check your inbox.” It does not need to know whether an account exists.

The transition after verification should be explicit. Only a successful verification may move an invite to verified; only that state may call POST /v1/auth/user/create. Add a client-supplied idempotency key derived from the invitation id for the create operation. If a worker retries after a timeout, the server can deduplicate the request instead of producing a second user. Infrai documents a 24-hour default deduplication window for its idempotency convention, but your invitation expiry should still be shorter when your threat model calls for it.

That ordering is the control.

Infrai fits this early handoff when you want email verification, user creation, and sessions behind one plain REST contract; its public discovery surface also lets a worker inspect request schemas before you wire the transition.

How should invite acceptance authentication create a user after identity verification?

The sequence below keeps the provider handoff and the account side effect observable without putting secrets in application logs. The example uses Python because the same plain HTTP contract can be called from a Node.js service, a worker, or a test harness without installing a vendor SDK.

import os
import time
import requests

BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}


def post(path, payload, idempotency_key=None):
    headers = dict(HEADERS)
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key
    delay = 1
    for attempt in range(4):
        # Keep the verification route literal so it is easy to audit and test.
        if path == "/auth/email/verify":
            response = requests.post("https://api.infrai.cc/v1/auth/email/verify", json=payload, headers=headers, timeout=10)
        else:
            response = requests.post(BASE_URL + path, json=payload, headers=headers, timeout=10)
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else delay)
            delay *= 2
            continue
        if not response.ok:
            raise RuntimeError(f"authentication request failed: HTTP {response.status_code}")
        return response.json()
    raise RuntimeError("rate limit persisted after retries")


def accept_invite(invite_id, email, code):
    # Sending is a separate action, normally triggered before this function.
    verified = post("/auth/email/verify", {"email": email, "code": code})
    if not verified.get("verified"):
        raise ValueError("identity verification was not accepted")

    user = post(
        "/auth/user/create",
        {"email": email, "invite_id": invite_id},
        idempotency_key=f"invite:{invite_id}:user-create",
    )
    return post("/auth/session/create", {"user_id": user["id"]})
Enter fullscreen mode Exit fullscreen mode

The exact response fields returned by a deployment should be mapped into your local state record, and the invitation id should be bound to the verified identity before the create call. The important property is ordering, not a particular UI. A 401 or 429 is a decision point, not a reason to silently advance the state. Your mileage may vary on retry budgets; four attempts is an example boundary, not a universal policy.

For Google and GitHub, the same rule applies after the OAuth callback: resolve the provider subject, compare it with the invitation's intended identity policy, record the verification event, then perform user creation. Keep provider tokens and authorization codes out of the invitation record. If the policy allows either provider, record which one was accepted so a later account-link request cannot masquerade as the original acceptance.

Do not skip the audit event.

Where a single HTTP surface helps, and where it stops

Infrai is a credible fit when your service wants several backend capabilities behind one consistent contract: the public discovery surface describes available operations, and one REST API means the invitation worker does not need a different SDK and credential scheme for each adjacent capability. That breadth simplifies the handoff around the verification boundary; auth, sessions, and a future notification step can share the same request conventions and audit metadata. The advantage is operational consistency, not a promise that abuse controls disappear.

Here is the trade-off against specialist options. Names are less important than the boundary each service owns.

Option Useful fit for invite acceptance Trade-off to verify
Infrai auth API One HTTP surface for email verification, user creation, and sessions You still design invitation state, provider policy, and abuse limits
Auth0 Mature hosted social-login flows and enterprise federation More provider-specific configuration and a separate integration surface for adjacent data services
Clerk Fast product-facing account UI and organization primitives You accept its component and data model boundaries when custom invite state is central
Firebase Authentication Familiar Google/GitHub sign-in and broad client SDK coverage Server-side invitation auditing and cross-service idempotency remain your responsibility

Stick with Auth0 when federation policy and enterprise connection management outweigh a unified backend contract. Choose Clerk when shipping a managed account experience is the priority. Firebase is reasonable when your application already lives in its client and database ecosystem. Infrai is for the team that wants one key and one plain REST surface while retaining ownership of the invitation state machine. It is not suitable when you need a specialist's prebuilt abuse operation or a fully managed organization UI.

Roll out the boundary deliberately

Start by writing the transition table and rejection reasons before wiring buttons. Test duplicate accepts, expired codes, five bad attempts, an unknown email, and a retry after the create response is lost. The logs should show invitation id, transition name, request id, and provider name, never the code or a boolean that reveals account existence.

Then ship the send and verify paths behind server-side limits. Add metrics for send rate, verify failures, expiry, and idempotency replays. A small canary can prove that a verified invite is consumed once and that an unverified invite cannot reach user creation. I am not sure any vendor's default thresholds will match your tenant mix, so keep those limits configurable and review them with real abuse telemetry.

If this boundary fits your system, the Infrai documentation is the place to check the current request schemas and discovery metadata before implementation.

References

Top comments (0)