diff --git a/dash_auth/oauth.py b/dash_auth/oauth.py index b980bbd..2e102f9 100644 --- a/dash_auth/oauth.py +++ b/dash_auth/oauth.py @@ -1,9 +1,9 @@ from __future__ import absolute_import -import datetime import flask -from flask_seasurf import SeaSurf import json import os +from textwrap import dedent +import itsdangerous from .auth import Auth @@ -14,7 +14,13 @@ class OAuthBase(Auth): # Name of the cookie containing the OAuth2 access token TOKEN_COOKIE_NAME = 'oauth_token' - def __init__(self, app, app_url, client_id=None): + def __init__( + self, + app, + app_url, + client_id=None, + secret_key=None, + salt=None): Auth.__init__(self, app) self.config = { @@ -24,7 +30,31 @@ def __init__(self, app, app_url, client_id=None): self._app = app self._app_url = app_url self._oauth_client_id = client_id - self._access_codes = self.create_access_codes() + + if secret_key is None and app.server.secret_key is None: + raise Exception(dedent(''' + app.server.secret_key is missing. + Generate a secret key in your Python session + with the following commands: + + >>> import os + >>> import base64 + >>> base64.b64encode(os.urandom(30)).decode('utf-8') + + and assign it to the property app.server.secret_key + (where app is your dash app instance). + Note that you should not do this dynamically: + you should create a key and then assign the value of + that key in your code. + ''')) + + if salt is None: + raise Exception(dedent(''' + salt is missing. The salt parameter needs to a string that + is unique to this individual Dash app. + ''')) + + self._signer = itsdangerous.TimestampSigner(secret_key, salt=salt) app.server.add_url_rule( '{}_dash-login'.format(app.config['routes_pathname_prefix']), @@ -52,36 +82,33 @@ def __init__(self, app, app_url, client_id=None): with open(os.path.join(_current_path, 'login.js'), 'r') as f: self.login_bundle = f.read() - def create_access_codes(self): - token = SeaSurf()._generate_token() - new_access_codes = { - 'access_granted': token, - 'expiration': ( - datetime.datetime.now() + datetime.timedelta( - seconds=self.config['permissions_cache_expiry'] - ) + def access_token_is_valid(self): + if self.AUTH_COOKIE_NAME not in flask.request.cookies: + return False + + access_token = flask.request.cookies[self.AUTH_COOKIE_NAME] + + try: + self._signer.unsign( + access_token, + max_age=self.config['permissions_cache_expiry'] ) - } - self._access_codes = new_access_codes - return self._access_codes + return True + except itsdangerous.SignatureExpired: + # Check access in case the user is valid but the token has expired + return False + except itsdangerous.BadSignature: + # Access tokens in previous versions of `dash-auth` + # weren't generated with itsdangerous + # and will raise `BadSignature` + return False def is_authorized(self): if self.TOKEN_COOKIE_NAME not in flask.request.cookies: return False oauth_token = flask.request.cookies[self.TOKEN_COOKIE_NAME] - - if (datetime.datetime.now() > self._access_codes['expiration']): - self.create_access_codes() - - if self.AUTH_COOKIE_NAME not in flask.request.cookies: - return self.check_view_access(oauth_token) - - access_cookie = flask.request.cookies[self.AUTH_COOKIE_NAME] - - # If there access was previously declined, - # check access again in case it has changed - if access_cookie != self._access_codes['access_granted']: + if not self.access_token_is_valid(): return self.check_view_access(oauth_token) return True @@ -118,12 +145,16 @@ def wrap(*args, **kwargs): # Python 3 if isinstance(response, str): response = flask.Response(response) - self.set_cookie( - response, - name=self.AUTH_COOKIE_NAME, - value=self._access_codes['access_granted'], - max_age=(60 * 60 * 24 * 7), # 1 week - ) + + # grant a new access token if expired, missing, or invalid + if not self.access_token_is_valid(): + access_token = self._signer.sign('access') + self.set_cookie( + response, + name=self.AUTH_COOKIE_NAME, + value=access_token, + max_age=(60 * 60 * 24 * 7), # 1 week + ) return response return wrap diff --git a/dash_auth/plotly_auth.py b/dash_auth/plotly_auth.py index 0be7627..80a4073 100644 --- a/dash_auth/plotly_auth.py +++ b/dash_auth/plotly_auth.py @@ -29,7 +29,12 @@ def __init__(self, app, app_name, sharing, app_url): Returns: None """ - super(PlotlyAuth, self).__init__(app, app_url) + super(PlotlyAuth, self).__init__( + app, + app_url, + secret_key=api_requests.credential('plotly_api_key'), + salt=app_name + ) self._fid = create_or_overwrite_dash_app( app_name, sharing, app_url diff --git a/setup.py b/setup.py index 24509b1..5a4f676 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,8 @@ 'flask-seasurf', 'plotly', 'dash>=0.18.3', - 'retrying' + 'retrying', + 'itsdangerous' ], include_package_data=True, url='https://plot.ly/dash', diff --git a/usage_plotly_auth.py b/usage_plotly_auth.py index 1791080..0c72ad8 100644 --- a/usage_plotly_auth.py +++ b/usage_plotly_auth.py @@ -18,6 +18,7 @@ 'private', 'http://localhost:8050' ) +server = app.server app.layout = html.Div([