dashboard-api: bootstrapUserWithIdentity creates new empty account for pre-Ory users on first login
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 1.6k
- Forks
- 438
- PR merge metrics
- No merged PRs in 30d
Description
Summary
When migrating a self-hosted deployment from a non-Ory authentication system to Ory (Hydra), existing users have no user_identities row. On their first Ory login, bootstrapUserWithIdentity finds no matching (oidc_iss, oidc_sub) entry and creates a brand-new user + team, leaving all their existing API keys, template sandboxes, and team memberships invisible.
Affected scenario
Self-hosted deployments that:
- Had users created before Ory integration (e.g. via Supabase auth, direct DB insert, or a previous auth system)
- Those users have data (API keys, templates) attached to their existing
user_idinpublic.users - Those users have no
user_identitiesrecord linking their Ory OIDCsubto their existinguser_id
Root cause
bootstrapUserWithIdentity in utils_team_provisioning.go only checks user_identities by (oidc_iss, oidc_sub):
existing, err := authTxDB.GetUserIdentity(ctx, authqueries.GetUserIdentityParams{
OidcIss: identity.Issuer,
OidcSub: identity.Subject,
})
switch {
case err == nil:
profile.UserID = existing.UserID
case !dberrors.IsNotFoundError(err):
return provisionedTeam{}, fmt.Errorf("get user identity: %w", err)
// not-found: falls through → profile.UserID = uuid.New() → new empty account
}
When the lookup returns not-found, a fresh UUID is used and a new empty team is created.
Impact
Pre-Ory users log in via Ory for the first time and see an empty dashboard. Their original API keys and template sandboxes are attached to the old user_id which is now orphaned.
Suggested fix
Add an email-based fallback after the identity lookup miss. If the OIDC token carries an email that matches an existing user's default-team email, and that user has no Ory identity yet, reuse their existing user_id and write the new user_identities row:
default:
// Identity not found: check if a pre-Ory user with this email exists
// and has not yet been linked to any Ory identity.
if profile.Email != "" {
if linkedUserID, found, err := s.authDB.FindUserIDByEmail(ctx, profile.Email); err == nil && found {
rows, err := authTxDB.GetUserIdentitiesByUserIDs(ctx, authqueries.GetUserIdentitiesByUserIDsParams{
OidcIss: identity.Issuer,
UserIds: []uuid.UUID{linkedUserID},
})
if err == nil && len(rows) == 0 {
// Safe: existing user has no Ory identity yet — merge
profile.UserID = linkedUserID
}
}
}
The safety guard (len(rows) == 0) ensures the merge only happens for users with no existing Ory identity for this issuer, preventing email-based account takeover for already-linked accounts.
After this change, UpsertPublicIdentity writes the permanent (oidc_iss, oidc_sub) → user_id link, so subsequent logins use the normal identity lookup path.
Notes
- This is a one-time migration path: once
user_identitiesis written on first login, subsequent logins follow the existing fast path - Requires the Ory-registered email to match
public.teams.emailfor the user's default team (case-insensitive) - For deployments where emails differ, a manual
INSERT INTO public.user_identitiesbefore the user's first login remains the alternative
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in utils_team_provisioning.go at bootstrapUserWithIdentity and trace the GetUserIdentity lookup and UpsertPublicIdentity flow. Implement and verify the pre-Ory email fallback while preserving the existing identity path and its safety guard. Done means a first Ory login reuses the existing user and subsequent logins use the stored identity without creating an empty team.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- authentication, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100