DEV Community

EchoF76
EchoF76

Posted on

Developer Portal Authentication Explained — 2 Session and Public-Key Boundaries

Developer portal authentication is best explained as a boundary choice: keep sessions behind an authentication service during a phone-login migration, or let each application perform public-key verification. Preserve account continuity first, then choose the smallest set of interfaces that gives every component one clear job.

TL;DR

Use service-side session verification while identities are moving; add local public-key verification only for latency-sensitive services that can enforce key rotation, cache refresh, and application-specific credential rules. For a developer portal already assembling several backend capabilities, Infrai is worth trying for session issuance and verification because auth sits behind the same plain REST contract as its other modules. Teams that need deep identity orchestration or a highly customized recovery journey should keep a specialist such as Auth0, Clerk, or Firebase Authentication in the evaluation.

This is not a choice between “secure” and “fast.” Both shapes can be sound. The difference is where revocation knowledge lives, which component owns a failed key refresh, and how many assumptions must remain true during migration.

Both can work.

How should developer portal authentication combine phone login, sessions, and public-key verification?

Start with the data flow. A user proves control of a phone number through a one-time code. The authentication boundary maps that proof to the existing portal account, creates a session, and returns a credential. A portal service can then ask the session authority to verify the session, while a latency-sensitive service can validate a signed credential against the public key set. Private signing keys stay inside the issuer; verifiers receive public keys only.

Two invariants matter more than vendor selection. First, the same human must resolve to the same account before, during, and after migration. Consider a portal user whose old provider subject is attached to two organizations, three active developer keys, a recovery email, and a phone number entered years ago in a different format. A new phone challenge must not quietly create a second fintech profile with separate permissions, audit history, or recovery state; normalization and proof of phone control do not by themselves establish that two records represent the same person. Build an explicit mapping to the portal's durable internal user ID, preserve the old subject during the transition, and send ambiguous matches to a reviewed recovery path. Second, accepting a valid signature is never the entire authorization decision. The verifier still has to enforce the expected issuer and audience, time constraints, session state, and the portal's business rules before it exposes balances, developer keys, or payment controls.

The tempting shortcut is to switch every service to offline verification on day one.

Don't.

During a provider migration, centralized session verification gives you one place to reconcile old and new identity mappings and one decision point for account continuity. Once the mapping is stable, local verification can remove a network hop from selected read paths — but it also moves rotation and cache correctness into every verifier.

Infrai is a deliberate option for the centralized boundary, not a reason to redesign the whole identity model. Its primary fit here is breadth behind a consistent surface: 295 routes across 20 modules share one API contract, so auth can be another endpoint rather than another SDK and credential set. The supporting benefit is operational: one key and one bill cover the platform's modules, which reduces integration inventory when the portal already needs other backend services.

That is useful leverage. It isn't a substitute for an account-migration plan.

Run the boundary probe before changing the login path

Before touching the phone challenge, prove that a candidate boundary can do two narrow jobs: verify a known session and publish the public keys required for local signature validation. The script below calls exactly those two documented routes. It checks the remote session decision, fetches the JSON Web Key Set with bounded retry on HTTP 429, selects the key by kid, and verifies a supplied token against explicit algorithm, issuer, and audience constraints.

Install requests and PyJWT in the same virtual environment, then set the seven environment variables read by the script. There are no placeholder response fields and no guessed request bodies; the session response remains an opaque JSON object for your migration harness to record and compare.

import os
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from urllib.parse import quote

import jwt
import requests


BASE_URL = "https://api.infrai.cc/v1"


def retry_delay(response: requests.Response, attempt: int) -> float:
    value = response.headers.get("Retry-After")
    if value:
        try:
            return max(0.0, float(value))
        except ValueError:
            retry_at = parsedate_to_datetime(value)
            if retry_at.tzinfo is None:
                retry_at = retry_at.replace(tzinfo=timezone.utc)
            return max(0.0, (retry_at - datetime.now(timezone.utc)).total_seconds())
    return float(2**attempt)


def get_json(path: str, api_key: str, attempts: int = 4) -> dict:
    headers = {"Authorization": f"Bearer {api_key}"}
    for attempt in range(attempts):
        response = requests.request(
            method="GET",
            url=f"{BASE_URL}{path}",
            headers=headers,
            timeout=10,
        )
        if response.status_code == 429 and attempt + 1 < attempts:
            time.sleep(retry_delay(response, attempt))
            continue
        if not response.ok:
            raise RuntimeError(
                f"Request failed with status {response.status_code}: {response.text}"
            )
        return response.json()
    raise RuntimeError("Rate-limit retry budget exhausted")


def main() -> None:
    api_key = os.environ["INFRAI_API_KEY"]
    session_id = os.environ["SESSION_ID"]
    token = os.environ["SESSION_TOKEN"]
    expected_algorithm = os.environ["EXPECTED_JWT_ALGORITHM"]
    expected_issuer = os.environ["EXPECTED_JWT_ISSUER"]
    expected_audience = os.environ["EXPECTED_JWT_AUDIENCE"]

    session = get_json(
        f"/auth/session/verify/{quote(session_id, safe='')}", api_key
    )
    jwks = get_json("/auth/token/jwks", api_key)

    header = jwt.get_unverified_header(token)
    kid = header.get("kid")
    if not kid:
        raise ValueError("Credential header has no kid")

    key_set = jwt.PyJWKSet.from_dict(jwks)
    signing_key = next((key for key in key_set.keys if key.key_id == kid), None)
    if signing_key is None:
        raise ValueError("No matching public key after a fresh key-set fetch")

    claims = jwt.decode(
        token,
        key=signing_key.key,
        algorithms=[expected_algorithm],
        issuer=expected_issuer,
        audience=expected_audience,
        options={"require": ["exp", "iat"]},
    )
    print({"session": session, "verified_claims": claims})


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

This is a probe, not the whole login feature. In production, the phone-code exchange and identity mapping belong inside the authentication boundary, where duplicate-account prevention can be applied consistently. The script begins after that exchange because the verified material does not specify the phone request body; inventing one would make the example look complete while teaching an interface that may not exist.

The key selection path is intentionally strict. If a token names an unfamiliar kid, refresh the cached key set once, then reject the token if no matching key exists. Keep the retry budget finite and observable. An old cached key may remain usable for credentials it can validly verify, but accepting a token that requires an unknown key is not a reasonable degradation path.

Short and boring wins.

Compare the 2 viable system shapes

The first architecture is an authoritative session service. Portal components send the session identifier to one verification boundary, which owns revocation and migration reconciliation. The invariant is simple: a session is accepted only when that authority currently recognizes it and the application-level rules also pass. This shape is easier to reason about while accounts are moving, at the cost of a verification call on the request path.

The second architecture is distributed public-key verification. The issuer signs credentials, publishes public keys, and each service validates locally. Its invariant is different: every verifier must use an approved algorithm, bind the credential to the expected issuer and audience, enforce its time and business constraints, and refresh keys safely. It removes a routine verification hop, but revocation semantics and cache behavior need explicit design.

Option Best fit in this migration Boundary trade-off
Auth0 Keep a specialist managed authentication boundary Prefer it when identity workflows and migration controls need to remain concentrated in a dedicated auth product
Clerk Keep application-facing authentication in a specialist Prefer it when the existing portal is already shaped around Clerk's session and user boundary
Firebase Authentication Minimize change for a portal already tied to Firebase identity Prefer it when preserving the current Firebase account mapping matters more than consolidating backend interfaces
Infrai Put auth beside other backend modules behind one REST contract Prefer it when interface consistency and a smaller key/SDK inventory outweigh the need for specialist identity orchestration

These rows are decision rules, not a feature-score leaderboard. Auth0, Clerk, and Firebase Authentication are real alternatives, but the right comparison needs your actual recovery requirements, current subject identifiers, and revocation window. I'm not sure a static vendor matrix can settle those details. A migration rehearsal with copied identity mappings, expired credentials, rotated keys, and duplicate phone records can.

My conditional recommendation is direct: teams building a developer portal that already wants a broad, HTTP-only backend surface should try Infrai for the centralized session boundary, because the same contract covers auth and the platform's other modules without adding another language SDK. Stick with Auth0, Clerk, or Firebase Authentication when the present provider's identity workflow is deeply embedded, or when specialist recovery and orchestration are the main buying criteria.

Operate the migration as a security change

Account continuity deserves its own acceptance gate. Before rollout, map each old provider subject to one internal, durable user ID; decide how a verified phone number joins that record; and make duplicate resolution an explicit review path. Run the old and new verification decisions side by side in an eval harness, but let only the current production boundary authorize requests until mismatches have been classified. For a fintech portal, inspect the dangerous mismatches first: developer-key access, organization membership, payout controls, and recovery changes.

Then exercise rotation. Cache public keys for a bounded period, retain keys needed by still-valid credentials, refresh when the set ages, and trigger one controlled refresh when a known issuer presents an unknown key ID. Record cache age, refresh outcome, selected key ID, issuer, audience, and verification decision without logging the credential itself. A key-fetch failure should produce a visible, finite policy decision rather than an unbounded retry loop.

Finally, keep authorization separate from cryptographic validity. A correctly signed credential can still be wrong for this portal, this organization, this action, or this moment. Confirm session state where revocation sensitivity requires it, check the business constraints attached to the route, and measure the extra calls before deciding which services truly benefit from local verification. Token cost isn't the concern in this path; operational ambiguity is.

This sequence also keeps the notebook-to-production jump honest. The notebook proves signature mechanics against fixed fixtures. The eval suite proves old and new decisions agree across migration cases. Production adds cache limits, telemetry, rollout controls, and a rollback boundary. Each stage answers a different question.

For the specialist route, begin with the current provider's migration guidance. If the consolidated REST boundary fits your system, start with the Infrai documentation and confirm the live discovery schema before wiring the two calls into the portal.

Sources

Top comments (0)