feat: fix ssr
cookie handling in all edge cases
#780
Closed
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Fixes many issues with
@supabase/ssr
when handling edge-cases for reading and setting cookies especially in NextJS.Some of these edge-cases are as follows:
There's no such thing as removing a cookie.
When cookies are "touched" by the Supabase client, they're either set to a non-empty value or they need to be set to an empty value. When set to an empty value, the
max-age
property must be set to 0 so the browser receiving theSet-Cookie
header knows to delete it from its store.For example, this delete method does not remove the cookie from the response but it just removes the
Set-Cookie
header that would have been sent. But that's not what's wanted -- the Supabase client wants to send aSet-Cookie
header. It's therefore not very useful and should not be used.Because storage items are often chunked into multiple cookies, existing cookie chunks must be managed.
There are 4 cases that need to be dealt with.
Suppose the storage item with key
xyz
is below the cookie chunk limit, so it's encoded as one cookie under the namexyz
. But in the session refresh step, the storage item has grown above the cookie chunk limit, so it now needs to be chunked into 2 cookiesxyz.0
andxyz.1
. In this case, thexyz
cookie must be set to''
andmax-age
to 0 so it's removed from the browser cookie store.Suppose the storage item with key
xyz
is encoded as two cookie chunksxyz.0
andxyz.1
. After the session refresh step, its size has grown and 3 cookie chunks are neededxyz.0
,xyz.1
andxyz.2
. Obviously the first two chunks need to be set to a value and in case they were set tomax-age
0 now need to be set to the indefinitemax-age
.Suppose the storage item with key
xyz
is encoded as three cookie chunksxyz.0
through.2
. After the session refresh step, the size has shrunk down to only needxyz.0
andxyz.1
. This means that chunkxyz.2
needs to be set to''
value andmax-age
of 0.Suppose the storage item with key
xyz
is encoded as two cookie chunksxyz.0
andxyz.1
. After the session refresh step, its size has shrunk below the cookie size so it's now encoded as one cookiexyz
. This means thatxyz.0
andxyz.1
must be set to''
andmax-age
of 0.Because (1), (3) and (4) are not handled properly at this time, they can leave chunk artifacts that float around on every request and can cause in the future problems such as:
JSON.parse()
failing, for example chunkxyz.1
ending like..."
and chunkxyz.2
starting like{
(which would fail because a,
is missing.xyz.1
andxyz.2
align properly to form fully valid JSON, but with content that is unrecognizable.Cookies don't always need to be set (server-side).
There are only 3 situations when cookies should be touched:
onAuthStateChange
event withTOKEN_REFRESHED
.onAuthStateChange
event withUSER_UPDATED
.This is because the user object is encoded in the cookies, and the flow server -> browser is secure.
SIGNED_OUT
event.If these events have not occurred on server side,
Set-Cookie
must not be present on the response. A missingSet-Cookie
header tells the browser to obey the cookie store it already has.Cookies should be set in one go instead of partially (server-side).
The NextResponse.next() API is truly awful in
middleware.ts
. It should only be called once after all session processing has been completed by the Supabase client.This is because:
NextResponse.next({ request: { headers: request.headers } })
must be called, but it can only be called after the Supabase client has finished processing the session.So the following pattern, as seen in the Supabase guide for
middleware.ts
:Is INCORRECT because a new response object is created on each cookie set/removal, meaning it will only contain always only one
Set-Cookie
header and not the multiple needed to encode chunked cookies or the PKCE state.Finally, combining it all together
To solve these multiple issues, this PR modifies the
cookies
option to use a new pattern like so:Cookies are only set once if the
onAuthStateChange
eventsTOKEN_REFRESHED
,USER_UPDATE
,SIGNED_OUT
were emitted. If these events do not occur, cookies must not be touched, so the initialresponse
will be used with the valid cookie chunks.If cookies need to be set, all the cookie chunks are managed properly, so that there are no chunk artifacts floating around. This means the
setAll
call will include both setting''
on certain cookie names and setting actual values.In the browser
The browser client will also use
getAll
andsetAll
similarly, but because this is the browser it's fine to use them directly with thedocument.cookie
API -- meaning cookies will always be gotten and set -- not only on events.This is still a WIP.