class BaseOAuthConsumerBlueprint(six.with_metaclass(ABCMeta, flask.Blueprint)): def __init__( self, name, import_name, static_folder=None, static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, root_path=None, login_url=None, authorized_url=None, storage=None, ): bp_kwargs = dict( name=name, import_name=import_name, static_folder=static_folder, static_url_path=static_url_path, template_folder=template_folder, url_prefix=url_prefix, subdomain=subdomain, url_defaults=url_defaults, root_path=root_path, ) # `root_path` didn't exist in 0.10, and will cause an error if it's # passed in that version. Only pass `root_path` if it's set. if bp_kwargs["root_path"] is None: del bp_kwargs["root_path"] flask.Blueprint.__init__(self, **bp_kwargs) login_url = login_url or "/{bp.name}" authorized_url = authorized_url or "/{bp.name}/authorized" self.add_url_rule( rule=login_url.format(bp=self), endpoint="login", view_func=self.login ) self.add_url_rule( rule=authorized_url.format(bp=self), endpoint="authorized", view_func=self.authorized, ) if storage is None: self.storage = SessionStorage() elif callable(storage): self.storage = storage() else: self.storage = storage self.logged_in_funcs = [] self.from_config = {} invalidate_token = lambda d: invalidate_cached_property(self.session, "token") self.config = CallbackDict(on_update=invalidate_token) self.before_app_request(self.load_config) def load_config(self): """ Used to dynamically load variables from the Flask application config into the blueprint. To tell this blueprint to pull configuration from the app, just set key-value pairs in the ``from_config`` dict. Keys are the name of the local variable to set on the blueprint object, and values are the variable name in the Flask application config. For example: blueprint.from_config["session.client_id"] = "GITHUB_OAUTH_CLIENT_ID" """ for local_var, config_var in self.from_config.items(): value = flask.current_app.config.get(config_var) if value: if "." in local_var: # this is a dotpath -- needs special handling body, tail = local_var.rsplit(".", 1) obj = getattrd(self, body) setattr(obj, tail, value) else: # just use a normal setattr call setattr(self, local_var, value) @property def token(self): _token = self.storage.get(self) if _token and _token.get("expires_in") and _token.get("expires_at"): # Update the `expires_in` value, so that requests-oauthlib # can handle automatic token refreshing. Assume that # `expires_at` is a valid Unix timestamp. expires_at = datetime.utcfromtimestamp(_token["expires_at"]) expires_in = expires_at - datetime.utcnow() _token["expires_in"] = expires_in.total_seconds() return _token @token.setter def token(self, value): _token = value if _token and _token.get("expires_in"): # Set the `expires_at` value, overwriting any value # that may already be there. delta = timedelta(seconds=_token["expires_in"]) expires_at = datetime.utcnow() + delta _token["expires_at"] = timestamp_from_datetime(expires_at) self.storage.set(self, _token) invalidate_cached_property(self.session, "token") @token.deleter def token(self): self.storage.delete(self) invalidate_cached_property(self.session, "token") @abstractproperty def session(self): """ This is a session between the consumer (your website) and the provider (e.g. Twitter). It is *not* a session between a user of your website and your website. """ raise NotImplementedError() @abstractmethod def login(self): raise NotImplementedError() @abstractmethod def authorized(self): """ This is the route/function that the user will be redirected to by the provider (e.g. Twitter) after the user has logged into the provider's website and authorized your app to access their account. """ raise NotImplementedError()
class BaseOAuthConsumerBlueprint(flask.Blueprint, metaclass=ABCMeta): def __init__( self, name, import_name, *, static_folder=None, static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, root_path=None, login_url=None, authorized_url=None, storage=None, rule_kwargs=None, ): bp_kwargs = dict( name=name, import_name=import_name, static_folder=static_folder, static_url_path=static_url_path, template_folder=template_folder, url_prefix=url_prefix, subdomain=subdomain, url_defaults=url_defaults, root_path=root_path, ) flask.Blueprint.__init__(self, **bp_kwargs) login_url = login_url or "/{bp.name}" authorized_url = authorized_url or "/{bp.name}/authorized" rule_kwargs = rule_kwargs or {} self.add_url_rule( rule=login_url.format(bp=self), endpoint="login", view_func=self.login, **rule_kwargs, ) self.add_url_rule( rule=authorized_url.format(bp=self), endpoint="authorized", view_func=self.authorized, **rule_kwargs, ) if storage is None: self.storage = SessionStorage() elif callable(storage): self.storage = storage() else: self.storage = storage self.logged_in_funcs = [] self.from_config = {} def invalidate_token(d): try: invalidate_cached_property(self.session, "token") except KeyError: pass self.config = CallbackDict(on_update=invalidate_token) self.before_app_request(self.load_config) def load_config(self): """ Used to dynamically load variables from the Flask application config into the blueprint. To tell this blueprint to pull configuration from the app, just set key-value pairs in the ``from_config`` dict. Keys are the name of the local variable to set on the blueprint object, and values are the variable name in the Flask application config. For example: blueprint.from_config["session.client_id"] = "GITHUB_OAUTH_CLIENT_ID" """ for local_var, config_var in self.from_config.items(): value = flask.current_app.config.get(config_var) if value: if "." in local_var: # this is a dotpath -- needs special handling body, tail = local_var.rsplit(".", 1) obj = getattrd(self, body) setattr(obj, tail, value) else: # just use a normal setattr call setattr(self, local_var, value) @property def storage(self): """ The :doc:`token storage <storages>` that this blueprint uses. """ return self._storage @storage.setter def storage(self, value): self._storage = value @storage.deleter def storage(self): del self._storage @property def token(self): """ This property functions as pass-through to the token storage. If you read from this property, you will receive the current value from the token storage. If you assign a value to this property, it will get set in the token storage. """ _token = self.storage.get(self) if _token and _token.get("expires_in") and _token.get("expires_at"): # Update the `expires_in` value, so that requests-oauthlib # can handle automatic token refreshing. Assume that # `expires_at` is a valid Unix timestamp. expires_at = datetime.utcfromtimestamp(_token["expires_at"]) expires_in = expires_at - datetime.utcnow() _token["expires_in"] = expires_in.total_seconds() return _token @token.setter def token(self, value): _token = value if _token and _token.get("expires_in"): # Set the `expires_at` value, overwriting any value # that may already be there. delta = timedelta(seconds=_token["expires_in"]) expires_at = datetime.utcnow() + delta _token["expires_at"] = timestamp_from_datetime(expires_at) self.storage.set(self, _token) try: invalidate_cached_property(self.session, "token") except KeyError: pass @token.deleter def token(self): self.storage.delete(self) try: invalidate_cached_property(self.session, "token") except KeyError: pass @abstractproperty def session(self): """ This is a session between the consumer (your website) and the provider (e.g. Twitter). It is *not* a session between a user of your website and your website. """ raise NotImplementedError() @abstractmethod def login(self): raise NotImplementedError() @abstractmethod def authorized(self): """ This is the route/function that the user will be redirected to by the provider (e.g. Twitter) after the user has logged into the provider's website and authorized your app to access their account. """ raise NotImplementedError()
class MastodonConsumerBlueprint(OAuth2ConsumerBlueprint): def __init__(self, *args, client_name, instance_host_backend=None, instance_credentials_backend=None, session_class=None, **kwargs): if session_class is None: session_class = MastodonSession OAuth2ConsumerBlueprint.__init__(self, *args, session_class=session_class, **kwargs) self.client_name = client_name if instance_host_backend is None: self.instance_host_backend = SessionStorage( key="{bp.name}_instance_host") elif callable(instance_host_backend): self.instance_host_backend = instance_host_backend() else: self.instance_host_backend = instance_host_backend if instance_credentials_backend is None: self.instance_credentials_backend = InMemoryBackend() elif callable(instance_credentials_backend): self.instance_credentials_backend = instance_credentials_backend() else: self.instance_credentials_backend = instance_credentials_backend def _get_instance_credentials(self, host): url = "https://{instance}/api/v1/apps".format(instance=host) try: response = requests.post( url, data=dict( client_name=self.client_name, redirect_uris=url_for(".authorized", _external=True), scopes=self.scope, ), ).json() if "error" in response: return None return dict(client_id=response["client_id"], client_secret=response["client_secret"]) except Exception: return None @property def credentials(self): if not self.instance_host: return {} cached = self.instance_credentials_backend.get(self) if cached: return cached retrieved = self._get_instance_credentials(self.instance_host) if not retrieved: return {} self.instance_credentials_backend.set(self, retrieved) return retrieved @property def instance_host(self): return self.instance_host_backend.get(self) @instance_host.setter def instance_host(self, value): self.instance_host_backend.set(self, value) instance_config = self.credentials self.client_id = instance_config.get("client_id") invalidate_cached_property(self, "session") @instance_host.deleter def instance_host(self): self.instance_host_backend.delete(self) invalidate_cached_property(self, "session") @property def client_secret(self): return self.credentials.get("client_secret") @client_secret.setter def client_secret(self, value): pass @property def authorization_url(self): instance = self.instance_host return "https://{instance}/oauth/authorize".format(instance=instance) @authorization_url.setter def authorization_url(self, value): pass @property def token_url(self): instance = self.instance_host return "https://{instance}/oauth/token".format(instance=instance) @token_url.setter def token_url(self, value): pass @cached_property def session(self): """ This is a session between the consumer (your website) and the provider (e.g. Twitter). It is *not* a session between a user of your website and your website. :return: """ instance = self.instance_host ret = self.session_class( client_id=self.credentials.get("client_id"), client=self.client, auto_refresh_url=self.auto_refresh_url, auto_refresh_kwargs=self.auto_refresh_kwargs, scope=self.scope, state=self.state, blueprint=self, base_url="https://{instance}".format(instance=instance), **self.kwargs) def token_updater(token): self.token = token ret.token_updater = token_updater return self.session_created(ret) def login(self): if "instance_host" in request.args: self.instance_host = request.args["instance_host"] return super().login()