Social Login Guide
Backend social login is implemented with a "provider adapter" pattern. Since #70 all three providers — GitHub, Google, WeChat — run the SAME closed loop: upstream code exchange → subject-mapping lookup → (first login) local account auto-create → first-party token pair issuance.
Uniform account model (#70)
All three providers map (provider, subject) — GitHub numeric id / Google sub / WeChat openid — to a local user through oauth2_subject_mappings:
- Existing mapping: tokens are issued for the linked local user (soft-deleted or locked linked users are rejected with the generic auth error — no account-status leak).
- No mapping + auto-create enabled (default): a local account is created (username
gh_<login>/google_<sub12>/wx_<openid12>; a collision retries once with a random suffix, then failsVALIDATION_USERNAME_TAKEN), the mapping row is written, the defaultuserrole is granted, and tokens are issued. - No mapping + auto-create disabled:
403 AUTH_SOCIAL_ACCOUNT_NOT_LINKEDand NO account side effects. 403 (not 401) because "not linked" is an authorization state the client can act on (guide the user to the link flow); probing requires the target account's own upstream provider code, so there is no cross-user enumeration vector.
The auto-create gate is GLOBAL — external_auth.auto_create_on_first_login (default true) governs GitHub/Google/WeChat together; it is a social policy, not a per-provider toggle. Already-linked users are unaffected by the switch.
First-party token issuance (#70)
The login endpoints mint an opaque access/refresh pair via the shared SocialTokenIssuer:
- Token rows store the platform subject (
users.public_sub) — the same value every Bearer-authenticated handler resolves (/api/me, change-password, MFA, WebAuthn). (GitHub previously stored the internal numeric id, which made its tokens 404 on every authenticated endpoint — fixed with the issuer extraction.) - Issued for the configured FIRST-PARTY client:
external_auth.social_token_client_id(defaultfulla-portal). This key must never point at a third-party client — the issuance has no consent interaction, so doing so would hand that client tokens nobody agreed to. - Scope
openid profile email. Note: theopenidscope value carries no OIDC semantics on this endpoint (no id_token is issued); it is kept for consistency with the first-party client's scope set.
Why an audit event and not a consent row
Issuance records a SOCIAL_LOGIN_TOKEN_ISSUED audit action (provider, client, scope, internal id) rather than an oauth2_user_consents row: a consent row would silently satisfy the consent screen's Tier-3 check and pre-approve the configured client for scopes the user was never prompted for — fabricated consent evidence. An explicit social-consent interaction is a registered follow-up; the audit event is the honest record until then.
This is a first-party extension endpoint, not one of RFC 6749's four grants — the same position the GitHub flow has always had, now documented and audit-traced. The standards-track alternative (social login establishes a browser session, then the SPA runs authorization-code + PKCE with a consent-exempt first-party client) is registered as follow-up work.
Provider tiers (v1.4.0)
Deployments differ in which external providers make sense (GitHub/Google are
unreachable from mainland China; WeChat is China-only). Availability is a
server-side gate with two switches under custom_config in config.json (env aliases
FULLA_EXTERNAL_TIER_DOMESTIC / FULLA_EXTERNAL_TIER_INTERNATIONAL):
"external_auth": {
"tiers": {
"domestic": true, // gates WeChat
"international": true // gates GitHub + Google
}
}
Effective enablement = tier on AND credentials configured (a tier switch
composes with the credential gate, never replaces it). The login page
discovers the result at runtime via the public GET /api/auth/providers
endpoint — it returns each enabled provider with a fully built
authorize_url (same redirect resolution as the link flow:
per-provider redirect_uri override, else frontend.url +
/callback/{provider}) — so toggling a tier takes effect on restart with
no frontend rebuild. Both tiers default to true (upgrade compatibility).
GitHub (fully wired, mainline)
- Backend route:
POST /api/github/login; thefrontends/userfrontend renders the "Sign in with GitHub" button from the runtime discovery endpointGET /api/auth/providers(v1.4.0 provider tiers — the button tracks the server-sideexternal_authgate, no frontend build variable involved). - Callback:
/callback/github.
Google (wired, #70)
- Create an OAuth 2.0 client on the Google Cloud side (Web application; authorized redirect URI =
<portal origin>/callback/google). - Backend configuration (
config.json):
"external_auth": {
"google": {
"client_id": "<your-client-id>",
"client_secret": "<your-client-secret>",
"redirect_uri": "<portal origin>/callback/google"
}
}
POST /api/google/login(codeparameter) completes the exchange and returns the token pair (see above).- Frontend: the login page renders "Sign in with Google" when the discovery endpoint reports Google enabled (unconfigured = hidden); the
/callback/googleroute (generalizedSocialCallbackPage) stores the tokens and lands the user on the home page.
WeChat (login wired; QR scan needs a mobile-agent surface)
- Create a website application on the WeChat Open Platform (localhost callbacks are not supported; an ICP-registered domain is required).
- Backend configuration follows the same structure (
external_auth.wechat: appid / app_secret). - Backend route:
POST /api/wechat/login— same closed loop; WeChat supplies no email, the created account mirrors GitHub's empty-email handling. - Frontend:
/callback/wechatexists; the desktop SPA only surfaces a hint when the discovery endpoint reports WeChat enabled — i.e. the domestic tier (external_auth.tiers.domestic, default on) AND credentials configured (the QR-scan authorization flow requires a WeChat-enabled device/browser surface — the desktop button-and-redirect UX cannot complete it; the explicit desktop QR flow is a registered follow-up). - Local development tricks: point the callback domain to 127.0.0.1 via the hosts file / an Nginx reverse proxy / an intranet tunnel.
General Security Notes
- The
stateon social callbacks must be verified (CSRF protection); the subject returned by a provider is trusted only from the server-side code exchange result — never trust user information submitted by the frontend. - Unlinking a social account has "last login method" protection and a known limitation around concurrent-unlink races
(see the social-link design archived under
docs/historyand the CHANGELOG #54/#69 fix records).
Merged from the retired google-guide.md and wechat-guide.md (docs governance A2), and fixed the self-contradiction in the Google guide where "there was no frontend button, yet users were told to click the button". #70 closed the loop for Google/WeChat and rewrote this page around the uniform account/issuance model.