forked from jupyterhub/oauthenticator
-
Notifications
You must be signed in to change notification settings - Fork 0
/
mediawiki.py
143 lines (115 loc) · 4.08 KB
/
mediawiki.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"""
Custom Authenticator to use MediaWiki OAuth with JupyterHub
Requires `mwoauth` package.
"""
import json
import os
from asyncio import wrap_future
from concurrent.futures import ThreadPoolExecutor
from jupyterhub.handlers import BaseHandler
from jupyterhub.utils import url_path_join
from mwoauth import ConsumerToken
from mwoauth import Handshaker
from mwoauth.tokens import RequestToken
from traitlets import Any
from traitlets import Integer
from traitlets import Unicode
from oauthenticator import OAuthCallbackHandler
from oauthenticator import OAuthenticator
# Name of cookie used to pass auth token between the oauth
# login and authentication phase
AUTH_REQUEST_COOKIE_NAME = 'mw_oauth_request_token_v2'
# Helpers to jsonify/de-jsonify request_token
# It is a named tuple with bytestrings, json.dumps balks
def jsonify(request_token):
return json.dumps(
[
request_token.key,
request_token.secret,
]
)
def dejsonify(js):
key, secret = json.loads(js)
return RequestToken(key, secret)
class MWLoginHandler(BaseHandler):
async def get(self):
consumer_token = ConsumerToken(
self.authenticator.client_id,
self.authenticator.client_secret,
)
handshaker = Handshaker(self.authenticator.mw_index_url, consumer_token)
redirect, request_token = await wrap_future(
self.authenticator.executor.submit(handshaker.initiate)
)
self.set_secure_cookie(
AUTH_REQUEST_COOKIE_NAME,
jsonify(request_token),
expires_days=1,
path=url_path_join(self.base_url, 'hub', 'oauth_callback'),
httponly=True,
)
self.log.info('oauth redirect: %r', redirect)
self.redirect(redirect)
class MWCallbackHandler(OAuthCallbackHandler):
"""
Override OAuthCallbackHandler to take out state parameter handling.
mwoauth doesn't seem to support it for now!
"""
def check_arguments(self):
pass
def get_state_url(self):
return None
class MWOAuthenticator(OAuthenticator):
login_service = 'MediaWiki'
login_handler = MWLoginHandler
callback_handler = MWCallbackHandler
mw_index_url = Unicode(
os.environ.get('MW_INDEX_URL', 'https://meta.wikimedia.org/w/index.php'),
config=True,
help='Full path to index.php of the MW instance to use to log in',
)
executor_threads = Integer(
12,
help="""Number of executor threads.
MediaWiki OAuth requests happen in this thread,
so it is mostly waiting for network replies.
""",
config=True,
)
executor = Any()
def normalize_username(self, username):
"""
Override normalize_username to avoid lowercasing usernames
"""
return username
def _executor_default(self):
return ThreadPoolExecutor(self.executor_threads)
async def authenticate(self, handler, data=None):
consumer_token = ConsumerToken(
self.client_id,
self.client_secret,
)
handshaker = Handshaker(self.mw_index_url, consumer_token)
request_token = dejsonify(handler.get_secure_cookie(AUTH_REQUEST_COOKIE_NAME))
handler.clear_cookie(AUTH_REQUEST_COOKIE_NAME)
access_token = await wrap_future(
self.executor.submit(
handshaker.complete, request_token, handler.request.query
)
)
identity = await wrap_future(
self.executor.submit(handshaker.identify, access_token)
)
if identity and 'username' in identity:
# this shouldn't be necessary anymore,
# but keep for backward-compatibility
return {
'name': identity['username'].replace(' ', '_'),
'auth_state': {
'ACCESS_TOKEN_KEY': access_token.key,
'ACCESS_TOKEN_SECRET': access_token.secret,
'MEDIAWIKI_USER_IDENTITY': identity,
},
}
else:
self.log.error("No username found in %s", identity)