Skip to content

Identity: authentication tokens API (per-user, per-provider) #177

@antosubash

Description

@antosubash

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

  • External login callback persists info.AuthenticationTokens via SetAuthenticationTokenAsync when present.
  • User-facing endpoint lists providers + token names (never values) for the current user, with revoke action.
  • Admin endpoint mirrors this for arbitrary users.
  • IExternalProviderTokenStore contract published from SimpleModule.Users.Contracts.
  • No code path returns a token value over the wire (covered by an explicit test).
  • Integration tests for set / get / remove and per-user / per-provider isolation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions