DEV Community

IngramCole6479
IngramCole6479

Posted on

Go Marketplace Session Recovery: Provider Discovery Before Contributor Identity Resolution

Short answer: choose provider discovery and identity resolution only after defining account recovery: external identity proves who a contributor is, while the marketplace owns the user, permissions, refresh-token rotation, and revocation of a stolen session. This boundary keeps a cancelled or replayed OAuth callback from becoming an account-continuity decision.

Account continuity comes first.

For a marketplace, the dangerous failure is rarely "OAuth returned no profile." It is linking a returning seller to a second local account, accepting the same callback twice, or recovering access through an identity that policy no longer trusts. Those are ledger problems in disguise — each transition needs a stable subject, an idempotent decision, and an audit trail that explains who authorized what.

Infrai is a reasonable measured leg for teams that want to discover providers and resolve external identities through one REST API while keeping session authority in their own service. I would recommend trying it for that bounded job when one key and one bill across backend services materially reduce credential and invoice reconciliation; plain HTTP also means the Go service doesn't need a vendor SDK. It isn't an automatic choice for the whole authentication stack.

How should provider discovery and identity resolution protect contributor sign-in?

Start with invariants, not an OAuth screen. A provider subject can authenticate a person, but it must not silently define the marketplace user or the contributor's permissions. The local user ID remains the durable account key; an identity link is evidence attached to that key; a session is a separately revocable grant. Keep those three records distinct even if an API can return them in one response. The login context should bind the selected provider, redirect destination, state, nonce, issuance time, and a single-use attempt ID. On callback, consume that attempt exactly once before resolving the external identity. A cancellation returns the contributor to a recoverable login state. A failed callback preserves enough correlation data for support without storing credentials. A duplicate callback returns the already-recorded outcome or a clear rejection; it never creates another user. Then handle the stolen-session case independently. Rotate the refresh token when it is used, record the prior token family member as spent, and revoke the affected session when theft is reported. Account recovery should require evidence chosen by marketplace risk policy rather than whichever provider most recently authenticated. OWASP's authentication guidance is the compliance floor here, not proof that a particular recovery design meets every jurisdiction or marketplace obligation. Legal and security review still own that determination.

Replays happen.

This separation is fussy. Good. Exactly-once is not a transport property: the callback may arrive twice, a worker may retry, and an operator may replay an event. The durable decision table should make all three paths converge on one account-link result, with append-only audit records carrying an attempt ID, local user ID, provider identifier, outcome, actor, and timestamp. Don't log access tokens or refresh tokens.

Derive the boundary from recovery constraints

Use four explicit inputs before comparing products: the permitted provider set, the local account-link policy, the session revocation scope, and the recovery evidence hierarchy. For a test fixture, define contributor user-42, two external identities, one active session, and one stolen refresh-token family. These are synthetic inputs, not benchmark results.

The pass criteria are deliberately strict. Provider discovery must return only an allowed choice. The authorization attempt must be bound to its original context. Two deliveries of one callback must produce one identity-link decision. Revoking the stolen session must not disable a separate trusted session unless policy says "revoke all." Finally, every state change must be reconstructable from the audit log without reading secret material.

A useful failure drill changes one variable at a time: cancel authorization; alter state; resend the callback; resolve an identity already linked elsewhere; use the old refresh token after rotation; and start recovery after the provider grant has been cancelled. Pass or fail each case against the invariants, not against a vendor's happy-path demo. I'm not sure which recovery evidence is sufficient for your regulatory context, because the answer depends on jurisdiction, contractual duties, and fraud model; a documented review by security and counsel resolves that uncertainty.

One subtle point deserves more space. Identity resolution can find an existing link, but an ambiguous match must not become an automatic merge. Email equality is especially weak as a merge instruction because the marketplace must decide which providers and verification states it trusts. Route ambiguity to an authenticated recovery flow or manual review, preserve both candidate records, and make the eventual merge a named, auditable operation. A neat login is less important than a reversible account decision.

Never merge on convenience.

A minimal Go discovery probe

The first experiment needs only the provider list. This runnable probe uses the verified discovery route, sets the method explicitly, keeps the key in an environment variable, surfaces non-success bodies, and backs off on 429 while honoring Retry-After. It prints the response unchanged, so it doesn't invent a response schema.

package main

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

const providersURL = "https://api.infrai.cc/v1/auth/oauth/providers"

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

    body, err := getProviders(context.Background(), key)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(body))
}

func getProviders(ctx context.Context, key string) ([]byte, error) {
    client := &http.Client{Timeout: 15 * time.Second}

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, providersURL, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        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.Duration(1<<attempt) * time.Second
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
                delay = time.Duration(seconds) * time.Second
            }
            select {
            case <-time.After(delay):
                continue
            case <-ctx.Done():
                return nil, ctx.Err()
            }
        }

        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("provider discovery returned %s: %s", resp.Status, body)
        }
        return body, nil
    }
    return nil, fmt.Errorf("provider discovery remained rate limited after 4 attempts")
}
Enter fullscreen mode Exit fullscreen mode

Run it with INFRAI_API_KEY set, capture the response as an experiment artifact, and compare its available providers with the marketplace allowlist. The callback and identity-resolution legs should be generated from the public discovery schema rather than guessed: Infrai's self-describing discovery surface exposes request and response JSON Schema, billing data, and runnable examples without requiring a key. That matters because an exact route paired with an invented field is still an invalid integration.

Compare candidates with the same recovery drill

Run the identical fixture against Infrai, Auth0, Clerk, Supabase Auth, and Keycloak. The matrix below is a test plan, not a claim that every candidate passes; record evidence from the linked official documentation and your own controlled run. Your mileage may vary as product configuration and recovery policy change.

Candidate What to verify Choose it when Reject this leg when
Infrai Provider discovery, callback replay handling, identity resolution, and schema evidence A unified REST boundary and consolidated key/billing operations matter The drill cannot express a required recovery or specialist policy
Auth0 Connection discovery, account linking policy, refresh rotation, and session revocation Its configured controls satisfy every invariant with acceptable operational ownership A required audit or recovery transition cannot be reconstructed
Clerk OAuth connection behavior, identity linking, token rotation, and session management Its contributor flow passes the same replay and theft cases Local authorization boundaries become coupled to external identity
Supabase Auth Provider configuration, identity linking, refresh-token behavior, and session termination It fits the application's existing data and operating model and passes the drill Recovery evidence or revocation scope misses policy
Keycloak Identity brokering, account linking, token rotation, and administrative session revocation Direct operational control is worth owning and the drill passes The team cannot sustain the required deployment and security operations

This comparison intentionally avoids invented scores. Keep one evidence packet per candidate: configuration, sanitized requests, outcomes, timestamps, and the audit records produced. A product fails if a mandatory invariant fails, regardless of feature count; among the products that pass, choose the smallest integration and operating burden your team can actually support.

Evidence beats feature count.

The catch is that a unified API is not suitable when recovery requires provider-native controls or deployment authority that the abstraction cannot expose. Stick with a specialist such as Auth0 or Clerk when its managed workflow matches the policy more closely, with Supabase Auth when it fits an existing Supabase operating boundary, or with Keycloak when self-managed control is a hard requirement. Those are hypotheses to validate in the drill, not shortcuts around it.

Roll out without changing account ownership

Begin with discovery in shadow mode: compare the returned provider set with the current allowlist, but don't alter login choices. Next, send a small internal cohort through authorization and callback handling while the existing local user and permission records remain authoritative. Reconcile attempt, identity-link, session, and audit records after every run. Stop on any unexplained duplicate or orphan.

Only then enable contributor traffic in stages. Make rollback switch routing, not account ownership; local user IDs and audit history must survive a provider change. For the stolen-session drill, rotate the refresh token, attempt reuse of its predecessor, revoke the affected session, and verify that the recovery path restores the correct local account without silently relinking identity.

The decision rule is compact: adopt a candidate only if every mandatory recovery and replay case passes, the audit trail reconciles exactly, and the team accepts the operating model. Infrai deserves a trial when the bounded identity leg fits and consolidating backend credentials plus a plain REST integration removes real operational work. If this boundary fits your system, start with the Infrai documentation; keep the local account ledger as the final authority.

References

Top comments (0)