Flask, Bokeh, externally signed sessions: Invalid token signature error

hi all!

I’m trying to serve Bokeh application through Flask using externally signed sessions. The idea is to generate a signed session for each authenticated user (in Flask) and then pull it from Bokeh server working alongside. Bokeh server should not be accessible at all through any other means.

The basic example is really simple and is adapted from server_session example:

from flask import Flask, render_template
from bokeh.client import pull_session
from bokeh.embed import server_session
from bokeh.util.token import generate_session_id

app_url = "http://localhost:5100/bokeh_app/"

app = Flask(__name__)

@app.route('/')
def bkapp_page():
    bokeh_session_id = generate_session_id(secret_key=<SECRET_KEY>, signed=True)

    with pull_session(session_id=bokeh_session_id, url=app_url) as session:
        script = server_session(session_id=session.id, url=app_url)
        return render_template("embed.html", script=script, template="Flask")

if __name__ == '__main__':
    app.run(port=8080)

for the Flask app and

import numpy as np
from bokeh.plotting import figure, curdoc

def add_circles():
    sample_plot.circle(x=np.random.normal(size=(10,)),
                       y=np.random.normal(size=(10,)))

bokeh_doc = curdoc()

sample_plot = figure(plot_height=400, plot_width=400)

bokeh_doc.add_root(sample_plot)
bokeh_doc.add_periodic_callback(add_circles, 1000)

for the Bokeh app. However, when trying to serve all of this with

export BOKEH_SECRET_KEY=<SECRET_KEY>
bokeh serve --port 5100 --allow-websocket-origin localhost:8080 --allow-websocket-origin 127.0.0.1:8080 --session-ids external-signed bokeh_app.py

and

python3 flask_app.py

I get an invalid token exception:

2021-01-16 21:41:44,376 Token for session 'IC4cV2wEuIxgNiK4U0knlc58uoFqnE1K0MsfqS1TeLQi.nvqsJ781xapwIIlB0gSaA3ab9eQN_wBdYSPlLT0vxLk' had invalid signature
2021-01-16 21:25:46,179 Uncaught exception GET /bokeh_app/ws (::1)
HTTPServerRequest(protocol='http', host='localhost:5100', method='GET', uri='/bokeh_app/ws', version='HTTP/1.1', remote_ip='::1')
Traceback (most recent call last):
  File "/usr/lib64/python3.8/site-packages/tornado/websocket.py", line 956, in _accept_connection
    open_result = handler.open(*handler.open_args, **handler.open_kwargs)
  File "/home/traveller/.local/lib/python3.8/site-packages/bokeh/server/views/ws.py", line 141, in open
    raise ProtocolError("Invalid token signature")
bokeh.protocol.exceptions.ProtocolError: Invalid token signature

in the Bokeh server logs. Flask in turn reports that it cannot connect:

OSError: Cannot pull session document because we failed to connect to the server (to start the server, try the 'bokeh serve' command)

I was able to pin down the exact failing path in check_token_signature by adding a print:

    if signed:
        print(token)
        token_pieces = token.split('.', 1)
        if len(token_pieces) != 2:
            return False

Despite the result of generate_session_id(secret_key=<SECRET_KEY>, signed=True) looking correct (<session_id>.<session_signature>), what we get from the print(token) above is something like

eyJzZXNzaW9uX2lkIjogIkdMaU9EWFM1UE1RcWR3VTA5VHZ6WXVJZUhRR2dVV012Z01ld2VtYm9MeDZxIiwgInNlc3Npb25fZXhwaXJ5IjogMTYxMDgyMTg0Nn0

i.e., no periods at all and len(token_pieces) != 2 resolves to True in check_token_signature, leading to the invalid token error.The funny thing is that session in

2021-01-16 21:41:44,376 Token for session 'IC4cV2wEuIxgNiK4U0knlc58uoFqnE1K0MsfqS1TeLQi.nvqsJ781xapwIIlB0gSaA3ab9eQN_wBdYSPlLT0vxLk' had invalid signature

looks exactly like in Flask app as bokeh_session_id. What I do not get, is why token, which goes to check_token_signature from WebSocket handler, does look like this.

I probably overlook some simple mistake and do not understand the inner mechanics, by it requires to get into Tornado, which I’m absolutely not familiar with.

What is the right way of doing this? What am I missing&

Thanks for any help!

@PhtRaveller Seems like it might just be a bug, possibly introduced when the token support was added last year. If so, I guess that would indicate that not many folks are using signed sessions. It will definitely require some investigation, please make a github issue with all these details.

@PhtRaveller Actually this may be a documentation issue. Your code works for me if I also set BOKEH_SIGN_SESSIONS=yes and BOKEH_SECRET_KEY="foo" for the Flask app. I am not sure that both should be necessary, though, so please still make an issue so we can investigate whether docs need updating, or the usage can be streamlined, or both

Note if you set BOKEH_SECRET_KEY for the Flask app you can then supply it via the settings module:

generate_session_id(secret_key=settings.secret_key, signed=True)

Offhand I would say the slight inconsistency here is that while generate_session_id can accept these signing parameters via the API, the pull_session codepath really only wants to draw values from settings (which usually means env vars). The solution is probably just to make it clearer that expected usage involves setting the appropriate env vars.

1 Like

@Bryan thanks! it indeed works as expected when setting the env vars for Flask app. I’ll collect all the combinations that work (1), and those, which do not and file an issue.

(1) - setting environment for Flask works even with generate_session_id(), for example.