mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-21 16:28:11 +08:00
separate backend base url from frontend (#1819)
* separate backend base url from frontend This is needed for auth, and to support a different location for the backend api server, than the frontend. part of chanzuckerberg/cellxgene#1778 new server config parameters: app__api_base_url, app__web_base_url Also changed api_base_url in the oauth config section to "oauth_api_base_url" to be less confusing with the app's api_base_url Other minor changes: changed how the jwt decode options are handled. Previously they needed to be set in a test case, and there was some extra logic to handle that. Now they are handled through comfig parameters, which makes it more general. Also, add a feature to set the CORS support credentials, which seems to be necessary for the backend/frontend separation, at least when run locally. This part is sort of experimental, and may be removed or changed later.
This commit is contained in:
+46
-32
@@ -1,7 +1,7 @@
|
||||
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
|
||||
from urllib.parse import urlencode, urlparse
|
||||
import json
|
||||
import requests
|
||||
import base64
|
||||
@@ -45,13 +45,20 @@ class AuthTypeOAuth(AuthTypeClientBase):
|
||||
if missingimport:
|
||||
raise ConfigurationError(f"oauth requires these modules: {', '.join(missingimport)}")
|
||||
self.algorithms = ["RS256"]
|
||||
self.api_base_url = server_config.authentication__params_oauth__api_base_url
|
||||
self.oauth_api_base_url = server_config.authentication__params_oauth__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.jwt_decode_options = server_config.authentication__params_oauth__jwt_decode_options
|
||||
|
||||
self._validate_cookie_params()
|
||||
self._validate_jwt_decode_options()
|
||||
|
||||
self.api_base_url = server_config.get_api_base_url()
|
||||
self.web_base_url = server_config.get_web_base_url()
|
||||
if self.api_base_url is None:
|
||||
raise ConfigurationError("oauth requires the app__api_base_url to be set")
|
||||
|
||||
# set the audience
|
||||
self.audience = self.client_id
|
||||
@@ -60,11 +67,13 @@ class AuthTypeOAuth(AuthTypeClientBase):
|
||||
# The JSON Web Key Set (JWKS) is a set of keys which contains the public keys used to verify
|
||||
# any JSON Web Token (JWT) issued by the authorization server and signed using the RS256
|
||||
try:
|
||||
jwksloc = f"{self.api_base_url}/.well-known/jwks.json"
|
||||
jwksloc = f"{self.oauth_api_base_url}/.well-known/jwks.json"
|
||||
jwksurl = requests.get(jwksloc)
|
||||
self.jwks = jwksurl.json()
|
||||
except Exception:
|
||||
raise ConfigurationError(f"error in oauth, api_url_base: {self.api_base_url}, cannot access {jwksloc}")
|
||||
raise ConfigurationError(
|
||||
f"error in oauth, api_url_base: {self.oauth_api_base_url}, cannot access {jwksloc}"
|
||||
)
|
||||
|
||||
def _validate_cookie_params(self):
|
||||
"""check the cookie_params, and raise a ConfigurationError if there is something wrong"""
|
||||
@@ -81,6 +90,20 @@ class AuthTypeOAuth(AuthTypeClientBase):
|
||||
if "key" not in keys:
|
||||
raise ConfigurationError("must have a key (name) in the cookie params")
|
||||
|
||||
def _validate_jwt_decode_options(self):
|
||||
"""check the jwt_decode_options, and raise a ConfigurationError if there is something wrong"""
|
||||
if self.jwt_decode_options is None:
|
||||
self.jwt_decode_options = {}
|
||||
return
|
||||
|
||||
valid_keys = {
|
||||
"verify_signature", "verify_aud", "verify_iat", "verify_exp", "verify_nbf", "verify_iss",
|
||||
"verify_sub", "verify_jti", "verify_at_hash", "leeway"}
|
||||
keys = set(self.jwt_decode_options.keys())
|
||||
unknown = keys - valid_keys
|
||||
if unknown:
|
||||
raise ConfigurationError(f"unexpected key in jwt_decode_options: {', '.join(unknown)}")
|
||||
|
||||
def is_valid_authentication_type(self):
|
||||
return True
|
||||
|
||||
@@ -88,27 +111,22 @@ class AuthTypeOAuth(AuthTypeClientBase):
|
||||
return True
|
||||
|
||||
def add_url_rules(self, app):
|
||||
app.add_url_rule("/login", "login", self.login, methods=["GET"])
|
||||
app.add_url_rule("/logout", "logout", self.logout, methods=["GET"])
|
||||
app.add_url_rule("/oauth2/callback", "callback", self.callback, methods=["GET"])
|
||||
parse = urlparse(self.api_base_url)
|
||||
app.add_url_rule(f"{parse.path}/login", "login", self.login, methods=["GET"])
|
||||
app.add_url_rule(f"{parse.path}/logout", "logout", self.logout, methods=["GET"])
|
||||
app.add_url_rule(f"{parse.path}/oauth2/callback", "callback", self.callback, methods=["GET"])
|
||||
|
||||
def complete_setup(self, flask_app):
|
||||
self.oauth = OAuth(flask_app)
|
||||
if self.callback_base_url is None:
|
||||
# In this case, assume the server is running on the same host as the client,
|
||||
# and the oauth provider has been configured
|
||||
# with a callback that understands a localhost callback (e.g. A http://localhost:5005).
|
||||
server_config = flask_app.app_config.server_config
|
||||
self.callback_base_url = f"http://{server_config.app__host}:{server_config.app__port}"
|
||||
|
||||
self.client = self.oauth.register(
|
||||
"auth0",
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
api_base_url=self.api_base_url,
|
||||
refresh_token_url=f"{self.api_base_url}/oauth/token",
|
||||
access_token_url=f"{self.api_base_url}/oauth/token",
|
||||
authorize_url=f"{self.api_base_url}/authorize",
|
||||
api_base_url=self.oauth_api_base_url,
|
||||
refresh_token_url=f"{self.oauth_api_base_url}/oauth/token",
|
||||
access_token_url=f"{self.oauth_api_base_url}/oauth/token",
|
||||
authorize_url=f"{self.oauth_api_base_url}/authorize",
|
||||
client_kwargs={"scope": "openid profile email offline_access"},
|
||||
)
|
||||
|
||||
@@ -138,9 +156,9 @@ class AuthTypeOAuth(AuthTypeClientBase):
|
||||
response.cache_control.update(dict(public=True, max_age=0, no_store=True, no_cache=True, must_revalidate=True))
|
||||
|
||||
def login(self):
|
||||
callbackurl = f"{self.callback_base_url}/oauth2/callback"
|
||||
callbackurl = f"{self.api_base_url}/oauth2/callback"
|
||||
return_path = request.args.get("dataset", "")
|
||||
return_to = f"{self.callback_base_url}/{return_path}"
|
||||
return_to = f"{self.web_base_url}/{return_path}/"
|
||||
# save the return path in the session cookie, accessed in the callback function
|
||||
session["oauth_callback_redirect"] = return_to
|
||||
response = self.client.authorize_redirect(redirect_uri=callbackurl)
|
||||
@@ -149,7 +167,7 @@ class AuthTypeOAuth(AuthTypeClientBase):
|
||||
|
||||
def logout(self):
|
||||
self.remove_tokens()
|
||||
params = {"returnTo": self.callback_base_url, "client_id": self.client_id}
|
||||
params = {"returnTo": self.web_base_url, "client_id": self.client_id}
|
||||
response = redirect(self.client.api_base_url + "/v2/logout?" + urlencode(params))
|
||||
self.update_response(response)
|
||||
return response
|
||||
@@ -228,14 +246,14 @@ class AuthTypeOAuth(AuthTypeClientBase):
|
||||
|
||||
def get_login_url(self, data_adaptor):
|
||||
"""Return the url for the login route"""
|
||||
if current_app.app_config.is_multi_dataset():
|
||||
return f"/login?dataset={data_adaptor.uri_path}/"
|
||||
if data_adaptor and current_app.app_config.is_multi_dataset():
|
||||
return f"{self.api_base_url}/login?dataset={data_adaptor.uri_path}/"
|
||||
else:
|
||||
return "/login"
|
||||
return f"{self.api_base_url}/login"
|
||||
|
||||
def get_logout_url(self, data_adaptor):
|
||||
"""Return the url for the logout route"""
|
||||
return "/logout"
|
||||
return f"{self.api_base_url}/logout"
|
||||
|
||||
def check_jwt_payload(self, id_token):
|
||||
try:
|
||||
@@ -254,18 +272,14 @@ class AuthTypeOAuth(AuthTypeClientBase):
|
||||
"e": key.get("e"),
|
||||
}
|
||||
if rsa_key:
|
||||
options = {}
|
||||
if not rsa_key["n"] or not rsa_key["e"]:
|
||||
# this is a mock auth server, do not validate
|
||||
options = {"verify_signature": False, "verify_iss": False}
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
id_token,
|
||||
rsa_key,
|
||||
algorithms=self.algorithms,
|
||||
audience=self.audience,
|
||||
issuer=self.api_base_url + "/",
|
||||
options=options,
|
||||
issuer=self.oauth_api_base_url + "/",
|
||||
options=self.jwt_decode_options,
|
||||
)
|
||||
return payload
|
||||
|
||||
@@ -321,7 +335,7 @@ class AuthTypeOAuth(AuthTypeClientBase):
|
||||
"client_secret": self.client_secret,
|
||||
}
|
||||
headers = {"content-type": "application/x-www-form-urlencoded"}
|
||||
request = requests.post(f"{self.api_base_url}/oauth/token", urlencode(params), headers=headers)
|
||||
request = requests.post(f"{self.oauth_api_base_url}/oauth/token", urlencode(params), headers=headers)
|
||||
if request.status_code != 200:
|
||||
# unable to refresh the token, log the user out
|
||||
self.remove_tokens()
|
||||
|
||||
Reference in New Issue
Block a user