Skip to content

Commit 37a91a0

Browse files
authored
fix: auto-recover from OAuth app deletion after token revocation (#3382)
1 parent 7b7334c commit 37a91a0

File tree

2 files changed

+52
-2
lines changed

2 files changed

+52
-2
lines changed

server/api/[server]/oauth/[origin].ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { stringifyQuery } from 'ufo'
22

3-
import { defaultUserAgent } from '~~/server/utils/shared'
3+
import { defaultUserAgent, invalidateApp } from '~~/server/utils/shared'
44

55
export default defineEventHandler(async (event) => {
66
let { server, origin } = getRouterParams(event)
@@ -43,7 +43,51 @@ export default defineEventHandler(async (event) => {
4343
const url = `/signin/callback?${stringifyQuery({ server, token: result.access_token, vapid_key: app.vapid_key })}`
4444
await sendRedirect(event, url, 302)
4545
}
46-
catch {
46+
catch (error: any) {
47+
// Check for invalid client error (OAuth app deleted)
48+
if (error?.data?.error === 'invalid_client'
49+
|| (error?.statusCode === 401 && error?.data?.error_description?.includes('Client authentication failed'))) {
50+
// Invalidate cached app and retry once
51+
await invalidateApp(origin, server)
52+
53+
try {
54+
const newApp = await getApp(origin, server)
55+
if (!newApp) {
56+
throw createError({
57+
statusCode: 400,
58+
statusMessage: `Failed to re-register app for server: ${server}`,
59+
})
60+
}
61+
62+
const retryResult: any = await $fetch(`https://${server}/oauth/token`, {
63+
method: 'POST',
64+
headers: {
65+
'user-agent': defaultUserAgent,
66+
},
67+
body: {
68+
client_id: newApp.client_id,
69+
client_secret: newApp.client_secret,
70+
redirect_uri: getRedirectURI(origin, server),
71+
grant_type: 'authorization_code',
72+
code,
73+
scope: 'read write follow push',
74+
},
75+
retry: 1,
76+
})
77+
78+
const url = `/signin/callback?${stringifyQuery({ server, token: retryResult.access_token, vapid_key: newApp.vapid_key })}`
79+
await sendRedirect(event, url, 302)
80+
return
81+
}
82+
catch {
83+
throw createError({
84+
statusCode: 400,
85+
statusMessage: 'OAuth application recovery failed. Please try again.',
86+
})
87+
}
88+
}
89+
90+
// Other errors (network, invalid code, etc.)
4791
throw createError({
4892
statusCode: 400,
4993
statusMessage: 'Could not complete log in.',

server/utils/shared.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ export async function deleteApp(server: string) {
111111
await storage.removeItem(key)
112112
}
113113

114+
export async function invalidateApp(origin: string, server: string) {
115+
const host = origin.replace(/^https?:\/\//, '').replace(/\W/g, '-').replace(/\?.*$/, '')
116+
const key = `servers:v4:${server}:${host}.json`.toLowerCase()
117+
await storage.removeItem(key)
118+
}
119+
114120
export async function listServers() {
115121
const keys = await storage.getKeys('servers:v4:')
116122
const servers = new Set<string>()

0 commit comments

Comments
 (0)