DEV Community

DimitriReed2158
DimitriReed2158

Posted on

Contributor Login: Provider Discovery and Identity Resolution During Migration

Short answer: use provider discovery and explicit identity resolution as separate steps when moving a gaming community off a managed auth service. Discovery tells you where a user can authenticate; resolution decides which internal contributor account receives the result. Keeping those decisions separate makes Google and GitHub sign-in testable, reversible, and resistant to duplicate accounts.

That separation matters most in an open source game community. A contributor may use a personal Google account for issue triage and a GitHub account for code review. The email strings can differ, change, or be hidden. Treating an email address as the primary key is a quiet way to hand one person two permission sets.

The failure signal I watch first

The pager rarely says “identity collision.” It says “contributors cannot publish” or “review queue is empty.” In one migration rehearsal, a callback retry turned a clean login into a 409 conflict because the account-linking write was not idempotent. The second request had the same provider subject, but the handler tried to create a new local row. That was enough to stop a release volunteer from uploading a build.

It failed.

I now start with the signal, not the login button. Track callback success by provider, the rate of unresolved identities, link attempts, and authorization decisions after login. A spike in successful OAuth callbacks paired with a flat session count usually means the resolver is dropping users after authentication. A rising 401 rate can mean an expired session, but a rising 409 rate points at a uniqueness rule or a replayed link request.

The useful unit of identity is the provider plus its immutable subject identifier. sub in OpenID Connect, or the provider's documented user ID in OAuth APIs, is more stable than a display name and safer than an unverified email. Store the issuer as part of the key too; two issuers can emit the same subject value.

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

Discovery answers a narrow question: which provider configuration should handle this request? For a game project's web console, the request can carry an explicit provider=google or provider=github choice. If the project later adds domain hints or an invite link, those are inputs to discovery, not proof of identity. The discovery result should be a small, versioned record containing issuer, authorization endpoint, scopes, and a state/nonce policy.

Resolution happens after the authorization code exchange and token validation. Validate the redirect URI, state, nonce, issuer, audience, signature, and token expiry before reading claims. Then map (issuer, subject) to a local contributor account. If no mapping exists, create a pending identity and ask the user to confirm a link to an already authenticated account. Do not silently merge on matching email alone.

Here is the boundary I keep in the service. It uses generic interfaces so the same tests run against a self-hosted adapter or a managed provider during migration.

package auth

import "context"

type ProviderConfig struct {
    Issuer string
    AuthURL string
    Scopes []string
}

type ExternalIdentity struct {
    Issuer  string
    Subject string
    Email   string
}

type IdentityStore interface {
    Find(ctx context.Context, issuer, subject string) (string, error)
    CreatePending(ctx context.Context, identity ExternalIdentity) (string, error)
    Link(ctx context.Context, accountID string, identity ExternalIdentity) error
}

func Resolve(ctx context.Context, store IdentityStore, identity ExternalIdentity, accountID string) (string, error) {
    if id, err := store.Find(ctx, identity.Issuer, identity.Subject); err == nil {
        return id, nil
    }
    if accountID != "" {
        if err := store.Link(ctx, accountID, identity); err != nil {
            return "", err
        }
        return accountID, nil
    }
    return store.CreatePending(ctx, identity)
}
Enter fullscreen mode Exit fullscreen mode

The production version also classifies “not found” separately from a database error; that distinction is important and intentionally visible in the interface. Every create and link operation gets an idempotency key derived from issuer and subject. A retry should return the existing account, not race a second insert.

Migration is a data exercise before it is an OAuth exercise

Export the managed provider's users into a staging database and build a collision report. Count records with the same verified email, records with no verified email, and records that already have more than one external identity. For the gaming community, I also sample maintainers, moderators, and build-bot owners because their roles make an accidental merge expensive. In the rehearsal, the report showed three classes that looked identical in a spreadsheet but had different answers in the product: two contributors shared a family mailbox, one maintainer had a masked email on GitHub, and a build-bot record had no interactive login at all. We marked the first two for human proof and excluded the bot from social sign-in. That extra pass took longer than the import itself, yet it prevented a role merge that would have been hard to explain after release.

The migration table needs an explicit status: unseen, mapped, pending, blocked, or verified. “Blocked” is not a failure of OAuth; it is a deliberate stop while a human confirms ownership. Keep the old provider as a read-only verification path until the new resolver has handled a full release cycle. That gives you a rollback that preserves account IDs and audit history.

No shortcut.

I would not make this change during a tournament weekend. The catch is operational: provider discovery can be stateless, while identity linking is a durable write with security consequences. If your team cannot staff manual review for collisions, stay with the managed provider until that workflow exists.

Testing the ugly paths

Unit tests cover claim validation and deterministic mapping. Integration tests exercise real redirect behavior with a disposable callback URL. The high-value cases are less glamorous: a user cancels consent, the same code is replayed, the nonce is wrong, the issuer is unexpected, and two browser tabs try to link the same GitHub identity. I first assumed a successful token exchange meant the hard part was over. It wasn't; authorization drift appeared later, when a valid session carried a stale contributor role into the release queue. That is why the test suite asks both “who signed in?” and “what may this account do now?”

I use a table like this in the runbook:

Event Expected result Alert signal
Unknown (issuer, subject) Pending identity, no elevated role Pending count above baseline
Replayed callback Existing session or explicit rejection Replay counter
Email collision Manual review, no auto-merge Collision queue
Provider metadata change Configuration version rollback Discovery failures

Keep authorization tests separate from authentication tests. A valid Google token must not grant repository write access until the local contributor account has the right role. Log a correlation ID, provider, issuer, subject hash, and resolver decision; never log raw access tokens or authorization codes.

Rollback and the decision rule

Rollback means switching discovery for new sessions while preserving resolved identities. Do not delete the link table. Freeze new linking, leave existing sessions to expire normally, and route sign-in to the previous provider configuration. Afterward, reconcile events from both paths before reopening the queue.

The approach is unsuitable when the project cannot protect signing keys, validate tokens server-side, or provide a recovery channel for maintainers who lose an external account. In that case, a managed service with a mature recovery workflow is the safer choice, even if migration is postponed. Your mileage may vary if your providers expose different subject semantics; verify that contract in their current specifications before importing data.

My rule is simple: choose discovery plus resolution when you need provider portability and can operate an identity review queue. Choose a single managed flow when the team lacks that operational capacity. The architecture is only successful when a duplicate delivery, a revoked account, and a provider outage leave an understandable audit trail.

References

Top comments (0)