Summary
Expose UserManager.SetAuthenticationTokenAsync / GetAuthenticationTokenAsync / RemoveAuthenticationTokenAsync so modules and admins can persist and retrieve per-user, per-provider tokens (typically OAuth refresh / access tokens from external login providers).
Why we need this
Whenever a user signs in with Google, Microsoft, GitHub, etc., the external provider hands back tokens that let our server call the provider's APIs on the user's behalf (read their calendar, send mail as them, list their repos). ASP.NET Identity has built-in storage for these tokens — AspNetUserTokens table, already in the schema — but nothing in SimpleModule reads or writes it.
Concrete scenarios that this unlocks:
- Calendar / mail integrations: after Microsoft login, store the refresh token; later, a background job grabs it via
GetAuthenticationTokenAsync("Microsoft", "refresh_token") and queries Graph as the user. Without storage, the refresh token vanishes when the cookie session ends.
- GitHub PR automation: store the GitHub access token after sign-in to call the API on behalf of the user (e.g., create issues, comment on PRs).
- Webhook delivery / API integrations: any feature where "do thing X in the user's third-party account" requires durable token storage tied to the user.
- Provider re-auth flows: detect expired refresh tokens and prompt the user to reconnect.
AspNetUserTokens is on the wire already because we use IdentityDbContext. We are paying the schema cost; we should get the value.
How callers will use it
This is a developer-facing API more than an end-user feature, but it surfaces in two places:
Programmatic (other modules):
// During external login callback (modify ExternalLoginEndpoint):
await userManager.SetAuthenticationTokenAsync(user, "Microsoft", "access_token", info.AuthenticationTokens.Single(t => t.Name == "access_token").Value);
await userManager.SetAuthenticationTokenAsync(user, "Microsoft", "refresh_token", info.AuthenticationTokens.Single(t => t.Name == "refresh_token").Value);
// Later, from a background job:
var refreshToken = await userManager.GetAuthenticationTokenAsync(user, "Microsoft", "refresh_token");
End-user UI: on Manage/ExternalLogins, show a green "Connected with API access" badge for providers that have stored tokens, vs. a plain "Linked" for providers we only used for sign-in. Add a "Disconnect API access" button that calls RemoveAuthenticationTokenAsync per token without removing the login link itself.
Admin UI: on the user security tab, show "Stored provider tokens: Microsoft (3), GitHub (1)" with a "Revoke all" button — useful for security incidents (compromised account, lost device).
Implementation notes
- Wire
SaveTokens = true on the relevant OAuthOptions in the external auth setup (without this, tokens never reach info.AuthenticationTokens).
- In
ExternalLoginEndpoint, on successful link/login, iterate info.AuthenticationTokens and persist each via SetAuthenticationTokenAsync.
- New endpoints in
modules/Users/src/SimpleModule.Users/Pages/Account/Manage/:
ProviderTokensEndpoint (GET) — list providers with stored tokens for the current user (token names only, never values).
RemoveProviderTokensEndpoint (POST) — remove all tokens for a given provider.
- Admin endpoints in
modules/Admin/src/SimpleModule.Admin/Endpoints/Admin/:
AdminUserProviderTokensEndpoint (GET) — same shape, for any user. Names only, never values, even to admins.
AdminRevokeProviderTokensEndpoint (POST).
- Security: token values must never appear in any API response, log, or audit event. Only token names and providers are surfaced. Internal use only.
- Provide a small
IExternalProviderTokenStore wrapper interface in SimpleModule.Users.Contracts so other modules can depend on the contract rather than UserManager directly.
- Tests: token round-trip, removal, scoping per user/provider, and a guard test asserting token values never appear in serialized responses.
Benefits
- Unblocks any feature that integrates with external provider APIs on the user's behalf.
- Already-on schema starts paying for itself.
- Centralizes token storage — every integration uses the same well-tested Identity primitives instead of inventing per-feature tables.
- Gives security ops a single revocation point for compromised accounts.
Acceptance criteria
Summary
Expose
UserManager.SetAuthenticationTokenAsync/GetAuthenticationTokenAsync/RemoveAuthenticationTokenAsyncso modules and admins can persist and retrieve per-user, per-provider tokens (typically OAuth refresh / access tokens from external login providers).Why we need this
Whenever a user signs in with Google, Microsoft, GitHub, etc., the external provider hands back tokens that let our server call the provider's APIs on the user's behalf (read their calendar, send mail as them, list their repos). ASP.NET Identity has built-in storage for these tokens —
AspNetUserTokenstable, already in the schema — but nothing in SimpleModule reads or writes it.Concrete scenarios that this unlocks:
GetAuthenticationTokenAsync("Microsoft", "refresh_token")and queries Graph as the user. Without storage, the refresh token vanishes when the cookie session ends.AspNetUserTokensis on the wire already because we useIdentityDbContext. We are paying the schema cost; we should get the value.How callers will use it
This is a developer-facing API more than an end-user feature, but it surfaces in two places:
Programmatic (other modules):
End-user UI: on
Manage/ExternalLogins, show a green "Connected with API access" badge for providers that have stored tokens, vs. a plain "Linked" for providers we only used for sign-in. Add a "Disconnect API access" button that callsRemoveAuthenticationTokenAsyncper token without removing the login link itself.Admin UI: on the user security tab, show "Stored provider tokens: Microsoft (3), GitHub (1)" with a "Revoke all" button — useful for security incidents (compromised account, lost device).
Implementation notes
SaveTokens = trueon the relevantOAuthOptionsin the external auth setup (without this, tokens never reachinfo.AuthenticationTokens).ExternalLoginEndpoint, on successful link/login, iterateinfo.AuthenticationTokensand persist each viaSetAuthenticationTokenAsync.modules/Users/src/SimpleModule.Users/Pages/Account/Manage/:ProviderTokensEndpoint(GET) — list providers with stored tokens for the current user (token names only, never values).RemoveProviderTokensEndpoint(POST) — remove all tokens for a given provider.modules/Admin/src/SimpleModule.Admin/Endpoints/Admin/:AdminUserProviderTokensEndpoint(GET) — same shape, for any user. Names only, never values, even to admins.AdminRevokeProviderTokensEndpoint(POST).IExternalProviderTokenStorewrapper interface inSimpleModule.Users.Contractsso other modules can depend on the contract rather thanUserManagerdirectly.Benefits
Acceptance criteria
info.AuthenticationTokensviaSetAuthenticationTokenAsyncwhen present.IExternalProviderTokenStorecontract published fromSimpleModule.Users.Contracts.