Skip to content

Commit 937c721

Browse files
committed
Merge branch 'saxtouri-feature-github'
2 parents 5fbc3f5 + f4a179f commit 937c721

File tree

3 files changed

+145
-1
lines changed

3 files changed

+145
-1
lines changed

example/internal_attributes.yaml.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ attributes:
66
displayname:
77
openid: [nickname]
88
orcid: [name.credit-name]
9+
github: [login]
910
saml: [displayName]
1011
edupersontargetedid:
1112
facebook: [id]
1213
orcid: [orcid]
14+
github: [id]
1315
openid: [sub]
1416
saml: [eduPersonTargetedID]
1517
givenname:
@@ -20,11 +22,13 @@ attributes:
2022
mail:
2123
facebook: [email]
2224
orcid: [emails.str]
25+
github: [email]
2326
openid: [email]
2427
saml: [email, emailAdress, mail]
2528
name:
2629
facebook: [name]
2730
orcid: [name.credit-name]
31+
github: [name]
2832
openid: [name]
2933
saml: [cn]
3034
surname:
@@ -34,4 +38,4 @@ attributes:
3438
saml: [sn, surname]
3539
hash: [edupersontargetedid]
3640
user_id_from_attrs: [edupersontargetedid]
37-
user_id_to_attr: edupersontargetedid
41+
user_id_to_attr: edupersontargetedid
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module: satosa.backends.github.GitHubBackend
2+
name: github
3+
config:
4+
authz_page: github/auth/callback
5+
base_url: https://www.example.org
6+
client_config:
7+
client_id: 123d45g678
8+
client_secret: 3h4h5j67l5k3l3l
9+
scope: [user]
10+
response_type: code
11+
allow_signup: false
12+
server_info: {
13+
authorization_endpoint: 'http://github.com/login/oauth/authorize',
14+
token_endpoint: 'https://github.com/login/oauth/access_token',
15+
user_info: 'https://api.github.com/user'
16+
}
17+
entity_info:
18+
organization:
19+
display_name:
20+
- ["GitHub", "en"]
21+
name:
22+
- ["GitHub", "en"]
23+
url:
24+
- ["https://www.github.com/", "en"]
25+
ui_info:
26+
description:
27+
- ["GitHub oauth", "en"]
28+
display_name:
29+
- ["GitHub", "en"]

src/satosa/backends/github.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""
2+
OAuth backend for LinkedIn
3+
"""
4+
import json
5+
import logging
6+
import requests
7+
8+
from oic.utils.authn.authn_context import UNSPECIFIED
9+
from oic.oauth2.consumer import stateID
10+
from oic.oauth2.message import AuthorizationResponse
11+
12+
from satosa.backends.oauth import _OAuthBackend
13+
from ..internal_data import InternalResponse
14+
from ..internal_data import AuthenticationInformation
15+
from ..response import Redirect
16+
from ..util import rndstr
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
class GitHubBackend(_OAuthBackend):
22+
"""GitHub OAuth 2.0 backend"""
23+
24+
def __init__(self, outgoing, internal_attributes, config, base_url, name):
25+
"""GitHub backend constructor
26+
:param outgoing: Callback should be called by the module after the
27+
authorization in the backend is done.
28+
:param internal_attributes: Mapping dictionary between SATOSA internal
29+
attribute names and the names returned by underlying IdP's/OP's as
30+
well as what attributes the calling SP's and RP's expects namevice.
31+
:param config: configuration parameters for the module.
32+
:param base_url: base url of the service
33+
:param name: name of the plugin
34+
:type outgoing:
35+
(satosa.context.Context, satosa.internal_data.InternalResponse) ->
36+
satosa.response.Response
37+
:type internal_attributes: dict[string, dict[str, str | list[str]]]
38+
:type config: dict[str, dict[str, str] | list[str] | str]
39+
:type base_url: str
40+
:type name: str
41+
"""
42+
config.setdefault('response_type', 'code')
43+
config['verify_accesstoken_state'] = False
44+
super().__init__(
45+
outgoing, internal_attributes, config, base_url, name, 'github',
46+
'id')
47+
48+
def start_auth(self, context, internal_request, get_state=stateID):
49+
"""
50+
:param get_state: Generates a state to be used in authentication call
51+
52+
:type get_state: Callable[[str, bytes], str]
53+
:type context: satosa.context.Context
54+
:type internal_request: satosa.internal_data.InternalRequest
55+
:rtype satosa.response.Redirect
56+
"""
57+
oauth_state = get_state(self.config["base_url"], rndstr().encode())
58+
context.state[self.name] = dict(state=oauth_state)
59+
60+
request_args = dict(
61+
client_id=self.config['client_config']['client_id'],
62+
redirect_uri=self.redirect_url,
63+
state=oauth_state,
64+
allow_signup=self.config.get('allow_signup', False))
65+
scope = ' '.join(self.config['scope'])
66+
if scope:
67+
request_args['scope'] = scope
68+
69+
cis = self.consumer.construct_AuthorizationRequest(
70+
request_args=request_args)
71+
return Redirect(cis.request(self.consumer.authorization_endpoint))
72+
73+
def auth_info(self, requrest):
74+
return AuthenticationInformation(
75+
UNSPECIFIED, None,
76+
self.config['server_info']['authorization_endpoint'])
77+
78+
def _authn_response(self, context):
79+
state_data = context.state[self.name]
80+
aresp = self.consumer.parse_response(
81+
AuthorizationResponse, info=json.dumps(context.request))
82+
self._verify_state(aresp, state_data, context.state)
83+
url = self.config['server_info']['token_endpoint']
84+
data = dict(
85+
code=aresp['code'],
86+
redirect_uri=self.redirect_url,
87+
client_id=self.config['client_config']['client_id'],
88+
client_secret=self.config['client_secret'], )
89+
headers = {'Accept': 'application/json'}
90+
91+
r = requests.post(url, data=data, headers=headers)
92+
response = r.json()
93+
if self.config.get('verify_accesstoken_state', True):
94+
self._verify_state(response, state_data, context.state)
95+
96+
user_info = self.user_information(response["access_token"])
97+
auth_info = self.auth_info(context.request)
98+
internal_response = InternalResponse(auth_info=auth_info)
99+
internal_response.attributes = self.converter.to_internal(
100+
self.external_type, user_info)
101+
internal_response.user_id = user_info[self.user_id_attr]
102+
del context.state[self.name]
103+
return self.auth_callback_func(context, internal_response)
104+
105+
def user_information(self, access_token):
106+
url = self.config['server_info']['user_info']
107+
headers = {'Authorization': 'token {}'.format(access_token)}
108+
r = requests.get(url, headers=headers)
109+
ret = r.json()
110+
ret['id'] = str(ret['id'])
111+
return r.json()

0 commit comments

Comments
 (0)