fix: OAuth access tokens expire 10 min after login (Phanpy 401)
Deploy Indiekit Server / deploy (push) Successful in 1m13s

The authorization code document is created with expiresAt = now + 10 min.
The token exchange updated the same document in-place but never cleared
expiresAt, so access tokens silently expired 10 minutes after the OAuth
flow — causing all authenticated Mastodon API endpoints to return 401.

patch-ap-oauth-token-expiry-fix adds $unset: { expiresAt } to the
authorization_code grant updateOne, matching what the refresh_token
grant already does.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sven
2026-04-09 17:02:58 +02:00
parent eaa9ff51ce
commit 5b6fd57aef
3 changed files with 83 additions and 2 deletions
+1
View File
@@ -181,6 +181,7 @@ npm install git+https://gitea.giersig.eu/svemagie/indiekit-endpoint-activitypub
| "Empty reply from server" on webmention poller | Poller routing through nginx (returns 444 for wrong Host) — must use `INDIEKIT_DIRECT_URL` |
| HTTP Signature 401 errors on all inbound activities | nginx forwarding wrong `Host` header — fixed by `patch-ap-signature-host-header` (overrides to `blog.giersig.eu`) |
| HTTP Signature verify errors flooding logs for deleted/migrated actors | Expected noise — `patch-ap-inbox-delivery-debug` suppresses to `fatal`; real errors surface at `error` level |
| Mastodon client (Phanpy, etc.) gets 401 on all authenticated endpoints ~10 min after login | OAuth access token inherited the auth code's 10-min `expiresAt` — fixed by `patch-ap-oauth-token-expiry-fix` (`$unset: { expiresAt }` during code exchange) |
| "OAuth callback failed. Missing parameters." | `state` parameter not echoed — fixed in fork (`b54146c`) |
| AP object 410 / Tombstone | Post was deleted — correct, served by FEP-4f05 |
+2 -2
View File
@@ -5,8 +5,8 @@
"main": "index.js",
"scripts": {
"preinstall": "node scripts/setup-gitea-url-rewrite.mjs",
"postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs && node scripts/patch-ap-signature-host-header.mjs && node scripts/patch-ap-compose-default-checked.mjs && node scripts/patch-ap-mastodon-reply-threading.mjs && node scripts/patch-ap-mastodon-status-id.mjs && node scripts/patch-ap-interactions-send-guard.mjs && node scripts/patch-ap-syndicate-dedup.mjs && node scripts/patch-ap-mastodon-delete-fix.mjs && node scripts/patch-ap-status-reply-id.mjs && node scripts/patch-ap-inbox-publication-url.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-micropub-gitea-dispatch-conditional.mjs && node scripts/patch-ap-inbox-delivery-debug.mjs && node scripts/patch-ap-repost-announce-fix.mjs",
"serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-microsub-no-bookmark-autofollow.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs && node scripts/patch-ap-signature-host-header.mjs && node scripts/patch-ap-compose-default-checked.mjs && node scripts/patch-ap-mastodon-reply-threading.mjs && node scripts/patch-ap-mastodon-status-id.mjs && node scripts/patch-ap-interactions-send-guard.mjs && node scripts/patch-ap-syndicate-dedup.mjs && node scripts/patch-ap-mastodon-delete-fix.mjs && node scripts/patch-ap-status-reply-id.mjs && node scripts/patch-ap-inbox-publication-url.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-ap-inbox-delivery-debug.mjs && node scripts/patch-ap-repost-announce-fix.mjs && node --require ./metrics-shim.cjs node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
"postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs && node scripts/patch-ap-signature-host-header.mjs && node scripts/patch-ap-compose-default-checked.mjs && node scripts/patch-ap-mastodon-reply-threading.mjs && node scripts/patch-ap-mastodon-status-id.mjs && node scripts/patch-ap-interactions-send-guard.mjs && node scripts/patch-ap-syndicate-dedup.mjs && node scripts/patch-ap-mastodon-delete-fix.mjs && node scripts/patch-ap-status-reply-id.mjs && node scripts/patch-ap-inbox-publication-url.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-micropub-gitea-dispatch-conditional.mjs && node scripts/patch-ap-inbox-delivery-debug.mjs && node scripts/patch-ap-repost-announce-fix.mjs && node scripts/patch-ap-oauth-token-expiry-fix.mjs",
"serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-microsub-no-bookmark-autofollow.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs && node scripts/patch-ap-signature-host-header.mjs && node scripts/patch-ap-compose-default-checked.mjs && node scripts/patch-ap-mastodon-reply-threading.mjs && node scripts/patch-ap-mastodon-status-id.mjs && node scripts/patch-ap-interactions-send-guard.mjs && node scripts/patch-ap-syndicate-dedup.mjs && node scripts/patch-ap-mastodon-delete-fix.mjs && node scripts/patch-ap-status-reply-id.mjs && node scripts/patch-ap-inbox-publication-url.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-ap-inbox-delivery-debug.mjs && node scripts/patch-ap-repost-announce-fix.mjs && node scripts/patch-ap-oauth-token-expiry-fix.mjs && node --require ./metrics-shim.cjs node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
@@ -0,0 +1,80 @@
/**
* Patch: fix OAuth access tokens expiring 10 minutes after login.
*
* Root cause:
* The authorization code document is created with expiresAt = now + 10 min
* (the authorization code TTL). When the access token is issued via the
* authorization_code grant, the existing document is updated in-place with
* $set: { accessToken, refreshToken, ... } but expiresAt is never cleared.
*
* The tokenRequired middleware then rejects the token once 10 minutes pass:
* if (token.expiresAt && token.expiresAt < new Date()) return null;
*
* The refresh_token grant already has $unset: { expiresAt: "" } — this patch
* applies the same fix to the authorization_code exchange path.
*
* Symptom: Mastodon clients (Phanpy, etc.) receive 200 on /api/v2/instance but
* 401 on all authenticated endpoints ~10 minutes after logging in.
*/
import { access, readFile, writeFile } from "node:fs/promises";
const MARKER = "// [patch] ap-oauth-token-expiry-fix";
const candidates = [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/oauth.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/oauth.js",
];
const OLD_SNIPPET = ` {
$set: {
accessToken,
refreshToken,
refreshExpiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL),
},
},`;
const NEW_SNIPPET = ` {
$set: {
accessToken,
refreshToken,
refreshExpiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL),
},
$unset: { expiresAt: "" }, ${MARKER}
},`;
async function exists(p) {
try { await access(p); return true; } catch { return false; }
}
let totalPatched = 0;
let totalChecked = 0;
for (const filePath of candidates) {
if (!(await exists(filePath))) continue;
totalChecked++;
const source = await readFile(filePath, "utf8");
if (source.includes(MARKER)) {
console.log(`[postinstall] patch-ap-oauth-token-expiry-fix: already applied to ${filePath}`);
continue;
}
if (!source.includes(OLD_SNIPPET)) {
console.warn(`[postinstall] patch-ap-oauth-token-expiry-fix: snippet not found in ${filePath} (upstream changed?)`);
continue;
}
const updated = source.replace(OLD_SNIPPET, NEW_SNIPPET);
await writeFile(filePath, updated, "utf8");
console.log(`[postinstall] Applied patch-ap-oauth-token-expiry-fix to ${filePath}`);
totalPatched++;
}
if (totalChecked === 0) {
console.log("[postinstall] patch-ap-oauth-token-expiry-fix: no target files found");
} else if (totalPatched === 0) {
console.log("[postinstall] patch-ap-oauth-token-expiry-fix: already up to date");
} else {
console.log(`[postinstall] patch-ap-oauth-token-expiry-fix: patched ${totalPatched} file(s)`);
}