flatrate / wiki-supabase-oauth
FlatRate Wiki Supabase OAuth 2.1 SSO provider and public-auth gate for Flarum 1.8.
Package info
github.com/mrkcntrmn/flatrate-wiki-supabase-oauth
Type:flarum-extension
pkg:composer/flatrate/wiki-supabase-oauth
Requires
- php: >=8.1
- flarum/core: ^1.8.1
- flarum/nicknames: ^1.8.3
- fof/extend: ^1.3.4
- fof/oauth: ^1.7.4
- league/oauth2-client: ^2.7
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.2.8
- v0.2.7
- v0.2.6
- v0.2.5
- v0.2.4
- v0.2.3
- v0.2.2
- v0.2.1
- v0.2.0
- dev-fix/growth001ui-footer-vote-order
- dev-reconcile/production-c12bad3-to-main
- dev-feat/growth001ui-upvote-only-thumb-r1
- dev-feat/forum-public-pseudonym-001-r2
- dev-feat/forum-member-dashboard-001h-community-activity-map
- dev-release/growth001f-live-oauth-pin
- dev-feat/growth001b-plain-vote-foundation-r1
- dev-release/phase1-e1b-dashboard-live-base-r1
- dev-release/phase1-e1-dashboard-prod-r1
- dev-fix/activity-external-drain-endpoint-r1
- dev-feat/forum-public-pseudonym-001b
- dev-fix/activity-prefix-repair-r1
- dev-feat/tech-club-premium-badge
- dev-feat/forum-member-dashboard-001ab
- dev-fix/forum-ui-reg-002h-mobile-manifest-handoff
- dev-fix/forum-ui-reg-002b-navigation-handoff
- dev-fix/forum-identity-002-r3-spa-boot
- dev-fix/forum-identity-002-r2-mariadb-migration
- dev-feat/forum-identity-002-member-number-display
- dev-feat/forum-identity-001-r3d-oauth
- dev-fix/forum-identity-001-r3de
- dev-fix/forum-identity-001-r3-reservation
- dev-feat/rep001fa1-activity-emitter
- dev-feat/forum-ia015-persistent-grouped-navigation
- dev-feat/forum-sub001-family-follow-notifications
- dev-fix/forum-sub000d-frontend-js-registration
- dev-fix/forum-email002-delivery-promotion
- dev-feat/forum-sub000a-follow-tags-email-policy
- dev-feat/forum-ia013-grouped-navigation
- dev-feat/brands-presentation-nav-tree
- dev-feat/forum-email-001-teaser-only
- dev-fix/mobile-brand-drawer-order-filter
- dev-fix/mobile-brand-drawer-placement
- dev-feat/forum-email-teaser-policy
- dev-feat/mobile-brand-sidebar-links
- dev-fix/affiliated-brand-username-itemlist-get
- dev-fix/affiliated-brand-header-inline-stack
- dev-fix/affiliated-brand-header-display-contents
- dev-fix/affiliated-brand-author-header-layout
- dev-feat/affiliated-brand-post-user
- dev-fix/meta001b-taglabel-header-alignment
- dev-fix/meta001b-reply-taglabel-persistence
- dev-fix/meta001a-flarum1-compat
- dev-codex/meta001-reply-job-breakdown-marker
- dev-fix-forum-direct-login-bundle
- dev-hotfix/forum-frontend-boot
- dev-cleanup/release-v0.2.7-workflow
- dev-release/v0.2.7
- dev-fix/forum-oauth-sequential-nickname
- dev-feat/direct-forum-login-redirect
- dev-feat/forum-sequential-public-nickname
- dev-chore/release-v0.2.6
- dev-fix/sso006-csrf-exemption
- dev-chore/release-v0.2.5
- dev-fix/sso005-managed-host-secret
- dev-chore/release-v0.2.4
- dev-feat/sso004-session-bootstrap
- dev-fix/top-level-community-settings-oauth
- dev-feat/sso-003-silent-provisioning
- dev-feat/public-nickname-model
- dev-feat/login-cta-icon
- dev-feat/login-button-polish
This package is auto-updated.
Last update: 2026-09-19 07:23:16 UTC
README
Flarum 1.8 extension for FlatRate Wiki identity integration and forum metadata.
Supabase Auth remains the canonical credential/account authority. Flarum keeps only local community identity/state linked to the immutable Supabase/OIDC sub.
The production target has two complementary paths:
- Primary product path: FlatRate.wiki provisions the linked Flarum identity server-to-server and enters Community with a short-lived, opaque, one-time session ticket. Users do not see an OAuth consent/callback flow when they click Community.
- Rollback/fallback path: the existing OAuth 2.1 authorization-code flow with PKCE
S256remains available during migration and for explicit legacy-account linking.
Security and identity contract
- Supabase
subis the only cross-system identity key. - Email is a private attribute and is never used to infer or auto-link an unrelated Flarum account.
- Flarum routing usernames are deterministic opaque handles:
tech_<stable-hash(sub)>. - Flarum Nicknames is the user-editable public display-name layer.
- New forum identities require a non-empty verified email attribute, but linkage remains keyed to
sub. - If a verified email is already owned by an unrelated local Flarum account, provisioning fails closed with
existing_account_requires_explicit_link. - Ordinary native Flarum password login/signup are blocked server-side; native administrator password login remains an unadvertised recovery path.
- FlatRate.wiki and Flarum do not share authentication cookies.
- Supabase access tokens, refresh tokens, passwords, and service-role credentials never appear in forum-entry URLs.
- Internal bridge requests require a deployment-only HMAC secret, timestamp, and nonce.
- Forum-entry tickets are cryptographically random, hashed at rest, expire after 45 seconds, and are atomically single-use.
Reserved internal email namespace (FORUM-EMAIL-002)
@users.flatrate.wiki is a reserved Flarum-internal namespace. FlatRate uses deterministic placeholder addresses there only to satisfy Flarum's unique email-shaped field while a verified phone user's real email remains unconfirmed.
Hard rules:
- the entire
users.flatrate.wikidomain isINTERNAL=trueandOUTBOUND_DELIVERABLE=false; - SSO
email_verified=truedoes not imply outbound email deliverability; - FlatRate replaces only Flarum's
emailnotification driver so placeholder-backed users still receive browser/on-site alerts; - do not use a global
Notification::beforeSending()recipient filter; - when a later SSO call carries a confirmed real email for the same
sub, the already-linked Flarum user is promoted one-way from the placeholder to that real address; - promotion never changes Flarum user id,
login_providersidentifier, Supabasesub, nickname, preferences, or discussion/post ownership; - never automatically downgrade a real email back to a placeholder, and never auto-replace real email A with real email B;
subremains authoritative; email remains a mutable attribute;- DNS for
users.flatrate.wikimust not be created as a workaround (DNS_CHANGE_REQUIRED=false).
Identity fields
| Concern | Source of truth | Example | Public? |
|---|---|---|---|
| Authentication identity | Supabase sub |
UUID-like subject | No |
| Login/account address | Supabase/Flarum email | tech@example.com |
No |
| Internal Flarum schema email | FlatRate placeholder | forum-<hash>@users.flatrate.wiki |
No |
| Flarum routing username | Derived from sub |
tech_a1b2c3d4 |
Yes (guest open-web display alias; see FORUM-PUBLIC-PSEUDONYM-001) |
| Community member number | Flarum users.id (FORUM-IDENTITY-002) |
322 |
Authenticated Community public as Member #322; hidden from guest/crawler projection |
| Permanent member identity | Derived from member number | tech_#322 |
Authenticated Community public (initial for new users); hidden from guest projection |
| Custom / grandfathered nickname | Flarum Nicknames + member profile | tech_20031 / DieselDave |
Authenticated Community public; hidden from guest projection |
| Legacy FlatRate tech number | Supabase assignment (R3 rollback only) | 20031 |
No |
Guest/open-web identity projection (FORUM-PUBLIC-PSEUDONYM-001, R2 on live runtime):
unauthenticated responses use the existing routing username (tech_<8hex>), omit Member # /
custom nickname attributes, and null custom avatarUrl. Authenticated Community behavior is
unchanged. See docs/forum-public-pseudonym-001a-inventory.md. Not production-deployed until
explicit production acceptance.
Reserved nickname namespaces
Legacy ^tech_[0-9]+$ and canonical ^tech_#[0-9]+$ (case-insensitive) are reserved.
- Human nickname edits and direct Flarum signup that set
attributes.nicknameto either reserved form are rejected. - Grandfathered users who already store
tech_Nkeep that visible nickname; unrelated profile saves withoutattributes.nicknameare not rejected. - Trusted member-display actions and SSO registration set nicknames on the user model (not via request
attributes.nickname). - Historical R3-A/R3-D
tech_N+ signedtech_number(≥ 20031) remains rollback compatibility. Canonical new users taketech_#<users.id>and do not require the 20031 allocator.
Requirements
- PHP
>=8.1 - Flarum
^1.8.1 flarum/nicknames:^1.8.3fof/oauth:^1.7.4fof/extend:^1.3.4league/oauth2-client:^2.7
Install
composer require flatrate/wiki-supabase-oauth:^0.2
For the managed PikaPods/Flarum image, persist the package in /data/extensions/list so it is restored after restart.
PikaPods does not expose an application console. Existing member profiles are created by lazy self-heal on provision/login; production bulk CLI backfill is not required.
PIKAPODS_APPLICATION_CONSOLE=unavailable
PRODUCTION_BULK_CLI_BACKFILL=not_required
EXISTING_PROFILE_MIGRATION=lazy_self_heal
Enable dependencies in this order:
- Nicknames
- FoF OAuth
- FlatRate Wiki Login
Seamless product-to-forum flow
The normal user journey is intentionally not a browser OAuth flow:
FlatRate.wiki signup / confirmation
-> authenticated Supabase user
-> POST forum /api/flatrate-sso/provision (server-to-server)
-> Flarum user + flatrate LoginProvider keyed by sub
User clicks Community
-> FlatRate.wiki verifies/refreshes its Supabase session
-> POST forum /api/flatrate-sso/ticket (server-to-server)
-> short-lived opaque one-time ticket
-> browser GET forum /auth/flatrate/session?ticket=<opaque>
-> Flarum consumes ticket atomically
-> Flarum issues its own normal remember/session cookie
-> redirect directly to requested Community path
The single top-level request to forum.flatrate.wiki is necessary so the forum can issue its own host-scoped cookie. There is no second password, popup, consent page, authorization-code callback page, or shared parent-domain cookie.
Internal bridge configuration
Set the same high-entropy deployment secret on both the FlatRate.wiki server and the Flarum/PikaPods runtime.
Environment configuration is preferred when the host exposes arbitrary environment variables:
FORUM_SSO_SHARED_SECRET=<at-least-32-random-characters>
On a managed host that does not expose that environment variable, open the FlatRate Wiki provider settings under FoF OAuth and enter the same value in Community SSO Shared Secret. The extension reads the environment variable first and otherwise falls back to the private FlatRate provider setting fof-oauth.flatrate.sso_shared_secret.
The provider setting is intended only as a managed-host deployment fallback. Do not reuse the OAuth client secret, and do not commit either secret to GitHub or public Flarum assets.
Internal requests use these headers:
X-FlatRate-Timestamp: <unix-seconds>
X-FlatRate-Nonce: <random-base64url-or-hex>
X-FlatRate-Signature: v1=<hex-hmac-sha256>
Canonical signing input:
<timestamp>\n
<nonce>\n
<METHOD>\n
<request-path>\n
<sha256(raw-request-body)>
The receiver rejects stale timestamps, malformed signatures, and duplicate nonces. Nonce hashes are stored only long enough to enforce replay protection.
Internal endpoints
POST /api/flatrate-sso/provision
Authenticated server-to-server only.
Request body:
{
"sub": "immutable-supabase-sub",
"email": "private@example.com",
"email_verified": true
}
Behavior is idempotent:
- return the user already linked by
login_providers(provider=flatrate, identifier=sub); or - create exactly one Flarum user with deterministic routing username and nickname
tech_#<Flarum users.id>; - create the
flatrateprovider link keyed tosub; - never join an unrelated account solely because email matches.
The provisioner uses Flarum's own RegistrationToken + RegisterUserHandler path so core validation/events, nickname persistence, email activation, and provider-link persistence remain intact.
POST /api/flatrate-sso/ticket
Authenticated server-to-server only. It accepts the same identity fields plus a relative return_to path. It idempotently ensures the linked forum user exists and returns an opaque entry path with a 45-second TTL.
Example response shape:
{
"ok": true,
"entry_path": "/auth/flatrate/session?ticket=<opaque>",
"expires_in": 45
}
GET /auth/flatrate/session?ticket=<opaque>
Browser entry endpoint. It:
- hashes and looks up the ticket;
- locks the row and confirms it is unexpired/unconsumed;
- marks it consumed in the same transaction;
- creates Flarum's normal
RememberAccessToken; - sets the normal Flarum remember cookie;
- redirects to the ticket-bound relative forum path.
Responses use Cache-Control: no-store and Referrer-Policy: no-referrer. Tickets contain no email, JWT, refresh token, or other PII.
Signup and self-healing provisioning
FlatRate.wiki should call /provision whenever a Supabase account becomes verified/authenticated:
- immediately after signup if Supabase returns a session;
- after email-confirmation verification;
- after accepting a confirmed callback session;
- on ordinary login as an idempotent repair path.
Community entry should call /ticket, which also runs the same idempotent provisioner. This means a missed signup webhook/callback cannot permanently strand the account.
Existing accounts
Existing login_providers(provider=flatrate, identifier=sub) rows created by the OAuth flow are reused unchanged by the new bridge. No migration to a new identity key is required.
Do not automatically link an existing Flarum-native account because its email matches a Supabase account. Use explicit linking for legacy accounts.
OAuth rollback/fallback path
The OAuth provider remains configured during rollout. It still uses:
/auth/v1/oauth/authorize
/auth/v1/oauth/token
/auth/v1/oauth/userinfo
with scopes:
openid email profile
and requires:
response_type=code
code_challenge=<non-empty value>
code_challenge_method=S256
The Flarum callback remains:
https://forum.flatrate.wiki/auth/flatrate
The Supabase OAuth client is confidential and uses client_secret_post.
A new identity arriving through this fallback path delegates to the same reusable FlatRateUserProvisioner, so OAuth and the ticket bridge cannot create divergent forum identities.
Explicit legacy account linking
While signed into the target native Flarum account, use:
https://forum.flatrate.wiki/auth/flatrate?linkTo=<FLARUM_USER_ID>
FoF OAuth verifies that the authenticated actor matches linkTo before creating the provider record. Keep this primarily for migration/recovery; ordinary product navigation should use the seamless ticket bridge.
Public-auth behavior
The extension:
- hides public native username/password login controls;
- hides public signup and forgot-password affordances;
- hides local Change Password / Change Email controls for ordinary users;
- exposes Nicknames for public identity management;
- rejects ordinary native password authentication server-side;
- rejects native public user creation without an OAuth registration token;
- preserves native administrator password login for recovery.
Reply Job Breakdown marker
Reply classification is stored as FlatRate-owned post metadata because Flarum tags are discussion-level relationships. The extension does not attach native Flarum tags to individual posts.
When marked, the reply reuses the existing Flarum Job Breakdown secondary tag's TagLabel presentation (name, color, icon) without modifying the discussion's tag relationship.
- The
flatrate_post_markerstable stores the controlledjob-breakdownmarker by post ID. - API post payloads expose the marker as
attributes.flatRateJobBreakdown. - Reply and edit composers show a compact Job Breakdown checkbox for replies.
- Marked replies resolve the canonical Flarum tag by slug
job-breakdownand render Flarum's owntags/helpers/tagLabeloutput in the post header. - Discussion starters are not valid marker targets, and the backend fails closed if a request tries to mark one.
- Deleting a post deletes its local marker rows.
- Marking a reply never adds or removes
discussion.tags().
Optional Affiliated Brand presentation
When FoF Masquerade is installed and enabled, the forum bundle can render one optional self-declared profile value directly beneath the author's username in discussion posts and replies.
- Masquerade field name:
Affiliated Brand(Dropdown /select, optional). - The renderer resolves the unique active Masquerade field by exact name and type from the already-loaded
masquerade-fieldstore; it does not hardcode production field IDs. - User answers are read from the loaded
user.masqueradeAnswers()relationship; the bundle does not issue per-post API requests. - Presentation is plain text (
span.FlatRateAffiliatedBrand) insidePostUser-name, not a TagLabel, badge, or OEM logo. - Row 1 preserves native nickname +
PostMetainline; row 2 renders affiliation beneath the nickname via scopedinline-grid(not avatar-edge offsets). - Blank or missing values render nothing (no spacer line).
- Masquerade is optional at runtime: if the extension or field is absent, SSO and other forum behavior continue unchanged.
This value is self-declared profile metadata only. It does not indicate employment, certification, dealership status, or OEM verification; it is not mirrored to Supabase; and it does not mutate discussion vehicle-make tags or Job Breakdown metadata.
FoF Masquerade stores dropdown option lists in fof_masquerade_fields.validation as a comma-separated in: rule. The upstream default column is VARCHAR(255), which truncates long brand lists. This extension widens that column to TEXT when Masquerade is present so the full Affiliated Brand vocabulary can be saved.
Brands navigation presentation
Mobile Brands navigation renders from the shared js/dist/brands-navigation.js presentation contract. GM and CDJR are top-level Brand links with Buick/Cadillac/Chevrolet/GMC and Chrysler/Dodge/Jeep/Ram nested beneath them for presentation only. The extension does not infer Brand membership from Flarum root tags, parent(), isChild(), or tag position.
Production proof gate
Before removing the OAuth product path, verify:
- a new confirmed FlatRate.wiki account creates exactly one linked Flarum identity without opening the forum;
- repeat provisioning creates no duplicates;
- existing linked users resolve the same Flarum row;
- clicking Community lands already authenticated at the requested forum path;
- clicking Community Settings lands at
/settingsalready authenticated; - reused, expired, malformed, and tampered tickets fail closed;
- replayed/stale HMAC requests fail closed;
- changing the Supabase email does not create a second forum identity;
- Flarum bans/suspensions still apply;
- PikaPods restart restores the extension and migrations;
- reply Job Breakdown markers can be created, edited, rendered, and deleted without changing forum tags;
- no bridge secret, Supabase token, password, or PII appears in browser URLs, logs, GitHub, or public assets.
Development
Run static contract tests:
node --test test/*.test.mjs
Run PHP syntax validation:
find . -name '*.php' -not -path './test/harness/*/vendor/*' -print0 | xargs -0 -n1 php -l
MariaDB 11.4 migration compatibility (requires a disposable mariadb:11.4 and the harness vendor tree):
composer install --no-interaction --prefer-dist --working-dir=test/harness/mariadb-migration php test/member-profile-mariadb-migration.php
The member-profile table is created through Flarum Migration::createTable / Blueprint so the active connection prefix is applied. CHECK invariants (member_number > 0, display_mode, custom_nickname_origin) are added with MariaDB ALTER TABLE because Illuminate 8's Blueprint has no check() helper.
Flarum 1.8.19 forum SPA boot (requires disposable MariaDB 11.4, Composer, and Playwright):
bash test/harness/flarum-spa-1.8.19/bin/bootstrap.sh php -S 127.0.0.1:8080 -t test/harness/flarum-spa-1.8.19/.work/flarum/public test/harness/flarum-spa-1.8.19/.work/flarum/router.php # in another shell cd test/harness/flarum-spa-1.8.19 && npm install && npx playwright install chromium && npx playwright test
See docs/forum-identity-002-r3-spa-boot.md. Production already applied
2026_09_12_000000_create_flatrate_member_profiles and created the control
user-322 profile row; do not rerun or redesign that migration.
License
MIT.