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)
@bpostlethwaite
Copy link
Author

Here is an example application using the new Authorization feature. The only new piece is the @auth.is_authorized_hook. This wrapped function will return a Boolean value that instructs the Dash Middleware to accept or deny the request.

It is up to the Dash App Author to define this function so you can do whatever you need to do and connect to any authorization services necessary to make this work. This function could be placed in a library and imported as necessary into Dash Apps.

As a convenience we have provided a set_user_data method on the auth instance. This can be called in the authorized_hook to send data to subsequent authorized callbacks. In the subsequent requests/callbacks you can use auth.get_user_data to access this data.

This current implementation uses the Plotly username which will be the same as the LDAP username.

@sudburyrob
Copy link

Can't comment on the code, but I would say fire this off unless Chelsea has objections. Ben, you can email this to: Thom . . . his email is: [email protected]

@thomhickey
Copy link

Hello all ... thanks so much for the quick turnaround on this. Really appreciated. Couple of comments/questions, but this looks great so far:

  • love that you left the is_authorized_hook wide open as far as implementation, this will work quite well for us
  • how often will the hook be called? I'm hoping once per session?
  • what happens when the hook returns false? e.g. what's the UX
  • set/get user data might really come in handy if we want to provide more granular authorization in the future - love this
  • I'm going to hide this as much as I can from the everyday data scientist app dev workflows. meaning I don't want them to have to paste any boilerplate into their app.py so I'm going to write an auth.py for us to use here that will register this callback hook and authorize the app with the only requirement from dash app devs being that they import and call auth(app) near the top of their app.py. all I will need is the username and app name. username you've supplied here, app name I'll pull from config. given this workflow do you see any problems when it comes to app lifecycle? my concern is that if I implement this way we won't get called on every authentication from every new user to the dash app.

Thanks again, I'm starting to breathe easy :)

@bpostlethwaite
Copy link
Author

bpostlethwaite commented Jun 28, 2018

Glad you like the API! We're pretty happy with the flexibility as well.

how often will the hook be called? I'm hoping once per session?

When the token signature expires, every five minutes.

what happens when the hook returns false? e.g. what's the UX

The same as authorization fail, the response will be 403, unauthorized.

'm going to hide this as much as I can from the everyday data scientist...

Yep this will work so long as:
A) The hook is registered before app.run_server
B) get_user_data and get_username must be called from a request context

We'll add some useful error messages for when these are not satisfied but if you abstract some of this away into a package and there is a convention for calling it early in the right place it shouldn't be much of a concern.

I'll send you a link to the Pull Request once we are satisfied with tests and stability. At that point you could test it out early or wait for official release.

@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