This commit is contained in:
2026-10-27 15:56:06 -04:00
parent d01f4a8fb7
commit aa18f6b895
15 changed files with 243 additions and 62 deletions
+36 -24
View File
@@ -8,15 +8,16 @@ Both payment methods feed the same LDAP group reconciliation and local expiry.
## Membership flow
1. Anyone can request a free account. A one-hour, single-use email link allows
1. Anyone can request a free level-0 account. A one-hour, single-use email link allows
them to choose a password. The account is created as an `inetOrgPerson` in
OpenLDAP; the password goes only through RFC 3062 Password Modify over
certificate-verified LDAPS/StartTLS by default (see the explicit plaintext
option below). This app never stores a password/hash.
2. Sign-in uses Dex Authorization Code + PKCE, state and nonce. Authlib verifies
the RS256 ID token, issuer, audience, signature and lifetime. A signed
`federated_claims` connector/user ID must match the configured LDAP connector
and LDAP `entryUUID`; accounts are **not** linked using email alone.
2. Sign-in uses the single Dex LDAP option with Authorization Code + PKCE, state
and nonce. Authlib verifies the RS256 ID token, issuer, audience, signature
and lifetime. The signed connector/UID must match the configured connector
and LDAP identity; account ownership is anchored to immutable `entryUUID`,
never email alone.
3. Authenticated users choose a server-allowlisted recurring Stripe Price.
Stripe hosts Checkout and the billing portal. Return URLs never grant access.
4. Stripe webhooks are signature-verified and durably deduplicated. The worker
@@ -45,13 +46,22 @@ The live Stripe dashboard was inspected on 2026-10-23. This integration reuses
**Subscription**, product `prod_Sc9N5J0vNAdLCn`, without modifying its prices,
metadata, Discourse integration, or existing subscriptions.
| Tier | Monthly USD | Existing live Price ID | Proposed dedicated LDAP group |
| Membership | Monthly USD | Existing live Price ID | Proposed dedicated LDAP group |
| --- | ---: | --- | --- |
| Level 0 | $4 | `price_1Rgv4xC5KPXqTvVLfMd6z7wn` | `www-level-0` |
| Level 1 | $8 | `price_1Rgv5QC5KPXqTvVLv1PaHWb5` | `www-level-1` |
| Level 2 | $16 | `price_1Rgv5rC5KPXqTvVL8Ol8sMUf` | `www-level-2` |
| Level 3 | $32 | `price_1Rgv6FC5KPXqTvVL39mAxYjm` | `www-level-3` |
| Level 4 | $64 | `price_1Rgv6mC5KPXqTvVLkcmznWZa` | `www-level-4` |
| Level 0 · Community | Free | — | — |
| Level 1 · Supporter | $4 | `price_1Rgv4xC5KPXqTvVLfMd6z7wn` | `www-level-1` |
| Level 2 · Insider | $8 | `price_1Rgv5QC5KPXqTvVLv1PaHWb5` | `www-level-2` |
| Level 3 · Collaborator | $16 | `price_1Rgv5rC5KPXqTvVL8Ol8sMUf` | `www-level-3` |
| Level 4 · Sustainer | $32 | `price_1Rgv6FC5KPXqTvVL39mAxYjm` | `www-level-4` |
| Level 5 · Patron | $64 | `price_1Rgv6mC5KPXqTvVLkcmznWZa` | `www-level-5` |
Every account has a non-null `membership_level`: new/free accounts start at 0,
and synchronization records the highest currently valid paid level. The former
paid levels 0–4 are shifted to 1–5; Stripe Price IDs remain unchanged. The
migration also updates stored subscription and prepaid-invoice tier keys and
backfills current account levels. Existing level-0 LDAP group grants are
revoked by the worker after the migration; retain that legacy group until the
first successful synchronization of existing accounts.
The deployable mapping is in [membership/tiers.eom.json](membership/tiers.eom.json).
The **group DNs are deployment proposals**, not verified existing groups: create
@@ -97,17 +107,18 @@ of these five prices when inspected.
and exposes bind passwords, registration passwords and directory data to the
network. The default is `0` (StartTLS required for `ldap://`); TLS failures never
trigger a plaintext fallback, and `ldaps://` always verifies certificates.
4. Add a dedicated Dex client/LDAP connector using
4. Add the website client to Dex and merge website account entries into its
existing LDAP connector using
[membership/deploy/dex-membership.example.yaml](membership/deploy/dex-membership.example.yaml).
The exact callback is `https://eom.dev/members/oidc/callback/` (trailing slash).
Set the same client secret in Dex and `DEX_CLIENT_SECRET`, and set
`DEX_CONNECTOR_ID=www-ldap`. This connector uses `idAttr: entryUUID`,
`preferredUsernameAttr: uid`, `emailAttr: mail`; scopes include `federated:id`.
**Do not change the existing connector's idAttr blindly**: that would change
subjects in other apps. Use the additional connector instead. Dex must search
the account OU without requiring membership in a paid group. RS256 and all
discovery endpoints must be HTTPS on the issuer's origin. Existing lowercase
LDAP usernames matching the registration syntax can sign in without signup.
`DEX_CONNECTOR_ID=ldap`. Keep the connector ID and `idAttr: uid` unchanged to
preserve subjects in other apps. Add the website `inetOrgPerson` branch to the
LDAP filter; this allows those entries to authenticate to all Dex clients, so
review that scope. Dex uses signed `federated:id` claims, while the app resolves
UID to LDAP and checks immutable `entryUUID`. RS256 and all discovery endpoints
must be HTTPS on the issuer's origin. Existing lowercase LDAP usernames
matching the registration syntax can sign in without signup.
5. The supplied tier file contains live Price IDs. Set `STRIPE_LIVE_MODE=1` only
with the matching live Stripe API key. For sandbox testing, override
`MEMBERSHIP_TIERS_JSON` with **test-mode** recurring prices and set live mode
@@ -193,8 +204,9 @@ To enable:
or triggers an API request. The worker polls known local invoice UUIDs over
authenticated HTTPS, so forged, repeated, missing or reordered callbacks
cannot grant access or extend a term.
5. Redeploy the image and run the migration service, which applies
[membership/members/migrations/0002_crypto_invoices.py](membership/members/migrations/0002_crypto_invoices.py).
5. Redeploy the image and run the migration service, which applies the pending
membership schema/data migrations, including
[membership/members/migrations/0003_membership_levels.py](membership/members/migrations/0003_membership_levels.py).
Keep the membership worker running; it independently refreshes Stripe and
SHKeeper, and performs a local-only expiry sweep before remote requests.
6. Smoke-test a low-value invoice on your actual deployment before offering
@@ -274,9 +286,9 @@ Payment lifecycle:
current implementation follows paid invoices plus subscription state. If a
refund/dispute should revoke access, cancel/suspend the subscription as part of
your reviewed refund/dispute process.
- Free accounts are `inetOrgPerson`, not automatically POSIX/shell accounts and
not automatically members of any service group. Password resets remain your
existing LDAP/Dex account recovery process.
- Level-0 accounts are `inetOrgPerson`, not automatically POSIX/shell accounts
and not automatically members of any service group. Password resets remain
your existing LDAP/Dex account recovery process.
## Verification