Skip to content

Commit 796e330

Browse files
NandkishorJadouncoderabbitai[bot]danielroeautofix-ci[bot]
authored
fix: preserve page context via cookie after Bluesky redirect (#930)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Daniel Roe <daniel@roe.dev> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent f660996 commit 796e330

File tree

3 files changed

+39
-6
lines changed

3 files changed

+39
-6
lines changed

app/components/Header/AuthModal.client.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import { useAtproto } from '~/composables/atproto/useAtproto'
33
import { authRedirect } from '~/utils/atproto/helpers'
44
55
const handleInput = shallowRef('')
6-
6+
const route = useRoute()
77
const { user, logout } = useAtproto()
88
99
async function handleBlueskySignIn() {
10-
await authRedirect('https://bsky.social')
10+
await authRedirect('https://bsky.social', { redirectTo: route.fullPath })
1111
}
1212
1313
async function handleCreateAccount() {
14-
await authRedirect('https://npmx.social', true)
14+
await authRedirect('https://npmx.social', { create: true, redirectTo: route.fullPath })
1515
}
1616
1717
async function handleLogin() {

app/utils/atproto/helpers.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import type { FetchError } from 'ofetch'
22
import type { LocationQueryRaw } from 'vue-router'
33

4+
interface AuthRedirectOptions {
5+
create?: boolean
6+
redirectTo?: string
7+
}
8+
49
/**
510
* Redirect user to ATProto authentication
611
*/
7-
export async function authRedirect(identifier: string, create: boolean = false) {
12+
export async function authRedirect(identifier: string, options: AuthRedirectOptions = {}) {
813
let query: LocationQueryRaw = { handle: identifier }
9-
if (create) {
14+
if (options.create) {
1015
query = { ...query, create: 'true' }
1116
}
17+
if (options.redirectTo) {
18+
query = { ...query, returnTo: options.redirectTo }
19+
}
1220
await navigateTo(
1321
{
1422
path: '/api/auth/atproto',

server/api/auth/atproto.get.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { handleResolver } from '#server/utils/atproto/oauth'
1010
import { Client } from '@atproto/lex'
1111
import * as app from '#shared/types/lexicons/app'
1212
import { ensureValidAtIdentifier } from '@atproto/syntax'
13+
// @ts-expect-error virtual file from oauth module
14+
import { clientUri } from '#oauth/config'
1315

1416
/**
1517
* Fetch the user's profile record to get their avatar blob reference
@@ -66,6 +68,25 @@ export default defineEventHandler(async event => {
6668
})
6769

6870
if (!query.code) {
71+
// Validate returnTo is a safe relative path (prevent open redirect)
72+
// Only set cookie on initial auth request, not the callback
73+
let redirectPath = '/'
74+
try {
75+
const clientOrigin = new URL(clientUri).origin
76+
const returnToUrl = new URL(query.returnTo?.toString() || '/', clientUri)
77+
if (returnToUrl.origin === clientOrigin) {
78+
redirectPath = returnToUrl.pathname + returnToUrl.search + returnToUrl.hash
79+
}
80+
} catch {
81+
// Invalid URL, fall back to root
82+
}
83+
84+
setCookie(event, 'auth_return_to', redirectPath, {
85+
maxAge: 60 * 5,
86+
httpOnly: true,
87+
// secure only if NOT in dev mode
88+
secure: !import.meta.dev,
89+
})
6990
try {
7091
const handle = query.handle?.toString()
7192
const create = query.create?.toString()
@@ -126,5 +147,9 @@ export default defineEventHandler(async event => {
126147
},
127148
})
128149
}
129-
return sendRedirect(event, '/')
150+
151+
const returnToURL = getCookie(event, 'auth_return_to') || '/'
152+
deleteCookie(event, 'auth_return_to')
153+
154+
return sendRedirect(event, returnToURL)
130155
})

0 commit comments

Comments
 (0)