mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-18 18:38:11 +08:00
oauth support, add the token in a configuration specified cookie (#1702)
* oauth support, add the token in a configuration specified cookie Previously, the id token was stored in the session token. Now, it can be placed in a different cookie with different properties.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from flask import session, request, redirect, current_app, has_request_context
|
||||
from flask import session, request, redirect, current_app, after_this_request, has_request_context, g
|
||||
from server.auth.auth import AuthTypeClientBase, AuthTypeFactory
|
||||
from server.common.errors import AuthenticationError, ConfigurationError
|
||||
from urllib.parse import urlencode
|
||||
@@ -24,15 +24,20 @@ class AuthTypeOAuth(AuthTypeClientBase):
|
||||
|
||||
CXG_ID_TOKEN = "id_token"
|
||||
|
||||
def __init__(self, app_config):
|
||||
def __init__(self, server_config):
|
||||
super().__init__()
|
||||
if missingimport:
|
||||
raise ConfigurationError(f"oauth requires these modules: {', '.join(missingimport)}")
|
||||
self.algorithms = ["RS256"]
|
||||
self.api_base_url = app_config.authentication__params_oauth__api_base_url
|
||||
self.client_id = app_config.authentication__params_oauth__client_id
|
||||
self.client_secret = app_config.authentication__params_oauth__client_secret
|
||||
self.callback_base_url = app_config.authentication__params_oauth__callback_base_url
|
||||
self.api_base_url = server_config.authentication__params_oauth__api_base_url
|
||||
self.client_id = server_config.authentication__params_oauth__client_id
|
||||
self.client_secret = server_config.authentication__params_oauth__client_secret
|
||||
self.callback_base_url = server_config.authentication__params_oauth__callback_base_url
|
||||
self.session_cookie = server_config.authentication__params_oauth__session_cookie
|
||||
self.cookie_params = server_config.authentication__params_oauth__cookie
|
||||
self._validate_cookie_params()
|
||||
|
||||
# set the audience
|
||||
self.audience = self.client_id
|
||||
|
||||
# load the jwks (JSON Web Key Set).
|
||||
@@ -45,6 +50,21 @@ class AuthTypeOAuth(AuthTypeClientBase):
|
||||
except Exception:
|
||||
raise ConfigurationError(f"error in oauth, api_url_base: {self.api_base_url}, cannot access {jwksloc}")
|
||||
|
||||
def _validate_cookie_params(self):
|
||||
"""check the cookie_params, and raise a ConfigurationError if there is something wrong"""
|
||||
if self.session_cookie:
|
||||
return
|
||||
|
||||
if not isinstance(self.cookie_params, dict):
|
||||
raise ConfigurationError("either session_cookie or cookie must be set")
|
||||
valid_keys = {"key", "max_age", "expires", "path", "domain", "secure", "httponly", "samesite"}
|
||||
keys = set(self.cookie_params.keys())
|
||||
unknown = keys - valid_keys
|
||||
if unknown:
|
||||
raise ConfigurationError(f"unexpected key in cookie params: {', '.join(unknown)}")
|
||||
if "key" not in keys:
|
||||
raise ConfigurationError("must have a key (name) in the cookie params")
|
||||
|
||||
def is_valid_authentication_type(self):
|
||||
return True
|
||||
|
||||
@@ -111,8 +131,15 @@ class AuthTypeOAuth(AuthTypeClientBase):
|
||||
return self.client.authorize_redirect(redirect_uri=callbackurl)
|
||||
|
||||
def logout(self):
|
||||
if self.CXG_ID_TOKEN in session:
|
||||
del session[self.CXG_ID_TOKEN]
|
||||
if self.session_cookie:
|
||||
if self.CXG_ID_TOKEN in session:
|
||||
del session[self.CXG_ID_TOKEN]
|
||||
else:
|
||||
@after_this_request
|
||||
def remove_cookie(response):
|
||||
response.set_cookie(self.cookie_params["key"], "", expires=0)
|
||||
return response
|
||||
|
||||
return_path = request.args.get("dataset", "")
|
||||
return_to = f"{self.callback_base_url}/{return_path}"
|
||||
params = {'returnTo' : return_to, 'client_id' : self.client_id}
|
||||
@@ -121,10 +148,23 @@ class AuthTypeOAuth(AuthTypeClientBase):
|
||||
def callback(self):
|
||||
token = self.client.authorize_access_token()
|
||||
id_token = token.get("id_token")
|
||||
session[self.CXG_ID_TOKEN] = id_token
|
||||
del session["oauth_callback_redirect"]
|
||||
oauth_callback_redirect = session.get("oauth_callback_redirect", "/")
|
||||
oauth_callback_redirect = session.pop("oauth_callback_redirect", "/")
|
||||
resp = redirect(oauth_callback_redirect)
|
||||
|
||||
if self.session_cookie:
|
||||
session[self.CXG_ID_TOKEN] = id_token
|
||||
else:
|
||||
args = self.cookie_params.copy()
|
||||
del args["key"]
|
||||
try:
|
||||
resp.set_cookie(
|
||||
self.cookie_params["key"],
|
||||
id_token,
|
||||
**args)
|
||||
g.token = id_token
|
||||
except Exception as e:
|
||||
raise AuthenticationError(f"unable to set_cookie {self.cookie_params}") from e
|
||||
|
||||
return resp
|
||||
|
||||
def get_login_url(self, data_adaptor):
|
||||
@@ -143,7 +183,14 @@ class AuthTypeOAuth(AuthTypeClientBase):
|
||||
|
||||
def get_token(self):
|
||||
"""Function to return the token"""
|
||||
return session.get(self.CXG_ID_TOKEN)
|
||||
if "token" in g:
|
||||
return g.token
|
||||
if self.session_cookie:
|
||||
g.token = session.get(self.CXG_ID_TOKEN)
|
||||
else:
|
||||
g.token = request.cookies.get(self.cookie_params["key"])
|
||||
|
||||
return g.token
|
||||
|
||||
def get_jwt_payload(self):
|
||||
if not has_request_context():
|
||||
|
||||
@@ -393,6 +393,7 @@ class ServerConfig(BaseConfig):
|
||||
def __init__(self, app_config, default_config):
|
||||
dictval_cases = [
|
||||
("app", "csp_directives"),
|
||||
("authentication", "params_oauth", "cookie"),
|
||||
("adaptor", "cxg_adaptor", "tiledb_ctx"),
|
||||
("multi_dataset", "dataroot"),
|
||||
]
|
||||
@@ -417,6 +418,8 @@ class ServerConfig(BaseConfig):
|
||||
self.authentication__params_oauth__client_secret = dc["authentication"]["params_oauth"]["client_secret"]
|
||||
self.authentication__params_oauth__callback_base_url = \
|
||||
dc["authentication"]["params_oauth"]["callback_base_url"]
|
||||
self.authentication__params_oauth__session_cookie = dc["authentication"]["params_oauth"]["session_cookie"]
|
||||
self.authentication__params_oauth__cookie = dc["authentication"]["params_oauth"]["cookie"]
|
||||
|
||||
self.multi_dataset__dataroot = dc["multi_dataset"]["dataroot"]
|
||||
self.multi_dataset__index = dc["multi_dataset"]["index"]
|
||||
@@ -531,6 +534,12 @@ class ServerConfig(BaseConfig):
|
||||
self.check_attr("authentication__params_oauth__client_id", ptypes)
|
||||
self.check_attr("authentication__params_oauth__client_secret", ptypes)
|
||||
self.check_attr("authentication__params_oauth__callback_base_url", (type(None), str))
|
||||
self.check_attr("authentication__params_oauth__session_cookie", bool)
|
||||
|
||||
if self.authentication__params_oauth__session_cookie:
|
||||
self.check_attr("authentication__params_oauth__cookie", (type(None), dict))
|
||||
else:
|
||||
self.check_attr("authentication__params_oauth__cookie", dict)
|
||||
# secret key: first, from CXG_OAUTH_CLIENT_SECRET environment variable
|
||||
# second, from config file
|
||||
self.authentication__params__oauth__client_secret = os.environ.get(
|
||||
|
||||
@@ -33,6 +33,15 @@ server:
|
||||
# A value of None, indicates the client and server are on the localhost. http://localhost:<port> will be used.
|
||||
callback_base_url: null
|
||||
|
||||
# if true, the jwt containing the id_token is stored in a session cookie
|
||||
session_cookie: true
|
||||
|
||||
# if session_cookie is false, then a regular cookie will be used. In that case
|
||||
# the cookie will be defined by a dictionary of parameters.
|
||||
# The keys of the dictionary match the parameters of the flask set_cookie api
|
||||
# (https://flask.palletsprojects.com/en/1.1.x/api/), and with the same meaning.
|
||||
# legal keys: key, max_age, expires, path, domain, secure, httponly, and samesite.
|
||||
cookie: null
|
||||
|
||||
multi_dataset:
|
||||
# If dataroot is set, then cellxgene may serve multiple datasets. This parameter is not
|
||||
|
||||
Reference in New Issue
Block a user