From 584ded2182e2b1225a67e55d8a5485bbbdc35658 Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Mon, 20 Apr 2026 14:41:12 +0800 Subject: [PATCH] docs: harden auth identity payment design --- ...-04-20-auth-identity-payment-foundation.md | 87 ++++++++-- ...auth-identity-payment-foundation-design.md | 149 +++++++++++++++--- 2 files changed, 199 insertions(+), 37 deletions(-) diff --git a/docs/superpowers/plans/2026-04-20-auth-identity-payment-foundation.md b/docs/superpowers/plans/2026-04-20-auth-identity-payment-foundation.md index e8fde9c0..2d44e058 100644 --- a/docs/superpowers/plans/2026-04-20-auth-identity-payment-foundation.md +++ b/docs/superpowers/plans/2026-04-20-auth-identity-payment-foundation.md @@ -2,7 +2,7 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Rebuild the auth identity, profile binding, payment routing, and OpenAI advanced scheduler foundation on top of a clean `origin/main` branch while preserving historical compatibility for existing email users and historical LinuxDo users. +**Goal:** Rebuild the auth identity, profile binding, payment routing, and OpenAI advanced scheduler foundation on top of a clean `origin/main` branch while preserving historical compatibility for existing email users, existing LinuxDo users, historical LinuxDo/WeChat/OIDC synthetic-email users, and historical WeChat `openid`-only records. **Architecture:** A unified identity foundation centered on durable provider subjects (`email`, `linuxdo`, `oidc`, `wechat`) and transactional pending-auth sessions; backend-owned payment source routing behind stable frontend methods (`alipay`, `wxpay`); compatibility-first migration/backfill before feature enablement. **Tech Stack:** Go, Gin, Ent, PostgreSQL, Redis, Vue 3, Pinia, TypeScript, Vitest, pnpm. @@ -10,8 +10,9 @@ ## Non-Negotiable Product Rules -- [ ] Preserve login continuity for existing email users and historical LinuxDo users. -- [ ] During migration, backfill historical LinuxDo synthetic-email users into explicit LinuxDo identities before first post-upgrade login. +- [ ] Preserve login continuity for existing email users, existing LinuxDo users, and historically migrated third-party users. +- [ ] During migration, backfill historical LinuxDo/WeChat/OIDC synthetic-email users into explicit third-party identities before first post-upgrade login whenever deterministic recovery is possible. +- [ ] During migration, surface historical WeChat `openid`-only records through explicit migration reports and remediation rules; do not silently reinterpret them as valid canonical identities. - [ ] Keep existing email login and add third-party login/bind for `linuxdo`, `oidc`, and `wechat`. - [ ] On first third-party login: - identity exists: direct login. @@ -37,6 +38,11 @@ - non-WeChat browser uses Open/QR login. - canonical identity uses `unionid`. - when `unionid` is unavailable, fail the login/bind flow under the approved option-1 policy. +- [ ] OIDC rules: + - browser authorization-code flow always uses PKCE `S256`. + - discovery issuer and ID token `iss` must match exactly. + - `userinfo.sub` must match ID token `sub` when UserInfo is used. + - upstream `email_verified` does not satisfy local email verification. - [ ] Payment UI rules: - user-facing methods stay `支付宝` and `微信支付`. - backend decides whether each method routes to official provider instance or EasyPay. @@ -49,20 +55,26 @@ - WeChat H5: MP/JSAPI first, fallback to H5 pay. - non-WeChat H5: H5 pay, or prompt to open in WeChat when unavailable. - [ ] Payment success pages are informational only; actual fulfillment depends on webhook or server-side reconciliation. +- [ ] WeChat in-app payment requiring `openid` must use a dedicated server-backed payment OAuth resume flow rather than frontend-only recovery state. - [ ] OpenAI advanced scheduler is available but default-disabled. ## Hard Technical Constraints From Audit - [ ] Browser-based third-party auth must use Authorization Code + PKCE `S256`. +- [ ] PKCE must not be admin-configurable off for browser authorization-code providers. - [ ] OIDC identity primary key must be `(issuer, subject)`, not email. - [ ] Email equality must never auto-link accounts. - [ ] Bind-existing-account must require explicit local re-authentication and TOTP verification when enabled. +- [ ] Bind-current-user must originate from an already-authenticated local user and preserve explicit bind intent across callback completion. - [ ] OAuth redirect URI must be fixed server config, exact-match, and never derived from user input. - [ ] User-supplied redirect may only choose a normalized same-origin internal route after completion. - [ ] WeChat canonical identity must be `unionid`; `openid` remains channel/app-scoped support data only. -- [ ] Every payment order must snapshot the selected provider instance and reuse that exact instance for callback verification, reconciliation, refund, and audit. +- [ ] Every canonical identity uniqueness rule must include provider namespace (`provider_key`) consistently. +- [ ] Callback completion must use backend session completion or a one-time opaque exchange code that is short-lived, one-time, browser-session-bound, `POST`-redeemed, and unusable as a bearer token. +- [ ] Every payment order must snapshot the selected provider instance plus the order-time verification inputs required for callback verification, reconciliation, refund, and audit. - [ ] Frontend must not receive first-party bearer tokens through callback URL fragments in the rebuilt flow. - [ ] Public payment result polling must not expose order data by raw `out_trade_no` alone; use authenticated lookup or signed opaque result token. +- [ ] WeChat Pay webhook handling must verify signature, decrypt payload, and compare `appid`, `mchid`, `out_trade_no`, `amount`, `currency`, and provider trade state against the order snapshot before fulfillment. ## Baseline Notes @@ -100,6 +112,8 @@ - [ ] `backend/internal/service/payment_order.go` - [ ] `backend/internal/service/payment_order_lifecycle.go` - [ ] `backend/internal/service/payment_fulfillment.go` +- [ ] `backend/internal/service/payment_resume_service.go` +- [ ] `backend/internal/service/payment_resume_service_test.go` - [ ] `backend/internal/service/openai_account_scheduler.go` - [ ] `backend/internal/handler/auth_pending_identity_flow.go` - [ ] `backend/internal/handler/auth_linuxdo_oauth.go` @@ -148,9 +162,10 @@ - `users.last_active_at` - grant-tracking columns/tables required to prevent double-award - [ ] Add uniqueness/index rules: - - one canonical identity per `(provider, provider_subject)` + - one canonical identity per `(provider, provider_key, provider_subject)` - one channel record per `(provider, provider_channel, provider_app_id, provider_channel_subject)` - one adoption decision per pending session +- [ ] Model `pending_auth_sessions` so immutable upstream claims and mutable local flow state are stored separately; do not reintroduce a mixed `metadata` catch-all. - [ ] Preserve null-safe compatibility defaults so historical rows remain readable before backfill finishes. - [ ] Add explicit rollback blocks only where safe; never repeat the destructive pattern observed in old `112_update_pending_auth_sessions.sql`. @@ -160,8 +175,10 @@ - existing email users into `auth_identities(provider=email, provider_subject=normalized_email)` - historical LinuxDo users into `auth_identities(provider=linuxdo, provider_subject=linuxdo_subject)` - historical synthetic-email LinuxDo users into explicit LinuxDo identity rows by parsing legacy email mode and legacy provider metadata + - historical synthetic-email WeChat users into explicit WeChat identities where `unionid` or equivalent deterministic provider identity is recoverable + - historical synthetic-email OIDC users into explicit OIDC identities where deterministic provider identity is recoverable - profile/channel rows from historical `user_external_identities`-style data when present in upgraded databases -- [ ] Write migration report output in `backend/internal/repository/auth_identity_migration_report.go` so production can inspect unmatched rows instead of silently skipping them. +- [ ] Write migration report output in `backend/internal/repository/auth_identity_migration_report.go` so production can inspect unmatched rows, `openid`-only WeChat rows, and non-deterministic synthetic-email rows instead of silently skipping them. - [ ] Set `signup_source` and provider provenance when recoverable from historical data. Do not flatten everything to `email`. ### Task 3. Provider default grant and scheduler config migration @@ -173,6 +190,7 @@ - profile avatar storage columns/settings - [ ] Implement `backend/migrations/111_payment_routing_and_scheduler_flags.sql` for: - stable payment method to provider-instance routing + - visible-method normalization from historical `supported_types`, `payment_mode`, and legacy aliases such as `wxpay_direct` - admin exclusivity flags for `alipay` and `wxpay` - advanced scheduler enable flag defaulting to disabled @@ -213,6 +231,7 @@ - update `last_login_at` and `last_active_at` - [ ] Add repository contract coverage in `backend/internal/repository/user_profile_identity_repo_contract_test.go`. - [ ] Enforce dual-write for email registration/login so `users.email` and `auth_identities(provider=email, ...)` stay consistent from this phase onward. +- [ ] Add repository coverage proving `last_login_at` and `last_active_at` use the required field names and are not silently replaced by derived `last_used_at` logic. ### Task 6. Rebuild transactional pending-auth service @@ -223,8 +242,15 @@ - bind pending identity to existing account after password/TOTP re-auth - apply configured provider defaults on the correct trigger only once - store provider nickname/avatar candidates and user opt-in replacement decisions independently +- [ ] Implement callback completion so pending auth can finish through backend session completion or a one-time exchange code: + - short TTL + - one-time use + - browser-session binding + - `POST` redemption only + - safe mixed-version bridge to legacy pending-token aliases during rollout - [ ] Keep pending session payload normalized: - provider identity fields live in typed columns/JSON structure + - mutable local progression lives separately from immutable upstream claims - avoid the old branch’s mixed `metadata` and `upstream_identity_payload` ambiguity - [ ] Do not call plain email registration helpers from this flow. The old feature branch bug where pending third-party signup fell back to `RegisterWithVerification` must not reappear. @@ -236,11 +262,13 @@ - `backend/internal/handler/auth_wechat_oauth.go` - [ ] For OIDC: - require PKCE `S256`, `state`, and `nonce` - - validate `iss`, `aud`, optional `azp`, `exp`, `nonce` + - validate discovery issuer, `iss`, `aud`, optional `azp`, `exp`, and `nonce` + - verify `userinfo.sub == id_token.sub` when UserInfo is used - persist canonical identity as `(issuer, sub)` - [ ] For WeChat: - MP flow in WeChat UA - Open/QR flow outside WeChat UA + - website login uses authorization-code flow and persists channel/app binding - persist channel identity by `(channel, appid, openid)` - persist canonical identity by `unionid` - hard-fail when `unionid` is absent under the approved product policy @@ -253,6 +281,10 @@ - submit verified email - choose create-new-account or bind-existing-account - submit nickname/avatar replacement choices +- [ ] Make bind-existing-account and bind-current-user flows explicit: + - no automatic linking on matching email + - fresh password/TOTP proof is scoped to the intended target account only + - no automatic metadata merge beyond explicitly selected nickname/avatar adoption - [ ] Update `backend/internal/handler/auth_handler.go` and `backend/internal/handler/user_handler.go` to expose: - current bindings summary - start-bind endpoints for LinuxDo/OIDC/WeChat @@ -312,6 +344,7 @@ - `frontend/src/api/auth.ts` - `frontend/src/stores/auth.ts` - [ ] Replace any token-fragment bootstrap with backend session completion or one-time exchange code flow. +- [ ] During rollout, keep temporary compatibility readers for legacy pending-token aliases behind a bounded bridge contract and explicit removal step. ### Task 12. Rebuild profile account binding and avatar UX @@ -351,11 +384,17 @@ - frontend visible methods remain `alipay` and `wxpay` - admin chooses which provider instance serves each method - runtime validation guarantees only one active source per visible method +- [ ] Add migration logic and tests to normalize historical provider-instance config: + - `supported_types` + - `payment_mode` + - legacy aliases such as `wxpay_direct` + - historical limit config - [ ] Rebuild `backend/internal/service/payment_order.go` and `backend/internal/service/payment_order_lifecycle.go` so order creation snapshots: - visible method - selected provider instance id - provider type - provider capability mode + - verification-critical provider fields needed for later callback/query/refund validation - [ ] Rebuild `backend/internal/handler/payment_handler.go` for UX rules: - Alipay PC: QR page - Alipay mobile: direct jump @@ -363,6 +402,11 @@ - WeChat H5 in WeChat: MP/JSAPI first, fallback to H5 - WeChat H5 outside WeChat: H5 or “open in WeChat” prompt when unavailable - [ ] Never derive canonical return URL from `Referer`; use configured or signed internal callback targets only. +- [ ] Implement `backend/internal/service/payment_resume_service.go` so WeChat in-app payment OAuth resume is server-backed rather than localStorage-backed: + - create `oauth_required` resume context + - persist amount/order_type/plan_id/visible method/redirect/state + - redeem callback into same-origin internal resume target + - expire and consume resume context safely ### Task 15. Make fulfillment and reconciliation provider-instance-safe @@ -370,6 +414,12 @@ - verification uses the order’s original provider instance - webhook processing is idempotent by provider event id and internal order id - missed webhook recovery uses server-side provider query, not frontend success return +- [ ] For WeChat Pay specifically, enforce: + - fixed HTTPS `notify_url` with no query params + - no dependency on user login state + - signature verification before decrypt + - APIv3 decrypt before business parsing + - comparison of `appid`, `mchid`, `out_trade_no`, `amount`, `currency`, and trade state against the order snapshot - [ ] Harden `frontend/src/views/user/PaymentResultView.vue` and `frontend/src/api/payment.ts` so result polling uses an authenticated order lookup or signed opaque token, not a raw public `out_trade_no` query. ### Task 16. Rebuild payment frontend views @@ -378,6 +428,10 @@ - only two buttons are shown to user: `支付宝` and `微信支付` - frontend does not leak official-vs-EasyPay distinction - route-specific copy handles QR, jump, MP, H5 fallback correctly +- [ ] Rebuild WeChat in-app payment resume UX around the server-backed resume context: + - handle `oauth_required` + - continue from same-origin resume target + - avoid long-lived localStorage as the source of truth - [ ] Add or update: - `frontend/src/views/user/__tests__/PaymentView.spec.ts` - `frontend/src/views/user/__tests__/PaymentResultView.spec.ts` @@ -416,16 +470,22 @@ - historical email-only user login after upgrade - historical LinuxDo user login after upgrade - historical synthetic-email LinuxDo user login after upgrade + - historical synthetic-email WeChat user login after upgrade + - historical synthetic-email OIDC user login after upgrade + - historical WeChat `openid`-only rows are reported or explicitly remediated - no retroactive grant replay during migration - first-bind grant fires once only when enabled - email identity dual-write stays consistent - bind-existing-account requires password and TOTP where configured + - mixed-version callback token bridge works during rollout and is removable afterward + - historical payment config is normalized into visible-method routing without refund/query regression - [ ] Add deploy sequencing note to release docs or internal runbook: 1. deploy schema and backfill release. 2. inspect migration report for unmatched rows. - 3. deploy backend identity/payment compatibility code. - 4. deploy frontend callback/profile/payment UI. - 5. enable strict email-required signup or provider bind grants only after metrics are healthy. + 3. deploy backend identity/payment compatibility code with exchange bridge and legacy token aliases still enabled. + 4. deploy frontend callback/profile/payment UI using session completion, exchange code, and server-backed WeChat payment resume. + 5. remove legacy callback/token parsing after mixed-version window closes. + 6. enable strict email-required signup or provider bind grants only after metrics are healthy. ### Task 20. Final verification and handoff @@ -449,6 +509,8 @@ - [ ] Run focused manual smoke checks: - email login with existing account - LinuxDo existing-account login after migration + - WeChat synthetic-email account login after migration + - OIDC synthetic-email account login after migration - third-party first login create-new-account path - third-party first login bind-existing-account path - first third-party bind with optional nickname/avatar replacement @@ -456,6 +518,7 @@ - mobile Alipay jump - PC WeChat QR - WeChat H5 MP/JSAPI path + - WeChat in-app OAuth resume path - non-WeChat H5 fallback path - [ ] Commit final checkpoint: ```bash @@ -468,9 +531,9 @@ - [ ] No flow still relies on provider email equality for account linking. - [ ] No flow still creates third-party users through plain email registration helpers. - [ ] No callback still returns first-party bearer tokens in URL fragments. +- [ ] No callback completion path can be replayed as a bearer token substitute. - [ ] No payment result view trusts provider return page as authoritative fulfillment. - [ ] No webhook verification path selects provider credentials from “currently active config” instead of the order snapshot. -- [ ] Existing email users and historical LinuxDo users are covered by migration tests. +- [ ] Existing email users, historical LinuxDo/WeChat/OIDC users, and `openid`-only WeChat remediation cases are covered by migration tests. - [ ] Avatar adoption and deletion semantics are explicit and reversible. - [ ] Grant timing is source-aware and one-time only. - diff --git a/docs/superpowers/specs/2026-04-20-auth-identity-payment-foundation-design.md b/docs/superpowers/specs/2026-04-20-auth-identity-payment-foundation-design.md index bfa250d9..790861b7 100644 --- a/docs/superpowers/specs/2026-04-20-auth-identity-payment-foundation-design.md +++ b/docs/superpowers/specs/2026-04-20-auth-identity-payment-foundation-design.md @@ -20,10 +20,10 @@ This design includes: - Profile binding management and avatar upload/delete - Source-based initial grants for balance, concurrency, and subscriptions - User management support for `last_login_at` and `last_active_at` sorting -- Unified payment display methods (`alipay`, `wechat`) mapped to a single active backend source each +- Unified payment display methods (`alipay`, `wxpay`) mapped to a single active backend source each - Alipay and WeChat UX routing rules across PC, mobile, H5, and WeChat environments - Admin settings for auth providers, source defaults, payment sources, and OpenAI advanced scheduling -- Incremental migration and compatibility for existing email users and historical LinuxDo synthetic-email users +- Incremental migration and compatibility for existing email users, existing LinuxDo users, historical LinuxDo/WeChat/OIDC synthetic-email users, and historical WeChat `openid`-only identity records This design does not treat unrelated upstream merges, docs churn, or license changes from the old branch as required scope. @@ -32,9 +32,11 @@ This design does not treat unrelated upstream merges, docs churn, or license cha ### Auth and identity - Existing email users remain valid and continue to log in with no manual action. +- Existing LinuxDo, OIDC, and WeChat users represented by historical third-party or synthetic-email data must remain recoverable during migration. - Third-party first login behavior: - Existing bound identity: direct login - Missing identity: start first-login flow +- Browser-based third-party authorization-code login always uses PKCE `S256`; this is not an admin-toggleable feature. - If `force_email_on_third_party_signup` is disabled, a first-login user may create an account without binding an email. - If `force_email_on_third_party_signup` is enabled, the user must provide an email. - If the provided and verified email already exists: @@ -43,11 +45,25 @@ This design does not treat unrelated upstream merges, docs churn, or license cha - allow "change email and continue registration" - do not allow bypassing the email requirement - Upstream provider email verification is not trusted as a local bound email. +- Matching upstream email must never auto-link to an existing local account. +- Linking to an existing local account is allowed only when: + - the user explicitly chooses that target account + - the target account passes fresh local re-authentication + - required TOTP verification succeeds +- New third-party bind initiated from profile must start from an already logged-in local account and preserve explicit bind intent end-to-end. +- `redirect_to` may only represent a normalized same-origin internal route. It must never contain a third-party URL and must never be derived from `Referer`. +- OIDC validation rules: + - canonical identity key is `issuer + sub` + - discovery issuer and ID token `iss` must match exactly + - `userinfo.sub` must match ID token `sub` when UserInfo is used + - upstream `email_verified` may improve UX copy but does not satisfy local email-binding requirements - WeChat login chooses channel by environment: - in WeChat environment: `mp` - outside WeChat: `open` - WeChat primary identity key is `unionid`. - If a WeChat login/bind flow cannot produce `unionid`, the flow fails and no fallback `openid` identity is created. +- Historical WeChat records that only contain `openid` are treated as migration-remediation cases, not as a valid long-term canonical identity model. +- WeChat website login uses authorization code flow, random `state`, and the provider channel/app binding must be persisted alongside the resolved identity. ### Profile adoption @@ -85,7 +101,7 @@ This design does not treat unrelated upstream merges, docs churn, or license cha - Frontend shows only two display methods: - `alipay` - - `wechat` + - `wxpay` - Users never choose between official providers and EasyPay explicitly. - Backend allows only one active source per display method at a time. - Alipay UX: @@ -96,6 +112,10 @@ This design does not treat unrelated upstream merges, docs churn, or license cha - non-WeChat H5: prefer H5 pay; if unavailable, tell the user to open in WeChat - WeChat environment: prefer MP/JSAPI pay; if unavailable, fall back to H5 pay - Payment success is confirmed by backend order state, webhook, and/or query, not only frontend return. +- Frontend-visible labels remain `支付宝` and `微信支付`, while internal visible-method identifiers remain `alipay` and `wxpay`. +- Public result pages must not verify order state by exposing raw `out_trade_no`; they use authenticated lookup or a signed opaque result token instead. +- Payment callback or return URLs must be fixed same-origin internal targets. They must not be inferred from `Referer`. +- WeChat payment webhook handling must use a fixed HTTPS `notify_url` with no query parameters and must not depend on user login state. ### OpenAI advanced scheduling @@ -105,9 +125,9 @@ This design does not treat unrelated upstream merges, docs churn, or license cha ## Architecture -Keep `users` as the account owner table and move login identities, channel mappings, pending auth state, and first-bind grant idempotency into dedicated tables and services. Keep email login working while progressively introducing unified identity reads and writes. +Keep `users` as the account owner table and move login identities, channel mappings, pending auth state, callback completion state, and first-bind grant idempotency into dedicated tables and services. Keep email login working while progressively introducing unified identity reads and writes. -Payment uses a similar split between user-visible display methods and backend provider sources. Frontend works only with stable display methods while backend resolves to the currently active source and capability matrix. +Payment uses a similar split between user-visible display methods and backend provider sources. Frontend works only with stable display methods while backend resolves to the currently active source and capability matrix, and stores enough order-time snapshot data to survive later provider-config changes. Compatibility is a first-class concern: migrations are additive, reads are compatibility-aware, and rollout must tolerate existing `main` data and short-lived frontend/backend version skew. @@ -148,9 +168,9 @@ Uniqueness: Rules: - email identity uses canonicalized local email -- LinuxDo uses stable provider subject -- OIDC uses stable issuer + subject -- WeChat uses `unionid` as canonical subject +- LinuxDo uses stable provider subject under the configured provider namespace +- OIDC uses stable issuer + subject, with issuer namespace represented consistently through `provider_key` and `issuer` +- WeChat uses `unionid` as canonical subject under the configured Open Platform namespace ### `auth_identity_channels` @@ -189,9 +209,12 @@ Fields: - `target_user_id` - `redirect_to` - `resolved_email` -- `pending_password_hash` -- `upstream_identity_payload` -- `metadata` +- `registration_password_hash` +- `upstream_identity_claims` +- `local_flow_state` +- `browser_session_key` +- `completion_code_hash` +- `completion_code_expires_at` - `email_verified_at` - `password_verified_at` - `totp_verified_at` @@ -205,19 +228,33 @@ Responsibilities: - persist nickname/avatar suggestions - persist explicit adoption decisions - survive navigation between auth pages +- support mixed-version rollout through short-lived legacy token aliases when required + +Security rules: + +- callback completion uses backend session completion or a one-time exchange code +- exchange codes are short-lived, one-time, bound to browser session and pending session, and redeemed via `POST` +- exchange codes must not behave as bearer tokens and must not be logged, stored in URL fragments, or reused after redemption +- `local_flow_state` stores mutable local progression only; immutable upstream claims remain in `upstream_identity_claims` ### `identity_adoption_decisions` -Persists user adoption preference for a specific identity. +Persists user adoption preference collected during a pending-auth flow and resolved onto the bound identity. Fields: +- `pending_auth_session_id` - `identity_id` - `adopt_display_name` - `adopt_avatar` - `decided_at` - timestamps +Rules: + +- one adoption-decision row exists per pending session +- `identity_id` is filled once final account creation or bind succeeds + ### `user_avatars` Stores the currently effective custom avatar. @@ -265,6 +302,7 @@ WeChat-specific rule: - `openid` never becomes the primary stored identity key - if only `openid` is available, login/bind fails with a configuration/identity error +- historical `openid`-only records must be reported and either remediated during migration or explicitly blocked from silent auto-upgrade ## Core Flows @@ -285,6 +323,7 @@ WeChat-specific rule: - Create `pending_auth_session` - Frontend callback flow decides next action +- Pending session creation stores immutable upstream claims separately from mutable local progress fields Branches: @@ -303,6 +342,7 @@ On new account creation: - create `users` row - create canonical third-party identity +- create or update canonical email identity when local email binding succeeds - apply source signup grants - apply adoption choices if selected @@ -310,21 +350,34 @@ On new account creation: - current user starts bind flow - callback resolves to `bind_current_user` +- bind intent is tied to the initiating local user session and cannot be re-targeted by email match - bind canonical identity to current user - if configured and first bind for that provider, apply first-bind grants - present nickname/avatar replacement choice ### Bind existing account during first-login flow +- user explicitly selects bind-existing-account - verify password for existing account - if account requires TOTP, verify TOTP - bind canonical identity to target account - optionally apply first-bind grants - present nickname/avatar replacement choice +- no automatic profile or metadata merge occurs beyond explicitly selected nickname/avatar replacement + +### Callback completion and exchange flow + +- third-party callback never returns first-party bearer tokens in URL fragments +- callback completion uses either: + - backend session completion tied to the initiating browser session + - one-time opaque exchange code redeemed by `POST` +- mixed-version rollout may temporarily emit legacy pending token aliases in addition to the new completion path +- legacy alias support is transitional and bounded to rollout windows only ### WeChat login and channel mapping - environment chooses `mp` or `open` +- website login uses authorization-code flow with provider-configured app/channel binding - callback must resolve to `unionid` - channel `openid` is optionally recorded in `auth_identity_channels` - failure to obtain `unionid` aborts flow @@ -344,7 +397,7 @@ On new account creation: ### User-visible methods - `alipay` -- `wechat` +- `wxpay` ### Backend source abstraction @@ -357,6 +410,12 @@ Each display method maps to exactly one active configured backend source: Frontend submits display method only. Backend resolves display method to active source and capability set. +### Legacy payment-config normalization + +- existing provider-instance `supported_types`, legacy aliases such as `wxpay_direct`, and per-type limit structures are migrated into the visible-method model +- migration preserves historical payment capability and refund semantics +- the system keeps one normalized visible-method mapping per provider instance for rollout and audit + ### Alipay routing - PC: create QR-oriented result and show QR in page @@ -372,11 +431,25 @@ Frontend submits display method only. Backend resolves display method to active - prefer MP/JSAPI - if unavailable, fall back to H5 pay +### WeChat payment OAuth recovery + +- if WeChat in-app payment requires `openid` and the current request does not already hold it, backend returns an `oauth_required` response instead of guessing +- backend creates a server-backed payment-resume context containing: + - target visible method + - amount/order type/plan context + - redirect target + - anti-replay state +- backend redirects through a dedicated WeChat payment OAuth start endpoint +- callback exchanges the provider code server-side, stores `openid` in the payment-resume context, and returns a same-origin internal resume target +- frontend resumes the original order flow through the resume context instead of trusting raw callback query state or long-lived local storage + ### Payment completion - frontend return restores context and UI state - backend order state remains source of truth - webhook and/or order query remain authoritative for fulfillment +- order fulfillment validates webhook or query payload against order-time snapshot data including provider instance, merchant identifiers, amount, currency, and provider order references +- result pages use authenticated lookup or signed opaque result tokens, never raw public `out_trade_no` ## Admin Configuration Model @@ -420,6 +493,9 @@ Compatibility is mandatory, especially for: - existing email users - existing LinuxDo users - historical LinuxDo synthetic-email accounts +- historical WeChat synthetic-email accounts +- historical OIDC synthetic-email accounts +- historical WeChat `openid`-only records created by older branches ### Additive migrations @@ -431,6 +507,8 @@ Compatibility is mandatory, especially for: - backfill canonical `email` identities for valid existing email users - backfill canonical `linuxdo` identities during migration for historical synthetic-email LinuxDo users +- backfill canonical `wechat` and `oidc` identities when historical synthetic-email or `user_external_identities` data allows deterministic reconstruction +- emit migration reports for historical WeChat `openid`-only records that cannot be safely promoted to canonical `unionid` - backfill must be idempotent and repeatable ### Compatibility reads @@ -438,7 +516,7 @@ Compatibility is mandatory, especially for: During rollout: - read new identity model first -- where necessary, retain compatibility logic for existing email and historical LinuxDo synthetic-email recognition +- where necessary, retain compatibility logic for existing email and historical LinuxDo/WeChat/OIDC synthetic-email recognition ### Grant idempotency @@ -453,17 +531,20 @@ Retain transitional support for legacy/new request and response shapes where nee - `pending_oauth_token` - old callback parsing expectations - historical profile field mappings +- legacy callback fragment readers during the bounded rollout window ### Settings and payment compatibility - preserve existing payment configs and order semantics from `main` - add new settings incrementally - avoid rewriting the entire settings schema in one cutover +- preserve legacy provider-instance capabilities by explicitly mapping historical `supported_types`, `payment_mode`, and limit config into normalized visible-method routing ### Rolling upgrade tolerance - do not assume simultaneous frontend/backend deployment - new backend must tolerate short-lived older frontend request shapes +- rollout must define the deployment order and removal point for legacy callback token parsing and legacy payment resume parsing ## Testing Strategy @@ -509,9 +590,13 @@ Retain transitional support for legacy/new request and response shapes where nee - existing email users - historical LinuxDo synthetic-email users +- historical WeChat synthetic-email users +- historical OIDC synthetic-email users +- historical WeChat `openid`-only records reported or remediated correctly - historical payment config - legacy auth payload field names - historical payment result handling +- mixed-version callback token bridge behavior ## Implementation Phases @@ -528,9 +613,12 @@ Retain transitional support for legacy/new request and response shapes where nee Implementation must follow current primary-source guidance: - OAuth 2.0 Security BCP (RFC 9700): strict redirect handling, state protection, mix-up resistant design -- PKCE (RFC 7636): use on authorization code flows where applicable +- PKCE (RFC 7636): require `S256` on browser authorization-code flows - OpenID Connect Core: stable issuer/subject handling for OIDC identities - Account linking best practice: require explicit user confirmation or re-authentication before linking to existing accounts +- WeChat UnionID and website-login guidance: treat `unionid` as canonical cross-channel subject and persist channel/app binding with website login responses +- WeChat Pay webhook guidance: verify signatures, decrypt payloads, and confirm merchant/order/amount fields against order-time state before fulfillment +- Payment success-page guidance: custom success pages are informational and must not be the only fulfillment trigger References: @@ -538,6 +626,10 @@ References: - RFC 7636: - OpenID Connect Core 1.0: - Auth0 account linking guidance: +- WeChat UnionID guidance: +- WeChat website login guidance: +- WeChat Pay callback/signature guidance: +- Stripe Checkout fulfillment guidance: ## Audit Synthesis @@ -553,7 +645,7 @@ The clean rebuild direction is not to copy either existing branch directly. - `personal-dev-branch` has the better real-world closure: - LinuxDo and WeChat callback flows are more operationally complete - profile binding and avatar UX is more complete - - historical synthetic-email users are recognized and recovered in live flows + - historical synthetic-email users across multiple providers are recognized and recovered in live flows - WeChat payment OAuth and recovery behavior is more complete - Primary-source guidance supplies hard constraints for OAuth/OIDC, account linking, WeChat identity handling, and payment completion semantics. @@ -584,6 +676,7 @@ Keep these operational flow ideas from `personal-dev-branch`: - profile bindings UX and “cannot disconnect last usable login method” rule - separate WeChat login OAuth and WeChat payment OAuth entry points - historical synthetic-email recognition logic as a migration bridge +- explicit WeChat payment OAuth recovery protocol as a product requirement, but reimplemented with server-backed resume state ### Adapt @@ -595,6 +688,7 @@ These areas must be reimplemented with the same intent but stricter boundaries: - WeChat payment recovery state must move from frontend-only storage to server-backed continuation state - avatar adoption fetches must be security-hardened and failure-visible - pending-auth payload modeling must clearly separate immutable upstream payload from mutable local metadata +- callback completion must use a real exchange/session model instead of fragment-delivered bearer tokens - profile binding/avatar DTOs must be simplified to one authoritative backend contract instead of sprawling frontend fallback parsing - admin settings should preserve capability while reducing duplicated or transitional config branches @@ -614,7 +708,7 @@ The audit and source review establish these hard constraints: ### Auth -- all authorization-code providers use PKCE where applicable +- all browser authorization-code providers use PKCE `S256` and do not expose an admin-off switch - callback handling uses strict `redirect_uri` discipline and state validation - OIDC identity key is `issuer + sub` - existing-account linking after email conflict must require explicit user action plus local-account verification @@ -624,7 +718,8 @@ The audit and source review establish these hard constraints: - existing email users must continue to work with no manual intervention - existing LinuxDo users must not split into duplicate accounts -- historical LinuxDo synthetic-email users must be backfilled into canonical LinuxDo identities during migration, not only lazily on next login +- historical LinuxDo/WeChat/OIDC synthetic-email users must be backfilled into canonical identities during migration when deterministic recovery is possible +- historical WeChat `openid`-only records must be surfaced through migration reporting and explicit remediation rules - migration backfills must not trigger signup or first-bind grants - legacy `pending_auth_token` and `pending_oauth_token` contracts must remain accepted during rollout - legacy auth/public setting aliases needed by older frontend builds must remain available during rollout @@ -634,7 +729,9 @@ The audit and source review establish these hard constraints: - frontend return pages do not determine final payment success - backend order state, webhook processing, and/or provider status query remain authoritative -- each visible method (`alipay`, `wechat`) may have only one active backend source at a time +- each visible method (`alipay`, `wxpay`) may have only one active backend source at a time +- public result pages must not expose raw `out_trade_no` lookup +- WeChat Pay callback handling must verify signature, decrypt payload, and compare order fields against order-time snapshot data ## Known Risks To Eliminate In Implementation @@ -649,6 +746,7 @@ These are specifically observed problems in the existing branches that the clean - WeChat payment recovery in `personal-dev-branch` is frontend-local and not robust across tabs or concurrent attempts - the existing pending-auth migration update is too destructive to reuse unchanged in a safer rollout - historical provider provenance should not be permanently flattened to `signup_source = email` +- design/plan drift can reintroduce ambiguous identity uniqueness or ambiguous adoption-decision ownership if not aligned before implementation ## Rollout Gates @@ -656,9 +754,10 @@ The rebuild is not ready for rollout until all of these are satisfied: 1. Identity schema and migration chain are linearized and production-safe. 2. Email identity backfill is complete and idempotent. -3. Historical LinuxDo synthetic-email backfill to canonical LinuxDo identity is complete and idempotent. -4. `signup_source` backfill is accurate for known historical provider-created users. -5. Dual token acceptance and required legacy field aliases are present. -6. Existing payment configs are normalized and verified against current frontend-visible capabilities. -7. New frontend flows are verified against mixed-version backend compatibility windows. -8. Duplicate-account creation, first-bind grants, and payment route selection have regression coverage. +3. Historical LinuxDo/WeChat/OIDC synthetic-email backfill to canonical identity is complete where deterministic, and non-recoverable rows are reported. +4. Historical WeChat `openid`-only rows are either remediated or explicitly blocked with operator-visible reporting. +5. `signup_source` backfill is accurate for known historical provider-created users. +6. Dual token acceptance, exchange bridge behavior, and required legacy field aliases are present for the bounded rollout window. +7. Existing payment configs are normalized and verified against current frontend-visible capabilities. +8. New frontend flows are verified against mixed-version backend compatibility windows. +9. Duplicate-account creation, first-bind grants, and payment route selection have regression coverage.