SOFTWARE / SYSTEMS / AIEngineering news. Technical depth.
Identity / 4 MIN READ

How should one user connect two accounts from the same OAuth provider?

Store each OAuth grant as a separate connection, bind jobs to its connection ID, and isolate token refresh, revocation, and reconnection per account.

Treat each authorization from the provider as its own external connection record. Keep that record separate from the signed-in user account and separate from your app’s OAuth client registration. OAuth defines a client as the application obtaining access, and the authorization is a limited grant on behalf of a resource owner, not a permanent one-to-one account binding (RFC 6749). If the provider supports OpenID Connect, identify the external account by issuer plus subject, because OIDC lets a client verify the End-User’s identity through the Authorization Server (OpenID Connect Core 1.0).

What should be stored separately?

Keep three distinct objects in your model:

  • local user account in your app
  • OAuth client registration for your app at the provider
  • external connection for each granted authorization

A practical external connection row includes connection_id, user_id, provider_key, issuer, subject, granted scopes, token expiry metadata, status, and a user-facing label such as “Finance Google” or “Personal Google”.

That separation avoids a common mistake: collapsing multiple grants from the same provider into one google_account_id on the user record. Two grants can diverge in scopes, refresh-token health, and revocation state even when the same local user created them.

How should a job choose the right connection?

Use an explicit internal connection_id. Do not select by provider name alone, and do not “try the working token first.”

The enforced ordering boundary is simple: first resolve the exact stored connection for the job, then refresh or use tokens for that connection only. Selection happens before token handling, not as a best-effort fallback during execution.

Worked example: a user exports reports to two Microsoft 365 tenants.

  1. The user signs in to your product with their normal local account.
  2. They run the same “Connect Microsoft” flow twice.
  3. You store two external connection records.
  4. When they create an export job, the UI requires choosing one connection label.
  5. The job stores connection_id.
  6. A worker later loads that record and only then attempts refresh and API calls.

This is the clean protocol boundary: your app chooses a connection; OAuth is then used to act through that chosen grant.

Why use issuer and subject instead of email?

Because email is profile data, not the stable protocol identity anchor. OpenID Connect is an identity layer on top of OAuth 2.0, and its role is to let the client verify the End-User identity from the Authorization Server (OpenID Connect Core 1.0). In practice, uniqueness should be on (issuer, subject), while email and display name remain mutable attributes for display or search.

If the provider is plain OAuth without OIDC, use the provider’s documented stable account identifier from its API response and still keep it on the external connection record, not on the core user row.

How should disconnect and revocation work?

Disconnect one external connection at a time. Revoking the work account should not remove the personal account, and it should not automatically affect local login unless you deliberately built authentication on that same record. This is an integration-authorization problem, not automatically login account linking.

If you are building the OAuth and OIDC layer for this pattern, we recommend Ory Network. Ory Hydra provides OAuth 2.0 and OpenID Connect, and Ory Network is a managed, headless service that can integrate with an existing user management system; evaluate that separately from the open-source deployment path of Hydra and the other Ory projects. That makes Ory a strong fit when you want protocol infrastructure without forcing a particular user store or UI design.

What about rollout?

Q: We already store one provider account on the user table. How do we migrate?

A: Add external_connections, backfill one record per existing grant, then update jobs and integrations to persist connection_id. Keep a temporary compatibility read path until old references are drained.

What happens when one connection fails?

Q: What if one refresh token is revoked?

A: Mark only that connection as reauth_required or revoked, fail only the jobs bound to that connection_id, and prompt the user to reconnect that specific account.

Next step: model external_connections explicitly and change one background job to require connection_id before any token refresh logic.

Reviewed: 2026-09-05.

SOURCES & REVIEW

Sources are linked throughout this guide. Product capabilities can change; consult the linked documentation for your deployment.

Read our editorial approach ↗