Summary
Improve the Manage/ExternalLogins page so users (and admins) see meaningful information about each linked provider — display name, email at the provider, when it was linked, and a "primary" indicator — rather than the bare provider name we surface today.
Why we need this
The existing page calls userManager.GetLoginsAsync(user) and renders the result, which is a list of UserLoginInfo objects with three fields: LoginProvider, ProviderKey, ProviderDisplayName. We display the provider name and a remove button. That is the bare minimum the API returns.
In practice, users with multiple linked accounts run into real confusion:
- "I have two Google logins linked — which is my work and which is personal?" Today there's no way to tell from the UI without unlinking and re-linking.
- "When did I link this? Was it me or someone who got into my account?" No timestamp.
- "If I unlink this provider, can I still sign in?" — there's no warning that unlinking the only login method while having no password set will lock the user out (
HasPasswordAsync check exists in the backend but the UI doesn't lean on it).
- For admins triaging support: "tell me about the GitHub login on this account" requires querying the DB directly.
Most of the additional information is already available — we just don't capture or render it.
How the user will use it
On Manage/ExternalLogins, each linked provider shows:
- Provider logo + display name (e.g., GitHub icon + "GitHub").
- Account identifier at the provider — usually the email or username we received in the external login claims (e.g.,
alice@gmail.com, @alice-gh). Pulled from the principal at link time and stored in a small extension table or as a claim on the user. Without this, two Google entries are indistinguishable.
- Linked on: timestamp.
- Remove button. If the user has no password set AND this is their only login, the button is disabled with a tooltip: "Set a password before removing your last sign-in method."
- A "Link another account" section listing available providers from
GetExternalAuthenticationSchemesAsync, which already works — just polish the layout.
For admins, mirror this on the user edit security tab so they can see at a glance which third-party accounts are tied to a user.
Implementation notes
- The
UserLoginInfo returned by GetLoginsAsync doesn't include timestamps or email — Identity's IdentityUserLogin table doesn't have those columns. Two options:
- Option A (preferred): at link time, also persist a small companion record (
ExternalLoginMetadata table: UserId, LoginProvider, ProviderKey, LinkedAt, ProviderEmail, ProviderDisplayName). Owned by the Users module. Populate in ExternalLoginEndpoint callback.
- Option B: store the metadata as authentication tokens via
SetAuthenticationTokenAsync (related to the auth-tokens issue). Lighter schema impact but slightly awkward semantics.
- Recommend Option A for clarity and queryability.
ExternalLoginEndpoint already has access to the ExternalLoginInfo with claims; pull ClaimTypes.Email and ClaimTypes.Name into the metadata at link time.
- Update
Manage/ExternalLoginsEndpoint to join logins with the metadata table.
- Front-end: update
Manage/ExternalLogins.tsx to render the richer cards. Include the "last login method" guard: if !hasPassword && logins.length === 1, disable remove with the tooltip.
- Provider icons:
@simplemodule/ui likely has access to a Lucide icon set; otherwise use a small map of known providers → svg.
- Audit event
ExternalLoginUnlinkedEvent already fires presumably; add ExternalLoginLinkedEvent if it doesn't.
- Tests: linking a provider populates metadata; removing the only login method without a password fails with a clear error (defense in depth — the UI disables it, but the backend must too).
Benefits
- Eliminates ambiguity for users with multiple logins of the same kind.
- Prevents the "removed my only login method, locked out" failure mode at both UI and API layers.
- Gives support / admins a much richer view of an account's auth surface.
- All changes are additive — existing flows keep working.
Acceptance criteria
Summary
Improve the
Manage/ExternalLoginspage so users (and admins) see meaningful information about each linked provider — display name, email at the provider, when it was linked, and a "primary" indicator — rather than the bare provider name we surface today.Why we need this
The existing page calls
userManager.GetLoginsAsync(user)and renders the result, which is a list ofUserLoginInfoobjects with three fields:LoginProvider,ProviderKey,ProviderDisplayName. We display the provider name and a remove button. That is the bare minimum the API returns.In practice, users with multiple linked accounts run into real confusion:
HasPasswordAsynccheck exists in the backend but the UI doesn't lean on it).Most of the additional information is already available — we just don't capture or render it.
How the user will use it
On
Manage/ExternalLogins, each linked provider shows:alice@gmail.com,@alice-gh). Pulled from the principal at link time and stored in a small extension table or as a claim on the user. Without this, twoGoogleentries are indistinguishable.GetExternalAuthenticationSchemesAsync, which already works — just polish the layout.For admins, mirror this on the user edit security tab so they can see at a glance which third-party accounts are tied to a user.
Implementation notes
UserLoginInforeturned byGetLoginsAsyncdoesn't include timestamps or email — Identity'sIdentityUserLogintable doesn't have those columns. Two options:ExternalLoginMetadatatable:UserId,LoginProvider,ProviderKey,LinkedAt,ProviderEmail,ProviderDisplayName). Owned by the Users module. Populate inExternalLoginEndpointcallback.SetAuthenticationTokenAsync(related to the auth-tokens issue). Lighter schema impact but slightly awkward semantics.ExternalLoginEndpointalready has access to theExternalLoginInfowith claims; pullClaimTypes.EmailandClaimTypes.Nameinto the metadata at link time.Manage/ExternalLoginsEndpointto join logins with the metadata table.Manage/ExternalLogins.tsxto render the richer cards. Include the "last login method" guard: if!hasPassword && logins.length === 1, disable remove with the tooltip.@simplemodule/uilikely has access to a Lucide icon set; otherwise use a small map of known providers → svg.ExternalLoginUnlinkedEventalready fires presumably; addExternalLoginLinkedEventif it doesn't.Benefits
Acceptance criteria
ExternalLoginMetadata(or equivalent) storage records linked-on, provider email, provider display name at link time.Manage/ExternalLoginsshows enriched cards: icon, identifier, linked-on, remove (guarded).