From bc6dec33d90d25da7644ee7f11229280e8f71261 Mon Sep 17 00:00:00 2001 From: Anthony Ciccarello Date: Mon, 24 Aug 2026 01:09:08 -0700 Subject: [PATCH 1/2] fix(endpoint-auth): use publication me when signing authorization code Previously, the me value supplied by the client in the authorization request was signed directly into the authorization code. If that value differed from publication.me (e.g. a redirect URL), the issued token would fail verification at query time. Per the IndieAuth spec (sections 5.2, 5.3.2, 5.3.3), the authorization server may ignore the client-supplied me and return its own canonical profile URL. publication.me is now always used when signing the code. The client-supplied me is still echoed back in the redirect response for backwards compatibility. Fixes #911 --- packages/endpoint-auth/lib/controllers/consent.js | 8 ++++++-- .../302-consent-submit-authenticate-with-me.js | 6 +++++- .../test/integration/302-consent-submit-authenticate.js | 4 ++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/endpoint-auth/lib/controllers/consent.js b/packages/endpoint-auth/lib/controllers/consent.js index 43de43a21..840bd1173 100644 --- a/packages/endpoint-auth/lib/controllers/consent.js +++ b/packages/endpoint-auth/lib/controllers/consent.js @@ -1,6 +1,7 @@ import process from "node:process"; import { IndiekitError } from "@indiekit/error"; +import { getCanonicalUrl } from "@indiekit/util"; import { validationResult } from "express-validator"; import { getRequestUriData } from "../pushed-authorization-request.js"; @@ -46,7 +47,7 @@ export const consentController = { * @see {@link https://indieauth.spec.indieweb.org/#authorization-response} */ post(request, response) { - const { application } = request.app.locals; + const { application, publication } = request.app.locals; let scope = request.body?.scope; const { @@ -77,13 +78,16 @@ export const consentController = { scope = scope.join(" "); } + // Always use canonical publication URL, per IndieAuth spec ยง5.2 + const canonicalMe = getCanonicalUrl(publication.me); + // Create authorization code const code = signToken({ client_id, ...(code_challenge && { code_challenge }), ...(code_challenge_method && { code_challenge_method }), jti: crypto.randomUUID(), - me, + me: canonicalMe, redirect_uri, ...(scope && { scope }), }); diff --git a/packages/endpoint-auth/test/integration/302-consent-submit-authenticate-with-me.js b/packages/endpoint-auth/test/integration/302-consent-submit-authenticate-with-me.js index 70c9d5145..08d88c72c 100644 --- a/packages/endpoint-auth/test/integration/302-consent-submit-authenticate-with-me.js +++ b/packages/endpoint-auth/test/integration/302-consent-submit-authenticate-with-me.js @@ -6,6 +6,7 @@ import { testServer } from "@indiekit-test/server"; import supertest from "supertest"; import { createPasswordHash } from "../../lib/password.js"; +import { verifyToken } from "../../lib/token.js"; await mockAgent("endpoint-auth"); const server = await testServer(); @@ -19,7 +20,7 @@ describe("endpoint-auth POST /auth/consent", () => { const response = await request .get("/auth") .query({ client_id: "https://auth-endpoint.example" }) - .query({ me: "https://website.example" }) + .query({ me: "https://another.example" }) .query({ redirect_uri: "https://auth-endpoint.example/redirect" }) .query({ response_type: "code" }) .query({ state: "12345" }); @@ -34,6 +35,8 @@ describe("endpoint-auth POST /auth/consent", () => { .send({ password: "foo" }); const { host, protocol } = new URL(response.request.url); const issuer = encodeURIComponent(`${protocol}//${host}`); + const code = new URL(response.headers.location).searchParams.get("code"); + const decoded = verifyToken(code); assert.equal(response.status, 302); assert.match( @@ -41,6 +44,7 @@ describe("endpoint-auth POST /auth/consent", () => { /code=(.*)&iss=(.*)&state=(.*)&me=(.*)/, ); assert.ok(response.headers.location.includes(`iss=${issuer}`)); + assert.equal(decoded.me, "https://website.example/"); }); after(() => server.close()); diff --git a/packages/endpoint-auth/test/integration/302-consent-submit-authenticate.js b/packages/endpoint-auth/test/integration/302-consent-submit-authenticate.js index f0beb3923..15d276248 100644 --- a/packages/endpoint-auth/test/integration/302-consent-submit-authenticate.js +++ b/packages/endpoint-auth/test/integration/302-consent-submit-authenticate.js @@ -6,6 +6,7 @@ import { testServer } from "@indiekit-test/server"; import supertest from "supertest"; import { createPasswordHash } from "../../lib/password.js"; +import { verifyToken } from "../../lib/token.js"; await mockAgent("endpoint-auth"); const server = await testServer(); @@ -31,9 +32,12 @@ describe("endpoint-auth POST /auth/consent", () => { .type("form") .query({ request_uri: `urn:ietf:params:oauth:request_uri:${reference}` }) .send({ password: "foo" }); + const code = new URL(result.headers.location).searchParams.get("code"); + const decoded = verifyToken(code); assert.equal(result.status, 302); assert.match(result.headers.location, /code=(.*)&iss=(.*)&state=(.*)/); + assert.equal(decoded.me, "https://website.example/"); }); after(() => server.close()); From 6a430dbb94d22ada119a15aeaeee8d6f0cbba5d4 Mon Sep 17 00:00:00 2001 From: Anthony Ciccarello Date: Fri, 9 Oct 2026 12:31:50 -0700 Subject: [PATCH 2/2] fix(endpoint-auth): address typecheck errors --- .../integration/302-consent-submit-authenticate-with-me.js | 3 ++- .../test/integration/302-consent-submit-authenticate.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/endpoint-auth/test/integration/302-consent-submit-authenticate-with-me.js b/packages/endpoint-auth/test/integration/302-consent-submit-authenticate-with-me.js index 08d88c72c..4d7ea6ea5 100644 --- a/packages/endpoint-auth/test/integration/302-consent-submit-authenticate-with-me.js +++ b/packages/endpoint-auth/test/integration/302-consent-submit-authenticate-with-me.js @@ -35,7 +35,8 @@ describe("endpoint-auth POST /auth/consent", () => { .send({ password: "foo" }); const { host, protocol } = new URL(response.request.url); const issuer = encodeURIComponent(`${protocol}//${host}`); - const code = new URL(response.headers.location).searchParams.get("code"); + const code = + new URL(response.headers.location).searchParams.get("code") ?? "MISSING"; const decoded = verifyToken(code); assert.equal(response.status, 302); diff --git a/packages/endpoint-auth/test/integration/302-consent-submit-authenticate.js b/packages/endpoint-auth/test/integration/302-consent-submit-authenticate.js index 15d276248..f5ee13972 100644 --- a/packages/endpoint-auth/test/integration/302-consent-submit-authenticate.js +++ b/packages/endpoint-auth/test/integration/302-consent-submit-authenticate.js @@ -32,7 +32,8 @@ describe("endpoint-auth POST /auth/consent", () => { .type("form") .query({ request_uri: `urn:ietf:params:oauth:request_uri:${reference}` }) .send({ password: "foo" }); - const code = new URL(result.headers.location).searchParams.get("code"); + const code = + new URL(result.headers.location).searchParams.get("code") ?? "MISSING"; const decoded = verifyToken(code); assert.equal(result.status, 302);