You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -28,20 +28,104 @@ This is the simplest possible design for an OAuth flow: send the user straight t
28
28
29
29
Here's [the full Claude transcript](https://gist.github.com/simonw/975b8934066417fe771561a1b672ad4f). Claude gave me almost *exactly* what I needed - the only missing detail was that it set the `redirectUri` to `url.origin` (just the site domain) when it should have been the full URL to the worker page.
30
30
31
-
I edited the code to its final version, which looked like this:
31
+
I tweaked the code to fix this, and later again to add error handling and then to address a potential security issue. My currently deployed code looks like this:
32
+
32
33
```javascript
33
34
exportdefault {
34
35
asyncfetch(request, env) {
35
-
consturl=newURL(request.url);
36
-
constclientId=env.GITHUB_CLIENT_ID;
37
-
constclientSecret=env.GITHUB_CLIENT_SECRET;
38
-
constredirectUri=env.GITHUB_REDIRECT_URI;
39
-
40
-
// If we have a code, exchange it for an access token
I find it hard to imagine a simpler implementation of this pattern.
94
194
95
195
The GitHub API has been around for a very long time, so it shouldn't be surprising that Claude knows exactly how to write the above code. I was still delighted at how much work it had saved me.
96
196
97
-
(I should mention now that I completed this entire project on my phone, before I got up to make the morning coffee.)
197
+
(I should mention now that I completed the entire initial project on my phone, before I got up to make the morning coffee.)
98
198
99
199
## Deploying this to Cloudflare Workers
100
200
@@ -166,7 +266,7 @@ Here's the page that uses that: https://tools.simonwillison.net/openai-audio-out
166
266
167
267
## Adding error handling
168
268
169
-
That code Claude wrote is missing an important detail: error handling. If the GitHub API returns an error - e.g. because the `?code=` is invalid - the page won't reflect that to the user.
269
+
The initial code that Claude wrote was missing an important detail: error handling. If the GitHub API returns an error - e.g. because the `?code=` is invalid - the page won't reflect that to the user.
170
270
171
271
I pasted in the code and prompted:
172
272
@@ -176,118 +276,69 @@ Claude [wrote more code](https://gist.github.com/simonw/85debbdf3d981ff7e54f8cdb
176
276
177
277
> `refactor that code to have less code for the HTML`
178
278
179
-
And [got back this](https://gist.github.com/simonw/85debbdf3d981ff7e54f8cdb6be47578#rewrite-untitled), which is much better. I've now deployed that as an update to the original script.
279
+
And [got back this](https://gist.github.com/simonw/85debbdf3d981ff7e54f8cdb6be47578#rewrite-untitled), which is much better.
document.body.innerHTML += '<p style="color: #c62828;">Warning: Unable to store token in localStorage</p>';
274
-
}
275
-
</script>
276
-
`
277
-
});
285
+
Robert Munteanu [on Mastodon](https://fosstodon.org/@rombert/113566295983095957):
286
+
287
+
> I have looked into OAuth 2.0 and OIDC recently, I wonder what your toughts are about adding CSRF protection? There seems to be no checking of the state parameter in the workers code.
He's absolutely right! The `state=` parameter was being set but in the first few versions of the code it wasn't being checked later.
292
+
293
+
### Understanding the attack
294
+
295
+
I started thinking this through with the help of Claude: I [pasted in the code](https://gist.github.com/simonw/e87e55dfe13e7201dc0ae5042bc4d4eb) and prompted:
296
+
297
+
> `Explain the consequences of this not checking the state parameter`
298
+
299
+
Some back and forth I'm ready to explain this in my own words.
300
+
301
+
The specific attack to worry about here is one where an attacker tricks/forces a user into signing in to an account that the _attacker_ controls.
302
+
303
+
For my Gist example here, imagine if I could create a brand new GitHub account and then trick you into signing in to that account using OAuth, while giving you the impression that you had signed into your own account.
304
+
305
+
If the user saves a Gist containing their private information, you can now access that as the real owner of the account.
306
+
307
+
With GitHub OAuth here's how that could happen: as an attacker, I could initiate the OAuth flow against my new dedicated malicious account, and then at the end intercept that final redirect URL with the `?code=` parameter:
278
308
279
-
} catch (error) {
280
-
returngenerateHTML({
281
-
title:'Unexpected Error',
282
-
content:`
283
-
<h3>Unexpected Error</h3>
284
-
<p>An unexpected error occurred during authentication.</p>
285
-
<p>Details: ${error.message}</p>
286
-
`,
287
-
isError:true
288
-
});
289
-
}
290
-
}
291
-
};
292
309
```
293
-
Here's [an example page](https://tools.simonwillison.net/github-auth?code=bad-code) showing the new error message.
Rather than visit that URL I instead send it to my target and trick them into clicking on it.
313
+
314
+
When they visit that page the Worker exchanges that `?code=` for an access token against MY account and stores that in my victim's `localStorage`.
315
+
316
+
Now any Gists they save will be visible to me as the account's real owner.
317
+
318
+
### How to prevent it
319
+
320
+
This attack is why the OAuth specification describes the `&state=` parameter. This is a random value that the client generates and then sends to the server. The server echoes that value back to the client in the final redirect URL as the `?state=` parameter.
321
+
322
+
To avoid CSRF attacks, we need to record that initial generated `state` and then compare it to the `state=` in the final redirect URL.
323
+
324
+
I pasted in another copy of my script and prompted Claude:
325
+
326
+
> `Modify this to store the state= parameter in an HTTP only session cookie called github_auth_state and then compare that when the user comes back and show an error if they do not match, otherwise unset the cookie and complete the operation`
327
+
328
+
Claude [wrote some code](https://gist.github.com/simonw/ae56f00572dd80f9180687f9532a8226#create-github-oauth-worker-with-state-validation), but when I tried it out on Cloudflare I got this error... which I pasted back into Claude as a follow-up prompt:
329
+
330
+
> `Unexpected Error An unexpected error occurred during authentication. Details: Can't modify immutable headers.`
331
+
332
+
This time it [wrote code that worked](https://gist.github.com/simonw/ae56f00572dd80f9180687f9532a8226#rewrite-untitled) - it turns out the correct pattern for sending custom HTTP headers in a Cloudflare Worker looks like this:
0 commit comments