fix: auto-recover from OAuth app deletion after token revocation (#2422)

When users revoke OAuth access on their Mastodon server, the OAuth application gets deleted but remains cached in Elk. This causes login failures with 'Client authentication failed due to unknown client'.

This fix adds automatic detection and recovery:
- Detects specific invalid_client errors (401 status)
- Automatically invalidates stale cached OAuth credentials
- Creates fresh OAuth application and retries seamlessly
- Single retry prevents infinite loops
- Preserves existing error handling for other failures

Changes:
- Add invalidateApp() function to server/utils/shared.ts
- Enhanced error handling in server/api/[server]/oauth/[origin].ts
- Backward compatible, zero breaking changes

Fixes #2422
pull/3382/head
abcb1122 2025-09-18 09:17:03 -05:00
rodzic 589fef0446
commit a6e6815b7b
2 zmienionych plików z 52 dodań i 2 usunięć

Wyświetl plik

@ -1,6 +1,6 @@
import { stringifyQuery } from 'ufo'
import { defaultUserAgent } from '~~/server/utils/shared'
import { defaultUserAgent, invalidateApp } from '~~/server/utils/shared'
export default defineEventHandler(async (event) => {
let { server, origin } = getRouterParams(event)
@ -43,7 +43,51 @@ export default defineEventHandler(async (event) => {
const url = `/signin/callback?${stringifyQuery({ server, token: result.access_token, vapid_key: app.vapid_key })}`
await sendRedirect(event, url, 302)
}
catch {
catch (error: any) {
// Check for invalid client error (OAuth app deleted)
if (error?.data?.error === 'invalid_client'
|| (error?.statusCode === 401 && error?.data?.error_description?.includes('Client authentication failed'))) {
// Invalidate cached app and retry once
await invalidateApp(origin, server)
try {
const newApp = await getApp(origin, server)
if (!newApp) {
throw createError({
statusCode: 400,
statusMessage: `Failed to re-register app for server: ${server}`,
})
}
const retryResult: any = await $fetch(`https://${server}/oauth/token`, {
method: 'POST',
headers: {
'user-agent': defaultUserAgent,
},
body: {
client_id: newApp.client_id,
client_secret: newApp.client_secret,
redirect_uri: getRedirectURI(origin, server),
grant_type: 'authorization_code',
code,
scope: 'read write follow push',
},
retry: 1,
})
const url = `/signin/callback?${stringifyQuery({ server, token: retryResult.access_token, vapid_key: newApp.vapid_key })}`
await sendRedirect(event, url, 302)
return
}
catch {
throw createError({
statusCode: 400,
statusMessage: 'OAuth application recovery failed. Please try again.',
})
}
}
// Other errors (network, invalid code, etc.)
throw createError({
statusCode: 400,
statusMessage: 'Could not complete log in.',

Wyświetl plik

@ -111,6 +111,12 @@ export async function deleteApp(server: string) {
await storage.removeItem(key)
}
export async function invalidateApp(origin: string, server: string) {
const host = origin.replace(/^https?:\/\//, '').replace(/\W/g, '-').replace(/\?.*$/, '')
const key = `servers:v4:${server}:${host}.json`.toLowerCase()
await storage.removeItem(key)
}
export async function listServers() {
const keys = await storage.getKeys('servers:v4:')
const servers = new Set<string>()