Skip to content

Commit 24b9d92

Browse files
tidy
1 parent 741c06f commit 24b9d92

1 file changed

Lines changed: 35 additions & 19 deletions

File tree

server/api/auth/atproto.get.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,14 @@ export default defineEventHandler(async event => {
104104
throw error
105105
}
106106
} catch (error) {
107-
// user cancelled explicitly
108-
if (query.error === 'access_denied' && error instanceof OAuthCallbackError && error.state) {
107+
if (error instanceof OAuthCallbackError && error.state) {
108+
// Always decode the state, to clean up the cookie
109109
const state = decodeOAuthState(event, error.state)
110-
return sendRedirect(event, state.redirectPath)
110+
111+
// user cancelled explicitly
112+
if (query.error === 'access_denied') {
113+
return sendRedirect(event, state.redirectPath)
114+
}
111115
}
112116

113117
const message = error instanceof Error ? error.message : 'Authentication failed.'
@@ -124,8 +128,7 @@ type OAuthStateData = {
124128
redirectPath: string
125129
}
126130

127-
const SID_COOKIE_PREFIX = 'atproto_oauth_sid'
128-
const SID_COOKIE_VALUE = '1'
131+
const OAUTH_REQUEST_COOKIE_PREFIX = 'atproto_oauth_req'
129132

130133
/**
131134
* This function encodes the OAuth state by generating a random SID, storing it
@@ -138,25 +141,35 @@ const SID_COOKIE_VALUE = '1'
138141
* and ensuring that the callback is part of an ongoing authentication flow
139142
* initiated by the same client.
140143
*
141-
* Note that this mechanism could use any other unique session mechanism the
142-
* server has with the client (e.g. UserServerSession). We don't do that though
143-
* to avoid polluting the session with ephemeral OAuth-specific data.
144-
*
145144
* @param event The H3 event object, used to set the cookie
146145
* @param state The original OAuth state to encode
147146
* @returns A JSON string encapsulating the original state and the generated SID
148147
*/
149148
function encodeOAuthState(event: H3Event, data: OAuthStateData): string {
150-
const sid = generateRandomHexString()
151-
setCookie(event, `${SID_COOKIE_PREFIX}_${sid}`, SID_COOKIE_VALUE, {
149+
const id = generateRandomHexString()
150+
// This uses an ephemeral cookie instead of useSession() to avoid polluting
151+
// the session with ephemeral OAuth-specific data. The cookie is set with a
152+
// short expiration time to limit the window of potential misuse, and is
153+
// deleted immediately after validating the callback to clean up any remnants
154+
// of the authentication flow. Using useSession() for this would require
155+
// additional logic to clean up the session in case of expired ephemeral data.
156+
157+
// We use the id as cookie name to allow multiple concurrent auth flows (e.g.
158+
// user opens multiple tabs and initiates auth in both, or initiates auth,
159+
// waits for a while, then initiates again before completing the first one),
160+
// without risk of cookie value collisions between them. The cookie value is a
161+
// constant since the actual value doesn't matter - it's just used as a flag
162+
// to validate the presence of the cookie on callback.
163+
setCookie(event, `${OAUTH_REQUEST_COOKIE_PREFIX}_${id}`, '1', {
152164
maxAge: 60 * 5,
153165
httpOnly: true,
154166
// secure only if NOT in dev mode
155167
secure: !import.meta.dev,
156168
sameSite: 'lax',
157169
path: event.path.split('?', 1)[0],
158170
})
159-
return JSON.stringify({ data, sid })
171+
172+
return JSON.stringify({ data, id })
160173
}
161174

162175
function generateRandomHexString(byteLength: number = 16): string {
@@ -170,9 +183,9 @@ function generateRandomHexString(byteLength: number = 16): string {
170183
* session performing the oauth callback.
171184
*
172185
* @param event The H3 event object, used to read and delete the cookie
173-
* @param state The JSON string containing the original state and SID
174-
* @returns The original OAuth state if the SID is valid
175-
* @throws An error if the SID is missing or invalid, indicating a potential issue with cookies or expired state
186+
* @param state The JSON string containing the original state and id
187+
* @returns The original OAuth state if the id is valid
188+
* @throws An error if the id is missing or invalid, indicating a potential issue with cookies or expired state
176189
*/
177190
function decodeOAuthState(event: H3Event, state: string | null): OAuthStateData {
178191
if (!state) {
@@ -187,11 +200,14 @@ function decodeOAuthState(event: H3Event, state: string | null): OAuthStateData
187200

188201
// The state sting was encoded using encodeOAuthState. No need to protect
189202
// against JSON parsing since the StateStore should ensure it's integrity.
190-
const decoded = JSON.parse(state) as { data: OAuthStateData; sid: string }
203+
const decoded = JSON.parse(state) as { data: OAuthStateData; id: string }
204+
const requestCookieName = `${OAUTH_REQUEST_COOKIE_PREFIX}_${decoded.id}`
191205

192-
const sid = getCookie(event, `${SID_COOKIE_PREFIX}_${decoded.sid}`)
193-
if (sid === SID_COOKIE_VALUE) {
194-
deleteCookie(event, `${SID_COOKIE_PREFIX}_${decoded.sid}`, {
206+
if (getCookie(event, requestCookieName) != null) {
207+
// The cookie will never be used again since the state store ensure unique
208+
// nonces, but we delete it to clean up any remnants of the authentication
209+
// flow.
210+
deleteCookie(event, requestCookieName, {
195211
httpOnly: true,
196212
secure: !import.meta.dev,
197213
sameSite: 'lax',

0 commit comments

Comments
 (0)