Problems with signed session ids from bokeh

Dear bokeh community,

I try to protect my bokeh webapp, by allowing only externally signed session ids that are generated in a very basic flask application after user authentication.
This works, but it seems that bokeh does not check for the signed session ids. Whoever goes on the hosted bokeh website directly gets a session assigned and can circumvent the authentication. It seems the argument session_ids=‘external-signed’ has no effect.

Something similar has been discussed here:
Flask, Bokeh, externally signed sessions: Invalid token signature error - Community Support - Bokeh Discourse

However, i could not reproduce the solution. I would like to be able to start both servers (flask and bokeh) from one script.
Here is the script that uses an invalid session id and should therefore be rejected:

import threading
from functools import wraps
from flask import Flask, request, Response, redirect
from bokeh.server.server import Server
from bokeh.plotting import figure
from bokeh.util.token import generate_session_id, generate_secret_key
from bokeh.models import ColumnDataSource, Slider, Div, TextAreaInput, CustomJS, PreText
from bokeh import layouts
import os
import logging
from bokeh.layouts import column
from random import random

def vola_tracker_app(doc):
    headline = Div(text="<h4>My headline</h4>")

    # Create the data source
    data = {'x': [], 'y': []}
    source = ColumnDataSource(data)

    # Create the figure
    fig = figure(title='Live Streaming Bokeh Plot')
    fig.line('x', 'y', source=source, line_width=2)

    # Function to update the plot data
    def update():
        new_data = {'x': [], 'y': []}

        # Generate new random data
        for i in range(10):
            new_data['x'].append(i)
            new_data['y'].append(random())

        source.data = new_data

    # Initialize regular callback to update the plot
    doc.add_periodic_callback(update, 2000)
    layout = column(headline, fig)
    doc.add_root(layout)





authentication_app = Flask(__name__)

def check_auth(username, password):
    return username == correct_username and password == correct_password

def authenticate(): #Version, where browser stores during the same session => reenter after restarting browser (a new tab is not enough to trigger an additional request)
    """Sends a 401 response that disables credential caching"""
    response = Response(
        'Could not verify your access level for that URL.\n'
        'You have to login with proper credentials', 401,
        {'WWW-Authenticate': 'Basic realm="Login Required"',
         'Cache-Control': 'no-store, no-cache, must-revalidate',
         'Pragma': 'no-cache'})
    return response

def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            return authenticate()
        return f(*args, **kwargs)
    return decorated

@authentication_app.route('/')
@requires_auth
def redirect_to_bokeh():
    s_id = generate_session_id(secret_key='123', signed=True) #TODO: How to generate valid session ids for bokeh here? And how to force bokeh to only accepted signed ids?
    print('http://'+str(bokeh_app_address)+':'+str(bokeh_app_port)+'/bokeh/?bokeh-session-id={}'.format(s_id))
    return redirect('http://'+str(bokeh_app_address)+':'+str(bokeh_app_port)+'/bokeh/?bokeh-session-id={}'.format(s_id), code=302)

def run_flask_app():
    authentication_app.run(host=authentication_app_host, port=authentication_app_port)




if __name__ == "__main__":
    correct_username = 'a'
    correct_password = 'b'
    bokeh_app_address = 'localhost'  # Host for bokeh app; 'localhost' or RDP address of (Linux Server)
    bokeh_app_port = 8000   # Port for bokeh app
    authentication_app_host = 'localhost' # Host for authentication/flask app; 'localhost' or RDP address of (Linux Server)
    authentication_app_port = 8888 # Port for authentication/flask app

    logging.getLogger().setLevel(logging.DEBUG)

    # Start the Flask app in a separate thread
    flask_thread = threading.Thread(target=run_flask_app)
    flask_thread.start()

    # Generate secret key with bokeh secret command
    bokeh_secret_key = generate_secret_key()
    os.environ['BOKEH_SECRET_KEY'] = bokeh_secret_key
    os.environ['BOKEH_SIGN_SESSIONS'] = 'True'

    # Start the Bokeh server in the main process
    server = Server({'/bokeh': vola_tracker_app}, address=bokeh_app_address, port=bokeh_app_port, allow_websocket_origin=['*'], session_ids='external-signed')
    server.start()

    # Keep the server running
    print('Running bokeh server on: http://' + str(bokeh_app_address) + ':' + str(bokeh_app_port) + '/bokeh')
    server.io_loop.add_callback(server.show, "/")
    server.io_loop.start()

I very much hope to get some help on this.

Hi there - is there nobody who can help with this?

First things first @Patrick please edit your post to use code formatting (either with the </> icon on the editing toolbar, or triple backtick ``` fences around the code blocks) so that everything can simply be copy-pasted and run directly in order to investigate.

Hi Bryan,
my apologies - i thought that i had done that. Here is the same code again formated:

def vola_tracker_app(doc):
    headline = Div(text="<h4>My headline</h4>")

    # Create the data source
    data = {'x': [], 'y': []}
    source = ColumnDataSource(data)

    # Create the figure
    fig = figure(title='Live Streaming Bokeh Plot')
    fig.line('x', 'y', source=source, line_width=2)

    # Function to update the plot data
    def update():
        new_data = {'x': [], 'y': []}

        # Generate new random data
        for i in range(10):
            new_data['x'].append(i)
            new_data['y'].append(random())

        source.data = new_data

    # Initialize regular callback to update the plot
    doc.add_periodic_callback(update, 2000)
    layout = column(headline, fig)
    doc.add_root(layout)





authentication_app = Flask(__name__)

def check_auth(username, password):
    return username == correct_username and password == correct_password

def authenticate(): #Version, where browser stores during the same session => reenter after restarting browser (a new tab is not enough to trigger an additional request)
    """Sends a 401 response that disables credential caching"""
    response = Response(
        'Could not verify your access level for that URL.\n'
        'You have to login with proper credentials', 401,
        {'WWW-Authenticate': 'Basic realm="Login Required"',
         'Cache-Control': 'no-store, no-cache, must-revalidate',
         'Pragma': 'no-cache'})
    return response

def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            return authenticate()
        return f(*args, **kwargs)
    return decorated

@authentication_app.route('/')
@requires_auth
def redirect_to_bokeh():
    s_id = generate_session_id(secret_key='123', signed=True) #TODO: How to generate valid session ids for bokeh here? And how to force bokeh to only accepted signed ids?
    print('http://'+str(bokeh_app_address)+':'+str(bokeh_app_port)+'/bokeh/?bokeh-session-id={}'.format(s_id))
    return redirect('http://'+str(bokeh_app_address)+':'+str(bokeh_app_port)+'/bokeh/?bokeh-session-id={}'.format(s_id), code=302)

def run_flask_app():
    authentication_app.run(host=authentication_app_host, port=authentication_app_port)




if __name__ == "__main__":
    correct_username = 'a'
    correct_password = 'b'
    bokeh_app_address = 'localhost'  # Host for bokeh app; 'localhost' or RDP address of (Linux Server)
    bokeh_app_port = 8000   # Port for bokeh app
    authentication_app_host = 'localhost' # Host for authentication/flask app; 'localhost' or RDP address of (Linux Server)
    authentication_app_port = 8888 # Port for authentication/flask app

    logging.getLogger().setLevel(logging.DEBUG)

    # Start the Flask app in a separate thread
    flask_thread = threading.Thread(target=run_flask_app)
    flask_thread.start()

    # Generate secret key with bokeh secret command
    bokeh_secret_key = generate_secret_key()
    os.environ['BOKEH_SECRET_KEY'] = bokeh_secret_key
    os.environ['BOKEH_SIGN_SESSIONS'] = 'True'

    # Start the Bokeh server in the main process
    server = Server({'/bokeh': vola_tracker_app}, address=bokeh_app_address, port=bokeh_app_port, allow_websocket_origin=['*'], session_ids='external-signed')
    server.start()

    # Keep the server running
    print('Running bokeh server on: http://' + str(bokeh_app_address) + ':' + str(bokeh_app_port) + '/bokeh')
    server.io_loop.add_callback(server.show, "/")
    server.io_loop.start()

@Patrick it looks like the code block above is missing imports? I forgot to mention but I also need detailed instructions to run, in order to make sure I am reproducing your actual situation.

@Bryan: My apologies - i had not included the imports. Here you go:

import threading
from functools import wraps
from flask import Flask, request, Response, redirect
from bokeh.server.server import Server
from bokeh.plotting import figure
from bokeh.util.token import generate_session_id, generate_secret_key
from bokeh.models import ColumnDataSource, Slider, Div, TextAreaInput, CustomJS, PreText
from bokeh import layouts
import os
import logging
from bokeh.layouts import column
from random import random



def vola_tracker_app(doc):
    headline = Div(text="<h4>My headline</h4>")

    # Create the data source
    data = {'x': [], 'y': []}
    source = ColumnDataSource(data)

    # Create the figure
    fig = figure(title='Live Streaming Bokeh Plot')
    fig.line('x', 'y', source=source, line_width=2)

    # Function to update the plot data
    def update():
        new_data = {'x': [], 'y': []}

        # Generate new random data
        for i in range(10):
            new_data['x'].append(i)
            new_data['y'].append(random())

        source.data = new_data

    # Initialize regular callback to update the plot
    doc.add_periodic_callback(update, 2000)
    layout = column(headline, fig)
    doc.add_root(layout)





authentication_app = Flask(__name__)

def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not (auth.username == correct_username and auth.password == correct_password):
            response = Response(
                'Could not verify your access level for that URL.\n'
                'You have to login with proper credentials',
                401,
                {
                    'WWW-Authenticate': 'Basic realm="Login Required"',
                    'Cache-Control': 'no-store, no-cache, must-revalidate',
                    'Pragma': 'no-cache'
                }
            )
        else:
            response = f(*args, **kwargs)
        return response
    return decorated

@authentication_app.route('/')
@requires_auth
def redirect_to_bokeh():
    s_id = generate_session_id(secret_key='123', signed=True) #TODO: How to generate valid session ids for bokeh here? And how to force bokeh to only accepted signed ids?
    print('http://'+str(bokeh_app_address)+':'+str(bokeh_app_port)+'/bokeh/?bokeh-session-id={}'.format(s_id))
    return redirect('http://'+str(bokeh_app_address)+':'+str(bokeh_app_port)+'/bokeh/?bokeh-session-id={}'.format(s_id), code=302)

def run_flask_app():
    authentication_app.run(host=authentication_app_host, port=authentication_app_port)




if __name__ == "__main__":
    correct_username = 'a'
    correct_password = 'b'
    bokeh_app_address = 'localhost'  # Host for bokeh app; 'localhost' or RDP address of (Linux Server)
    bokeh_app_port = 8000   # Port for bokeh app
    authentication_app_host = 'localhost' # Host for authentication/flask app; 'localhost' or RDP address of (Linux Server)
    authentication_app_port = 8888 # Port for authentication/flask app

    logging.getLogger().setLevel(logging.DEBUG)

    # Start the Flask app in a separate thread
    flask_thread = threading.Thread(target=run_flask_app)
    flask_thread.start()

    # Generate secret key with bokeh secret command
    bokeh_secret_key = generate_secret_key()
    os.environ['BOKEH_SECRET_KEY'] = bokeh_secret_key
    os.environ['BOKEH_SIGN_SESSIONS'] = 'True'

    # Start the Bokeh server in the main process
    server = Server({'/bokeh': vola_tracker_app}, address=bokeh_app_address, port=bokeh_app_port, allow_websocket_origin=['*'], session_ids='external-signed')
    server.start()

    # Keep the server running
    print('Running bokeh server on: http://' + str(bokeh_app_address) + ':' + str(bokeh_app_port) + '/bokeh')
    server.io_loop.add_callback(server.show, "/")
    server.io_loop.start()

In order to run just execute the script. This will start a bokeh server on localhost 8000 and a flask server on localhost 8888. If you go on localhost:8888 you will get a flask login window. Type username “a” and password “b” and you will be directed to the bokeh webpage - which is fine. The problem ist that you can also directly go on the bokeh webpage on localhost:8000, thereby circumventing the authentication. This is not fine and i try to avoid that by only allowing signed session ids with bokeh that are generated upon successful login in the flask app. But whatever I try it seems that bokeh accepts everything and always starts a new session, regardless the setting (at least i could not figure it out after many hours of trying). Thank you for your help!

@Patrick The environment variables are really only for configuring the bokeh serve application. Since you are running the server programmatically via API, you need to also configure everything programmatically via API as well, i.e. you should pass sign_sessions=True, etc. to Server

@Bryan: But I did here:

Would you mind adding the correct setting - I really could not find more than what is shown above.

No, you didn’t. :slight_smile: That is a configuration for signed sessions when they are enabled. You need to actually enable signed sessions by passing sign_sessions=True (as well as the secret key).

@Bryan: ok great that is alrealy progress.
How do i pass that key? Do you have anywhere a code snipped for reference?

These are actually parameters to BokehTornado but unless you are creating the tornado application manually (very low level) then these args are forwarded from Server. From the Server docstring:

Any remaining keyword arguments will be passed as-is to BokehTornado .

@Bryan
Like that (stil i can access just by going to the browser on localhost:8000):

import threading
from functools import wraps
from flask import Flask, request, Response, redirect
from bokeh.server.server import Server
from bokeh.plotting import figure
from bokeh.util.token import generate_session_id, generate_secret_key
from bokeh.models import ColumnDataSource, Slider, Div, TextAreaInput, CustomJS, PreText
from bokeh import layouts
import os
import logging
from bokeh.layouts import column
from random import random



def vola_tracker_app(doc):
    headline = Div(text="<h4>My headline</h4>")

    # Create the data source
    data = {'x': [], 'y': []}
    source = ColumnDataSource(data)

    # Create the figure
    fig = figure(title='Live Streaming Bokeh Plot')
    fig.line('x', 'y', source=source, line_width=2)

    # Function to update the plot data
    def update():
        new_data = {'x': [], 'y': []}

        # Generate new random data
        for i in range(10):
            new_data['x'].append(i)
            new_data['y'].append(random())

        source.data = new_data

    # Initialize regular callback to update the plot
    doc.add_periodic_callback(update, 2000)
    layout = column(headline, fig)
    doc.add_root(layout)





authentication_app = Flask(__name__)

def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not (auth.username == correct_username and auth.password == correct_password):
            response = Response(
                'Could not verify your access level for that URL.\n'
                'You have to login with proper credentials',
                401,
                {
                    'WWW-Authenticate': 'Basic realm="Login Required"',
                    'Cache-Control': 'no-store, no-cache, must-revalidate',
                    'Pragma': 'no-cache'
                }
            )
        else:
            response = f(*args, **kwargs)
        return response
    return decorated

@authentication_app.route('/')
@requires_auth
def redirect_to_bokeh():
    s_id = generate_session_id(secret_key='123', signed=True) #TODO: How to generate valid session ids for bokeh here? And how to force bokeh to only accepted signed ids?
    print('http://'+str(bokeh_app_address)+':'+str(bokeh_app_port)+'/bokeh/?bokeh-session-id={}'.format(s_id))
    return redirect('http://'+str(bokeh_app_address)+':'+str(bokeh_app_port)+'/bokeh/?bokeh-session-id={}'.format(s_id), code=302)

def run_flask_app():
    authentication_app.run(host=authentication_app_host, port=authentication_app_port)




if __name__ == "__main__":
    correct_username = 'a'
    correct_password = 'b'
    bokeh_app_address = 'localhost'  # Host for bokeh app; 'localhost' or RDP address of (Linux Server)
    bokeh_app_port = 8000   # Port for bokeh app
    authentication_app_host = 'localhost' # Host for authentication/flask app; 'localhost' or RDP address of (Linux Server)
    authentication_app_port = 8888 # Port for authentication/flask app

    logging.getLogger().setLevel(logging.DEBUG)

    # Start the Flask app in a separate thread
    flask_thread = threading.Thread(target=run_flask_app)
    flask_thread.start()

    # Generate secret key with bokeh secret command
    bokeh_secret_key = generate_secret_key()
    print(bokeh_secret_key)
    #os.environ['BOKEH_SECRET_KEY'] = bokeh_secret_key
    #os.environ['BOKEH_SIGN_SESSIONS'] = 'True'

    # Start the Bokeh server in the main process
    server = Server({'/bokeh': vola_tracker_app}, address=bokeh_app_address, port=bokeh_app_port, allow_websocket_origin=['*'], session_ids='external-signed', sign_sessions=True, secret_key=bokeh_secret_key)
    server.start()

    # Keep the server running
    print('Running bokeh server on: http://' + str(bokeh_app_address) + ':' + str(bokeh_app_port) + '/bokeh')
    server.io_loop.add_callback(server.show, "/")
    server.io_loop.start()

Almost there, you also need to tell Bokeh not ot auto-generate valid session ids when none is provided:

 generate_session_ids=False

@Bryan: Cool - now i think it works:

import threading
from functools import wraps
from flask import Flask, request, Response, redirect
from bokeh.server.server import Server
from bokeh.plotting import figure
from bokeh.util.token import generate_session_id, generate_secret_key
from bokeh.models import ColumnDataSource, Slider, Div, TextAreaInput, CustomJS, PreText
from bokeh import layouts
import os
import logging
from bokeh.layouts import column
from random import random



def vola_tracker_app(doc):
    headline = Div(text="<h4>My headline</h4>")

    # Create the data source
    data = {'x': [], 'y': []}
    source = ColumnDataSource(data)

    # Create the figure
    fig = figure(title='Live Streaming Bokeh Plot')
    fig.line('x', 'y', source=source, line_width=2)

    # Function to update the plot data
    def update():
        new_data = {'x': [], 'y': []}

        # Generate new random data
        for i in range(10):
            new_data['x'].append(i)
            new_data['y'].append(random())

        source.data = new_data

    # Initialize regular callback to update the plot
    doc.add_periodic_callback(update, 2000)
    layout = column(headline, fig)
    doc.add_root(layout)





authentication_app = Flask(__name__)

def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not (auth.username == correct_username and auth.password == correct_password):
            response = Response(
                'Could not verify your access level for that URL.\n'
                'You have to login with proper credentials',
                401,
                {
                    'WWW-Authenticate': 'Basic realm="Login Required"',
                    'Cache-Control': 'no-store, no-cache, must-revalidate',
                    'Pragma': 'no-cache'
                }
            )
        else:
            response = f(*args, **kwargs)
        return response
    return decorated

@authentication_app.route('/')
@requires_auth
def redirect_to_bokeh():
    s_id = generate_session_id(secret_key=bokeh_secret_key , signed=True) #TODO: How to generate valid session ids for bokeh here? And how to force bokeh to only accepted signed ids?
    print('http://'+str(bokeh_app_address)+':'+str(bokeh_app_port)+'/bokeh/?bokeh-session-id={}'.format(s_id))
    return redirect('http://'+str(bokeh_app_address)+':'+str(bokeh_app_port)+'/bokeh/?bokeh-session-id={}'.format(s_id), code=302)

def run_flask_app():
    authentication_app.run(host=authentication_app_host, port=authentication_app_port)




if __name__ == "__main__":
    correct_username = 'a'
    correct_password = 'b'
    bokeh_app_address = 'localhost'  # Host for bokeh app; 'localhost' or RDP address of (Linux Server)
    bokeh_app_port = 8000   # Port for bokeh app
    authentication_app_host = 'localhost' # Host for authentication/flask app; 'localhost' or RDP address of (Linux Server)
    authentication_app_port = 8888 # Port for authentication/flask app

    logging.getLogger().setLevel(logging.DEBUG)

    # Start the Flask app in a separate thread
    flask_thread = threading.Thread(target=run_flask_app)
    flask_thread.start()

    # Generate secret key with bokeh secret command
    bokeh_secret_key = generate_secret_key()
    print(bokeh_secret_key)
    #os.environ['BOKEH_SECRET_KEY'] = bokeh_secret_key
    #os.environ['BOKEH_SIGN_SESSIONS'] = 'True'

    # Start the Bokeh server in the main process
    # Parameters from here: https://docs.bokeh.org/en/latest/docs/reference/server/tornado.html
    server = Server({'/bokeh': vola_tracker_app}, address=bokeh_app_address, port=bokeh_app_port, allow_websocket_origin=['*'], session_ids='external-signed', sign_sessions=True, secret_key=bokeh_secret_key, generate_session_ids=False)
    server.start()

    # Keep the server running
    print('Running bokeh server on: http://' + str(bokeh_app_address) + ':' + str(bokeh_app_port) + '/bokeh')
    server.io_loop.add_callback(server.show, "/")
    server.io_loop.start()

In the following lines: Are all settings correct?

server = Server({'/bokeh': vola_tracker_app}, address=bokeh_app_address, port=bokeh_app_port, allow_websocket_origin=['*'], session_ids='external-signed', sign_sessions=True, secret_key=bokeh_secret_key, generate_session_ids=False)

s_id = generate_session_id(secret_key=bokeh_secret_key , signed=True)

Especially the session_ids=‘external-signed’ and the signed=True ?

@Bryan: In any case - thank you very much for solving this!

They look reasonable, offhand. I have to be up-front and say that this is a very little-used feature. There’s lots of tests from when it was initially introduced, but in the years since, almost no-one has ever actually asked about, so I don’t have any especially deep insight or experience to share about it.

@Bryan: Indeed, it seems that there is little experience around with this.
I do not really understand why. Because what are the other options to have a live sever that is expsoed to the public internet? You don’t want everybody to be able to access it obviously.
So the only other option I see is to use a reverse proxy like ngrok. I tried that and it has its problems as well. For example it seems that not all dynamic ressources are forwarded with the proper link through the proxy and then you can only access the site on the localhost, etc…
So I really would be curious to know, how others do that…

@Patrick I expect most usage is local usage either in notebooks or running and interacting with the pages in local browsers. Then among the subset of people who are deploying Bokeh server apps for an external audience, in fact it seems most of those case are involving a public-facing app that can be accessible by anyone. The vast majority of questions (that I can recall) regarded deployments for public sites on the open internet. So then of the even smaller fraction that need “private” access of some sort:

  • some are inside air-gapped or internal networks, so there’s nothing to do
  • many probably already have Nginx or Apache, etc set up as a reverse proxy, so just use that
  • for folks who aren’t embedding a Bokeh app inside another page, the more recent auth hooks are probably a better solution to restrict access, if you want actual authentication anyway

So this case is a specialized subset of a subset of a subset of users…

Edit: auth hooks can probably useful for the embedded case too, but I don’t have any actual code to point to. I’m not sure anyone has tried that specific combination. @Philipp_Rudiger can comment in case anything like that is used in Panel.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.