-
Notifications
You must be signed in to change notification settings - Fork 76
/
Copy pathgithub.py
336 lines (266 loc) · 11.3 KB
/
github.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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2015-2018 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.
"""Pre-configured remote application for enabling sign in/up with GitHub.
1. Ensure you have ``github3.py`` package installed:
.. code-block:: console
cdvirtualenv src/invenio-oauthclient
pip install -e .[github]
2. Edit your configuration and add:
.. code-block:: python
from invenio_oauthclient.contrib import github
OAUTHCLIENT_REMOTE_APPS = dict(
github=github.REMOTE_APP,
)
GITHUB_APP_CREDENTIALS = dict(
consumer_key='changeme',
consumer_secret='changeme',
)
3. Go to GitHub and register a new application:
https://github.com/settings/applications/new. When registering the
application ensure that the *Authorization callback URL* points to:
``CFG_SITE_SECURE_URL/oauth/authorized/github/`` (e.g.
``http://localhost:4000/oauth/authorized/github/`` for development).
4. Grab the *Client ID* and *Client Secret* after registering the application
and add them to your instance configuration (``invenio.cfg``):
.. code-block:: python
GITHUB_APP_CREDENTIALS = dict(
consumer_key='<CLIENT ID>',
consumer_secret='<CLIENT SECRET>',
)
5. Now go to ``CFG_SITE_SECURE_URL/oauth/login/github/`` (e.g.
http://localhost:4000/oauth/login/github/)
6. Also, you should see GitHub listed under Linked accounts:
http://localhost:4000/account/settings/linkedaccounts/
By default the GitHub module will try first look if a link already exists
between a GitHub account and a user. If no link is found, the module tries to
retrieve the user email address from GitHub to match it with a local user. If
this fails, the user is asked to provide an email address to sign-up.
In templates you can add a sign in/up link:
.. code-block:: jinja
<a href='{{url_for('invenio_oauthclient.login', remote_app='github')}}'>
Sign in with GitHub
</a>
For more details you can play with a :doc:`working example <examplesapp>`.
"""
import github3
from flask import current_app, redirect, url_for
from flask_login import current_user
from invenio_db import db
from invenio_oauthclient import current_oauthclient
from invenio_oauthclient.contrib.settings import OAuthSettingsHelper
from invenio_oauthclient.errors import OAuthResponseError
from invenio_oauthclient.handlers import authorized_signup_handler, oauth_error_handler
from invenio_oauthclient.handlers.rest import (
authorized_signup_handler as authorized_signup_rest_handler,
)
from invenio_oauthclient.handlers.rest import (
oauth_resp_remote_error_handler,
response_handler,
)
from invenio_oauthclient.handlers.utils import require_more_than_one_external_account
from invenio_oauthclient.models import RemoteAccount
from invenio_oauthclient.oauth import oauth_link_external_id, oauth_unlink_external_id
class GitHubOAuthSettingsHelper(OAuthSettingsHelper):
"""Default configuration for GitHub OAuth provider."""
def __init__(
self,
title=None,
description=None,
base_url=None,
app_key=None,
icon=None,
precedence_mask=None,
signup_options=None,
):
"""Constructor."""
super().__init__(
title or "GitHub",
description or "Software collaboration platform.",
base_url or "https://api.github.com/",
app_key or "GITHUB_APP_CREDENTIALS",
icon=icon or "fa fa-github",
request_token_params={"scope": "user,user:email"},
access_token_url="https://github.com/login/oauth/access_token",
authorize_url="https://github.com/login/oauth/authorize",
precedence_mask=precedence_mask,
signup_options=signup_options,
)
self._handlers = dict(
authorized_handler="invenio_oauthclient.handlers:authorized_signup_handler",
disconnect_handler="invenio_oauthclient.contrib.github:disconnect_handler",
signup_handler=dict(
info="invenio_oauthclient.contrib.github:account_info",
info_serializer="invenio_oauthclient.contrib.github:account_info_serializer",
setup="invenio_oauthclient.contrib.github:account_setup",
view="invenio_oauthclient.handlers:signup_handler",
),
)
self._rest_handlers = dict(
authorized_handler="invenio_oauthclient.handlers.rest"
":authorized_signup_handler",
disconnect_handler="invenio_oauthclient.contrib.github"
":disconnect_rest_handler",
signup_handler=dict(
info="invenio_oauthclient.contrib.github:account_info",
info_serializer="invenio_oauthclient.contrib.github:account_info_serializer",
setup="invenio_oauthclient.contrib.github:account_setup",
view="invenio_oauthclient.handlers.rest:signup_handler",
),
response_handler="invenio_oauthclient.handlers.rest"
":default_remote_response_handler",
authorized_redirect_url="/",
disconnect_redirect_url="/",
signup_redirect_url="/",
error_redirect_url="/",
)
def get_handlers(self):
"""Return GitHub auth handlers."""
return self._handlers
def get_rest_handlers(self):
"""Return GitHub auth REST handlers."""
return self._rest_handlers
_github_app = GitHubOAuthSettingsHelper()
BASE_APP = _github_app.base_app
REMOTE_APP = _github_app.remote_app
"""GitHub remote application configuration."""
REMOTE_REST_APP = _github_app.remote_rest_app
"""GitHub remote rest application configuration."""
def _extract_email(emails):
"""Get user email from GitHub."""
return next((x.email for x in emails if x.verified and x.primary), None)
def account_info_serializer(remote, resp, user_info, emails_info, **kwargs):
"""Serialize the account info response object.
:param remote: The remote application.
:param resp: The response of the `authorized` endpoint.
:param user_info: The response of the `me` endpoint.
:param emails_info: The response of the `emails` endpoint.
:returns: A dictionary with serialized user information.
"""
return dict(
user=dict(
email=_extract_email(emails_info),
profile=dict(
username=user_info.login,
full_name=user_info.name,
),
),
external_id=str(user_info.id),
external_method=remote.name,
)
def account_info(remote, resp):
"""Retrieve remote account information used to find local user.
It returns a dictionary with the following structure:
.. code-block:: python
{
'user': {
'email': '...',
'profile': {
'username': '...',
'full_name': '...',
}
},
'external_id': 'github-unique-identifier',
'external_method': 'github',
}
Information inside the user dictionary are available for other modules.
For example, they are used from the module invenio-userprofiles to fill
the user profile.
:param remote: The remote application.
:param resp: The response of the `authorized` endpoint.
:returns: A dictionary with the user information.
"""
github_obj = github3.login(token=resp["access_token"])
user_info = github_obj.me()
emails_info = github_obj.emails()
handlers = current_oauthclient.signup_handlers[remote.name]
# `remote` param automatically injected via `make_handler` helper
return handlers["info_serializer"](resp, user_info, emails_info=emails_info)
def account_setup(remote, token, resp):
"""Perform additional setup after user have been logged in.
:param remote: The remote application.
:param token: The token value.
:param resp: The response.
"""
gh = github3.login(token=resp["access_token"])
with db.session.begin_nested():
me = gh.me()
token.remote_account.extra_data = {"login": me.login, "id": me.id}
# Create user <-> external id link.
oauth_link_external_id(
token.remote_account.user, dict(id=str(me.id), method=remote.name)
)
@oauth_error_handler
def authorized(resp, remote):
"""Authorized callback handler for GitHub.
:param resp: The response.
:param remote: The remote application.
"""
if resp and "error" in resp:
if resp["error"] == "bad_verification_code":
# See https://developer.github.com/v3/oauth/#bad-verification-code
# which recommends starting auth flow again.
return redirect(url_for("invenio_oauthclient.login", remote_app="github"))
elif resp["error"] in ["incorrect_client_credentials", "redirect_uri_mismatch"]:
raise OAuthResponseError(
"Application mis-configuration in GitHub", remote, resp
)
return authorized_signup_handler(resp, remote)
@oauth_resp_remote_error_handler
def authorized_rest(resp, remote):
"""Authorized callback handler for GitHub.
:param resp: The response.
:param remote: The remote application.
"""
if resp and "error" in resp:
if resp["error"] == "bad_verification_code":
# See https://developer.github.com/v3/oauth/#bad-verification-code
# which recommends starting auth flow again.
return redirect(
url_for("invenio_oauthclient.rest_login", remote_app="github")
)
elif resp["error"] in ["incorrect_client_credentials", "redirect_uri_mismatch"]:
raise OAuthResponseError(
"Application mis-configuration in GitHub", remote, resp
)
return authorized_signup_rest_handler(resp, remote)
@require_more_than_one_external_account
def _disconnect(remote, *args, **kwargs):
"""Handle unlinking of remote account.
:param remote: The remote application.
:returns: The HTML response.
"""
if not current_user.is_authenticated:
return current_app.login_manager.unauthorized()
remote_account = RemoteAccount.get(
user_id=current_user.get_id(), client_id=remote.consumer_key
)
external_ids = [
i.id for i in current_user.external_identifiers if i.method == remote.name
]
if external_ids:
oauth_unlink_external_id(dict(id=external_ids[0], method=remote.name))
if remote_account:
with db.session.begin_nested():
remote_account.delete()
def disconnect_handler(remote, *args, **kwargs):
"""Handle unlinking of remote account.
:param remote: The remote application.
:returns: The HTML response.
"""
_disconnect(remote, *args, **kwargs)
return redirect(url_for("invenio_oauthclient_settings.index"))
def disconnect_rest_handler(remote, *args, **kwargs):
"""Handle unlinking of remote account.
:param remote: The remote application.
:returns: The HTML response.
"""
_disconnect(remote, *args, **kwargs)
redirect_url = current_app.config["OAUTHCLIENT_REST_REMOTE_APPS"][remote.name][
"disconnect_redirect_url"
]
return response_handler(remote, redirect_url)