# One profile format doesn't mean one identity

Google and GitHub return different profile data. A normalized response makes that data easier to use, but your account model still needs a stable subject, nullable fields, and deliberate account linking.

By Gabe. Published 2026-10-06.
Author: https://anyoauth.com/blog/authors/gabe/
Source: https://anyoauth.com/blog/one-profile-every-provider/

1. Provider account supplies identity data
2. AnyOAuth returns a consistent profile shape
3. Your app resolves a user by stable subject

Normalization standardizes the field names. It does not merge two accounts or manufacture fields a provider didn't supply.

The awkward part of a profile integration usually appears after the happy-path demo. A username is missing. A user changes their email. Someone signs in through a different provider with the same address. Your database needs an answer that isn't “the example had an email, so we used that.”

Separate the profile's identity key from its display fields. One determines which user record you resolve. The others help you present that user to people.

## Use the subject to resolve identity

A stable subject belongs in your identity mapping. Names, avatars, usernames, and email addresses do not.

In AnyOAuth's contract, `subject` identifies the provider account within a project. `provider` tells you which provider supplied the identity. `providerSubject` preserves the provider's identifier. Your application can map the stable AnyOAuth subject to its own internal user ID, then attach sessions and permissions to that internal record.

Don't turn a display field into a primary key because it looks more readable. A human-readable identifier and a stable identifier answer different questions.

| Profile field | Appropriate use | Unsafe shortcut |
|---|---|---|
| `subject` | Resolve the project-scoped identity | Assume it is a global cross-project ID |
| `email` | Contact/display when supplied and appropriate | Automatically merge users |
| `emailVerified` | Interpret the provider's verification information | Treat it as proof of an existing app account |
| `name`, `username` | Display or optional onboarding defaults | Require them to exist for every provider |
| `avatarUrl` | Optional profile presentation | Treat it as an authorization signal |

The [OAuth vs OIDC reference](/blog/oauth-vs-oidc/) explains why identity validation is more than obtaining any access token.

## Null is part of the contract

A normalized profile can have a predictable shape without having a value for every field. Missing provider data is returned as null. Your application should treat that as an ordinary input, not a corrupt login.

For display, choose a fallback such as a user-selected name or a neutral account label. If a feature requires a contact email, collect it through an explicit onboarding step. Don't quietly substitute a provider username into an email field to satisfy a database constraint.

This matters when working with coding assistants. Ask for nullable types and a missing-field case in the UI. Generated forms often make the example response's values mandatory, even when the real contract doesn't.

## Equal emails aren't an account-linking protocol

Two provider identities remain distinct even when their email fields match. Verification information doesn't establish that the user controls an existing application account or that two providers should be merged in your database.

A deliberate linking flow starts from an authenticated application account, proves control of the identity being added, and handles conflicts with existing mappings. That's application logic. It should not emerge accidentally from a “find user by email” helper.

<figure class="article-diagram"><ol><li><strong>Google identity:</strong> its own subject, with an email that may match another profile.</li><li><strong>GitHub identity:</strong> a different subject, even when the email string is identical.</li><li><strong>Your linking decision:</strong> proof of control and explicit application policy before joining the identities.</li></ol><figcaption>Matching profile data is an observation. Linking accounts is a separate authenticated action.</figcaption></figure>

## Where AnyOAuth fits

AnyOAuth, which we build, returns a consistent profile response after a backend code exchange. Google identity is validated using OIDC; GitHub uses its OAuth flow and fresh provider profile lookup. Your application receives the normalized contract, not the provider tokens.

The service doesn't automatically link accounts by email. It also doesn't decide whether a user is allowed into your team, billing page, or admin area. Fetching the profile establishes an input to those decisions; your user model establishes the permissions.

If you need linking, add a design for it in your application or choose an identity platform whose linking model fits your requirements. Don't assume normalization includes it.

## Review the user lookup before the login button

Read the callback handler and follow the returned `subject` into the database. Does it resolve one identity mapping? Does the mapping point to an internal user? Can nullable display data pass through without inventing values?

Then inspect the permission check. The authenticated identity should not grant access merely because its email resembles an administrator's address. Your application should grant permissions through its own trusted records.

Once that is clear, [create your application session](/blog/your-app-owns-the-session/). A consistent profile is the starting point, not the whole account system.

## Sources and implementation references

- [AnyOAuth profile quickstart](/docs/#profile), reviewed 2026-10-06.
- [Google: OpenID Connect](https://developers.google.com/identity/protocols/oauth2/openid-connect), including stable `sub` guidance, retrieved 2026-10-06.
- [GitHub: REST API endpoints for users](https://docs.github.com/en/rest/users/users), retrieved 2026-10-06.