Repos / pytaku / bb3839b866
commit bb3839b86675853b857ba905f1dda75ae52f3e2b
Author: Bùi Thành Nhân <hi@imnhan.com>
Date:   Wed Aug 26 20:53:39 2020 +0700

    cookie-based auth
    
    Gave up on session/localStorate. WHY THE FUCK is sessionStorage not
    shared between tabs? It's impossible to implement a straightforward "log
    out when browser closes" scenario.

diff --git a/src/pytaku/decorators.py b/src/pytaku/decorators.py
index 22015e7..01d1a8d 100644
--- a/src/pytaku/decorators.py
+++ b/src/pytaku/decorators.py
@@ -9,15 +9,11 @@ def process_token(required=True):
     def decorator(f):
         @wraps(f)
         def decorated_function(*args, **kwargs):
-            header = request.headers.get("Authorization")
-            if not header or not header.startswith("Bearer "):
+            token = request.cookies.get("token")
+            if not token:
                 if required:
                     return (
-                        jsonify(
-                            {
-                                "message": "Missing `Authorization: Bearer <token>` header."
-                            }
-                        ),
+                        jsonify({"message": "Missing 'token' cookie."}),
                         401,
                     )
                 else:
@@ -25,7 +21,6 @@ def decorated_function(*args, **kwargs):
                     request.user_id = None
                     return f(*args, **kwargs)
 
-            token = header[len("Bearer ") :]
             user_id = verify_token(token)
             if user_id is None:
                 return jsonify({"message": "Invalid token."}), 401
diff --git a/src/pytaku/main.py b/src/pytaku/main.py
index 09970cf..2574598 100644
--- a/src/pytaku/main.py
+++ b/src/pytaku/main.py
@@ -309,7 +309,11 @@ def api_login():
         return jsonify({"message": "Wrong username/password combination."}), 401
 
     token = create_token(user_id, remember)
-    return jsonify({"user_id": user_id, "token": token}), 200
+    resp = make_response(jsonify({"user_id": user_id, "token": token}))
+    resp.set_cookie(
+        "token", token, max_age=31536000 if remember else None, samesite="Strict"
+    )
+    return resp
 
 
 @app.route("/api/verify-token", methods=["GET"])
@@ -324,7 +328,9 @@ def api_logout():
     num_deleted = delete_token(request.token)
     if num_deleted != 1:
         return jsonify({"message": "Invalid token."}), 401
-    return "{}", 200
+    resp = make_response("{}", 200)
+    resp.set_cookie("token", "", expires=0)
+    return resp
 
 
 @app.route("/api/follows", methods=["GET"])
diff --git a/src/pytaku/static/js/models.js b/src/pytaku/static/js/models.js
index 0159684..e683512 100644
--- a/src/pytaku/static/js/models.js
+++ b/src/pytaku/static/js/models.js
@@ -1,7 +1,18 @@
+function readCookie(cookieString) {
+  // This is terrible and wrong but it's enough for what we need i.e. just get
+  // the damn token.
+  const result = {};
+  cookieString.split(";").forEach((pair) => {
+    const [key, val] = pair.split("=");
+    result[key] = val;
+  });
+  return result;
+}
+
 const Auth = {
   username: sessionStorage.getItem("username"),
   userId: sessionStorage.getItem("userId"),
-  token: localStorage.getItem("token"),
+  token: readCookie(document.cookie).token || null,
   isLoggedIn: () => Auth.username !== null && Auth.userId !== null,
   init: () => {
     // Already logged in, probably from another tab:
@@ -21,7 +32,6 @@ const Auth = {
       .request({
         method: "GET",
         url: "/api/verify-token",
-        headers: { Authorization: `Bearer ${Auth.token}` },
       })
       .then((result) => {
         // Success! Set user info for this session now
@@ -41,21 +51,8 @@ const Auth = {
   },
 
   saveLoginResults: ({ userId, username, token, remember }) => {
-    // FIXME: currently, even when remember=false we're still storing the token
-    // in localStorage, simply because sessionStorage isn't shared across tabs.
-    // Unfortunately this means when user logs in without checking "remember
-    // me", the token will still linger in localstorage after browser is
-    // closed, and if an adversary reopens browser within the token's
-    // server-enforced lifespan (1 day), then user is pwned.
-    //
-    // Either that or we stick to sessionStorage and forget multitab support.
-    // _OR_ we do a convoluted song and dance with storage events:
-    // > https://stackoverflow.com/a/32766809
-    //
-    // 0 days since web APIs last made me sad.
     sessionStorage.setItem("userId", userId);
     sessionStorage.setItem("username", username);
-    localStorage.setItem("token", token);
     Auth.userId = userId;
     Auth.username = username;
     Auth.token = token;
@@ -77,15 +74,16 @@ const Auth = {
     Auth.username = null;
     Auth.token = null;
     Auth.userId = null;
-    localStorage.clear();
     sessionStorage.clear();
   },
 
   request: (options) => {
-    if (Auth.isLoggedIn()) {
-      options.headers = { Authorization: `Bearer ${Auth.token}` };
-    }
-    return m.request(options);
+    return m.request(options).catch((err) => {
+      if (err.code == 401) {
+        Auth.clearCredentials();
+      }
+      throw err;
+    });
   },
 };