Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ The pages are React components using [FiestaUI](https://github.com/Fiestaboard/F
loads the UI when a person is needed, so the common case does not wait for React.
- `src/prerender.tsx` and `scripts/prerender.mjs` render each page into the built HTML.

## OAuth client metadata documents

Some providers (Todoist, Hugging Face) accept a URL as the `client_id` and fetch the app's
details from it, so nobody has to register an app
([draft-ietf-oauth-client-id-metadata-document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/)).
`public/clients/<name>.json` is copied into the site unchanged and served at
`https://fiestaboard.app/auth/clients/<name>.json`, which is also its `client_id`.

Like the redirect URL, **these URLs must never change**: FiestaBoard plugins ship them as
their client ID. Each lists both redirect URIs and `"token_endpoint_auth_method": "none"`
(a public client using PKCE); none may ever carry a secret.

## Development

```sh
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"build": "tsc --noEmit && vite build && vite build --ssr src/prerender.tsx --outDir dist-ssr && node scripts/prerender.mjs",
"preview": "vite preview --port 8787",
"typecheck": "tsc --noEmit",
"test": "node --test tests/board-address.test.js",
"test": "node --test tests/board-address.test.js tests/clients.test.js",
"test:dist": "node --test tests/dist.test.js"
},
"dependencies": {
Expand Down
10 changes: 10 additions & 0 deletions public/clients/huggingface.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"client_id": "https://fiestaboard.app/auth/clients/huggingface.json",
"client_name": "FiestaBoard",
"client_uri": "https://fiestaboard.app/",
"redirect_uris": [
"https://fiestaboard.app/auth/oauth/redirect",
"https://fiestaboard.app/auth/oauth/redirect.html"
],
"token_endpoint_auth_method": "none"
}
13 changes: 13 additions & 0 deletions public/clients/todoist.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"client_id": "https://fiestaboard.app/auth/clients/todoist.json",
"client_name": "FiestaBoard",
"client_uri": "https://fiestaboard.app/",
"redirect_uris": [
"https://fiestaboard.app/auth/oauth/redirect",
"https://fiestaboard.app/auth/oauth/redirect.html"
],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"scope": "data:read"
}
35 changes: 35 additions & 0 deletions tests/clients.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// OAuth client metadata documents (draft-ietf-oauth-client-id-metadata-document).
//
// A provider that supports these fetches the client_id itself: the client_id
// is the URL of the document. Vite copies public/ into dist/ unchanged, so
// public/clients/<name>.json is served at https://fiestaboard.app/auth/clients/<name>.json.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { test } from "node:test";

const SITE = "https://fiestaboard.app/auth/";
// Both forms are registered: FiestaBoard 9.5.0 through 9.7.x sends the .html one.
const REDIRECT_URIS = [`${SITE}oauth/redirect`, `${SITE}oauth/redirect.html`];
const CLIENTS = ["todoist", "huggingface"];

const load = (name) => JSON.parse(readFileSync(resolve(import.meta.dirname, `../public/clients/${name}.json`), "utf8"));

for (const name of CLIENTS) {
test(`${name}: client_id is the URL the document is served at`, () => {
assert.equal(load(name).client_id, `${SITE}clients/${name}.json`);
});

test(`${name}: a public client that returns through the relay`, () => {
const doc = load(name);
assert.equal(doc.token_endpoint_auth_method, "none");
assert.deepEqual(doc.redirect_uris, REDIRECT_URIS);
assert.equal(typeof doc.client_name, "string");
assert.ok(!("client_secret" in doc));
});

test(`${name}: small enough for providers' fetch limits`, () => {
// Todoist refuses documents over 5 KB.
assert.ok(readFileSync(resolve(import.meta.dirname, `../public/clients/${name}.json`)).length < 5000);
});
}
8 changes: 8 additions & 0 deletions tests/dist.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,11 @@ test("no script or stylesheet references another host", () => {
assert.doesNotMatch(css, /@import\s+(url\()?["']?https?:/);
}
});

test("OAuth client metadata documents are published at their client_id URLs", () => {
// The client_id is this URL; moving a document breaks every sign-in that uses it.
for (const name of ["todoist", "huggingface"]) {
const doc = JSON.parse(read(`clients/${name}.json`));
assert.equal(doc.client_id, `https://fiestaboard.app/auth/clients/${name}.json`);
}
});
Loading