DEV Community

BeckettHayes6821
BeckettHayes6821

Posted on

Contributor Sign-In: Provider Discovery and Identity Resolution Keep Accounts Continuous

Short answer: choose provider discovery and identity resolution when contributor sign-in must resist abuse without making an external login the permanent owner of a community account; discover what can authenticate the user, bind the callback to the attempt, then resolve that external identity to a user and permissions your own system controls.

Picture the page first. A gaming project's on-call gets an alert because signup completions have dropped while attempts have climbed, and the dashboard shows a widening gap between captcha challenges issued, OAuth callbacks accepted, and contributor accounts resolved. The page says "authentication is unhealthy," but that isn't yet an action. The operator needs to know whether bots are failing the captcha, people are abandoning consent, callbacks are being rejected, or valid identities can no longer be connected to existing community accounts.

Those are different failure domains. Treating them as one login-success counter makes the alert easy to configure and miserable to operate. The responder has to reconstruct the state machine from unrelated logs while the aggregate counter keeps moving, and the tempting mitigation — relax the captcha or retry every callback — can increase either abuse or load without restoring one legitimate contributor account.

The earlier signal should have been the conversion between those stages, segmented by provider and callback outcome, with an SLO attached to the part the platform owns. A captcha gates abusive registration attempts; it doesn't decide who a returning contributor is. An external provider authenticates an identity; it doesn't get to become the source of truth for repository roles, moderation rights, or account continuity.

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

They should protect two boundaries: which authentication methods are available now, and how a successfully authenticated external identity maps onto a durable local user. That separation matters in an open-source community because the same contributor may return through a changed provider account, while the site's permissions and history still belong to the local account.

Start by reading the available providers, then generate an authorization URL for the selected provider. The callback must be bound to the context created at login initiation and guarded against replay. After authentication succeeds, identity resolution should connect the external identity to a local user; authorization remains local. Cancellation, callback rejection, and duplicate delivery each need an explicit recovery path, because "try again" without preserved context can turn a recoverable consent cancellation into an accidental second account.

Keep the trust statement narrow. Provider output proves only what the provider and the callback validation establish. It doesn't prove that a user should merge accounts, inherit maintainer access, or skip the signup captcha. Those are policy decisions with a larger blast radius, so they need local evidence and an auditable rule.

This is also where capacity planning enters the auth design. Provider discovery is a read on the critical path, callback processing is bursty after community announcements or game releases, and identity resolution may contend on a uniqueness boundary. Size each stage independently, cap retries, and make duplicate callback handling harmless. Don't let a provider slowdown turn into unbounded goroutines or an account-linking race.

Work backward from the page

The page should name the stage that breached its objective. A useful alert could say that callback acceptance is outside its SLO while captcha verification and provider selection remain within theirs. That tells the responder to inspect callback context and replay rejection, rather than weakening the bot gate because the aggregate signup number looks bad.

Use a small event vocabulary: captcha_verified, provider_selected, callback_accepted, identity_resolved, and signup_completed. Record a correlation identifier created at the start of the attempt, the provider identifier, the outcome class, and elapsed time for the stage. Avoid putting tokens, authorization codes, captcha answers, or raw identity claims into logs. The useful question is where the state machine stopped, not what secret passed through it.

Then define objectives at boundaries you control. For example, monitor the ratio of accepted callbacks that reach a resolved identity, but separate user cancellation from platform rejection. A cancelled consent screen is not the same reliability event as an invalid or replayed callback. I'm not sure a universal target is defensible here; the right threshold depends on provider mix, expected signup traffic, and the community's tolerance for delayed access. A week of segmented baseline data, plus a review of low-volume periods, would resolve that uncertainty better than a borrowed percentage.

One alert is enough only if it leads to one action. Usually it won't.

Instrument the transition, not just the endpoint

The first instrumentation change is to emit one structured event at every state transition, using the same attempt identifier and a bounded outcome enum. That lets an operator compare adjacent stages rather than infer a cause from the final signup count. It also makes duplicate callbacks visible as a class without treating an expected replay rejection as a service failure.

Provider discovery belongs in that trace because it establishes the choices actually offered to the user. Read it with GET /v1/auth/oauth/providers; don't infer the menu from a stale application configuration. The following Go program calls that route using an API root and key supplied through environment variables, retries HTTP 429 with bounded exponential backoff while honoring Retry-After, and treats the success body as raw JSON rather than inventing provider response fields.

package main

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

func main() {
    if err := run(context.Background()); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func run(ctx context.Context) error {
    baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/")
    key := os.Getenv("INFRAI_API_KEY")
    if baseURL == "" || key == "" {
        return fmt.Errorf("INFRAI_BASE_URL and INFRAI_API_KEY are required")
    }

    client := &http.Client{Timeout: 10 * time.Second}
    url := baseURL + "/v1/auth/oauth/providers"
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+key)

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

        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Second << attempt
            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 ctx.Err()
            }
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return fmt.Errorf("provider discovery returned %s: %s", resp.Status, body)
        }

        var providers json.RawMessage
        if err := json.Unmarshal(body, &providers); err != nil {
            return fmt.Errorf("decode provider response: %w", err)
        }
        fmt.Println(string(providers))
        return nil
    }
    return fmt.Errorf("provider discovery remained rate limited after 4 attempts")
}
Enter fullscreen mode Exit fullscreen mode

The next two transitions are authorization callback handling and local identity resolution. Measure them separately, but keep their shared attempt context intact. A callback that has lost its initiating context should stop before identity resolution; a duplicate callback should reach the same safe outcome without creating another user. For local account linking, serialize or otherwise enforce uniqueness at the mapping boundary, because two workers making individually reasonable decisions can still produce one unreasonable account graph.

Buy or build the authentication boundary

The decision isn't "managed good, self-hosted bad." It is which operational burden the platform team is prepared to own while preserving the local account boundary. Auth0 and Clerk are managed products worth evaluating when a team wants a packaged identity workflow. Keycloak is the obvious comparison when self-hosting and direct control are requirements. Infrai is a fourth option when the integration constraint is a plain REST API: there is no SDK or client-library version to maintain, and the same key can cover other backend capabilities behind a consistent interface.

Option Operational ownership Integration posture Prefer it when Main trade-off to validate
Auth0 Managed service Product-specific integration The organization already standardizes its identity operations there Confirm that account-linking policy and provider workflow match the local user model
Clerk Managed service Packaged authentication workflow The application team values a more integrated sign-in product Confirm that the workflow leaves authorization and contributor history under local control
Keycloak Self-hosted software Operated by your platform team Data-plane control and self-hosting justify owning upgrades and on-call Capacity, patching, failover, and specialist operational load stay with you
Infrai Managed REST surface Direct HTTP with no required SDK A language-neutral API and fewer client dependencies matter across services Confirm that the required auth routes and provider choices cover the community's policy

The catch is lock-in moves rather than disappears. A packaged user model can couple application policy to a vendor; a self-hosted system couples the roadmap to internal operational capacity; a thin REST integration still needs a local adapter so provider-specific concepts don't leak through the codebase. Keep Auth0 or Clerk when their packaged workflow is already the organizational standard and migration would add risk without improving the account boundary. Choose Keycloak when self-hosting is a hard requirement and the team has enough sustained on-call capacity to run identity infrastructure. Consider Infrai when plain HTTP, a single credential, and a consistent backend interface reduce integration surface, but don't choose it merely to avoid designing local identity and authorization rules.

No exceptions.

Buying authentication plumbing does not outsource the decision to grant maintainer access.

Set thresholds by false-positive cost

Close the loop by asking what happens when the alert threshold is wrong. If it is too sensitive, normal consent cancellation or a low-volume provider's noisy ratio pages the on-call, responders learn to distrust the alert, and the next real replay or resolution problem waits in the same queue. If it is too loose, bots can consume signup capacity or legitimate contributors can accumulate unresolved attempts before anyone sees a coherent signal.

Use both a minimum event count and a ratio window, and route low-volume anomalies to a non-paging review until there is enough evidence to act. Keep captcha rejection separate from callback and identity-resolution objectives. During an abuse spike, a rising captcha rejection rate may mean the gate is doing its job; paging on that rate alone would punish correct behavior. Page when the platform-owned path for legitimate, context-bound attempts breaches its objective, and attach the stage breakdown that tells the responder where to look.

The capacity consequence is easy to miss: aggressive client retries can manufacture load exactly when a provider or callback stage is constrained. Honor Retry-After on 429, use exponential backoff, bound attempts, and budget retry traffic in the same forecast as normal sign-ins. Recovery paths should send a cancelled user back to a clean provider choice, let a callback failure restart with fresh context, and make repeat delivery safe. None of those paths should silently create a new local identity.

The selection rule is therefore plain. Choose the smallest set of clearly owned interfaces that can discover authentication providers, create and bind an authorization attempt, validate its callback, and resolve the external identity to a durable local account. Add the signup captcha for bot resistance, but don't confuse an abuse gate with identity continuity. The best option is the one whose failure domains you can observe, whose operational load you can staff, and whose account model you can keep under local control.

Further reading

Top comments (0)