TL;DR
Add a native endpoint for "link this new OAuth identity to my current session's person" — equivalent to common "Add login method" UX in SaaS apps. Today, this can only be approximated by abusing the invite-acceptance mechanism (replaceInviteWithCredentials self-link), which is verbose for apps to implement and unclear in intent.
Motivation
A user with an existing Entu person (e.g., logged in via Google as alice@gmail.com) may want to add an additional verified email or OAuth provider to the SAME person — e.g., add alice@work.com via the e-mail provider, or link an Apple ID — so future invitations / lookups across either identity resolve to one person.
Today's behavior: visiting /auth/{provider} while logged in with a different identity creates a separate person (find-or-create logic in routes/auth/index.get.js), bifurcating the user instead of linking. There's no "attach to current session" branch.
Today's workaround (discovered while designing polyphony): exploit the invite-acceptance code path. App creates a placeholder entu_user on the user's own person with an invite token + the new email, sends an invite link to that address, user clicks → e-mail OAuth → replaceInviteWithCredentials swaps the placeholder. The "Same entity: clean up orphaned invite property" branch handles this case.
This works but:
- Requires apps to implement placeholder management
- Conflates the "invite a new user" semantics with "link my own identity"
- Has no documented support; relies on a code branch labeled for a different purpose
- Adds ~30+ lines of BFF orchestration per app that wants this
Proposed API
GET /auth/{provider}?link=true&next=<callback>
Authorization: Bearer <existing JWT>
Or alternatively as a dedicated endpoint:
GET /auth/link/{provider}?next=<callback>
Authorization: Bearer <existing JWT>
Flow:
- Caller has a valid JWT for person P
- Redirects browser to
/auth/link/{provider} (or equivalent) with current JWT
- User completes OAuth at the provider (verification of new identity)
- On callback:
- If the new OAuth identity (
{provider, uid}) is already attached to ANOTHER person → return error (collision; user must resolve manually)
- Otherwise → add a new
entu_user entry to person P with {provider, uid, email} from the OAuth response
- Return success (no new JWT needed; existing session continues)
Behavior details
- Requires authenticated request (current JWT validates the target person)
- Verification is intrinsic — OAuth round-trip proves access to the new identity
- Collision case (new identity already belongs to another person): API returns 409 Conflict with the conflicting person's ID; client UI prompts user to handle (merge accounts is a separate problem)
- Idempotent: if the new identity already belongs to the current session's person, no-op (or return existing entu_user entry)
- Works for any provider Entu supports (Google, Apple, e-mail, Smart-ID, Mobile-ID, ID-card, etc.)
Security considerations
- Same security model as standard OAuth: user must complete the provider's auth flow to prove access
- Collision check prevents account hijacking via newly-claimed-but-already-bound identities
- Requires valid existing session (can't link to an arbitrary person)
- No additional attack surface beyond standard OAuth flow
Migration
Existing self-link workarounds (via invite-acceptance) continue to work — this is purely additive. Apps can migrate from the workaround to the native endpoint as it becomes available.
Reference
This RFC arose from designing polyphony's email-trust model on Entu. See the polyphony case study, Section E5 (replaceInviteWithCredentials reusable for self-link) and the related entu/www docs issue:
Polyphony is deferring its "add verified email" feature until this lands, rather than implementing the invite-mechanism workaround at this phase.
TL;DR
Add a native endpoint for "link this new OAuth identity to my current session's person" — equivalent to common "Add login method" UX in SaaS apps. Today, this can only be approximated by abusing the invite-acceptance mechanism (
replaceInviteWithCredentialsself-link), which is verbose for apps to implement and unclear in intent.Motivation
A user with an existing Entu person (e.g., logged in via Google as
alice@gmail.com) may want to add an additional verified email or OAuth provider to the SAME person — e.g., addalice@work.comvia the e-mail provider, or link an Apple ID — so future invitations / lookups across either identity resolve to one person.Today's behavior: visiting
/auth/{provider}while logged in with a different identity creates a separate person (find-or-create logic inroutes/auth/index.get.js), bifurcating the user instead of linking. There's no "attach to current session" branch.Today's workaround (discovered while designing polyphony): exploit the invite-acceptance code path. App creates a placeholder
entu_useron the user's own person with an invite token + the new email, sends an invite link to that address, user clicks → e-mail OAuth →replaceInviteWithCredentialsswaps the placeholder. The "Same entity: clean up orphaned invite property" branch handles this case.This works but:
Proposed API
Or alternatively as a dedicated endpoint:
Flow:
/auth/link/{provider}(or equivalent) with current JWT{provider, uid}) is already attached to ANOTHER person → return error (collision; user must resolve manually)entu_userentry to person P with{provider, uid, email}from the OAuth responseBehavior details
Security considerations
Migration
Existing self-link workarounds (via invite-acceptance) continue to work — this is purely additive. Apps can migrate from the workaround to the native endpoint as it becomes available.
Reference
This RFC arose from designing polyphony's email-trust model on Entu. See the polyphony case study, Section E5 (
replaceInviteWithCredentialsreusable for self-link) and the related entu/www docs issue:replaceInviteWithCredentialsself-link use case (adding verified emails to existing person) www#7Polyphony is deferring its "add verified email" feature until this lands, rather than implementing the invite-mechanism workaround at this phase.