Skip to content

Instantly share code, notes, and snippets.

@bpostlethwaite
Forked from T4rk1n/dash_username_auth.md
Last active July 13, 2018 16:57
Show Gist options
  • Save bpostlethwaite/d28bb9428ba74f65706c5430a62dc7ec to your computer and use it in GitHub Desktop.
Save bpostlethwaite/d28bb9428ba74f65706c5430a62dc7ec to your computer and use it in GitHub Desktop.

Additional auth cookies:

  • dash_user
    • signed with itsdangerous.
    • the username appears in clear text in the cookie as user.TOKEN
  • dash_user_data
    • json web signature with itsdangerous.
    • The json web signature is not entirely safe, do not add sensitive data.

The users cookies have no expiry, they are validated by the python package itsdangerous.

New methods on Auth objects:

These methods must be called from a request context (a callback).

  • get_username
    • Get the username from the signed cookie.
  • set_username
    • PlotlyAuth calls this from the auth response to get the plotly username.
  • get_user_data
    • get the json metadata for the user.
    • Example: user_data = auth.get_user_data()
  • set_user_data
    • set custom json metadata for the user.
    • Example: auth.set_user_data({"last_login": time.time()})

is_authorized_hook

Use as a decorator to add a callback when is_authorized is called. Takes a single argument which is the response from the auth service response. is_authorized is called only when a user logs in. It must return a boolean to indicate if the user is_authorized. Can have multiple hooks.

other

  • Added more options to Oauth.create_cookie
    • httponly - only access the cookie from the server (default=True)
    • SameSite - prevent the browser from sending the cookie to other site (default='Strict')

Example

import dash
import dash_auth
import dash_html_components as html
from dash.dependencies import Output, Input

import requests

app = dash.Dash()
auth = dash_auth.PlotlyAuth(
    app, 'my_app', 'private',
    'http://localhost:8050')


app.layout = html.Div([
    html.Div(id='content'),
    html.Button('Need perms', id='btn'),
    html.Div(id='authorized')],
    id='container')


@app.callback(Output('content', 'children'), [Input('content', 'id')])
def _give_name(_):
    username = auth.get_username()
    return username


@auth.is_authorized_hook
def _is_authorized(data):
    active = data.get('is_active')
    if active:
        auth.set_user_data(data.get('ldap_dn'))
    return active

@app.callback(Output('authorized', 'children'), [Input('btn', 'n_clicks')])
def _check_perms(n_clicks):
    if n_clicks:
        perms = auth.get_user_data()
        perm_click_button = perms.get('click_button')
        if not perm_click_button:
            return 'unauthorized'
        else:
            return 'authorized'


if __name__ == '__main__':
    app.run_server(debug=True)
@thomhickey
Copy link

wonderful ... look forward to seeing the PR

@thomhickey
Copy link

Hey all ... hmm, caveats, caveats. So for this to really work for us, we'd need to get access to more than just the username. The reason is that the ldap groups to which a user belongs are returned by our identity provider upon successful login (if configured to do so, which we can do for Plotly/Dash). In our content portal database we don't attempt to mirror these groups as our portal is not the source of truth for these groups and their membership, but our authorization services within the portal know to check the user's session for ldap group info and extend permissions accordingly. So as you can see we won't have the same visibility to the ldap groups when we authorize one of these back-channel calls from the Dash app. When the SAML response comes back to Dash from the IDP there will be a 'group_list' attribute in the response ... any chance that can be returned as part of the user's json metadata? Or perhaps will it already without any changes other than to have our IDP configured to return this info to Dash?

@thomhickey
Copy link

thomhickey commented Jul 5, 2018

can't seem to get this to work, the app is getting this in the docker logs, just trying to spin up haven't even hit it yet:

send: b'GET /v2/files/lookup?path=dash-template HTTP/1.1\r\nHost: 10.39.94.88\r\nUser-Agent: python-requests/2.19.1\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\nplotly-client-platform: dash-auth\r\ncontent-type: application/json\r\nAuthorization: Basic OnlvdXItcGxvdGx5LWFwaS1rZXk=\r\n\r\n' reply: 'HTTP/1.1 401 Unauthorized\r\n'

That seems to be the first error encountered, although after that a bunch of bad stuff happens. Happy to include the entire log if necessary :)

I followed this gist closely:

import dash
import dash_auth
import dash_html_components as html
import requests
import config

# from auth import auth


app = dash.Dash(
    __name__,
    # Serve any files that are available in the `static` folder
    static_folder='static'
)
# auth(app)
auth = dash_auth.PlotlyAuth(
    app, config.DASH_APP_NAME, 'public',
    'https://pa-dashboard.gene.com')
server = app.server  # Expose the server variable for deployments

app.layout = html.Div("dash app template for PA")


@auth.is_authorized_hook
def _is_authorized():
    res = requests.post(
        config.PORTAL_API_ENDPOINT, json={'username': auth.get_username(), 'appname': config.DASH_APP_NAME},
        headers={'Authorization': 'Bearer {}'.format(config.PORTAL_API_KEY)},
    )
    return res.status_code == 200


if __name__ == '__main__':
    app.run_server(debug=True)

Any ideas what could be wrong?

@thomhickey
Copy link

Figured it out - api key is required for these even though they are public apps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment