Skip to content

Commit f343d4c

Browse files
committed
Merge branch 'saxtouri-feature-linkedin'
2 parents 937c721 + c876dfc commit f343d4c

File tree

3 files changed

+143
-0
lines changed

3 files changed

+143
-0
lines changed

example/internal_attributes.yaml.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,20 @@ attributes:
1010
saml: [displayName]
1111
edupersontargetedid:
1212
facebook: [id]
13+
linkedin: [id]
1314
orcid: [orcid]
1415
github: [id]
1516
openid: [sub]
1617
saml: [eduPersonTargetedID]
1718
givenname:
1819
facebook: [first_name]
20+
linkedin: [email-address]
1921
orcid: [name.given-names.value]
2022
openid: [given_name]
2123
saml: [givenName]
2224
mail:
2325
facebook: [email]
26+
linkedin: [email-address]
2427
orcid: [emails.str]
2528
github: [email]
2629
openid: [email]
@@ -33,6 +36,7 @@ attributes:
3336
saml: [cn]
3437
surname:
3538
facebook: [last_name]
39+
linkedin: [lastName]
3640
orcid: [name.family-name.value]
3741
openid: [family_name]
3842
saml: [sn, surname]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module: satosa.backends.linkedin.LinkedInBackend
2+
name: linkedin
3+
config:
4+
authz_page: linkedin/auth/callback
5+
base_url: https://www.example.org
6+
client_config:
7+
client_id: 12345678
8+
client_secret: a2s3d4f5g6h7j8k9
9+
scope: [r_basicprofile,r_emailaddress,rw_company_admin,w_share]
10+
response_type: code
11+
server_info: {
12+
authorization_endpoint: 'https://www.linkedin.com/oauth/v2/authorization',
13+
token_endpoint: 'https://www.linkedin.com/oauth/v2/accessToken',
14+
user_info: 'https://api.linkedin.com/v1/people/~'
15+
}
16+
entity_info:
17+
organization:
18+
display_name:
19+
- ["LinkedIn", "en"]
20+
name:
21+
- ["LinkedIn", "en"]
22+
url:
23+
- ["https://www.linkedin.com/", "en"]
24+
ui_info:
25+
description:
26+
- ["LinkedIn oauth", "en"]
27+
display_name:
28+
- ["LinkedIn", "en"]

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

0 commit comments

Comments
 (0)