Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch with redirect:follow fails to pass through cookies #27065

Open
chrisveness opened this issue Nov 25, 2024 · 1 comment
Open

Fetch with redirect:follow fails to pass through cookies #27065

chrisveness opened this issue Nov 25, 2024 · 1 comment

Comments

@chrisveness
Copy link
Contributor

Version: Deno 2.1.1

A standard sign-in page will return authentication details (e.g. JWT) in the cookies header of the 302 (redirect) response, which can then be used to validate requests to access secure pages.

Testing such behaviour with fetch() using redirect:follow (and credentials:include) fails in Deno, as fetch() doesn't retain the cookies from the 302 response and supply them in the subsequent GET request.

Steps to reproduce:

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/* server.js: "deno run -N server.js" to start server                                             */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

import * as http from 'jsr:@std/http@^1';

Deno.serve({ port: 8080 }, async (req) => {
    console.info(req.method, req.url, http.getCookies(req.headers));

    if (req.method == 'POST' && req.url == 'http://localhost:8080/sign-in') {
        const res = new Response(null, { status:  302 }); // POST always redirects
        const form = Object.fromEntries(await req.formData());
        const location = (form.username == 'hello' && form.password == 'world') ? '/secure-page' : '/sign-in'
        res.headers.append('location', location);
        http.setCookie(res.headers, { name: 'auth', value: 'welcome' });
        return res;
    }

    if (req.method == 'GET' && req.url == 'http://localhost:8080/secure-page') {
        const cookies = http.getCookies(req.headers);
        if (cookies.auth != 'welcome') {
            return new Response(null, { status: 401 });
        }
        const res = new Response('Hello World');
        http.setCookie(res.headers, { name: 'auth', value: cookies.auth });
        return res;
    }
});
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/* test.js: "deno test -N test.js" to run test; expects server running on localhost:8080          */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

import { assert } from 'jsr:@std/assert@^1';

Deno.test('Sign-in with fetch redirect:manual', async function () {
    const form = { username: 'hello', password: 'world' };
    const options1 = { method: 'POST', body: new URLSearchParams(form), credentials: 'include', redirect: 'manual' };
    const response1 = await fetch('http://localhost:8080/sign-in', options1);
    assert(response1.status == 302, response1.status);
    assert(response1.headers.get('location') == '/secure-page');
    assert(response1.headers.get('set-cookie') == 'auth=welcome');
    await response1.body.cancel();

    const options2 = { headers: { cookie: response1.headers.get('set-cookie') }, credentials: 'include' };
    const response2 = await fetch('http://localhost:8080/secure-page', options2);
    assert(response2.status == 200, response2.status);
    assert(response2.headers.get('set-cookie') == 'auth=welcome');
    assert(await response2.text() == 'Hello World');
});

Deno.test('Sign-in with fetch redirect:follow', async function () {
    const form = { username: 'hello', password: 'world' };
    const options = { method: 'POST', body: new URLSearchParams(form), credentials: 'include' }; // uses redirect:follow
    const response = await fetch('http://localhost:8080/sign-in', options);
    assert(response.status == 200, response.status); // <-- FAILS WITH 401
    assert(response.url == 'http://localhost:8080/secure-page');
    await response.body.cancel();
});

I think retaining cookies should be default behaviour, but if not, is there some option that can be set to facilitate cookie retention?

Thanks

@rexxars
Copy link
Contributor

rexxars commented Nov 25, 2024

According to the docs this is intentional:

The Deno user agent does not have a cookie jar. As such, the set-cookie header on a response is not processed, or filtered from the visible response headers.

Existing threads about this exist: #5862, and there doesn't seem to be a clear answer on whether to implement a cookie jar or not. Ryan Dahl said

I agree. Deno ought to behave exactly as a browser does when it comes to the fetch() API.
…back in May 25, 2020. But since then there's been no updates on the front, and given it's now documented, it may be that the deno team has changed their minds on the matter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants