from collections.abc import Callable
from typing import Any, Concatenate, cast
from urllib.parse import urlsplit
from requests_oauthlib import OAuth2Session
# note: some of the MWOAuth2 docstrings are partially copied in the other classes,
# try to keep them in sync šļø
[docs]
class MWOAuth2[**P]:
"""Base class managing OAuth 2 authentication against a MediaWiki wiki.
A single instance of this class can be used for multiple users,
and is typically set up at tool initialization time.
It contains the details of the client wiki,
the OAuth credentials,
and accessors for some kind of session store where the OAuth state is kept.
To start the authentication flow for a user,
call :meth:`authorization_url` and send the user to the returned URL.
Once they return, call :meth:`fetch_token` with the request URL.
Afterwards, you can use :meth:`oauth2_session` to make authenticated requests.
To check if the user is logged in or not,
use :meth:`has_access_token`.
:param str host: The host of the wiki, e.g. ``https://en.wikipedia.org``.
Should include the protocol (scheme) and host name, but no path
(use ``api_path`` if necessary).
:param str client_id: The OAuth client ID, a string of 32 hexadecimal characters.
:param str client_secret: The OAuth client secret, a string of 40 hexadecimal characters.
This should come from some kind of secret configuration source,
e.g. a config file or environment variables.
:param Callable[P, dict[str, Any] | None] get_oauth_state: This,
along with the next five parameters, represents the interface to the session store.
This should be backed by some kind of user session,
usually provided by your web framework,
and is used to store the OAuth state (during login) and access token (subsequently).
The type parameter ``P`` specifies the parameters that are necessary to access the session store;
these parameters are passed through from the other functions (:meth:`fetch_token` etc.)
to the session store accessors.
Most of the time, this will be either a single āsessionā object
(e.g. the Django :attr:`request.session <django.http.HttpRequest.session>` or the FastAPI dependency-injected ``session``)
or no parameters at all (e.g. the :data:`flask.session` proxy, see :class:`MWOAuth2Flask`).
This function should return the OAuth state if it has been set, otherwise ``None``.
:param Callable[Concatenate[dict[str, Any], P], None] set_oauth_state: Set the OAuth state to the given value.
:param Callable[P, dict[str, Any]] pop_oauth_state: Return and remove the OAuth state,
or raise a :class:`KeyError` if it has not been set.
:param Callable[P, dict[str, Any] | None] get_access_token: Get the access token if it has been set,
otherwise ``None``.
:param Callable[Concatenate[dict[str, Any], P], None] set_access_token: Set the access token to the given value.
:param Callable[P, dict[str, Any]] pop_access_token: Return and remove the access token,
or raise a :class:`KeyError` if it has not been set.
This is the only one of the six functions that is useful for a tool to call directly:
calling this will log the user out.
:param str user_agent: The User-Agent string that should be sent with all requests,
in accordance with the `User-Agent Policy <https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Wikimedia_Foundation_User-Agent_Policy>`__.
Toolforge tools can get a suitable string from :func:`toolforge.set_user_agent`.
:param str api_path: The path to the ``api.php`` endpoint,
relative to the ``host`` (see above).
The default of ``/w/api.php`` is suitable for all Wikimedia wikis and many other wikis as well.
"""
def __init__(
self,
host: str,
client_id: str,
client_secret: str,
get_oauth_state: Callable[P, dict[str, Any] | None],
set_oauth_state: Callable[Concatenate[dict[str, Any], P], None],
pop_oauth_state: Callable[P, dict[str, Any]],
get_access_token: Callable[P, dict[str, Any] | None],
set_access_token: Callable[Concatenate[dict[str, Any], P], None],
pop_access_token: Callable[P, dict[str, Any]],
user_agent: str,
api_path: str = '/w/api.php',
):
self.host = host
self.client_id = client_id
self.client_secret = client_secret
self.get_oauth_state = get_oauth_state
self.set_oauth_state = set_oauth_state
self.pop_oauth_state = pop_oauth_state
self.get_access_token = get_access_token
self.set_access_token = set_access_token
self.pop_access_token = pop_access_token
self.user_agent = user_agent
self.api_path = api_path
def _oauth2session(self, **kwargs: Any) -> OAuth2Session:
session = OAuth2Session(self.client_id, **kwargs)
session.headers['User-Agent'] = self.user_agent
return session
def _rest_url(self) -> str:
api_url = self.host + self.api_path
assert api_url.endswith('api.php')
return api_url[: -len('api.php')] + 'rest.php'
[docs]
def has_access_token(self, *args: P.args, **kwargs: P.kwargs) -> bool:
"""Check whether the session store contains an access token.
If true, the user should be treated as logged in;
if false, they are logged out,
and :meth:`authorization_url` can be used to start the login flow.
:param args: Arguments passed through to the session store accessors.
:param kwargs: Likewise.
"""
return self.get_access_token(*args, **kwargs) is not None
[docs]
def authorization_url(self, *args: P.args, **kwargs: P.kwargs) -> str:
"""Generate a URL to start the login flow.
If the user is not logged in (according to :meth:`has_access_token`),
call this function and redirect the user to the returned URL.
When the user returns to your tool, call :meth:`fetch_token` to complete the login.
Calling this function multiple times with the same session store state
will return the same URL, so there is no need to save the URL anywhere:
just call :meth:`authorization_url` again each time the user wants to log in,
regardless of whether a login is already pending or not.
:param args: Arguments passed through to the session store accessors.
:param kwargs: Likewise.
"""
session = self._oauth2session()
oauth_state = self.get_oauth_state(*args, **kwargs)
if oauth_state is None:
oauth_state = {'state': session.new_state()} # type: ignore # new_state() has no return type in typeshed
# store the state as a dict for future extensibility, e.g. PKCE code verifier (requests/requests-oauthlib#550)
self.set_oauth_state(oauth_state, *args, **kwargs)
redirect, _ = session.authorization_url(f'{self._rest_url()}/oauth2/authorize', state=oauth_state['state'])
return redirect
[docs]
def fetch_token(
self,
request_url: str,
*args: P.args,
**kwargs: P.kwargs,
) -> None:
"""Complete the login flow.
After the user has been redirected back to your tool by MediaWiki,
call this function with the request URL, which should contain a ``code`` parameter.
The resulting access token will be saved in the session store;
afterwards, :meth:`has_access_token` will return true,
and :meth:`oauth2_session` will return a non-``None`` session
which you can use to make authenticated requests.
OAuthLib insists that the request URL uses HTTPS (raising an ``InsecureTransportError`` otherwise).
In order to accommodate some common setups,
this function will pretend that ``localhost`` request URLs used HTTPS
(as there is no risk of the token being intercepted on the network in this case),
and will also coerce URLs under Wikimedia Cloud Services domains to HTTPS
(under the assumption that HTTPS will have been enforced via HSTS Preloading,
but the URL seen by your tool may be an HTTP URL
because your tool has not been configured to trust the incoming ``X-Forwarded-Proto: https`` header).
:param str request_url: The request URL with which your tool was called.
:param args: Arguments passed through to the session store accessors.
:param kwargs: Likewise.
"""
split = urlsplit(request_url)
if split.scheme == 'http' and (
split.hostname == 'localhost'
or (split.hostname or '').endswith('.toolforge.org')
or (split.hostname or '').endswith('.wmcloud.org')
or (split.hostname or '').endswith('.wmflabs.org')
):
# OAuthLib raises InsecureTransportError for all HTTP URLs;
# fix localhost (for local development)
# and WMCS domains (WMCS does TLS termination, tool did not apply `X-Forwarded-Proto: https`)
# to prevent this
request_url = split._replace(scheme='https').geturl()
oauth_state = self.pop_oauth_state(*args, **kwargs)
access_token = self._oauth2session(state=oauth_state['state']).fetch_token(
f'{self._rest_url()}/oauth2/access_token',
client_secret=self.client_secret,
authorization_response=request_url,
)
self.set_access_token(access_token, *args, **kwargs)
[docs]
def oauth2_session(
self,
*args: P.args,
**kwargs: P.kwargs,
) -> OAuth2Session | None:
"""Return an OAuth 2 session (if the user is logged in).
If :meth:`has_access_token` is ``False``,
this function returns ``None``.
Otherwise, it returns a ``requests.Session``-like object
which can be used to make authenticated requests.
mwapi users will want to use :meth:`~mwoauth2.MWOAuth2MWApi.mwapi_session` instead.
:param args: Arguments passed through to the session store accessors.
:param kwargs: Likewise.
"""
access_token = self.get_access_token(*args, **kwargs)
if access_token is None:
return None
return self._oauth2session(
token=access_token,
token_updater=lambda token: self.set_access_token(token, *args, **kwargs),
auto_refresh_url=f'{self._rest_url()}/oauth2/access_token',
auto_refresh_kwargs={
'client_id': self.client_id,
'client_secret': self.client_secret,
},
)
__all__ = [
'MWOAuth2',
]
try:
import flask
except ModuleNotFoundError:
pass
else:
def _get_oauth_state_flask_session() -> dict[str, Any] | None:
return flask.session.get('oauth_state')
def _set_oauth_state_flask_session(oauth_state: dict[str, Any]) -> None:
flask.session['oauth_state'] = oauth_state
def _pop_oauth_state_flask_session() -> dict[str, Any]:
return cast(dict[str, Any], flask.session.pop('oauth_state'))
def _get_access_token_flask_session() -> dict[str, Any] | None:
return flask.session.get('oauth_access_token')
def _set_access_token_flask_session(access_token: dict[str, Any]) -> None:
flask.session['oauth_access_token'] = access_token
def _pop_access_token_flask_session() -> dict[str, Any]:
return cast(dict[str, Any], flask.session.pop('oauth_access_token'))
[docs]
class MWOAuth2FlaskUninitializedException(Exception):
"""Raised when OAuth has not been initialized.
:class:`MWOAuth2Flask` and :class:`MWOAuth2FlaskMWApi` support three ways
to configure the OAuth client ID and client secret:
a Flask app with ``['OAUTH']['CLIENT_ID']`` in the :attr:`~flask.Flask.config` may be passed into the constructor,
or such a Flask app may be passed into :meth:`~MWOAuth2Flask.init_app` later,
or the constructor may be called with explicit ``client_id`` and ``client_secret`` arguments.
If none of these conditions is fulfilled and you try to use OAuth anyway,
this exception is raised.
"""
def __init__(self, class_name: str):
super().__init__(
f'{class_name} was not initialized correctly. '
'You must either configure it with a Flask app, '
'by passing it into the constructor '
'or calling init_app() later, '
'or you must pass a client_id and client_secret into the constructor, '
'before it can be used.'
)
self.class_name = class_name
def __reduce__(self) -> tuple[Callable[[str], 'MWOAuth2FlaskUninitializedException'], tuple[str]]:
return (MWOAuth2FlaskUninitializedException, (self.class_name,))
[docs]
class MWOAuth2Flask(MWOAuth2[[]]):
"""Flask extension managing OAuth 2 authentication against a MediaWiki wiki.
A single instance of this class can be used for multiple users,
and is typically set up at tool initialization
(either passing the Flask app into the constructor or calling :meth:`init_app` later).
It contains the details of the client wiki
and (by default) uses the Flask configuration for OAuth credentials
and the Flask session for the OAuth state.
The Flask appās configuration should contain an ``OAUTH`` key whose value is a dict with two members:
the client ID (a string of 32 hexadecimal characters) in ``CLIENT_ID``,
and the client secret (a string of 40 hexadecimal characters) in ``CLIENT_SECRET``.
For instance, when using :meth:`~flask.Config.from_prefixed_env` with the prefix ``TOOL``,
these would be set using the environment variables ``TOOL_OAUTH__CLIENT_ID``
and ``TOOL_OAUTH__CLIENT_SECRET``.
To start the authentication flow for a user,
call :meth:`authorization_url` and send the user to the returned URL.
Once they return, call :meth:`fetch_token`.
Afterwards, you can use :meth:`oauth2_session` to make authenticated requests.
To check if the user is logged in or not,
use :meth:`has_access_token`.
:param str host: The host of the wiki, e.g. ``https://en.wikipedia.org``.
Should include the protocol (scheme) and host name, but no path
(use ``api_path`` if necessary).
:param str user_agent: The User-Agent string that should be sent with all requests,
in accordance with the `User-Agent Policy <https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Wikimedia_Foundation_User-Agent_Policy>`__.
Toolforge tools can get a suitable string from :func:`toolforge.set_user_agent`.
:param flask.Flask app: The Flask app. If this is not ``None``, :meth:`init_app` will be called with it;
otherwise, you should call :meth:`init_app` later.
(Doing the init separately can be useful with the application factory pattern
or to avoid circular import issues.)
:param bool check_access_token_before_request: If ``True``,
register a :meth:`~flask.Flask.before_request` handler to check
if the access token is valid (according to :meth:`~MWOAuth2.has_well_formed_access_token`).
This is only useful for tools that migrated from OAuth 1.0a to OAuth 2,
and which also stored the OAuth 1.0a access token in the Flask session under the key ``oauth_access_token``.
In that case, enable this feature so that the outdated OAuth 1.0a access token will automatically be discarded
(i.e. the user has to log in again),
instead of trying to use it as an OAuth 2 access token and crashing.
Disabled by default.
:param str|None client_id: The OAuth client ID.
By default, this is loaded from the app config as described above,
so you normally do not need to specify this.
:param str|None client_secret: The OAuth client secret, likewise.
:param get_oauth_state: By default, this uses the Flask session and you do not need to specify it.
(If you do want to change it, you will have to change all six session store accessor functions together;
see the :class:`MWOAuth2` documentation in that case.)
:param set_oauth_state: Likewise.
:param pop_oauth_state: Likewise.
:param get_access_token: Likewise.
:param set_access_token: Likewise.
:param pop_access_token: Likewise.
:param str api_path: The path to the ``api.php`` endpoint,
relative to the ``host`` (see above).
The default of ``/w/api.php`` is suitable for all Wikimedia wikis and many other wikis as well.
.. method:: has_access_token() -> bool
Check whether the session store contains an access token.
If true, the user should be treated as logged in;
if false, they are logged out,
and :meth:`authorization_url` can be used to start the login flow.
.. method:: has_well_formed_access_token() -> bool
Check whether the session store contains a *well-formed* access token.
You generally donāt need to call this directly ā
instead, see the ``check_access_token_before_request`` constructor parameter.
(For more information on this method, see :meth:`MWOAuth2.has_well_formed_access_token`.)
.. method:: authorization_url() -> str
Generate a URL to start the login flow.
If the user is not logged in (according to :meth:`has_access_token`),
call this function and redirect the user to the returned URL.
When the user returns to your tool, call :meth:`fetch_token` to complete the login.
Calling this function multiple times with the same session store state
will return the same URL, so there is no need to save the URL anywhere:
just call :meth:`authorization_url` again each time the user wants to log in,
regardless of whether a login is already pending or not.
.. method:: oauth2_session() -> requests_oauthlib.OAuth2Session | None
Return an OAuth 2 session (if the user is logged in).
If :meth:`has_access_token` is ``False``,
this function returns ``None``.
Otherwise, it returns a ``requests.Session``-like object
which can be used to make authenticated requests.
mwapi users will want to use :meth:`~mwoauth2.MWOAuth2FlaskMWApi.mwapi_session` instead.
"""
def __init__(
self,
host: str,
user_agent: str,
app: flask.Flask | None = None,
check_access_token_before_request: bool = False,
client_id: str | None = None,
client_secret: str | None = None,
get_oauth_state: Callable[[], dict[str, Any] | None] | None = None,
set_oauth_state: Callable[[dict[str, Any]], None] | None = None,
pop_oauth_state: Callable[[], dict[str, Any]] | None = None,
get_access_token: Callable[[], dict[str, Any] | None] | None = None,
set_access_token: Callable[[dict[str, Any]], None] | None = None,
pop_access_token: Callable[[], dict[str, Any]] | None = None,
api_path: str = '/w/api.php',
):
super().__init__(
host=host,
client_id=cast(str, client_id), # may be set to non-None only in init_app() later
client_secret=cast(str, client_secret), # may be set to non-None only in init_app() later
get_oauth_state=get_oauth_state or _get_oauth_state_flask_session,
set_oauth_state=set_oauth_state or _set_oauth_state_flask_session,
pop_oauth_state=pop_oauth_state or _pop_oauth_state_flask_session,
get_access_token=get_access_token or _get_access_token_flask_session,
set_access_token=set_access_token or _set_access_token_flask_session,
pop_access_token=pop_access_token or _pop_access_token_flask_session,
user_agent=user_agent,
api_path=api_path,
)
self.check_access_token_before_request = check_access_token_before_request
if app is not None:
self.init_app(app)
[docs]
def init_app(self, app: flask.Flask) -> None:
"""Initialize using the given Flask app.
If you passed a Flask app into the constructor, this method will be called automatically.
Otherwise, you should manually call it later.
It reads the OAuth client ID and secret from the Flask configuration as described above,
and also sets up the ``check_access_token_before_request`` feature if enabled.
"""
if self.client_id is None:
self.client_id = app.config['OAUTH']['CLIENT_ID']
if self.client_secret is None:
self.client_secret = app.config['OAUTH']['CLIENT_SECRET']
if self.check_access_token_before_request:
app.before_request(self._check_access_token)
def _check_access_token(self) -> None:
if self.has_access_token() and not self.has_well_formed_access_token():
self.pop_access_token()
def _oauth2session(self, **kwargs: Any) -> OAuth2Session:
if self.client_id is None or self.client_secret is None:
raise MWOAuth2FlaskUninitializedException(type(self).__name__)
return super()._oauth2session(**kwargs)
[docs]
def fetch_token(
self,
request_url: str | None = None,
) -> None:
"""Complete the login flow.
After the user has been redirected back to your tool by MediaWiki,
call this function.
(The ``request_url`` defaults to the Flask request URL, so you can usually omit it.)
The resulting access token will be saved in the session store;
afterwards, :meth:`has_access_token` will return true,
and :meth:`oauth2_session` will return a non-``None`` session
which you can use to make authenticated requests.
OAuthLib insists that the request URL uses HTTPS (raising an ``InsecureTransportError`` otherwise).
In order to accommodate some common setups,
this function will pretend that ``localhost`` request URLs used HTTPS
(as there is no risk of the token being intercepted on the network in this case),
and will also coerce URLs under Wikimedia Cloud Services domains to HTTPS
(under the assumption that HTTPS will have been enforced via HSTS Preloading,
but the URL seen by your tool may be an HTTP URL
because your tool has not been configured to trust the incoming ``X-Forwarded-Proto: https`` header).
:param str|None request_url: The request URL with which your tool was called,
if you want to override the default Flask request URL.
"""
super().fetch_token(
request_url=request_url or flask.request.url,
)
__all__ += [
'MWOAuth2Flask',
'MWOAuth2FlaskUninitializedException',
]
try:
import mwapi
except ModuleNotFoundError:
pass
else:
[docs]
class MWOAuth2MWApi(MWOAuth2[[]]):
"""Class managing OAuth 2 authentication against a MediaWiki wiki.
A single instance of this class can be used for multiple users,
and is typically set up at tool initialization time.
It contains the details of the client wiki,
the OAuth credentials,
and accessors for some kind of session store where the OAuth state is kept.
To start the authentication flow for a user,
call :meth:`~MWOAuth2.authorization_url` and send the user to the returned URL.
Once they return, call :meth:`~MWOAuth2.fetch_token` with the request URL.
Afterwards, you can use :meth:`mwapi_session` to make authenticated requests.
To check if the user is logged in or not,
use :meth:`~MWOAuth2.has_access_token`.
:param str host: The host of the wiki, e.g. ``https://en.wikipedia.org``.
Should include the protocol (scheme) and host name, but no path
(use ``api_path`` if necessary).
This is also used as the default for :meth:`mwapi_session`.
:param str client_id: The OAuth client ID, a string of 32 hexadecimal characters.
:param str client_secret: The OAuth client secret, a string of 40 hexadecimal characters.
This should come from some kind of secret configuration source,
e.g. a config file or environment variables.
:param Callable[P, dict[str, Any] | None] get_oauth_state: This,
along with the next five parameters, represents the interface to the session store.
This should be backed by some kind of user session,
usually provided by your web framework,
and is used to store the OAuth state (during login) and access token (subsequently).
The type parameter ``P`` specifies the parameters that are necessary to access the session store;
these parameters are passed through from the other functions (:meth:`fetch_token` etc.)
to the session store accessors.
Most of the time, this will be either a single āsessionā object
(e.g. the Django ``request.session`` or the FastAPI dependency-injected ``session``)
or no parameters at all (e.g. the :data:`flask.session` proxy, see :class:`MWOAuth2FlaskMWApi`).
This function should return the OAuth state if it has been set, otherwise ``None``.
:param Callable[Concatenate[dict[str, Any], P], None] set_oauth_state: Set the OAuth state to the given value.
:param Callable[P, dict[str, Any]] pop_oauth_state: Return and remove the OAuth state,
or raise a :class:`KeyError` if it has not been set.
:param Callable[P, dict[str, Any] | None] get_access_token: Get the access token if it has been set,
otherwise ``None``.
:param Callable[Concatenate[dict[str, Any], P], None] set_access_token: Set the access token to the given value.
:param Callable[P, dict[str, Any]] pop_access_token: Return and remove the access token,
or raise a :class:`KeyError` if it has not been set.
This is the only one of the six functions that is useful for a tool to call directly:
calling this will log the user out.
:param str user_agent: The User-Agent string that should be sent with all requests,
in accordance with the `User-Agent Policy <https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Wikimedia_Foundation_User-Agent_Policy>`__.
Toolforge tools can get a suitable string from :func:`toolforge.set_user_agent`.
This is also used in :meth:`mwapi_session`.
:param str api_path: The path to the ``api.php`` endpoint,
relative to the ``host`` (see above).
The default of ``/w/api.php`` is suitable for all Wikimedia wikis and many other wikis as well.
This is also used as the default for :meth:`mwapi_session`.
"""
[docs]
def mwapi_session(
self,
host: str | None = None,
api_path: str | None = None,
) -> mwapi.Session | None:
"""Return an authenticated mwapi session (if the user is logged in).
If :meth:`~MWOAuth2.has_access_token` is ``False``,
this function returns ``None``.
Otherwise, it returns an :class:`mwapi.Session`
which can be used to make authenticated requests.
:param str|None host: The host of the wiki, e.g. ``https://en.wikipedia.org``.
Defaults to the ``host`` parameter of the class, but can be changed,
e.g. to authenticate against ``https://meta.wikimedia.org``
and subsequently make requests to ``https://en.wikipedia.org``,
``https://commons.wikimedia.org`` etc.,
or more generally to target different wikis in a wiki farm
with a central authentication system.
:param str api_path: The path to the ``api.php`` endpoint,
relative to the ``host`` (see above).
Defaults to the ``api_path`` parameter of the class.
"""
oauth2_session = self.oauth2_session()
if oauth2_session is None:
return None
return mwapi.Session(
host=host or self.host,
api_path=api_path or self.api_path,
user_agent=self.user_agent,
session=oauth2_session,
)
__all__ += [
'MWOAuth2MWApi',
]
try:
[docs]
class MWOAuth2FlaskMWApi(MWOAuth2Flask, MWOAuth2MWApi):
"""Flask extension managing OAuth 2 authentication against a MediaWiki wiki.
A single instance of this class can be used for multiple users,
and is typically set up at tool initialization
(either passing the Flask app into the constructor or calling :meth:`init_app` later).
It contains the details of the client wiki
and (by default) uses the Flask configuration for OAuth credentials
and the Flask session for the OAuth state.
The Flask appās configuration should contain an ``OAUTH`` key whose value is a dict with two members:
the client ID (a string of 32 hexadecimal characters) in ``CLIENT_ID``,
and the client secret (a string of 40 hexadecimal characters) in ``CLIENT_SECRET``.
For instance, when using :meth:`~flask.Config.from_prefixed_env` with the prefix ``TOOL``,
these would be set using the environment variables ``TOOL_OAUTH__CLIENT_ID``
and ``TOOL_OAUTH__CLIENT_SECRET``.
To start the authentication flow for a user,
call :meth:`authorization_url` and send the user to the returned URL.
Once they return, call :meth:`fetch_token`.
Afterwards, you can use :meth:`mwapi_session` to make authenticated requests.
To check if the user is logged in or not,
use :meth:`has_access_token`.
:param str host: The host of the wiki, e.g. ``https://en.wikipedia.org``.
Should include the protocol (scheme) and host name, but no path
(use ``api_path`` if necessary).
:param str user_agent: The User-Agent string that should be sent with all requests,
in accordance with the `User-Agent Policy <https://foundation.wikimedia.org/wiki/Special:MyLanguage/Policy:Wikimedia_Foundation_User-Agent_Policy>`__.
Toolforge tools can get a suitable string from :func:`toolforge.set_user_agent`.
:param flask.Flask app: The Flask app. If this is not ``None``, :meth:`init_app` will be called with it;
otherwise, you should call :meth:`init_app` later.
(Doing the init separately can be useful with the application factory pattern
or to avoid circular import issues.)
:param bool check_access_token_before_request: If ``True``,
register a :meth:`~flask.Flask.before_request` handler to check
if the access token is valid (according to :meth:`~MWOAuth2.has_well_formed_access_token`).
This is only useful for tools that migrated from OAuth 1.0a to OAuth 2,
and which also stored the OAuth 1.0a access token in the Flask session under the key ``oauth_access_token``.
In that case, enable this feature so that the outdated OAuth 1.0a access token will automatically be discarded
(i.e. the user has to log in again),
instead of trying to use it as an OAuth 2 access token and crashing.
Disabled by default.
:param str|None client_id: The OAuth client ID.
By default, this is loaded from the app config as described above,
so you normally do not need to specify this.
:param str|None client_secret: The OAuth client secret, likewise.
:param get_oauth_state: By default, this uses the Flask session and you do not need to specify it.
(If you do want to change it, you will have to change all six session store accessor functions together;
see the :class:`MWOAuth2` documentation in that case.)
:param set_oauth_state: Likewise.
:param pop_oauth_state: Likewise.
:param get_access_token: Likewise.
:param set_access_token: Likewise.
:param pop_access_token: Likewise.
:param str api_path: The path to the ``api.php`` endpoint,
relative to the ``host`` (see above).
The default of ``/w/api.php`` is suitable for all Wikimedia wikis and many other wikis as well.
.. method:: init_app(app: flask.Flask) -> None
Initialize using the given Flask app.
If you passed a Flask app into the constructor, this method will be called automatically.
Otherwise, you should manually call it later.
It reads the OAuth client ID and secret from the Flask configuration as described above,
and also sets up the ``check_access_token_before_request`` feature if enabled.
.. method:: has_access_token() -> bool
Check whether the session store contains an access token.
If true, the user should be treated as logged in;
if false, they are logged out,
and :meth:`authorization_url` can be used to start the login flow.
.. method:: has_well_formed_access_token() -> bool
Check whether the session store contains a *well-formed* access token.
You generally donāt need to call this directly ā
instead, see the ``check_access_token_before_request`` constructor parameter.
(For more information on this method, see :meth:`MWOAuth2.has_well_formed_access_token`.)
.. method:: authorization_url() -> str
Generate a URL to start the login flow.
If the user is not logged in (according to :meth:`has_access_token`),
call this function and redirect the user to the returned URL.
When the user returns to your tool, call :meth:`fetch_token` to complete the login.
Calling this function multiple times with the same session store state
will return the same URL, so there is no need to save the URL anywhere:
just call :meth:`authorization_url` again each time the user wants to log in,
regardless of whether a login is already pending or not.
.. method:: fetch_token(request_url: str | None = None) -> None
Complete the login flow.
After the user has been redirected back to your tool by MediaWiki,
call this function.
(The ``request_url`` defaults to the Flask request URL, so you can usually omit it.)
The resulting access token will be saved in the session store;
afterwards, :meth:`has_access_token` will return true,
and :meth:`mwapi_session` will return a non-``None`` session
which you can use to make authenticated requests.
OAuthLib insists that the request URL uses HTTPS (raising an ``InsecureTransportError`` otherwise).
In order to accommodate some common setups,
this function will pretend that ``localhost`` request URLs used HTTPS
(as there is no risk of the token being intercepted on the network in this case),
and will also coerce URLs under Wikimedia Cloud Services domains to HTTPS
(under the assumption that HTTPS will have been enforced via HSTS Preloading,
but the URL seen by your tool may be an HTTP URL
because your tool has not been configured to trust the incoming ``X-Forwarded-Proto: https`` header).
:param str|None request_url: The request URL with which your tool was called,
if you want to override the default Flask request URL.
.. method:: mwapi_session(host: str | None = None, api_path: str | None = None) -> mwapi.Session | None
Return an authenticated mwapi session (if the user is logged in).
If :meth:`has_access_token` is ``False``,
this function returns ``None``.
Otherwise, it returns an :class:`mwapi.Session`
which can be used to make authenticated requests.
:param str|None host: The host of the wiki, e.g. ``https://en.wikipedia.org``.
Defaults to the ``host`` parameter of the class, but can be changed,
e.g. to authenticate against ``https://meta.wikimedia.org``
and subsequently make requests to ``https://en.wikipedia.org``,
``https://commons.wikimedia.org`` etc.,
or more generally to target different wikis in a wiki farm
with a central authentication system.
:param str api_path: The path to the ``api.php`` endpoint,
relative to the ``host`` (see above).
Defaults to the ``api_path`` parameter of the class.
"""
except NameError:
pass
else:
__all__ += [
'MWOAuth2FlaskMWApi',
]