nandi/frqpublic Fork 0
2d13303
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Signing in from a page served off one's own machine

A `client_id` is a URL an authorization server fetches, and a dev
server is not on the public web. The spec's way out is that a
`client_id` whose origin is exactly `http://localhost` is not fetched
at all: the server reads a virtual metadata document out of the query
string.

Exactly `localhost`. `127.0.0.1` is not accepted, which is what
`Invalid client ID "http://127.0.0.1:8000/client-metadata.json"` was
saying, and no port and no path either. The redirect URI declared in
that query string is matched on its path but not on its port -- which
is the point, a dev server's port being whatever was free -- so the
redirect stays this page, address and port and all. Only the identity
is the fiction.

`nim/web/test/authorize.js` takes `FRQ_WEB_ORIGIN` now and pushes a
real authorization request as either origin. bsky.social hands back a
`request_uri` for both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-20T11:33:48-07:00 Browse files
2d13303 parent: e099158
modified flutter/web/frq_oauth.js +31 -2
@@ -39,7 +39,36 @@
3939 // asks for `/index.htmlclient-metadata.json` and is told, quite correctly,
4040 // Not Found.
4141 const origin = () => window.location.origin + '/';
42- const clientId = () => origin() + 'client-metadata.json';
42+
43+ // Scope is declared in two places and they have to agree: here, where the
44+ // authorization request asks for it, and in the metadata document, which
45+ // says what this client may ever ask for.
46+ const SCOPE = 'atproto transition:generic';
47+
48+ // A page served from a developer's own machine cannot publish a metadata
49+ // document that an authorization server can reach, so the spec makes an
50+ // exception for it: a `client_id` whose origin is exactly `http://localhost`
51+ // is not fetched at all, and the server builds a virtual document out of the
52+ // query string instead.
53+ //
54+ // Three things about that exception cost a rejection each to learn. The
55+ // hostname must be the word `localhost` -- `127.0.0.1` is *not* accepted,
56+ // which is exactly what `Invalid client ID "http://127.0.0.1:8000/..."`
57+ // was saying. There must be no port and no path, so the `client_id` is
58+ // `http://localhost` and nothing more before the `?`. And the redirect URI
59+ // we declare there is matched on its path but *not* on its port, which is
60+ // the whole point -- a dev server's port is whatever was free.
61+ //
62+ // So the redirect stays this page, loopback address and port and all; only
63+ // the identity is the fiction.
64+ const loopback = () =>
65+ /^(localhost|127(\.\d+){3}|\[::1\])$/.test(window.location.hostname);
66+
67+ const clientId = () =>
68+ loopback()
69+ ? 'http://localhost?redirect_uri=' + encodeURIComponent(origin()) +
70+ '&scope=' + encodeURIComponent(SCOPE)
71+ : origin() + 'client-metadata.json';
4372
4473 const PENDING = 'frq:oauth:pending';
4574 const SESSION = 'frq:oauth:session';
@@ -201,7 +230,7 @@
201230 ['client_id', clientId()],
202231 ['redirect_uri', origin()],
203232 ['response_type', 'code'],
204- ['scope', 'atproto transition:generic'],
233+ ['scope', SCOPE],
205234 ['state', state],
206235 ['code_challenge', challenge],
207236 ['code_challenge_method', 'S256'],
@@ -39,7 +39,36 @@
39 // asks for `/index.htmlclient-metadata.json` and is told, quite correctly,39 // asks for `/index.htmlclient-metadata.json` and is told, quite correctly,
40 // Not Found.40 // Not Found.
41 const origin = () => window.location.origin + '/';41 const origin = () => window.location.origin + '/';
42- const clientId = () => origin() + 'client-metadata.json';42+
43+ // Scope is declared in two places and they have to agree: here, where the
44+ // authorization request asks for it, and in the metadata document, which
45+ // says what this client may ever ask for.
46+ const SCOPE = 'atproto transition:generic';
47+
48+ // A page served from a developer's own machine cannot publish a metadata
49+ // document that an authorization server can reach, so the spec makes an
50+ // exception for it: a `client_id` whose origin is exactly `http://localhost`
51+ // is not fetched at all, and the server builds a virtual document out of the
52+ // query string instead.
53+ //
54+ // Three things about that exception cost a rejection each to learn. The
55+ // hostname must be the word `localhost` -- `127.0.0.1` is *not* accepted,
56+ // which is exactly what `Invalid client ID "http://127.0.0.1:8000/..."`
57+ // was saying. There must be no port and no path, so the `client_id` is
58+ // `http://localhost` and nothing more before the `?`. And the redirect URI
59+ // we declare there is matched on its path but *not* on its port, which is
60+ // the whole point -- a dev server's port is whatever was free.
61+ //
62+ // So the redirect stays this page, loopback address and port and all; only
63+ // the identity is the fiction.
64+ const loopback = () =>
65+ /^(localhost|127(\.\d+){3}|\[::1\])$/.test(window.location.hostname);
66+
67+ const clientId = () =>
68+ loopback()
69+ ? 'http://localhost?redirect_uri=' + encodeURIComponent(origin()) +
70+ '&scope=' + encodeURIComponent(SCOPE)
71+ : origin() + 'client-metadata.json';
43 72
44 const PENDING = 'frq:oauth:pending';73 const PENDING = 'frq:oauth:pending';
45 const SESSION = 'frq:oauth:session';74 const SESSION = 'frq:oauth:session';
@@ -201,7 +230,7 @@
201 ['client_id', clientId()],230 ['client_id', clientId()],
202 ['redirect_uri', origin()],231 ['redirect_uri', origin()],
203 ['response_type', 'code'],232 ['response_type', 'code'],
204- ['scope', 'atproto transition:generic'],233+ ['scope', SCOPE],
205 ['state', state],234 ['state', state],
206 ['code_challenge', challenge],235 ['code_challenge', challenge],
207 ['code_challenge_method', 'S256'],236 ['code_challenge_method', 'S256'],
modified nim/web/test/authorize.js +7 -1
@@ -16,8 +16,14 @@
1616 // login, and the request it pushes expires unused.
1717 const fs = require('fs');
1818 let assigned = '';
19+// The deployed origin by default; `FRQ_WEB_ORIGIN=http://127.0.0.1:8000` runs
20+// the same probe as a page served from a developer's own machine, which takes
21+// the other branch of `clientId` -- the `http://localhost` exception.
22+const served = new URL(process.env.FRQ_WEB_ORIGIN ||
23+ 'https://codegod100--frq-web-serve.modal.run');
1924 global.window = {
20- location: { origin: 'https://codegod100--frq-web-serve.modal.run', pathname: '/', search: '',
25+ location: { origin: served.origin, hostname: served.hostname,
26+ pathname: '/', search: '',
2127 assign: (u) => { assigned = u; } },
2228 history: { replaceState() {} },
2329 };
@@ -16,8 +16,14 @@
16 // login, and the request it pushes expires unused.16 // login, and the request it pushes expires unused.
17 const fs = require('fs');17 const fs = require('fs');
18 let assigned = '';18 let assigned = '';
19+// The deployed origin by default; `FRQ_WEB_ORIGIN=http://127.0.0.1:8000` runs
20+// the same probe as a page served from a developer's own machine, which takes
21+// the other branch of `clientId` -- the `http://localhost` exception.
22+const served = new URL(process.env.FRQ_WEB_ORIGIN ||
23+ 'https://codegod100--frq-web-serve.modal.run');
19 global.window = {24 global.window = {
20- location: { origin: 'https://codegod100--frq-web-serve.modal.run', pathname: '/', search: '',25+ location: { origin: served.origin, hostname: served.hostname,
26+ pathname: '/', search: '',
21 assign: (u) => { assigned = u; } },27 assign: (u) => { assigned = u; } },
22 history: { replaceState() {} },28 history: { replaceState() {} },
23 };29 };