javascript - Accessing document.cookie returns empty string even though cookies are listed in developer tools with httpOnly flag

Sometimes*, when accessing document.cookie in the login page I get an empty string even though: cookies

Sometimes*, when accessing document.cookie in the login page I get an empty string even though:

  1. cookies are listed in the Chrome and Firefox developer tools,
  2. httpOnly flag of cookie I'm interested in is set to false,
  3. path of cookie I'm interested in is set to '/'.

Desired behaviour

My React single page application (SPA) has a login page which contains a <form /> element for sending login credentials to the backend. When the response from the backend is received and the authentication was successful, I check whether the authentication cookie has been set, properly. If that's the case a redirect will be triggered that shows the content for logged in users.

Actual behaviour

Unfortunately, in like 15% of the login attempts, document.cookie returns an empty string which prevents the redirection and keeps the user on the login page. Pressing F5 doesn't do the trick but when manually replacing a path of the url after a successful login request (e.g. updating 'www.website.tld/login' to 'www.website.tld/start') the user gets forwarded to the desired page which is for logged in users, only.

I'm not able to reproduce the error manually. It just seems to happen randomly. But when it occurs and I have a look into the developer console I can see all the cookies from the backend (set correctly).

Additional information

  • a django server is running in the backend
  • the desired cookie is set with response.set_cookie('key', 'value', secure=False httponly=False, samesite='strict')
  • JS libs (axios, react-router)

Related:

  • Can't access cookies from document.cookie in JS, but browser shows cookies exist (httpOnly)
  • Can't access a cookie using document.cookie in JS (httpOnly)

Login page (JSX)

    import React, { useState } from "react";
    import { Redirect } from "react-router-dom";
    import axios from "axios";

    /**
     * We're using cookies.js to read cookies.
     * Source: .js
     */
    function hasItem(sKey) {
      return new RegExp(
        "(?:^|;\\s*)" +
          encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") +
          "\\s*\\="
      ).test(document.cookie);
    }

    export const LoginPage = () => {
      const [isAuthenticated, setIsAuthenticated] = useState(false);
      const [username, setUsername] = useState("");
      const [password, setPassword] = useState("");

      function handleSubmit(e) {
        e.preventDefault();

        function onSuccess(response) {
          // handle response
          // [...]

          // sometimes console.log(document.cookie) returns empty string
          if (hasItem("auth_cookie")) {
            setIsAuthenticated(true);
          } else {
            console.warn("Cookie not found!");
          }
        }

        function onFailure(error) {
          // handle error
        }

        const conf = {
          headers: new Headers({
            "Content-Type": "application/json; charset=UTF-8",
            Origin: window.location.origin
          })
        };

        axios
          .post("/api/login/", { username, password }, conf)
          .then(response => {
            onSuccess(response);
          })
          .catch(error => {
            onFailure(error);
          });
      }

      if (isAuthenticated) {
        return <Redirect to="/start" />;
      }

      return (
        <div className="login-page">
          <form
            name="login-form"
            method="post"
            onSubmit={e => handleSubmit(e)}
            action="api/login"
            target="hiddenFrame"
          >
            <iframe className="invisible-frame" src="" name="hiddenFrame" />
            <div>
              <label htmlFor="username">Email</label>
              <input
                name="username"
                type="text"
                onChange={e => setUsername(e.target.value)}
              />
            </div>
            <div>
              <label htmlFor="password">Password</label>
              <input
                name="password"
                type="password"
                onChange={e => setPassword(e.target.value)}
              />
            </div>

            <button type="submit">Submit</button>
          </form>
        </div>
      );
    };

Routing (JSX)

    import React from "react";
    import { Route, Redirect } from "react-router-dom";

    const RootLayout = () => {
      return (
        <div className="root-layout">
          <Switch>
            <PublicRoute path="/login" ponent={LoginPage} />
            <PrivateRoute path="/" ponent={App} />
          </Switch>
        </div>
      );
    };

    /**
     * Component that handles redirection when user is logged in already
     */
    const PublicRoute = ({ ponent: ChildComponent, ...remainingProps }) => {
      let isAuthenticated = hasItem("auth_cookie");
      return (
        <Route
          render={props =>
            isAuthenticated ? <Redirect to="/" /> : <ChildComponent {...props} />
          }
          {...remainingProps}
        />
      );
    };

    /**
     * Component that handles redirection when user has been logged out.
     * E.g. when authentication cookie expires.
     */
    const PrivateRoute = ({ ponent: ChildComponent, ...remainingProps }) => {
      let isAuthenticated = hasItem("auth_cookie");
      return (
        <Route
          render={props =>
            !isAuthenticated ? (
              <Redirect to="/login" />
            ) : (
              <ChildComponent {...props} />
            )
          }
          {...remainingProps}
        />
      );
    };

    const App = () => (
      <Switch>
        <Route exact path="/" render={() => <Redirect to="/start" />} />
        <Route exact path="/start" ponent={StartPage} />
        <Route exact path="/blog" ponent={BlogPage} />
        {/*...*/}
      </Switch>
    );

* I know, that's probably not how a post should start...

Sometimes*, when accessing document.cookie in the login page I get an empty string even though:

  1. cookies are listed in the Chrome and Firefox developer tools,
  2. httpOnly flag of cookie I'm interested in is set to false,
  3. path of cookie I'm interested in is set to '/'.

Desired behaviour

My React single page application (SPA) has a login page which contains a <form /> element for sending login credentials to the backend. When the response from the backend is received and the authentication was successful, I check whether the authentication cookie has been set, properly. If that's the case a redirect will be triggered that shows the content for logged in users.

Actual behaviour

Unfortunately, in like 15% of the login attempts, document.cookie returns an empty string which prevents the redirection and keeps the user on the login page. Pressing F5 doesn't do the trick but when manually replacing a path of the url after a successful login request (e.g. updating 'www.website.tld/login' to 'www.website.tld/start') the user gets forwarded to the desired page which is for logged in users, only.

I'm not able to reproduce the error manually. It just seems to happen randomly. But when it occurs and I have a look into the developer console I can see all the cookies from the backend (set correctly).

Additional information

  • a django server is running in the backend
  • the desired cookie is set with response.set_cookie('key', 'value', secure=False httponly=False, samesite='strict')
  • JS libs (axios, react-router)

Related:

  • Can't access cookies from document.cookie in JS, but browser shows cookies exist (httpOnly)
  • Can't access a cookie using document.cookie in JS (httpOnly)

Login page (JSX)

    import React, { useState } from "react";
    import { Redirect } from "react-router-dom";
    import axios from "axios";

    /**
     * We're using cookies.js to read cookies.
     * Source: https://github./madmurphy/cookies.js
     */
    function hasItem(sKey) {
      return new RegExp(
        "(?:^|;\\s*)" +
          encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") +
          "\\s*\\="
      ).test(document.cookie);
    }

    export const LoginPage = () => {
      const [isAuthenticated, setIsAuthenticated] = useState(false);
      const [username, setUsername] = useState("");
      const [password, setPassword] = useState("");

      function handleSubmit(e) {
        e.preventDefault();

        function onSuccess(response) {
          // handle response
          // [...]

          // sometimes console.log(document.cookie) returns empty string
          if (hasItem("auth_cookie")) {
            setIsAuthenticated(true);
          } else {
            console.warn("Cookie not found!");
          }
        }

        function onFailure(error) {
          // handle error
        }

        const conf = {
          headers: new Headers({
            "Content-Type": "application/json; charset=UTF-8",
            Origin: window.location.origin
          })
        };

        axios
          .post("/api/login/", { username, password }, conf)
          .then(response => {
            onSuccess(response);
          })
          .catch(error => {
            onFailure(error);
          });
      }

      if (isAuthenticated) {
        return <Redirect to="/start" />;
      }

      return (
        <div className="login-page">
          <form
            name="login-form"
            method="post"
            onSubmit={e => handleSubmit(e)}
            action="api/login"
            target="hiddenFrame"
          >
            <iframe className="invisible-frame" src="" name="hiddenFrame" />
            <div>
              <label htmlFor="username">Email</label>
              <input
                name="username"
                type="text"
                onChange={e => setUsername(e.target.value)}
              />
            </div>
            <div>
              <label htmlFor="password">Password</label>
              <input
                name="password"
                type="password"
                onChange={e => setPassword(e.target.value)}
              />
            </div>

            <button type="submit">Submit</button>
          </form>
        </div>
      );
    };

Routing (JSX)

    import React from "react";
    import { Route, Redirect } from "react-router-dom";

    const RootLayout = () => {
      return (
        <div className="root-layout">
          <Switch>
            <PublicRoute path="/login" ponent={LoginPage} />
            <PrivateRoute path="/" ponent={App} />
          </Switch>
        </div>
      );
    };

    /**
     * Component that handles redirection when user is logged in already
     */
    const PublicRoute = ({ ponent: ChildComponent, ...remainingProps }) => {
      let isAuthenticated = hasItem("auth_cookie");
      return (
        <Route
          render={props =>
            isAuthenticated ? <Redirect to="/" /> : <ChildComponent {...props} />
          }
          {...remainingProps}
        />
      );
    };

    /**
     * Component that handles redirection when user has been logged out.
     * E.g. when authentication cookie expires.
     */
    const PrivateRoute = ({ ponent: ChildComponent, ...remainingProps }) => {
      let isAuthenticated = hasItem("auth_cookie");
      return (
        <Route
          render={props =>
            !isAuthenticated ? (
              <Redirect to="/login" />
            ) : (
              <ChildComponent {...props} />
            )
          }
          {...remainingProps}
        />
      );
    };

    const App = () => (
      <Switch>
        <Route exact path="/" render={() => <Redirect to="/start" />} />
        <Route exact path="/start" ponent={StartPage} />
        <Route exact path="/blog" ponent={BlogPage} />
        {/*...*/}
      </Switch>
    );

* I know, that's probably not how a post should start...

Share Improve this question edited Mar 31, 2020 at 8:50 davsto asked Mar 30, 2020 at 16:42 davstodavsto 5526 silver badges16 bronze badges 6
  • 1 Can you add the code where you are checking this? – hiddenuser.2524 Commented Mar 30, 2020 at 17:22
  • Thanks for your ment, @tudor.gergely. I added some code, now. – davsto Commented Mar 31, 2020 at 8:53
  • can you try adding withCredentials: true to your axios conf? – hiddenuser.2524 Commented Mar 31, 2020 at 9:20
  • It sounds like it could be related to "samesite". Did you try setting it to "lax"? And generally, I would advise to use httponly=True. The frontend has no need to access the auth cookies at all and you should use the backend for authentication. – str Commented Mar 31, 2020 at 10:17
  • Setting samesite="lax" (or "none") helps! I haven't tested all features yet but the first impression is good. I was able to reproduce the error, now. Accessing the cookies only works, when I enter the URL in the browser manually or use a bookmark. When entering the SPA through a link on an external site the cookies can't be accessed. Is the page that includes the link to the SPA the 'first party' which prevents me from reading the cookies when their samesite flag is set to "strict"? – davsto Commented Apr 1, 2020 at 18:31
 |  Show 1 more ment

1 Answer 1

Reset to default 3

You are running into an issue with sameSite cookies. See SameSite cookies explained for an explanation:

If you set SameSite to Strict, your cookie will only be sent in a first-party context. [...] When the user is on your site, then the cookie will be sent with the request as expected. However when following a link into your site, say from another site or via an email from a friend, on that initial request the cookie will not be sent. This is good when you have cookies relating to functionality that will always be behind an initial navigation, such as changing a password or making a purchase, but is too restrictive for promo_shown. If your reader follows the link into the site, they want the cookie sent so their preference can be applied.

Now you have at least two options:

  • Remended: Keep samesite=strict and refactor your client code. The frontend does not need access to the auth cookies at all and thus you could set httponly=True. Then introduce a backend API that validates a cookie based on requests from the client code. That gives you the added advantage to be less vulnerable to XSS attacks as the frontend code has no access to the auth cookie.
  • Not remended: Set samesite to none or lax.

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745384792a4625390.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信