| 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 |
|