Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make refresh_token available in Auth #4227

Merged
merged 2 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/user_guide/Authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ Once a user is authorized with the chosen OAuth provider certain user informatio

* **`pn.state.user`**: A unique name, email or ID that identifies the user.
* **`pn.state.access_token`**: The access token issued by the OAuth provider to authorize requests to its APIs.
* **`pn.state.refresh_token`**: The refresh token issued by the OAuth provider to authorize requests to its APIs (if available these are usually longer lived than the `access_token`).
* **`pn.state.user_info`**: Additional user information provided by the OAuth provider. This may include names, email, APIs to request further user information, IDs and more.

## Authorization
Expand Down
3 changes: 3 additions & 0 deletions doc/user_guide/Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ The `pn.config` object allows setting various configuration variables, the confi
> - `throttled`: Whether sliders and inputs should be throttled until release of mouse.

#### Python and Environment variables

> - `comms` (`PANEL_COMMS`): Whether to render output in Jupyter with the default Jupyter extension or use the `jupyter_bokeh` ipywidget model.
> - `console_output` (`PANEL_CONSOLE_OUTPUT`): How to log errors and stdout output triggered by callbacks from Javascript in the notebook. Options include `'accumulate'`, `'replace'` and `'disable'`.
> - `embed` (`PANEL_EMBED`): Whether plot data will be [embedded](./Deploy_and_Export.rst#Embedding).
Expand All @@ -175,6 +176,7 @@ The `pn.config` object allows setting various configuration variables, the confi

The `pn.state` object makes various global state available and provides methods to manage that state:

- - `access_token`: The access token issued by the OAuth provider to authorize requests to its APIs.
> - `busy`: A boolean value to indicate whether a callback is being actively processed.
> - `cache`: A global cache which can be used to share data between different processes.
> - `cookies`: HTTP request cookies for the current session.
Expand All @@ -189,6 +191,7 @@ The `pn.state` object makes various global state available and provides methods
> * `protocol` (readonly): protocol in window.location e.g. 'http:' or 'https:'
> * `port` (readonly): port in window.location e.g. '80'
> - `headers`: HTTP request headers for the current session.
> - `refresh_token`: The refresh token issued by the OAuth provider to authorize requests to its APIs (if available these are usually longer lived than the `access_token`).
> - `session_args`: When running a server session this return the request arguments.
> - `session_info`: A dictionary tracking information about server sessions:
> * `total` (int): The total number of sessions that have been opened
Expand Down
18 changes: 13 additions & 5 deletions panel/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ async def _fetch_access_token(self, code, redirect_uri, client_id, client_secret
return

log.debug("%s received user information.", type(self).__name__)
return self._on_auth(user, body['access_token'])
return self._on_auth(user, body['access_token'], body.get('refresh_token'))

def get_state_cookie(self):
"""Get OAuth state from cookies
Expand Down Expand Up @@ -323,16 +323,20 @@ async def get(self):
self.set_state_cookie(state)
await self.get_authenticated_user(**params)

def _on_auth(self, user_info, access_token):
def _on_auth(self, user_info, access_token, refresh_token=None):
user_key = config.oauth_jwt_user or self._USER_KEY
user = user_info[user_key]
self.set_secure_cookie('user', user, expires_days=config.oauth_expiry)
id_token = base64url_encode(json.dumps(user_info))
if state.encryption:
access_token = state.encryption.encrypt(access_token.encode('utf-8'))
id_token = state.encryption.encrypt(id_token.encode('utf-8'))
if refresh_token:
refresh_token = state.encryption.encrypt(refresh_token.encode('utf-8'))
self.set_secure_cookie('access_token', access_token, expires_days=config.oauth_expiry)
self.set_secure_cookie('id_token', id_token, expires_days=config.oauth_expiry)
if refresh_token:
self.set_secure_cookie('refresh_token', refresh_token, expires_days=config.oauth_expiry)
return user

def _on_error(self, response, body=None):
Expand All @@ -349,7 +353,7 @@ def _on_error(self, response, body=None):
log.warning(f"{provider} OAuth provider failed to fully "
f"authenticate returning the following response:"
f"{body}.")
raise HTTPError(500, f"{provider} authentication failed")
raise HTTPError(500, f"{provider} authentication failed {body}")


class GenericLoginHandler(OAuthLoginHandler, OAuth2Mixin):
Expand Down Expand Up @@ -628,9 +632,9 @@ async def _fetch_access_token(self, code, redirect_uri, client_id, client_secret

access_token = body['access_token']
id_token = body['id_token']
return self._on_auth(id_token, access_token)
return self._on_auth(id_token, access_token, body.get('refresh_token'))

def _on_auth(self, id_token, access_token):
def _on_auth(self, id_token, access_token, refresh_token=None):
decoded = decode_id_token(id_token)
user_key = config.oauth_jwt_user or self._USER_KEY
if user_key in decoded:
Expand All @@ -643,8 +647,12 @@ def _on_auth(self, id_token, access_token):
if state.encryption:
access_token = state.encryption.encrypt(access_token.encode('utf-8'))
id_token = state.encryption.encrypt(id_token.encode('utf-8'))
if refresh_token:
refresh_token = state.encryption.encrypt(refresh_token.encode('utf-8'))
self.set_secure_cookie('access_token', access_token, expires_days=config.oauth_expiry)
self.set_secure_cookie('id_token', id_token, expires_days=config.oauth_expiry)
if refresh_token:
self.set_secure_cookie('refresh_token', refresh_token, expires_days=config.oauth_expiry)
return user


Expand Down
21 changes: 14 additions & 7 deletions panel/io/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -821,18 +821,25 @@ def sync_busy(self, indicator: BooleanIndicator) -> None:
# Public Properties
#----------------------------------------------------------------

@property
def access_token(self) -> str | None:
def _decode_cookie(self, cookie_name):
from tornado.web import decode_signed_value

from ..config import config
access_token = self.cookies.get('access_token')
if access_token is None:
cookie = self.cookies.get(cookie_name)
if cookie is None:
return None
access_token = decode_signed_value(config.cookie_secret, 'access_token', access_token)
cookie = decode_signed_value(config.cookie_secret, cookie_name, cookie)
if self.encryption is None:
return access_token.decode('utf-8')
return self.encryption.decrypt(access_token).decode('utf-8')
return cookie.decode('utf-8')
return self.encryption.decrypt(cookie).decode('utf-8')

@property
def access_token(self) -> str | None:
return self._decode_cookie('access_token')

@property
def refresh_token(self) -> str | None:
return self._decode_cookie('refresh_token')

@property
def app_url(self) -> str | None:
Expand Down