Skip to content

Commit 5fbc3f5

Browse files
Merge pull request #115 from saxtouri/feature-orcid
Support ORCID OAuth2.0 backend
2 parents 6176abd + 98a0d1a commit 5fbc3f5

File tree

3 files changed

+150
-0
lines changed

3 files changed

+150
-0
lines changed

example/internal_attributes.yaml.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,35 @@
11
attributes:
22
address:
33
openid: [address.street_address]
4+
orcid: [addresses.str]
45
saml: [postaladdress]
56
displayname:
67
openid: [nickname]
8+
orcid: [name.credit-name]
79
saml: [displayName]
810
edupersontargetedid:
911
facebook: [id]
12+
orcid: [orcid]
1013
openid: [sub]
1114
saml: [eduPersonTargetedID]
1215
givenname:
1316
facebook: [first_name]
17+
orcid: [name.given-names.value]
1418
openid: [given_name]
1519
saml: [givenName]
1620
mail:
1721
facebook: [email]
22+
orcid: [emails.str]
1823
openid: [email]
1924
saml: [email, emailAdress, mail]
2025
name:
2126
facebook: [name]
27+
orcid: [name.credit-name]
2228
openid: [name]
2329
saml: [cn]
2430
surname:
2531
facebook: [last_name]
32+
orcid: [name.family-name.value]
2633
openid: [family_name]
2734
saml: [sn, surname]
2835
hash: [edupersontargetedid]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module: satosa.backends.orcid.OrcidBackend
2+
name: orcid
3+
config:
4+
authz_page: orcid/auth/callback
5+
base_url: https://example.org
6+
client_config:
7+
client_id: 4123r355242
8+
client_secret: 23rt2t4tg42te-42t4ter2t4-23rt2t-234t
9+
scope: [/authenticate]
10+
response_type: code
11+
allow_signup: true
12+
server_info: {
13+
authorization_endpoint: 'https://orcid.org/oauth/authorize',
14+
token_endpoint: 'https://pub.orcid.org/oauth/token',
15+
user_info: 'https://pub.orcid.org/v2.0/'
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/orcid.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""
2+
OAuth backend for Orcid
3+
"""
4+
import json
5+
import requests
6+
import logging
7+
from urllib.parse import urljoin
8+
9+
from oic.utils.authn.authn_context import UNSPECIFIED
10+
from oic.oauth2.consumer import stateID
11+
from oic.oauth2.message import AuthorizationResponse
12+
13+
from satosa.backends.oauth import _OAuthBackend
14+
from ..internal_data import InternalResponse
15+
from ..internal_data import AuthenticationInformation
16+
from ..response import Redirect
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
class OrcidBackend(_OAuthBackend):
22+
"""Orcid OAuth 2.0 backend"""
23+
24+
def __init__(self, outgoing, internal_attributes, config, base_url, name):
25+
"""Orcid 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, 'orcid',
46+
'orcid')
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+
request_args = dict(
58+
client_id=self.config['client_config']['client_id'],
59+
redirect_uri=self.redirect_url,
60+
scope=' '.join(self.config['scope']), )
61+
cis = self.consumer.construct_AuthorizationRequest(
62+
request_args=request_args)
63+
return Redirect(cis.request(self.consumer.authorization_endpoint))
64+
65+
def auth_info(self, requrest):
66+
return AuthenticationInformation(
67+
UNSPECIFIED, None,
68+
self.config['server_info']['authorization_endpoint'])
69+
70+
def _authn_response(self, context):
71+
aresp = self.consumer.parse_response(
72+
AuthorizationResponse, info=json.dumps(context.request))
73+
url = self.config['server_info']['token_endpoint']
74+
data = dict(
75+
grant_type='authorization_code',
76+
code=aresp['code'],
77+
redirect_uri=self.redirect_url,
78+
client_id=self.config['client_config']['client_id'],
79+
client_secret=self.config['client_secret'], )
80+
headers = {'Accept': 'application/json'}
81+
82+
r = requests.post(url, data=data, headers=headers)
83+
response = r.json()
84+
token = response['access_token']
85+
orcid, name = response['orcid'], response['name']
86+
user_info = self.user_information(token, orcid, name)
87+
auth_info = self.auth_info(context.request)
88+
internal_response = InternalResponse(auth_info=auth_info)
89+
internal_response.attributes = self.converter.to_internal(
90+
self.external_type, user_info)
91+
internal_response.user_id = orcid
92+
return self.auth_callback_func(context, internal_response)
93+
94+
def user_information(self, access_token, orcid, name):
95+
base_url = self.config['server_info']['user_info']
96+
url = urljoin(base_url, '{}/person'.format(orcid))
97+
headers = {
98+
'Accept': 'application/orcid+json',
99+
'Authorization type': 'Bearer',
100+
'Access token': access_token,
101+
}
102+
r = requests.get(url, headers=headers)
103+
r = r.json()
104+
emails, addresses = r['emails']['email'], r['addresses']['address']
105+
ret = dict(
106+
address=', '.join([e['address'] for e in addresses]),
107+
displayname=name,
108+
edupersontargetedid=orcid, orcid=orcid,
109+
mail=' '.join([e['email'] for e in emails]),
110+
name=name,
111+
givenname=r['name']['given-names']['value'],
112+
surname=r['name']['family-name']['value'],
113+
)
114+
return ret

0 commit comments

Comments
 (0)