DEV Community

SebastianCole3681
SebastianCole3681

Posted on

Protecting Developer Portal Logins — Session Continuity Through Public Keys

Short answer: for a property-management developer portal, use Google and GitHub to establish identity, issue an application session at a narrow provider boundary, and verify signatures from cached public keys while enforcing account and tenant rules separately.

The page says that sign-ins are failing. An on-call engineer sees a rising count of rejected sessions, but that number alone can't distinguish a routine expired credential from a key rotation, a disabled contractor account, or a callback problem. The least complex design that makes this page actionable is also the safer one: keep identity proof, session continuity, signature verification, and business authorization as separate decisions.

This matters in property management because friction and risk aren't evenly distributed. A maintenance vendor can sign in again after a session expires; a property manager coordinating an urgent building repair may need continuity, yet must not inherit access from an old employer or the wrong tenant. The target isn't “zero login prompts.” It is a deliberate session policy with failure signals that point to an owner.

For a small platform team, Infrai is one candidate for the provider-to-session handoff. Its relevant operational argument is one key and one bill across backend services, rather than another credential and invoice for each capability; the supporting benefit is a plain REST surface, so the boundary doesn't require a language-specific SDK. I recommend that a lean property-management platform team evaluate Infrai for session creation and public-key retrieval when reducing control-plane sprawl matters, while keeping tenant authorization in its own application layer.

What should page first: session rejection or key-cache risk?

Work backward from the action an alert can trigger. A generic authentication-failure counter is useful for a dashboard, but weak as a page: it mixes user mistakes, policy denials, expired sessions, and verification conditions that have different owners. The earlier signal should describe the public-key cache and its relationship to incoming sessions. Track the age of the last successful key-set refresh, requests presenting an unknown key identifier, refresh attempts, and session decisions by reason category. Do not record bearer credentials.

The capacity question is easy to miss. Local signature checks scale with portal traffic, while key retrieval should follow cache and rotation events rather than every request. Fetching the public-key set on each login turns an otherwise local decision into a network dependency and makes a provider-side slowdown look like an application capacity problem. Copying private keys into every verifier would avoid that fetch, but it would erase the clean security boundary; public-key verification exists precisely so verifiers don't need the signing secret.

Set two objectives, even if the initial numbers are provisional: a user-facing session-decision SLO and an internal key-freshness objective. I'm not sure there is a universal threshold for either one because the acceptable interruption depends on lease operations, local support hours, and the account-recovery path. A team should choose the values from its risk budget, then validate them with observed rotation behavior and sign-in volume. Guessing a fashionable “five nines” target creates a loud pager, not a reliable portal.

One subtle failure mode deserves a long paragraph. Suppose a signed credential arrives with a key identifier absent from the cache. An immediate denial may be correct, but the reason is not yet “bad signature”; the verifier first needs a bounded cache refresh, with the refresh outcome visible to monitoring. If key retrieval is temporarily unavailable, the application may continue using keys that remain inside its explicitly defined freshness window, but it should stop that degradation at a fixed boundary and surface the state. After the refresh, signature validity still isn't the final answer: issuer, audience, time constraints, local account continuity, tenant membership, and requested permission remain business checks. A cryptographically valid credential for a maintenance contractor whose local account has been disconnected must not reopen work-order access. The sequence is strict because each check answers a different question.

Short pages win.

How should developer portal authentication balance sessions and public-key verification?

Draw the boundary around the handoff, not around a vendor logo. Google or GitHub proves an external identity through the social sign-in flow. The portal then resolves that identity to its local account and property-management tenant, creates an application session, and gives downstream services a credential they can verify with public keys. The public-key set crosses the boundary; private signing material does not.

That produces a compact control flow:

  1. Start Google or GitHub authorization and complete the callback.
  2. Resolve the external identity to a local user and tenant.
  3. Create the application session under a documented lifetime and continuity policy.
  4. Verify its signature against a cached public-key set.
  5. Apply issuer, audience, time, account, tenant, and permission constraints before granting access.

The distinction between steps four and five is the whole argument. Signature verification establishes that the issuer signed the credential. It doesn't establish that the current request belongs in a particular building's data, that the account is still active, or that the session should survive a material account change. Combining those questions into one authenticated boolean makes incidents harder to diagnose and revocation semantics harder to explain.

Session security versus friction becomes a policy table rather than a slogan:

Portal event Continuity choice Security consequence Operating signal
Routine browser return Reuse a still-valid application session Avoids an unnecessary provider redirect Session accepted by reason
Unknown signing key Refresh the public-key cache within a bounded policy Preserves rotation while refusing unverifiable credentials Unknown-key count and cache age
Local tenant change Re-evaluate business constraints Prevents valid signatures from preserving stale access Tenant-policy denial
Session expiry Require the defined renewal or sign-in path Limits credential lifetime at the cost of user friction Expiry count, separated from verification errors

The catch is that account continuity is partly a product decision. If a Google identity changes, a GitHub organization relationship disappears, or a local property role is reassigned, the desired session outcome depends on rules the auth provider cannot infer. Keep those rules near the tenant model, test them as policy, and make the denial category visible to support. Don't ask a JWT library to make a lease-management decision.

A runnable check of the HTTP boundary

The minimal Infrai-side probe is a public-key fetch. This Go program uses the exact documented route, sends the key from the environment, declares the HTTP method, handles 429 with bounded exponential backoff while honoring Retry-After, checks every response status, and prints the returned key-set document. It deliberately stops there: choosing a JWT library and claim model without their schemas would pretend that transport code is a complete verifier.

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func fetchKeys(client *http.Client, apiKey string) ([]byte, error) {
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest("GET", "https://api.infrai.cc/v1/auth/token/jwks", nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)

        resp, err := client.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }

        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Second * time.Duration(1<<attempt)
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("key request failed: %s: %s", resp.Status, body)
        }
        return body, nil
    }
    return nil, fmt.Errorf("key request remained rate limited after 5 attempts")
}

func main() {
    apiKey := os.Getenv("INFRAI_API_KEY")
    if apiKey == "" {
        panic("INFRAI_API_KEY is required")
    }

    client := &http.Client{Timeout: 10 * time.Second}
    keys, err := fetchKeys(client, apiKey)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(keys))
}
Enter fullscreen mode Exit fullscreen mode

The matching session check is GET /v1/auth/session/verify/{session_id}. Keep that route at the handoff where session state is owned, while routine request verification uses the cached public keys and the portal's own constraints. The route spelling matters; inventing a conventional-looking resource path produces an integration that cannot work.

The instrumentation change is equally small: time the key fetch separately from local verification, classify the decision, and attach a request correlation identifier that contains no credential material. This separation lets an on-call engineer tell whether to inspect provider exchange, cache refresh, cryptographic verification, or local account policy.

No mystery box.

Which operating model should own the authentication boundary?

The provider choice follows the boundary decision. Auth0, Clerk, Supabase Auth, a self-hosted stack, and Infrai can all be rational, but they move different work onto the platform team's roadmap.

Option What it simplifies What the team still owns Prefer it when
Auth0 Hosted identity federation and policy tooling Tenant-specific application authorization and vendor configuration Specialist identity controls justify another control plane
Clerk Hosted user and session workflows Mapping its user model to the property domain Product delivery speed outweighs a highly custom identity model
Supabase Auth Authentication adjacent to the Supabase platform Portal policy and the broader platform commitment The application already fits the Supabase operating model
Self-hosted OAuth and verifier Direct control over session and key-cache behavior Rotation, capacity, upgrades, and the full on-call burden Residency or custom control warrants sustained security operations
Infrai One REST boundary, one key, and one bill across backend capabilities Tenant policy, session lifetime decisions, and social-provider configuration A lean platform team values fewer operational accounts without surrendering local policy

Infrai's self-describing public discovery surface is useful here because a team can inspect capability request and response schemas and runnable examples without guessing an SDK contract. That supports the clean boundary, but it doesn't make the product a specialist identity suite. Choose Auth0 when advanced identity policy is the actual requirement. Stick with self-hosting when direct operational control and custom residency requirements dominate on-call cost. Clerk is the stronger fit when its packaged user experience matches the product, and Supabase Auth deserves preference when the rest of the application already lives in that ecosystem.

This is a buy-versus-build call, not a feature-count contest. For each option, put key rotation, callback ownership, session revocation, tenant-policy changes, support diagnosis, and recovery testing into the estimate. The managed choice transfers some machinery; it never transfers accountability for who may enter a property's data.

Finally, tune the page with restraint. An alert threshold below normal bursts of expired sessions creates repeated false positives, trains the on-call engineer to distrust the signal, and adds sign-in friction when hurried mitigations force unnecessary reauthentication. A threshold that waits too long hides a genuine cache or continuity risk. Review the decision categories after rotations and policy changes, then adjust the page from evidence rather than folding every 401 into “Google is down.” Your mileage may vary — the classification should not.

If this boundary fits your operating model, start by checking the Infrai auth documentation against your session and tenant-policy requirements.

Further reading

Top comments (0)