Repository: bokeh/bokeh

Issue:
[BUG] Timezone bug in generate_jwt_token 
#### ALL software version info (bokeh, python, notebook, OS, browser, any other relevant packages)
bokeh: ```2.0.1-42-g47e7da4b7```
python: ```3.8.2```
OS: ```Mac OS X 10.15.3```

#### Description of expected behavior and the observed behavior
see https://discourse.bokeh.org/t/timezone-bug-in-generate-jwt-token/5206

#### Stack traceback and/or browser JavaScript console output
On the django server we get
```
  File "/Users/simon/anaconda3/envs/owsforecasts/lib/python3.7/site-packages/bokeh/client/session.py", line 119, in pull_session
    session.pull()
  File "/Users/simon/anaconda3/envs/owsforecasts/lib/python3.7/site-packages/bokeh/client/session.py", line 348, in pull
    raise IOError("Cannot pull session document because we failed to connect to the server (to start the server, try the 'bokeh serve' command)")
OSError: Cannot pull session document because we failed to connect to the server (to start the server, try the 'bokeh serve' command)
```

From the  Django app we get

```
Traceback (most recent call last):
  File "/Users/simon/anaconda3/lib/python3.6/site-packages/tornado/websocket.py", line 956, in _accept_connection
    open_result = handler.open(*handler.open_args, **handler.open_kwargs)
  File "/Users/simon/anaconda3/lib/python3.6/site-packages/bokeh/server/views/ws.py", line 135, in open
    raise ProtocolError("Token is expired.")
bokeh.protocol.exceptions.ProtocolError: Token is expired
```
#### Screenshots or screencasts of the bug in action



Discussion:

Resolving patch:
diff --git a/bokeh/server/django/consumers.py b/bokeh/server/django/consumers.py
--- a/bokeh/server/django/consumers.py
+++ b/bokeh/server/django/consumers.py
@@ -197,7 +197,7 @@ async def connect(self):
             self.close()
             raise RuntimeError("No token received in subprotocol header")
 
-        now = calendar.timegm(dt.datetime.now().utctimetuple())
+        now = calendar.timegm(dt.datetime.utcnow().utctimetuple())
         payload = get_token_payload(token)
         if 'session_expiry' not in payload:
             self.close()
diff --git a/bokeh/server/views/ws.py b/bokeh/server/views/ws.py
--- a/bokeh/server/views/ws.py
+++ b/bokeh/server/views/ws.py
@@ -125,7 +125,7 @@ def open(self):
             self.close()
             raise ProtocolError("No token received in subprotocol header")
 
-        now = calendar.timegm(dt.datetime.now().utctimetuple())
+        now = calendar.timegm(dt.datetime.utcnow().utctimetuple())
         payload = get_token_payload(token)
         if 'session_expiry' not in payload:
             self.close()
diff --git a/bokeh/util/token.py b/bokeh/util/token.py
--- a/bokeh/util/token.py
+++ b/bokeh/util/token.py
@@ -103,7 +103,7 @@ def generate_jwt_token(session_id: str,
     Returns:
         str
     """
-    now = calendar.timegm(dt.datetime.now().utctimetuple())
+    now = calendar.timegm(dt.datetime.utcnow().utctimetuple())
     payload = {'session_id': session_id, 'session_expiry': now+expiration}
     if extra_payload:
         if "session_id" in extra_payload:


Test patch:
diff --git a/tests/unit/bokeh/util/test_token.py b/tests/unit/bokeh/util/test_token.py
--- a/tests/unit/bokeh/util/test_token.py
+++ b/tests/unit/bokeh/util/test_token.py
@@ -15,13 +15,15 @@
 #-----------------------------------------------------------------------------
 
 # Standard library imports
+import calendar
 import codecs
+import datetime as dt
 import json
 import os
 import random
 
 # External imports
-from mock import patch
+from mock import Mock, patch
 
 # Bokeh imports
 from bokeh.util.token import (
@@ -35,6 +37,7 @@
     generate_secret_key,
     generate_session_id,
     get_session_id,
+    get_token_payload,
 )
 
 # Module under test
@@ -162,6 +165,16 @@ def test_string_encoding_does_not_affect_session_id_check(self) -> None:
         token = generate_jwt_token(session_id, signed=True, secret_key="abc")
         assert check_token_signature(token, secret_key="abc", signed=True)
 
+    def test_jwt_token_uses_utc_time(self) -> None:
+        # django server generates token using UTC timezone
+        token = generate_jwt_token("foo", expiration=0)
+        with patch.object(dt, "datetime", Mock(wraps=dt.datetime)) as patched_dt:
+            # mock bokeh server localtime to be UTC + 10
+            patched_dt.now.return_value = dt.datetime.utcnow() + dt.timedelta(hours=10)
+            payload = get_token_payload(token)
+        utcnow = calendar.timegm(dt.datetime.utcnow().utctimetuple())
+        assert utcnow -1 <= payload['session_expiry'] <= utcnow + 1
+
 #-----------------------------------------------------------------------------
 # Dev API
 #-----------------------------------------------------------------------------