mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-05 13:18:13 +08:00
Refactor czi_hosted and server into backend directory, pull common code into backend/common, refactor tests (#2102)
* move local_server -> backend/server server-> backend/czi_hosted, pull common code into backend/common update imports, tests and make commands
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# import the built in auth types so they can be registered
|
||||
|
||||
import backend.czi_hosted.auth.auth_test # noqa: F401
|
||||
import backend.czi_hosted.auth.auth_session # noqa: F401
|
||||
import backend.czi_hosted.auth.auth_oauth # noqa: F401
|
||||
import backend.czi_hosted.auth.auth_none # noqa: F401
|
||||
@@ -0,0 +1,91 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class AuthTypeBase(ABC):
|
||||
"""Base type for all authentication types."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
@abstractmethod
|
||||
def is_valid_authentication_type(self):
|
||||
"""Return True if the auth type is valid, e.g. it can return userinfo and username.
|
||||
(AuthTypeNone is the only one type that returns False)"""
|
||||
pass
|
||||
|
||||
def requires_client_login(self):
|
||||
"""Return True if the user needs to login from the client (e.g. Login button is shown)"""
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def complete_setup(self, app):
|
||||
"""complete any setup that may be needed by this auth type. The Flask app is passed in.
|
||||
This is the last auth function called before the server starts to run."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_user_authenticated(self):
|
||||
"""Return True if the user is authenticated"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_user_id(self):
|
||||
"""Return the id for this user (string)"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_user_name(self):
|
||||
"""Return the name of the user (string)"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_user_email(self):
|
||||
"""Return the name of the user (string)"""
|
||||
pass
|
||||
|
||||
def get_user_picture(self):
|
||||
"""Return the location to the user's picture"""
|
||||
return None
|
||||
|
||||
|
||||
class AuthTypeClientBase(AuthTypeBase):
|
||||
"""Base type for all authentication types that require the client to login"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def requires_client_login(self):
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def add_url_rules(self, selfapp):
|
||||
"""Add url rules to the app (like /login, /logout, etc)"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_login_url(self, data_adaptor):
|
||||
"""Return the url for the login route"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_logout_url(self, data_adaptor):
|
||||
"""Return the url for the logout route"""
|
||||
pass
|
||||
|
||||
|
||||
class AuthTypeFactory:
|
||||
"""Factory class to create an authentication type"""
|
||||
|
||||
auth_types = {}
|
||||
|
||||
@staticmethod
|
||||
def register(name, auth_type):
|
||||
assert issubclass(auth_type, AuthTypeBase)
|
||||
AuthTypeFactory.auth_types[name] = auth_type
|
||||
|
||||
@staticmethod
|
||||
def create(name, app_config):
|
||||
auth_type = AuthTypeFactory.auth_types.get(name)
|
||||
if auth_type is None:
|
||||
return None
|
||||
return auth_type(app_config)
|
||||
@@ -0,0 +1,27 @@
|
||||
from backend.czi_hosted.auth.auth import AuthTypeBase, AuthTypeFactory
|
||||
|
||||
|
||||
class AuthTypeNone(AuthTypeBase):
|
||||
def __init__(self, app_config):
|
||||
super().__init__()
|
||||
|
||||
def is_valid_authentication_type(self):
|
||||
return False
|
||||
|
||||
def complete_setup(self, app):
|
||||
pass
|
||||
|
||||
def is_user_authenticated(self):
|
||||
return True
|
||||
|
||||
def get_user_id(self):
|
||||
return None
|
||||
|
||||
def get_user_name(self):
|
||||
return None
|
||||
|
||||
def get_user_email(self):
|
||||
return None
|
||||
|
||||
|
||||
AuthTypeFactory.register(None, AuthTypeNone)
|
||||
@@ -0,0 +1,385 @@
|
||||
from flask import session, request, redirect, current_app, after_this_request, has_request_context, g
|
||||
from backend.czi_hosted.auth.auth import AuthTypeClientBase, AuthTypeFactory
|
||||
from backend.common.errors import AuthenticationError, ConfigurationError
|
||||
from urllib.parse import urlencode, urlparse
|
||||
import json
|
||||
import requests
|
||||
import base64
|
||||
|
||||
# It is not required to have authlib or jose.
|
||||
# However, it is a configuration error to use this auth type if they are not installed.
|
||||
missingimport = []
|
||||
try:
|
||||
from authlib.integrations.flask_client import OAuth
|
||||
except ModuleNotFoundError:
|
||||
missingimport.append("authlib")
|
||||
|
||||
try:
|
||||
from jose import jwt
|
||||
from jose.exceptions import ExpiredSignatureError, JWTError, JWTClaimsError
|
||||
except ModuleNotFoundError:
|
||||
missingimport.append("jose")
|
||||
|
||||
|
||||
class Tokens:
|
||||
"""Simple class to represent the tokens that are saved/restored from the cookie"""
|
||||
|
||||
def __init__(self, access_token, id_token, refresh_token, expires_at, **kwargs):
|
||||
self.access_token = access_token
|
||||
self.id_token = id_token
|
||||
self.refresh_token = refresh_token
|
||||
self.expires_at = expires_at
|
||||
|
||||
# expires_at may be None after a token refresh, and so it is not checked here
|
||||
if not (access_token and id_token and refresh_token):
|
||||
raise KeyError(str(self.__dict__))
|
||||
|
||||
|
||||
class AuthTypeOAuth(AuthTypeClientBase):
|
||||
"""An authentication type for oauth2 logins."""
|
||||
|
||||
CXG_TOKENS = "auth_tokens"
|
||||
|
||||
def __init__(self, server_config):
|
||||
super().__init__()
|
||||
if missingimport:
|
||||
raise ConfigurationError(f"oauth requires these modules: {', '.join(missingimport)}")
|
||||
self.algorithms = ["RS256"]
|
||||
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.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
|
||||
|
||||
# load the jwks (JSON Web Key Set).
|
||||
# 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.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.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"""
|
||||
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 _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
|
||||
|
||||
def requires_client_login(self):
|
||||
return True
|
||||
|
||||
def add_url_rules(self, app):
|
||||
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}/logout_redirect", "logout_redirect", self.logout_redirect, 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)
|
||||
|
||||
self.client = self.oauth.register(
|
||||
"auth0",
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
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"},
|
||||
)
|
||||
|
||||
def is_user_authenticated(self):
|
||||
payload = self.get_userinfo()
|
||||
return payload is not None
|
||||
|
||||
def get_user_id(self):
|
||||
payload = self.get_userinfo()
|
||||
return payload.get("sub") if payload else None
|
||||
|
||||
def get_user_name(self):
|
||||
payload = self.get_userinfo()
|
||||
return payload.get("name") if payload else None
|
||||
|
||||
def get_user_email(self):
|
||||
payload = self.get_userinfo()
|
||||
return payload.get("email") if payload else None
|
||||
|
||||
def get_user_picture(self):
|
||||
payload = self.get_userinfo()
|
||||
return payload.get("picture") if payload else None
|
||||
|
||||
def update_response(self, response):
|
||||
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.api_base_url}/oauth2/callback"
|
||||
return_path = request.args.get("dataset", "")
|
||||
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)
|
||||
self.update_response(response)
|
||||
return response
|
||||
|
||||
def logout(self):
|
||||
"""
|
||||
We would like for the user to remain on the same dataset after logout. oauth requires that
|
||||
the redirect `returnTo` path be whitelisted by the oauth server, therefore a level of
|
||||
indirection is used. We first redirect to a single path "logout_redirect", and logout_redirect
|
||||
will redirect the user's browser back to the current page.
|
||||
"""
|
||||
self.remove_tokens()
|
||||
redirect_path = request.args.get("dataset", "")
|
||||
redirect_to = f"{self.web_base_url}/{redirect_path}"
|
||||
session["oauth_logout_redirect"] = redirect_to
|
||||
|
||||
return_to = f"{self.api_base_url}/logout_redirect"
|
||||
params = {"returnTo": return_to, "client_id": self.client_id}
|
||||
response = redirect(self.client.api_base_url + "/v2/logout?" + urlencode(params))
|
||||
self.update_response(response)
|
||||
return response
|
||||
|
||||
def logout_redirect(self):
|
||||
oauth_logout_redirect = session.pop("oauth_logout_redirect", "/")
|
||||
response = redirect(oauth_logout_redirect)
|
||||
self.update_response(response)
|
||||
return response
|
||||
|
||||
def callback(self):
|
||||
data = self.client.authorize_access_token()
|
||||
tokens = Tokens(
|
||||
access_token=data.get("access_token"),
|
||||
id_token=data.get("id_token"),
|
||||
refresh_token=data.get("refresh_token"),
|
||||
expires_at=data.get("expires_at"),
|
||||
)
|
||||
self.save_tokens(tokens)
|
||||
oauth_callback_redirect = session.pop("oauth_callback_redirect", "/")
|
||||
response = redirect(oauth_callback_redirect)
|
||||
self.update_response(response)
|
||||
return response
|
||||
|
||||
def get_tokens(self):
|
||||
"""Extract the tokens from the cookie, and store them in the flask global context"""
|
||||
if "tokens" in g:
|
||||
return g.tokens
|
||||
|
||||
try:
|
||||
if self.session_cookie:
|
||||
value = session.get(self.CXG_TOKENS)
|
||||
if value:
|
||||
g.tokens = Tokens(**value)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
value = request.cookies.get(self.cookie_params["key"])
|
||||
if value is None:
|
||||
return None
|
||||
value = base64.b64decode(value)
|
||||
value = json.loads(value)
|
||||
g.tokens = Tokens(**value)
|
||||
|
||||
except Exception:
|
||||
# there are many types of exceptions that can be raise in the above section.
|
||||
# It is impractical to list all the exceptions here, since that would be brittle.
|
||||
# If an exception occurs, then return None, meaning that no token could be retrieved.
|
||||
current_app.logger.warning(f"auth cookie is in the wrong format: {str(value)}")
|
||||
g.pop("tokens", None)
|
||||
return None
|
||||
|
||||
return g.tokens
|
||||
|
||||
def save_tokens(self, tokens):
|
||||
g.tokens = tokens
|
||||
if self.session_cookie:
|
||||
session[self.CXG_TOKENS] = tokens.__dict__
|
||||
else:
|
||||
|
||||
@after_this_request
|
||||
def set_cookie(response):
|
||||
args = self.cookie_params.copy()
|
||||
value = base64.b64encode(json.dumps(tokens.__dict__).encode("utf-8"))
|
||||
del args["key"]
|
||||
try:
|
||||
response.set_cookie(self.cookie_params["key"], value, **args)
|
||||
except Exception as e:
|
||||
raise AuthenticationError(f"unable to set_cookie {self.cookie_params}") from e
|
||||
return response
|
||||
|
||||
def remove_tokens(self):
|
||||
g.pop("tokens", None)
|
||||
if self.session_cookie:
|
||||
if self.CXG_TOKENS in session:
|
||||
del session[self.CXG_TOKENS]
|
||||
else:
|
||||
|
||||
@after_this_request
|
||||
def remove_cookie(response):
|
||||
response.set_cookie(self.cookie_params["key"], "", expires=0)
|
||||
self.update_response(response)
|
||||
return response
|
||||
|
||||
def get_login_url(self, data_adaptor):
|
||||
"""Return the url for the login route"""
|
||||
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 f"{self.api_base_url}/login"
|
||||
|
||||
def get_logout_url(self, data_adaptor):
|
||||
"""Return the url for the logout route"""
|
||||
if data_adaptor and current_app.app_config.is_multi_dataset():
|
||||
return f"{self.api_base_url}/logout?dataset={data_adaptor.uri_path}/"
|
||||
else:
|
||||
return f"{self.api_base_url}/logout"
|
||||
|
||||
def check_jwt_payload(self, id_token):
|
||||
try:
|
||||
unverified_header = jwt.get_unverified_header(id_token)
|
||||
except JWTError:
|
||||
return None
|
||||
|
||||
rsa_key = {}
|
||||
for key in self.jwks["keys"]:
|
||||
if key["kid"] == unverified_header["kid"]:
|
||||
rsa_key = {
|
||||
"kty": key["kty"],
|
||||
"kid": key["kid"],
|
||||
"use": key["use"],
|
||||
"n": key.get("n"),
|
||||
"e": key.get("e"),
|
||||
}
|
||||
if rsa_key:
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
id_token,
|
||||
rsa_key,
|
||||
algorithms=self.algorithms,
|
||||
audience=self.audience,
|
||||
issuer=self.oauth_api_base_url + "/",
|
||||
options=self.jwt_decode_options,
|
||||
)
|
||||
return payload
|
||||
|
||||
except ExpiredSignatureError:
|
||||
# This exception is handled in get_userinfo
|
||||
raise
|
||||
except JWTClaimsError as e:
|
||||
raise AuthenticationError(f"invalid claims {str(e)}") from e
|
||||
except JWTError as e:
|
||||
raise AuthenticationError(f"invalid signature: {str(e)}") from e
|
||||
|
||||
raise AuthenticationError("Unable to find the appropriate key")
|
||||
|
||||
def get_userinfo(self):
|
||||
if not has_request_context():
|
||||
return None
|
||||
|
||||
# check if the userinfo has been retrieved already in this request
|
||||
if "userinfo" in g:
|
||||
return g.get("userinfo")
|
||||
|
||||
# if there is no id_token, return None (user is not authenticated)
|
||||
tokens = self.get_tokens()
|
||||
|
||||
if tokens is None or tokens.id_token is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
# check the jwt payload. This raises an AuthenticationError if the token is not valid.
|
||||
# It the token has expired, we attempt to refresh the token
|
||||
g.userinfo = self.check_jwt_payload(tokens.id_token)
|
||||
return g.userinfo
|
||||
|
||||
except ExpiredSignatureError:
|
||||
tokens = self.refresh_expired_token(tokens.refresh_token)
|
||||
if tokens is None or tokens.id_token is None:
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
g.userinfo = self.check_jwt_payload(tokens.id_token)
|
||||
return g.userinfo
|
||||
except JWTError as e:
|
||||
raise AuthenticationError(f"error during token refresh: {str(e)}") from e
|
||||
|
||||
except AuthenticationError:
|
||||
self.remove_tokens()
|
||||
raise
|
||||
|
||||
def refresh_expired_token(self, refresh_token):
|
||||
params = {
|
||||
"grant_type": "refresh_token",
|
||||
"client_id": self.client_id,
|
||||
"refresh_token": refresh_token,
|
||||
"client_secret": self.client_secret,
|
||||
}
|
||||
headers = {"content-type": "application/x-www-form-urlencoded"}
|
||||
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()
|
||||
return None
|
||||
data = request.json()
|
||||
tokens = Tokens(
|
||||
access_token=data.get("access_token"),
|
||||
id_token=data.get("id_token"),
|
||||
refresh_token=data.get("refresh_token", refresh_token),
|
||||
expires_at=data.get("expires_at"),
|
||||
)
|
||||
self.save_tokens(tokens)
|
||||
return tokens
|
||||
|
||||
|
||||
AuthTypeFactory.register("oauth", AuthTypeOAuth)
|
||||
@@ -0,0 +1,40 @@
|
||||
from flask import session
|
||||
from uuid import uuid4
|
||||
|
||||
from backend.czi_hosted.auth.auth import AuthTypeBase, AuthTypeFactory
|
||||
|
||||
|
||||
class AuthTypeSession(AuthTypeBase):
|
||||
"""Session based authentication. The user is always logged. The user id is a random number
|
||||
associated with the session. This is a good choice for desktop servers."""
|
||||
|
||||
# key in the session token for userid
|
||||
CXGUID = "cxguid"
|
||||
|
||||
def __init__(self, app_config):
|
||||
super().__init__()
|
||||
|
||||
def is_valid_authentication_type(self):
|
||||
return True
|
||||
|
||||
def complete_setup(self, app):
|
||||
pass
|
||||
|
||||
def is_user_authenticated(self):
|
||||
# always authenticated
|
||||
return True
|
||||
|
||||
def get_user_id(self):
|
||||
if self.CXGUID not in session:
|
||||
session[self.CXGUID] = uuid4().hex
|
||||
session.permanent = True
|
||||
return session[self.CXGUID]
|
||||
|
||||
def get_user_name(self):
|
||||
return "anonymous"
|
||||
|
||||
def get_user_email(self):
|
||||
return None
|
||||
|
||||
|
||||
AuthTypeFactory.register("session", AuthTypeSession)
|
||||
@@ -0,0 +1,80 @@
|
||||
from flask import session, request, redirect, current_app
|
||||
|
||||
from backend.czi_hosted.auth.auth import AuthTypeClientBase, AuthTypeFactory
|
||||
|
||||
|
||||
class AuthTypeTest(AuthTypeClientBase):
|
||||
"""An authentication type for testing client based logins. When the login route is accessed
|
||||
the user is automatically logged in with a default or configured username"""
|
||||
|
||||
# key in session token with userid and username
|
||||
CXGUID = "cxguid_test"
|
||||
CXGUNAME = "cxguname_test"
|
||||
CXGUEMAIL = "cxguemail_test"
|
||||
CXGUPICTURE = "cxgupicture_test"
|
||||
|
||||
def __init__(self, app_config):
|
||||
super().__init__()
|
||||
self.user_name = "test_account"
|
||||
self.user_id = "id0001"
|
||||
self.user_email = "test_account@test.com"
|
||||
self.user_picture = None
|
||||
|
||||
def is_valid_authentication_type(self):
|
||||
return True
|
||||
|
||||
def requires_client_login(self):
|
||||
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"])
|
||||
|
||||
def complete_setup(self, app):
|
||||
pass
|
||||
|
||||
def is_user_authenticated(self):
|
||||
return self.CXGUID in session
|
||||
|
||||
def get_user_id(self):
|
||||
return session.get(self.CXGUID)
|
||||
|
||||
def get_user_name(self):
|
||||
return session.get(self.CXGUNAME)
|
||||
|
||||
def get_user_email(self):
|
||||
return session.get(self.CXGUEMAIL)
|
||||
|
||||
def get_user_picture(self):
|
||||
return session.get(self.CXGUPICTURE)
|
||||
|
||||
def login(self):
|
||||
args = request.args
|
||||
return_to = args.get("dataset", "/")
|
||||
session[self.CXGUID] = args.get("userid", self.user_id)
|
||||
session[self.CXGUNAME] = args.get("username", self.user_name)
|
||||
session[self.CXGUEMAIL] = args.get("email", self.user_email)
|
||||
session[self.CXGUPICTURE] = args.get("picture", self.user_picture)
|
||||
return redirect(return_to)
|
||||
|
||||
def logout(self):
|
||||
session.clear()
|
||||
return_to = request.args.get("dataset", "/")
|
||||
return redirect(return_to)
|
||||
|
||||
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}"
|
||||
else:
|
||||
return "/login"
|
||||
|
||||
def get_logout_url(self, data_adaptor):
|
||||
"""Return the url for the logout route"""
|
||||
if current_app.app_config.is_multi_dataset():
|
||||
return f"/logout?dataset={data_adaptor.uri_path}"
|
||||
else:
|
||||
return "/logout"
|
||||
|
||||
|
||||
AuthTypeFactory.register("test", AuthTypeTest)
|
||||
Reference in New Issue
Block a user