Skip to main content

max / makenotwork

4.5 KB · 117 lines History Blame Raw
1 # OAuth Integration Guide for External Implementers
2
3 How to integrate "Log in with MNW" into an external service (Multithreaded, future MNW-integrated services).
4
5 This document is the contract. Implementers should not reverse-engineer entitlement logic from MNW source code — read the `perks` object and the rules below.
6
7 ---
8
9 ## Flow
10
11 Standard OAuth 2.0 Authorization Code + PKCE (RFC 7636). See `src/routes/oauth.rs` for the server side.
12
13 1. Redirect the user to `GET /oauth/authorize?response_type=code&client_id=...&redirect_uri=...&state=...&code_challenge=...&code_challenge_method=S256`.
14 2. User authenticates with MNW, gets redirected to your `redirect_uri` with `?code=...&state=...`.
15 3. Exchange the code at `POST /oauth/token` (form-encoded): `grant_type=authorization_code&code=...&redirect_uri=...&code_verifier=...&client_id=...`.
16 4. Use the returned `access_token` as a Bearer token on subsequent requests.
17
18 ---
19
20 ## `GET /oauth/userinfo`
21
22 Canonical "what is this user entitled to" endpoint. Always returns fresh state from the MNW database — no MNW-side caching.
23
24 **Auth:** `Authorization: Bearer <access_token>`
25
26 **Response (200):**
27 ```json
28 {
29 "user_id": "uuid",
30 "username": "alice",
31 "display_name": "Alice Example",
32 "avatar_url": "https://...",
33 "perks": {
34 "fan_plus": false,
35 "is_creator": true,
36 "creator_tier": {
37 "tier": "big_files",
38 "features": ["file_uploads", "large_files"]
39 }
40 }
41 }
42 ```
43
44 **Errors:**
45 - `401 invalid_token` — missing, malformed, or revoked access token.
46 - `401 user_not_found` — token valid but user deactivated/deleted.
47
48 ---
49
50 ## The `perks` contract
51
52 `perks` is the extension point. Implementers consume the fields they care about. New capabilities are added here as they ship — old fields are not renamed or removed without coordination.
53
54 ### `fan_plus: bool`
55
56 True iff the user has an active Fan+ consumer subscription (`fan_plus_subscriptions.status = 'active'`).
57
58 ### `is_creator: bool`
59
60 True iff the user has an active creator subscription at any tier. Equivalent to `creator_tier != null` and provided for ergonomic boolean checks.
61
62 ### `creator_tier: { tier, features } | null`
63
64 `null` when the user is not a creator. Otherwise:
65
66 - `tier`: snake-cased tier name (`"basic" | "small_files" | "big_files" | "everything"`). Implementers **should not** gate features on this string — gate on `features` instead. The tier names exist for display and analytics.
67 - `features`: array of capability strings backed by live platform behavior. Today: `"file_uploads"` (SmallFiles+), `"large_files"` (BigFiles+). New capabilities (e.g., `"live_streaming"`) are added when they actually launch — never as "coming soon" placeholders.
68
69 ### Why structured, not flat booleans
70
71 A flat `creator_tier: "big_files"` would force every implementer to memorize the tier lineup. Adding a new tier (or splitting an existing one) would break callers. The structured form means implementers gate on capabilities, and the platform owns the mapping.
72
73 ---
74
75 ## Refresh ergonomics
76
77 Implementers cache `perks` per session, not per request. State changes (Fan+ subscribe, tier upgrade, cancellation) become visible only after refresh.
78
79 **Refresh on:**
80
81 1. Login (initial `userinfo` call after token exchange).
82 2. Session cycle / token refresh.
83 3. **On demand** — when the user takes an action that should have changed perks. Example: after returning from a Fan+ checkout flow. Hit `userinfo` again and overwrite cached fields.
84
85 There is no push notification of perk changes. If pull-on-demand isn't sufficient, an `/internal/perks-webhooks/register` API will be added — talk to MNW maintainers before relying on stale data.
86
87 ---
88
89 ## Recommended implementer pattern (Rust)
90
91 ```rust
92 struct CachedSession {
93 user_id: Uuid,
94 username: String,
95 perks: Perks,
96 fetched_at: DateTime<Utc>,
97 }
98
99 // One place that calls /oauth/userinfo and overwrites cached perks.
100 async fn refresh_session(session_id: &str) -> Result<()> { /* … */ }
101
102 // Authorization check used everywhere.
103 fn effective_plus(perks: &Perks) -> bool {
104 perks.fan_plus || perks.is_creator
105 }
106 ```
107
108 Every gated route reads `effective_plus(&session.perks)`. Refresh-on-demand routes call `refresh_session` first.
109
110 ---
111
112 ## Stability rules
113
114 - **Additive only.** New `perks` fields are non-breaking; missing fields default to "absent" (false / null / empty array).
115 - **No renames.** Once a `features` string ships, it stays. Mistakes are deprecated, not deleted.
116 - **No silent semantics changes.** Behavior changes to existing capability strings (e.g., raising the size threshold for `large_files`) are coordinated with implementers.
117