././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1723649323.2614264 mwoauth-0.4.0/0000755000175000017500000000000014657146453013674 5ustar00ahalfakerahalfaker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/LICENSE0000644000175000017500000000211414657145412014671 0ustar00ahalfakerahalfakerThe MIT License (MIT) Copyright (c) 2014 Aaron Halfaker / Filippo Valsorda Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/MANIFEST.in0000644000175000017500000000005414657145412015423 0ustar00ahalfakerahalfakerinclude LICENSE README.rst requirements.txt ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1723649323.2614264 mwoauth-0.4.0/PKG-INFO0000644000175000017500000000410514657146453014771 0ustar00ahalfakerahalfakerMetadata-Version: 2.1 Name: mwoauth Version: 0.4.0 Summary: A generic MediaWiki OAuth handshake helper. Home-page: https://github.com/mediawiki-utilities/python-mwoauth Author: Aaron Halfaker / Filippo Valsorda Author-email: aaron.halfaker@gmail.com License: MIT Classifier: Development Status :: 4 - Beta Classifier: Topic :: Security Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Software Development :: Libraries :: Python Modules License-File: LICENSE Requires-Dist: PyJWT>=1.0.1 Requires-Dist: oauthlib Requires-Dist: requests Requires-Dist: requests-oauthlib Provides-Extra: flask Requires-Dist: flask; extra == "flask" MediaWiki OAuth Library ======================= ``mwoauth`` is an open licensed (MIT) library designed to provide a simple means to performing an OAuth handshake with a MediaWiki installation with the `OAuth Extension `_ installed. **Compatible with python 3.x** **Install with pip:** ``pip install mwoauth`` **Documentation:** http://pythonhosted.org/mwoauth Usage ===== .. code-block:: python from mwoauth import ConsumerToken, Handshaker # Construct a "consumer" from the key/secret provided by MediaWiki import config consumer_token = ConsumerToken(config.consumer_key, config.consumer_secret) # Construct handshaker with wiki URI and consumer handshaker = Handshaker("https://en.wikipedia.org/w/index.php", consumer_token) # Step 1: Initialize -- ask MediaWiki for a temporary key/secret for user redirect, request_token = handshaker.initiate() # Step 2: Authorize -- send user to MediaWiki to confirm authorization print("Point your browser to: %s" % redirect) # response_qs = input("Response query string: ") # Step 3: Complete -- obtain authorized key/secret for "resource owner" access_token = handshaker.complete(request_token, response_qs) print(str(access_token)) # Step 4: Identify -- (optional) get identifying information about the user identity = handshaker.identify(access_token) print("Identified as {username}.".format(**identity)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/README.rst0000644000175000017500000000267014657145412015362 0ustar00ahalfakerahalfakerMediaWiki OAuth Library ======================= ``mwoauth`` is an open licensed (MIT) library designed to provide a simple means to performing an OAuth handshake with a MediaWiki installation with the `OAuth Extension `_ installed. **Compatible with python 3.x** **Install with pip:** ``pip install mwoauth`` **Documentation:** http://pythonhosted.org/mwoauth Usage ===== .. code-block:: python from mwoauth import ConsumerToken, Handshaker # Construct a "consumer" from the key/secret provided by MediaWiki import config consumer_token = ConsumerToken(config.consumer_key, config.consumer_secret) # Construct handshaker with wiki URI and consumer handshaker = Handshaker("https://en.wikipedia.org/w/index.php", consumer_token) # Step 1: Initialize -- ask MediaWiki for a temporary key/secret for user redirect, request_token = handshaker.initiate() # Step 2: Authorize -- send user to MediaWiki to confirm authorization print("Point your browser to: %s" % redirect) # response_qs = input("Response query string: ") # Step 3: Complete -- obtain authorized key/secret for "resource owner" access_token = handshaker.complete(request_token, response_qs) print(str(access_token)) # Step 4: Identify -- (optional) get identifying information about the user identity = handshaker.identify(access_token) print("Identified as {username}.".format(**identity)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1723649323.2614264 mwoauth-0.4.0/mwoauth/0000755000175000017500000000000014657146453015360 5ustar00ahalfakerahalfaker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/mwoauth/__init__.py0000644000175000017500000000123114657145412017460 0ustar00ahalfakerahalfaker"""Provides a collection of utilities for easily working with MediaWiki's OAuth1.0a implementation.""" from .handshaker import Handshaker from .tokens import AccessToken, ConsumerToken, RequestToken from .functions import initiate, complete, identify from .errors import OAuthException from .about import (__name__, __version__, __author__, __author_email__, __description__, __license__, __url__) __all__ = [ ConsumerToken, RequestToken, AccessToken, initiate, complete, identify, Handshaker, OAuthException, __name__, __version__, __author__, __author_email__, __description__, __license__, __url__ ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/mwoauth/about.py0000644000175000017500000000061314657145412017036 0ustar00ahalfakerahalfaker__name__ = "mwoauth" __version__ = "0.4.0" __author__ = "Aaron Halfaker / Filippo Valsorda" __author_email__ = "aaron.halfaker@gmail.com" __description__ = "A generic MediaWiki OAuth handshake helper." __license__ = "MIT" __url__ = "https://github.com/mediawiki-utilities/python-mwoauth" all = [__name__, __version__, __author__, __author_email__, __description__, __license__, __url__] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/mwoauth/defaults.py0000644000175000017500000000006114657145412017530 0ustar00ahalfakerahalfakerUSER_AGENT = "python-mwoauth default user agent" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/mwoauth/errors.py0000644000175000017500000000005214657145412017235 0ustar00ahalfakerahalfakerclass OAuthException(Exception): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/mwoauth/flask.py0000644000175000017500000002331314657145412017026 0ustar00ahalfakerahalfaker""" .. autoclass:: mwoauth.flask.MWOAuth :members: :member-order: bysource .. autofunction:: mwoauth.flask.authorized """ import logging from functools import wraps from urllib.parse import urljoin import flask from requests_oauthlib import OAuth1 from .errors import OAuthException from .handshaker import Handshaker from .tokens import AccessToken, RequestToken logger = logging.getLogger(__name__) class MWOAuth: """ Implements a basic MediaWiki OAuth pattern with a set of routes - /mwoauth/initiate -- Starts an OAuth handshake - /mwoauth/callback -- Completes an OAuth handshake - /mwoauth/identify -- Gets identity information about an authorized user - /mwoauth/logout -- Discards OAuth tokens and user identity There's also a convenient decorator provided :func:`~mwoauth.flask.MWOAuth.authorized`. When applied to a routing function, this decorator will redirect non-authorized users to /mwoauth/initiate with a "?next=" that will return them to the originating route once authorization is completed. :Example: .. code-block:: python from flask import Flask import mwoauth import mwoauth.flask app = Flask(__name__) @app.route("/") def index(): return "Hello world" flask_mwoauth = mwoauth.flask.MWOAuth( "https://en.wikipedia.org", mwoauth.ConsumerToken("...", "...")) app.register_blueprint(flask_mwoauth.bp) @app.route("/my_settings/") @mwoauth.flask.authorized def my_settings(): return flask_mwoauth.identity() :Parameters: host : str The host name (including protocol) of the MediaWiki wiki to use for the OAuth handshake. consumer_token : :class:`mwoauth.ConsumerToken` The consumer token information user_agent : str A User-Agent header to include with requests. A warning will be logged is this is not set. default_next : str Where should the user be redirected after an OAuth handshake when no '?next=' param is provided render_logout : func A method that renders the logout page seen at /mwoauth/logout render_identify : func A method that renders the identify page seen at /mwoauth/identify. Takes one positional argument -- the `identity` dictionary returned by MediaWiki. render_error : func A method that renders an error. Takes two arguements: * message : str (The error message) * status : int (The https status number) **kwargs : dict Parameters to be passed to :class:`flask.Blueprint` during its construction. """ def __init__(self, host, consumer_token, user_agent=None, default_next="index", render_logout=None, render_indentify=None, render_error=None, **kwargs): self.bp = flask.Blueprint('mwoauth', __name__, **kwargs) self.host = host self.user_agent = user_agent self.consumer_token = consumer_token self.handshaker = None self.default_next = default_next self.render_logout = render_logout or generic_logout self.render_identify = render_indentify or generic_identify self.render_error = render_error or generic_error @self.bp.route("/mwoauth/initiate/") def mwoauth_initiate(): """Start an OAuth handshake.""" mw_authorizer_url, request_token = self._handshaker().initiate() rt_session_key = _str(request_token.key) + "_request_token" next_session_key = _str(request_token.key) + "_next" # Ensures that Flask's default session storage strategy will work flask.session[rt_session_key] = \ dict(zip(request_token._fields, request_token)) if 'next' in flask.request.args: flask.session[next_session_key] = \ flask.request.args.get('next') return flask.redirect(mw_authorizer_url) @self.bp.route("/mwoauth/callback/") def mwoauth_callback(): """Complete the oauth handshake.""" # Generate session keys request_token_key = _str( flask.request.args.get('oauth_token', 'None')) rt_session_key = request_token_key + "_request_token" next_session_key = request_token_key + "_next" # Make sure we're continuing an in-progress handshake if rt_session_key not in flask.session: flask.session.pop(rt_session_key, None) flask.session.pop(next_session_key, None) return self.render_error( "OAuth callback failed. " + "Couldn't find request_token in session. " + "Are cookies disabled?", 403) # Complete the handshake try: access_token = self._handshaker().complete( RequestToken(**flask.session[rt_session_key]), _str(flask.request.query_string)) except OAuthException as e: flask.session.pop(rt_session_key, None) flask.session.pop(next_session_key, None) return self.render_error( "OAuth callback failed. " + str(e), 403) # Store the access token flask.session['mwoauth_access_token'] = \ dict(zip(access_token._fields, access_token)) # Identify the user identity = self._handshaker().identify(access_token) flask.session['mwoauth_identity'] = identity # Redirect to wherever we're supposed to go if next_session_key in flask.session: return flask.redirect( flask.url_for(flask.session[next_session_key])) else: return flask.redirect( flask.url_for(self.default_next)) @self.bp.route("/mwoauth/identify/") @authorized def mwoauth_identify(): """Return user information if authenticated.""" return flask.jsonify(flask.session['mwoauth_identity']) @self.bp.route("/mwoauth/logout/") def mwoauth_logout(): """Delete the local session.""" flask.session.pop('mwoauth_access_token', None) flask.session.pop('mwoauth_identity', None) if 'next' in flask.request.args: return flask.redirect( flask.url_for(flask.request.args.get('next'))) else: return self.render_logout() def _handshaker(self): if not self.handshaker: full_callback = urljoin( flask.request.url_root, flask.url_for("mwoauth.mwoauth_callback")) print(full_callback) self.handshaker = Handshaker( self.host, self.consumer_token, user_agent=self.user_agent, callback=full_callback) return self.handshaker @staticmethod def identify(): return flask.session.get('mwoauth_identity') def mwapi_session(self, *args, **kwargs): """ Create :class:`mwapi.Session` that is authorized for the current user. `args` and `kwargs` are passed directly to :class:`mwapi.Session` """ import mwapi auth1 = self.generate_auth() return mwapi.Session(*args, user_agent=self.user_agent, auth=auth1, **kwargs) def requests_session(self, *args, **kwargs): """ Create :class:`requests.Session` that is authorized for the current user. `args` and `kwargs` are passed directly to :class:`requests.Session` """ import requests auth1 = self.generate_auth() return requests.Session(*args, auth=auth1, **kwargs) def generate_auth(self): if 'mwoauth_access_token' in flask.session: access_token = AccessToken( **flask.session['mwoauth_access_token']) auth1 = OAuth1(self.consumer_token.key, client_secret=self.consumer_token.secret, resource_owner_key=access_token.key, resource_owner_secret=access_token.secret) return auth1 else: raise OAuthException( "Cannot generate auth. User has not authorized.") def authorized(route): """ Wrap a flask route. Ensure that the user has authorized via OAuth or redirect the user to the authorization endpoint with a delayed redirect back to the originating endpoint. """ @wraps(route) def authorized_route(*args, **kwargs): if 'mwoauth_access_token' in flask.session: return route(*args, **kwargs) else: return flask.redirect( flask.url_for('mwoauth.mwoauth_initiate') + "?next=" + flask.request.endpoint) return authorized_route def generic_logout(): return "Logged out" def generic_identify(identity): return flask.jsonify(identity) def generic_error(message, status): return '' + message + '', status def encode_token(token): return dict(zip(token._fields, token)) def _str(val): """Ensure that the val is the default str() type for python2 or 3.""" if str == bytes: if isinstance(val, str): return val else: return str(val) else: if isinstance(val, str): return val else: return str(val, 'ascii') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/mwoauth/functions.py0000644000175000017500000002420514657145412017737 0ustar00ahalfakerahalfaker""" A set of stateless functions that can be used to complete various steps of an OAuth handshake or to identify a MediaWiki user. :Example: .. code-block:: python from mwoauth import ConsumerToken, initiate, complete, identify # Consruct a "consumer" from the key/secret provided by MediaWiki import config consumer_token = ConsumerToken( config.consumer_key, config.consumer_secret) mw_uri = "https://en.wikipedia.org/w/index.php" # Step 1: Initialize -- ask MediaWiki for a temporary key/secret for # user redirect, request_token = initiate(mw_uri, consumer_token) # Step 2: Authorize -- send user to MediaWiki to confirm authorization print("Point your browser to: %s" % redirect) # response_qs = input("Response query string: ") # Step 3: Complete -- obtain authorized key/secret for "resource owner" access_token = complete( mw_uri, consumer_token, request_token, response_qs) print(str(access_token)) # Step 4: Identify -- (optional) get identifying information about the # user identity = identify(mw_uri, consumer_token, access_token) print("Identified as {username}.".format(**identity)) """ import json import re import time from urllib.parse import parse_qs, urlencode, urlparse import jwt import requests from requests_oauthlib import OAuth1 from . import defaults from .errors import OAuthException from .tokens import AccessToken, RequestToken def force_unicode(val, encoding="unicode-escape"): if type(val) == str: return val else: return val.decode(encoding, errors="replace") def initiate(mw_uri, consumer_token, callback='oob', user_agent=defaults.USER_AGENT): """ Initiate an oauth handshake with MediaWiki. :Parameters: mw_uri : `str` The base URI of the MediaWiki installation. Note that the URI should end in ``"index.php"``. consumer_token : :class:`~mwoauth.ConsumerToken` A token representing you, the consumer. Provided by MediaWiki via ``Special:OAuthConsumerRegistration``. callback : `str` Callback URL. Defaults to 'oob'. :Returns: A `tuple` of two values: * a MediaWiki URL to direct the user to * a :class:`~mwoauth.RequestToken` representing a request for access """ auth = OAuth1(consumer_token.key, client_secret=consumer_token.secret, callback_uri=callback) r = requests.post(url=mw_uri, params={'title': "Special:OAuth/initiate"}, auth=auth, headers={'User-Agent': user_agent}) request_token = process_request_token(r.text) params = {'title': "Special:OAuth/authenticate", 'oauth_token': request_token.key, 'oauth_consumer_key': consumer_token.key} return (mw_uri + "?" + urlencode(params), request_token) def process_request_token(content): if content.startswith("Error: "): raise OAuthException(content[len("Error: "):]) credentials = parse_qs(content) if credentials is None or credentials == {}: raise OAuthException( "Expected x-www-form-urlencoded response from " + "MediaWiki, but got something else: " + "{0}".format(repr(content))) elif 'oauth_token' not in credentials or \ 'oauth_token_secret' not in credentials: raise OAuthException( "MediaWiki response lacks token information: " "{0}".format(repr(credentials))) else: return RequestToken( credentials.get('oauth_token')[0], credentials.get('oauth_token_secret')[0] ) def complete(mw_uri, consumer_token, request_token, response_qs, user_agent=defaults.USER_AGENT): """ Complete an OAuth handshake with MediaWiki by exchanging an :Parameters: mw_uri : `str` The base URI of the MediaWiki installation. Note that the URI should end in ``"index.php"``. consumer_token : :class:`~mwoauth.ConsumerToken` A key/secret pair representing you, the consumer. request_token : :class:`~mwoauth.RequestToken` A temporary token representing the user. Returned by `initiate()`. response_qs : `bytes` The query string of the URL that MediaWiki forwards the user back after authorization. :Returns: An `AccessToken` containing an authorized key/secret pair that can be stored and used by you. """ callback_data = parse_qs(force_unicode(response_qs)) if callback_data is None or callback_data == {}: raise OAuthException( "Expected URL query string, but got " + "something else instead: {0}".format(str(response_qs))) elif 'oauth_token' not in callback_data or \ 'oauth_verifier' not in callback_data: raise OAuthException( "Query string lacks token information: " "{0}".format(repr(callback_data))) # Process the callback_data request_token_key = callback_data.get('oauth_token')[0] verifier = callback_data.get('oauth_verifier')[0] # Check if the query string references the right temp resource owner key if not request_token.key == request_token_key: raise OAuthException( "Unexpect request token key {0!r}, expected {1!r}.".format( request_token_key, request_token.key)) # Construct a new auth with the verifier auth = OAuth1(consumer_token.key, client_secret=consumer_token.secret, resource_owner_key=request_token.key, resource_owner_secret=request_token.secret, verifier=verifier) # Send the verifier and ask for an authorized resource owner key/secret r = requests.post(url=mw_uri, params={'title': "Special:OAuth/token"}, auth=auth, headers={'User-Agent': user_agent}) # Parse response and construct an authorized resource owner credentials = parse_qs(r.text) if credentials is None: raise OAuthException( "Expected x-www-form-urlencoded response, " + "but got some else instead: {0}".format(r.text)) access_token = AccessToken( credentials.get('oauth_token')[0], credentials.get('oauth_token_secret')[0] ) return access_token def identify(mw_uri, consumer_token, access_token, leeway=10.0, user_agent=defaults.USER_AGENT): """ Gather identifying information about a user via an authorized token. :Parameters: mw_uri : `str` The base URI of the MediaWiki installation. Note that the URI should end in ``"index.php"``. consumer_token : :class:`~mwoauth.ConsumerToken` A token representing you, the consumer. access_token : :class:`~mwoauth.AccessToken` A token representing an authorized user. Obtained from `complete()` leeway : `int` | `float` The number of seconds of leeway to account for when examining a tokens "issued at" timestamp. :Returns: A dictionary containing identity information. """ # Construct an OAuth auth auth = OAuth1(consumer_token.key, client_secret=consumer_token.secret, resource_owner_key=access_token.key, resource_owner_secret=access_token.secret) # Request the identity using auth r = requests.post(url=mw_uri, params={'title': "Special:OAuth/identify"}, auth=auth, headers={'User-Agent': user_agent}) # Special:OAuth/identify unhelpfully returns 200 status even when there is # an error in the API call. Check for error messages manually. try: identity = jwt.decode(r.content, consumer_token.secret, audience=consumer_token.key, algorithms=["HS256"], leeway=leeway) except jwt.InvalidTokenError as e: if r.text.startswith('{'): try: resp = json.loads(r.text) if 'error' in resp: raise OAuthException( "A MediaWiki API error occurred: {0}" .format(resp['message'])) else: raise OAuthException( "Unknown JSON response: {0!r}" .format(r.text[:100])) except ValueError as e: raise OAuthException( "An error occurred while trying to read json " + "content: {0}".format(e)) else: raise OAuthException( "Could not read response from 'Special:OAuth/identify'. " + "Maybe your MediaWiki is not configured correctly? " + "Expected JSON but instead got: {0!r}".format(r.text[:100])) # Verify the issuer is who we expect (server sends $wgCanonicalServer) issuer = urlparse(identity['iss']).netloc expected_domain = urlparse(mw_uri).netloc if not issuer == expected_domain: raise OAuthException( "Unexpected issuer " + "{0}, expected {1}".format(issuer, expected_domain)) # Check that the identity was issued in the past. now = time.time() issued_at = float(identity['iat']) if not now >= (issued_at - leeway): raise OAuthException( "Identity issued {0} ".format(issued_at - now) + "seconds in the future!") # Verify that the nonce matches our request nonce, # to avoid a replay attack authorization_header = force_unicode(r.request.headers['Authorization']) request_nonce = re.search(r'oauth_nonce="(.*?)"', authorization_header).group(1) if identity['nonce'] != request_nonce: raise OAuthException( 'Replay attack detected: {0} != {1}'.format( identity['nonce'], request_nonce)) return identity ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/mwoauth/handshaker.py0000644000175000017500000001047414657145412020042 0ustar00ahalfakerahalfaker""" A client for managing an OAuth handshake with MediaWiki. :Example: .. code-block:: python from mwoauth import ConsumerToken, Handshaker # Consruct a "consumer" from the key/secret provided by MediaWiki import config consumer_token = ConsumerToken( config.consumer_key, config.consumer_secret) # Construct handshaker with wiki URI and consumer handshaker = Handshaker( "https://en.wikipedia.org/w/index.php", consumer_token) # Step 1: Initialize -- ask MediaWiki for a temporary key/secret for # user redirect, request_token = handshaker.initiate() # Step 2: Authorize -- send user to MediaWiki to confirm authorization print("Point your browser to: %s" % redirect) # response_qs = input("Response query string: ") # Step 3: Complete -- obtain authorized key/secret for "resource owner" access_token = handshaker.complete(request_token, response_qs) print(str(access_token)) # Step 4: Identify -- (optional) get identifying information about the # user identity = handshaker.identify(access_token) print("Identified as {username}.".format(**identity)) """ import logging from . import defaults from .functions import complete, identify, initiate logger = logging.getLogger(__name__) class Handshaker(object): """ :Parameters: mw_uri : `str` The base URI of the wiki (provider) to authenticate with. This uri should end in ``"index.php"``. consumer_token : :class:`~mwoauth.ConsumerToken` A token representing you, the consumer. Provided by MediaWiki via ``Special:OAuthConsumerRegistration``. callback : `str` Callback URL. Defaults to 'oob'. """ def __init__(self, mw_uri, consumer_token, callback='oob', user_agent=None): self.mw_uri = mw_uri self.consumer_token = consumer_token self.callback = callback if user_agent is None: logger.warning("Sending requests with default User-Agent. " + "Set 'user_agent' on mwoauth.flask.MWOAuth to " + "quiet this message.") self.user_agent = defaults.USER_AGENT else: self.user_agent = user_agent def initiate(self, callback=None): """ Initiate an OAuth handshake with MediaWiki. :Parameters: callback : `str` Callback URL. Defaults to 'oob'. :Returns: A `tuple` of two values: * a MediaWiki URL to direct the user to * a :class:`~mwoauth.RequestToken` representing an access request """ return initiate(self.mw_uri, self.consumer_token, callback=callback or self.callback, user_agent=self.user_agent) def complete(self, request_token, response_qs): """ Complete an OAuth handshake with MediaWiki by exchanging an :Parameters: request_token : `RequestToken` A temporary token representing the user. Returned by `initiate()`. response_qs : `bytes` The query string of the URL that MediaWiki forwards the user back after authorization. :Returns: An :class:`~mwoauth.AccessToken` containing an authorized key/secret pair that can be stored and used by you. """ return complete( self.mw_uri, self.consumer_token, request_token, response_qs, user_agent=self.user_agent) def identify(self, access_token, leeway=10.0): """ Gather identifying information about a user via an authorized token. :Parameters: access_token : `AccessToken` A token representing an authorized user. Obtained from `complete()`. leeway : `int` | `float` The number of seconds of leeway to account for when examining a tokens "issued at" timestamp. :Returns: A dictionary containing identity information. """ return identify(self.mw_uri, self.consumer_token, access_token, leeway=leeway, user_agent=self.user_agent) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1723649323.2614264 mwoauth-0.4.0/mwoauth/tests/0000755000175000017500000000000014657146453016522 5ustar00ahalfakerahalfaker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/mwoauth/tests/__init__.py0000644000175000017500000000000014657145412020613 0ustar00ahalfakerahalfaker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/mwoauth/tests/test_functions.py0000644000175000017500000000200214657145412022127 0ustar00ahalfakerahalfaker# -*- coding: UTF-8 -*- import pytest from ..errors import OAuthException from ..functions import process_request_token def test_process_request_token(): request_token = process_request_token( "oauth_token=iamatoken&oauth_token_secret=iamasecret") assert request_token.key == "iamatoken" assert request_token.secret == "iamasecret" def test_process_request_token_errors(): text = "Error: Произошла ошибка в протоколе OAuth: " + \ "Invalid consumer key" #content = bytes(text, "utf-8") with pytest.raises(OAuthException, match=text[len("Error: "):]): process_request_token(text) with pytest.raises(OAuthException, match="I am an error"): process_request_token("Error: I am an error") with pytest.raises(OAuthException, match=r"Expected x-www-form-.*"): process_request_token("TOTAL NONSENSE") with pytest.raises(OAuthException, match=r"MediaWiki response lacks.*"): process_request_token("foo=bar&baz=bum") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/mwoauth/tokens.py0000644000175000017500000000263414657145412017234 0ustar00ahalfakerahalfaker""" A set of tokens (key/secret pairs) used to identify actors during and after an OAuth handshake. """ from collections import namedtuple ConsumerToken = namedtuple("ConsumerToken", ['key', 'secret']) """ Represents a consumer (you). This key/secrets pair is provided by MediaWiki when you register an OAuth consumer (see ``Special:OAuthConsumerRegistration``). Note that Extension:OAuth must be installed in order in order for ``Special:OAuthConsumerRegistration`` to appear. :Parameters: key : `str` A hex string identifying the user secret : `str` A hex string used to sign communications """ RequestToken = namedtuple("RequestToken", ['key', 'secret']) """ Represents a request for access during authorization. This key/secret pair is provided by MediaWiki via ``Special:OAuth/initiate``. Once the user authorize you, this token can be traded for an `AccessToken` via `complete()`. :Parameters: key : `str` A hex string identifying the user secret : `str` A hex string used to sign communications """ AccessToken = namedtuple("AccessToken", ['key', 'secret']) """ Represents an authorized user. This key and secret is provided by MediaWiki via ``Special:OAuth/complete`` and later used to show MediaWiki evidence of authorization. :Parameters: key : `str` A hex string identifying the user secret : `str` A hex string used to sign communications """ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1723649323.2614264 mwoauth-0.4.0/mwoauth.egg-info/0000755000175000017500000000000014657146453017052 5ustar00ahalfakerahalfaker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723649323.0 mwoauth-0.4.0/mwoauth.egg-info/PKG-INFO0000644000175000017500000000410514657146453020147 0ustar00ahalfakerahalfakerMetadata-Version: 2.1 Name: mwoauth Version: 0.4.0 Summary: A generic MediaWiki OAuth handshake helper. Home-page: https://github.com/mediawiki-utilities/python-mwoauth Author: Aaron Halfaker / Filippo Valsorda Author-email: aaron.halfaker@gmail.com License: MIT Classifier: Development Status :: 4 - Beta Classifier: Topic :: Security Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Software Development :: Libraries :: Python Modules License-File: LICENSE Requires-Dist: PyJWT>=1.0.1 Requires-Dist: oauthlib Requires-Dist: requests Requires-Dist: requests-oauthlib Provides-Extra: flask Requires-Dist: flask; extra == "flask" MediaWiki OAuth Library ======================= ``mwoauth`` is an open licensed (MIT) library designed to provide a simple means to performing an OAuth handshake with a MediaWiki installation with the `OAuth Extension `_ installed. **Compatible with python 3.x** **Install with pip:** ``pip install mwoauth`` **Documentation:** http://pythonhosted.org/mwoauth Usage ===== .. code-block:: python from mwoauth import ConsumerToken, Handshaker # Construct a "consumer" from the key/secret provided by MediaWiki import config consumer_token = ConsumerToken(config.consumer_key, config.consumer_secret) # Construct handshaker with wiki URI and consumer handshaker = Handshaker("https://en.wikipedia.org/w/index.php", consumer_token) # Step 1: Initialize -- ask MediaWiki for a temporary key/secret for user redirect, request_token = handshaker.initiate() # Step 2: Authorize -- send user to MediaWiki to confirm authorization print("Point your browser to: %s" % redirect) # response_qs = input("Response query string: ") # Step 3: Complete -- obtain authorized key/secret for "resource owner" access_token = handshaker.complete(request_token, response_qs) print(str(access_token)) # Step 4: Identify -- (optional) get identifying information about the user identity = handshaker.identify(access_token) print("Identified as {username}.".format(**identity)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723649323.0 mwoauth-0.4.0/mwoauth.egg-info/SOURCES.txt0000644000175000017500000000065714657146453020746 0ustar00ahalfakerahalfakerLICENSE MANIFEST.in README.rst requirements.txt setup.cfg setup.py mwoauth/__init__.py mwoauth/about.py mwoauth/defaults.py mwoauth/errors.py mwoauth/flask.py mwoauth/functions.py mwoauth/handshaker.py mwoauth/tokens.py mwoauth.egg-info/PKG-INFO mwoauth.egg-info/SOURCES.txt mwoauth.egg-info/dependency_links.txt mwoauth.egg-info/requires.txt mwoauth.egg-info/top_level.txt mwoauth/tests/__init__.py mwoauth/tests/test_functions.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723649323.0 mwoauth-0.4.0/mwoauth.egg-info/dependency_links.txt0000644000175000017500000000000114657146453023120 0ustar00ahalfakerahalfaker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723649323.0 mwoauth-0.4.0/mwoauth.egg-info/requires.txt0000644000175000017500000000010014657146453021441 0ustar00ahalfakerahalfakerPyJWT>=1.0.1 oauthlib requests requests-oauthlib [flask] flask ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723649323.0 mwoauth-0.4.0/mwoauth.egg-info/top_level.txt0000644000175000017500000000001014657146453021573 0ustar00ahalfakerahalfakermwoauth ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/requirements.txt0000644000175000017500000000007514657145412017154 0ustar00ahalfakerahalfakerPyJWT == 2.4.0 requests == 2.21.0 requests-oauthlib == 1.2.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1723649323.2614264 mwoauth-0.4.0/setup.cfg0000644000175000017500000000015014657146453015511 0ustar00ahalfakerahalfaker[flake8] exclude = .tox,build,dist,doc,examples,*.egg,*.egg-info [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1723648778.0 mwoauth-0.4.0/setup.py0000644000175000017500000000173514657145412015406 0ustar00ahalfakerahalfakerimport os from setuptools import find_packages, setup def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() about_path = os.path.join(os.path.dirname(__file__), "mwoauth/about.py") exec(compile(open(about_path).read(), about_path, "exec")) setup( name=__name__, # noqa version=__version__, # noqa author=__author__, # noqa author_email=__author_email__, # noqa description=__description__, # noqa url=__url__, # noqa license=__license__, # noqa packages=find_packages(), long_description=read('README.rst'), install_requires=[ 'PyJWT>=1.0.1', 'oauthlib', 'requests', 'requests-oauthlib', ], extras_require={ 'flask': ['flask'], }, classifiers=[ "Development Status :: 4 - Beta", "Topic :: Security", "License :: OSI Approved :: MIT License", "Topic :: Software Development :: Libraries :: Python Modules" ] )