litchee 0.1.5

Async, builder-pattern Rust client for the Lichess API: full endpoint coverage, NDJSON streaming, and OAuth2 PKCE ('Log in with Lichess').
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# Logging in with Lichess: a step-by-step PKCE guide

This guide walks you through **"Log in with Lichess"** using `litchee`, from
zero. It is written for beginners — no prior OAuth knowledge is assumed. By the
end you will have an access token you can use to call the Lichess API on a
user's behalf.

---

## What problem are we solving?

You want your app to act on behalf of a Lichess user — read their account, play
games, create challenges, etc. You **could** ask them to paste a personal access
token, but that is clumsy and dangerous. Instead, Lichess lets the user click a
button, log in on lichess.org, and approve your app. Your app then receives a
token. This is the **OAuth2 Authorization Code flow**.

### Why "PKCE"?

The classic OAuth2 flow assumes your app can keep a **client secret** hidden on a
server. But many apps can't: a mobile app, a single-page web app, or a desktop
CLI all ship their code to the user, so any embedded secret is exposed.

**PKCE** (pronounced "pixy", short for *Proof Key for Code Exchange*) fixes this
**without a secret**. The idea, in one sentence:

> Before sending the user off to log in, your app invents a random secret
> (the **verifier**), sends only a *hash* of it (the **challenge**) to Lichess,
> and later proves it owns the original by sending the verifier back.

Because only your app ever knows the original verifier, an attacker who
intercepts the redirect can't complete the login. `litchee` generates and hashes
the verifier for you, so you never compute a SHA-256 by hand.

---

## The flow at a glance

```
   YOUR APP                         LICHESS                      USER
      │                                │                          │
      │ 1. build authorization URL     │                          │
      │    (litchee makes verifier     │                          │
      │     + challenge + state)       │                          │
      │                                │                          │
      │ 2. send user to that URL ──────┼────────────────────────► │
      │                                │   user logs in & approves │
      │                                │                          │
      │ ◄──────────────────────────────  3. redirect back with    │
      │     code + state                  ?code=...&state=...      │
      │                                │                          │
      │ 4. verify state matches        │                          │
      │                                │                          │
      │ 5. exchange code + verifier ──►│                          │
      │ ◄────────────── access token ──│                          │
      │                                │                          │
      │ 6. use token to call the API   │                          │
```

Three things `litchee` hands you in step 1 and you must **keep until step 5**:

| Thing      | What it is                              | Why you keep it                          |
|------------|-----------------------------------------|------------------------------------------|
| `url`      | Where to send the user                  | You redirect/open the browser to it      |
| `state`    | A random anti-forgery string            | You compare it to what comes back (CSRF) |
| `verifier` | The PKCE secret (`PkceVerifier`)        | You need it to exchange the code         |

---

## Before you start

You need to decide two things up front. **Neither is a secret** — both are
public and chosen by you.

1. **`client_id`** — any string that identifies your app, e.g.
   `"com.example.my-chess-app"`. You make it up; there is no registration step
   on Lichess.
2. **`redirect_uri`** — the absolute URL Lichess sends the user back to after
   they approve. It must match exactly in steps 1 and 5.
   - Web server: `https://myapp.example.com/callback`
   - Local dev / desktop: `http://localhost:8080/callback`

You also pick the **scopes** — the permissions you are requesting. Ask for the
least you need. Examples (`litchee::api::auth::oauth::Scope`):

| Scope                  | Lets you…                          |
|------------------------|------------------------------------|
| `Scope::PreferenceRead`| read the user's preferences        |
| `Scope::ChallengeWrite`| create and accept challenges       |
| `Scope::BoardPlay`     | play games via the Board API       |
| `Scope::BotPlay`       | play games as a bot                |
| `Scope::EmailRead`     | read the user's email address      |

> An empty scope list is allowed and gives you only public, read-only access.

---

## Step 1 — Build the authorization URL

> ### What does "build the URL" mean?
> It is easy to misread "build the URL" as "**open** the URL" or "**send a
> request** to it". It does **not** mean that. Here, *build* means **construct
> the URL string** — assemble a `https://lichess.org/oauth?...` address with all
> the right query parameters glued on. Nothing is sent anywhere in this step; no
> network call happens. You are just *creating an address*.
>
> Think of it like writing down a destination on an envelope. You are not mailing
> anything yet — you're only preparing where it will eventually go. Your app
> never visits this URL itself; in [Step 2]#step-2--send-the-user-to-that-url
> you hand it to the *user's browser*, which is what actually opens it.
>
> **How is it built?** A valid authorization URL needs several query parameters
> in a specific format — `response_type`, `client_id`, `redirect_uri`,
> `code_challenge`, `code_challenge_method=S256`, `scope`, and `state`. Getting
> any of them wrong (especially the PKCE challenge, which is a SHA-256 hash,
> base64-url encoded) breaks the flow. So **you don't build the string by hand**> you fill in an `AuthorizationRequest` with your plain inputs, and
> `client.oauth().authorization_url(...)` does the assembling, hashing, and
> URL-encoding for you. The "building" is `litchee`'s job; your job is to supply
> the inputs and then use the result.

Create a client (no token yet — we're trying to *get* one) and ask `litchee` to
build (assemble) the URL.

```rust
use litchee::LichessClient;
use litchee::api::auth::oauth::{AuthorizationRequest, Scope};

let client = LichessClient::builder()
    .user_agent("my-chess-app/1.0")
    .build()?;

let request = AuthorizationRequest {
    client_id: "com.example.my-chess-app",
    redirect_uri: "http://localhost:8080/callback",
    scopes: &[Scope::PreferenceRead, Scope::ChallengeWrite],
    username_hint: None, // or Some("thibault") to pre-fill the login form
};

let authorization = client.oauth().authorization_url(&request)?;

// You now have three values to use/keep:
println!("Send the user here:\n{}", authorization.url);
let state    = authorization.state;     // keep this
let verifier = authorization.verifier;  // keep this (it's the PKCE secret)
```

`authorization.url` already contains the `code_challenge`, `code_challenge_method=S256`,
`state`, `scope`, and your `client_id`/`redirect_uri`. You do not assemble any of
that yourself.

---

## Step 2 — Send the user to that URL

How you do this depends on your app:

- **Web app:** respond with an HTTP redirect (`302 Found`) to `authorization.url`.
- **Desktop / CLI:** open the system browser, e.g. with the [`open`]https://crates.io/crates/open
  crate, or just print the URL and ask the user to paste it.
- **Mobile:** open it in an in-app browser tab.

The user now logs in on lichess.org and approves (or denies) your requested
scopes. **Lichess never sees your verifier — only its hash.**

> ### Where do I keep `state` and `verifier`?
> They must survive until the redirect comes back.
> - **Web server:** store them in the user's session (keyed by a cookie).
> - **Desktop/CLI:** keep them in memory in the same running process — the
>   simplest case, since you control the whole flow.
>
> The `PkceVerifier`'s `Debug` output is redacted, so it won't leak into logs by
> accident. Don't write it to disk or send it anywhere.

---

## Step 3 — Receive the redirect

When the user approves, Lichess redirects the browser to your `redirect_uri`
with two query parameters:

```
http://localhost:8080/callback?code=abc123...&state=THE_SAME_STATE
```

- `code` — a short-lived **authorization code** (not yet a token).
- `state` — should equal the `state` you saved in step 1.

If the user **denies**, you instead get `?error=access_denied` — handle that as a
normal "user said no" outcome, not a crash.

> **Desktop/CLI tip:** to catch the redirect on `http://localhost:8080`, run a
> tiny one-request HTTP server that reads the query string, then shuts down. Any
> minimal HTTP server crate works.

---

## Step 4 — Verify `state` (don't skip this!)

This single check is what protects you against cross-site request forgery. If
the returned `state` doesn't match what you stored, **abort** — the request may
be forged.

```rust
let returned_state = /* the `state` query param from the redirect */;

if returned_state != state {
    // Reject: do NOT exchange the code.
    return Err("state mismatch — possible CSRF, aborting".into());
}
```

---

## Step 5 — Exchange the code for a token

Now prove you own the verifier. Pass the `code` from the redirect plus your saved
`verifier`. **`redirect_uri` and `client_id` must match step 1 exactly**, or
Lichess rejects the exchange.

```rust
use litchee::api::auth::oauth::CodeExchange;

let returned_code = /* the `code` query param from the redirect */;

let exchange = CodeExchange {
    code: returned_code,
    code_verifier: &verifier,                       // the secret from step 1
    redirect_uri: "http://localhost:8080/callback", // same as step 1
    client_id: "com.example.my-chess-app",          // same as step 1
};

let token = client.oauth().exchange_code(&exchange).await?;

println!("Got a token! It expires in {} seconds.", token.expires_in);
// token.access_token holds the secret bearer string (Debug-redacted).
```

On success you get a [`LichessToken`] with:
- `access_token` — the bearer string you authenticate with,
- `token_type` — always `"Bearer"`,
- `expires_in` — lifetime in seconds (Lichess tokens are long-lived, ~1 year).

If something is wrong (e.g. a mismatched verifier or a reused code), `litchee`
returns a typed `LichessError::OAuth` you can match on — for example the
`invalid_grant` case.

---

## Step 6 — Use the token

Build a **new** client carrying the token, and call the API:

```rust
let user_client = LichessClient::builder()
    .user_agent("my-chess-app/1.0")
    .token(token.access_token.into_inner())
    .build()?;

// Now authenticated calls work, e.g.:
let me = user_client.account().profile().await?;
println!("Logged in as {}", me.user.username);
```

Store `access_token` securely if you need it across restarts (OS keychain,
encrypted store, server-side session) — treat it like a password.

---

## Step 7 (optional) — Log out / revoke

When the user logs out, revoke the token so it can't be reused:

```rust
user_client.oauth().revoke_token().await?;
```

---

## Common mistakes & how to avoid them

| Symptom                                   | Likely cause                                              | Fix                                              |
|-------------------------------------------|-----------------------------------------------------------|--------------------------------------------------|
| `invalid_grant` on exchange               | `redirect_uri` or `client_id` differs from step 1         | Make all three params byte-for-byte identical    |
| `invalid_grant` / "code already used"     | Reused an authorization code, or it expired               | Each code is single-use & short-lived; start over|
| State mismatch every time                 | You didn't persist `state` across the redirect            | Save it in the session / process before step 2   |
| Lost the verifier after redirect          | Stored it somewhere that didn't survive the round trip    | Keep it with `state` in the same session         |
| Token works for some calls, 403 on others | Missing scope                                             | Request the needed `Scope` in step 1             |

---

## The whole thing, end to end

For a desktop/CLI app that owns the full flow in one process:

```rust
use litchee::LichessClient;
use litchee::api::auth::oauth::{AuthorizationRequest, CodeExchange, Scope};

const CLIENT_ID: &str = "com.example.my-chess-app";
const REDIRECT_URI: &str = "http://localhost:8080/callback";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = LichessClient::builder()
        .user_agent("my-chess-app/1.0")
        .build()?;

    // 1. Build the URL + keep state & verifier.
    let auth = client.oauth().authorization_url(&AuthorizationRequest {
        client_id: CLIENT_ID,
        redirect_uri: REDIRECT_URI,
        scopes: &[Scope::PreferenceRead],
        username_hint: None,
    })?;

    // 2. Send the user there (open browser / print the URL).
    println!("Open this URL and approve:\n{}", auth.url);

    // 3. Receive the redirect on localhost:8080 and read `code` + `state`.
    let (returned_code, returned_state) = wait_for_redirect().await?; // your server

    // 4. Verify state.
    if returned_state != auth.state {
        return Err("state mismatch — aborting".into());
    }

    // 5. Exchange for a token.
    let token = client.oauth().exchange_code(&CodeExchange {
        code: &returned_code,
        code_verifier: &auth.verifier,
        redirect_uri: REDIRECT_URI,
        client_id: CLIENT_ID,
    }).await?;

    // 6. Use it.
    let user_client = LichessClient::builder()
        .user_agent("my-chess-app/1.0")
        .token(token.access_token.into_inner())
        .build()?;
    let me = user_client.account().profile().await?;
    println!("Logged in as {}", me.user.username);

    Ok(())
}
```

`wait_for_redirect` is the small local HTTP listener you write to capture the
`code` and `state` query parameters — that part is yours, everything else is
`litchee`. For a complete, runnable version of exactly this (listener included),
see [`examples/oauth_flow.rs`](examples/oauth_flow.rs):
`cargo run --example oauth_flow`.

---

## Recap

1. **Build** the authorization URL — `litchee` makes the verifier, challenge, and state.
2. **Send** the user to it.
3. **Receive** the redirect with `code` + `state`.
4. **Verify** `state` matches.
5. **Exchange** `code` + verifier for a token.
6. **Use** the token in a new client.
7. **Revoke** it on logout.

The golden rules: keep the **verifier** and **state** safe until the exchange,
always **check state**, and make `client_id` + `redirect_uri` **identical** in
steps 1 and 5.

---

## Glossary

New to OAuth? Here are the terms used in this guide, in plain language.

**OAuth2** — The industry-standard protocol for letting one app act on a user's
behalf in another service *without* the user handing over their password. "Log in
with Google/GitHub/Lichess" buttons are all OAuth2.

**Authorization Code flow** — The specific OAuth2 variant used here: the user
approves your app, you receive a temporary **authorization code**, and you swap
that code for a token. The "two-step handoff" is what makes it safe.

**PKCE** (*Proof Key for Code Exchange*, pronounced "pixy", RFC 7636) — An
add-on to the Authorization Code flow that removes the need for a client secret.
Your app invents a random secret and sends only its hash up front, then proves
ownership later. Designed for apps that can't keep a secret (mobile, desktop,
single-page web).

**Client** — Your application. (Not to be confused with the *user*.) In this
guide, also the `LichessClient` you build with `litchee`.

**`client_id`** — A public string that identifies your app. You choose it; there
is no registration step on Lichess. Not a secret.

**Client secret** — A password-like string that *server-side* OAuth apps use to
authenticate themselves. PKCE exists precisely so you **don't** need one — this
guide never uses a client secret.

**Authorization server** — The service that logs the user in and issues tokens.
Here, that's lichess.org.

**Resource server** — The API that accepts the token and serves protected data.
Here, also Lichess (the `/api/...` endpoints).

**Scope** — A single named permission you request, e.g. `board:play` or
`preference:read`. The token you receive is limited to the scopes the user
approved. Ask for the least you need.

**`redirect_uri`** (a.k.a. *callback URL*) — The absolute URL Lichess sends the
user back to after they approve. Must match **exactly** between building the URL
and exchanging the code.

**Authorization URL** — The `https://lichess.org/oauth?...` address you send the
user to so they can log in and approve. Your app *constructs* it but never visits
it itself — the user's browser does.

**Authorization code** (the `code`) — A short-lived, single-use string Lichess
returns at your `redirect_uri` after approval. It is **not** a token yet; you
must exchange it. Reusing or delaying it causes `invalid_grant`.

**Code verifier** (`PkceVerifier`) — The high-entropy random secret your app
generates and keeps private. Sent only at the final exchange step to prove your
app started the flow.

**Code challenge** — The transformed (`SHA-256` hashed, base64-url encoded) form
of the verifier, sent in the authorization URL. Lichess stores it and later
checks it against your verifier. `litchee` computes it for you.

**`code_challenge_method`** — Tells the server how the challenge was derived.
This guide always uses `S256` (SHA-256), the secure option.

**`state`** — A random anti-forgery (CSRF) value your app generates, sends in the
authorization URL, and checks when the redirect returns. If it doesn't match,
abort — the request may be forged.

**CSRF** (*Cross-Site Request Forgery*) — An attack where a malicious page tricks
a logged-in user's browser into making an unwanted request. The `state` check is
what defends against it here.

**Access token** (`LichessToken`) — The bearer string you finally receive. You
send it on every authenticated API call. Treat it like a password.

**Bearer token** — A token used by simply *presenting* it (in an
`Authorization: Bearer <token>` header). Whoever holds it can use it, hence
"keep it secret".

**`expires_in`** — How many seconds the access token stays valid. Lichess tokens
are long-lived (about a year).

**Revoke** — To invalidate a token before it expires, e.g. on logout, so it can
no longer be used.