diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f5a3b408..e188a4ca1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased](https://github.com/python-social-auth/social-core/commits/master) +- ID4me backend ## [3.1.0](https://github.com/python-social-auth/social-core/releases/tag/3.1.0) - 2019-02-20 diff --git a/requirements-id4me.txt b/requirements-id4me.txt new file mode 100644 index 000000000..dfcdc6822 --- /dev/null +++ b/requirements-id4me.txt @@ -0,0 +1,3 @@ +python-jose>=3.0.0 +pyjwt>=1.7.1 +dnspython>=1.16.0 diff --git a/social_core/backends/id4me.py b/social_core/backends/id4me.py new file mode 100644 index 000000000..04ee73662 --- /dev/null +++ b/social_core/backends/id4me.py @@ -0,0 +1,285 @@ +""" + ID4me OpenID Connect backend, description at: https://id4me.org/for-developers/ +""" +import datetime +import json +import re +from calendar import timegm + +import dns +import jwt +import requests +from dns.resolver import NXDOMAIN, Timeout +from jose import jwk, jwt +from jose.jwt import JWTError, JWTClaimsError, ExpiredSignatureError +from social_core.backends.open_id_connect import OpenIdConnectAuth +from social_core.exceptions import AuthUnreachableProvider, AuthForbidden, AuthMissingParameter, AuthTokenError +from social_core.utils import handle_http_errors + + +class ID4meAssociation(object): + """ Use Association model to save the client account.""" + + def __init__(self, handle, secret='', issued=0, lifetime=0, assoc_type=''): + self.handle = handle # as client_id and client_secret + self.secret = secret.encode() # not use + self.issued = issued # not use + self.lifetime = lifetime # not use + self.assoc_type = assoc_type # as state + + def __str__(self): + return self.handle + + +def is_valid_domain(domain): + if domain[-1] == ".": + domain = domain[:-1] + allowed = re.compile("(?!-)[A-Z\d-]{1,63}(? id_token['exp']: + raise AuthTokenError(self, 'Incorrect id_token: exp') + + def validate_and_return_user_token(self, user_token): + client_id, client_secret = self.get_key_and_secret() + key = self.find_agent_valid_key(user_token) + + if not key: + raise AuthTokenError(self, 'Signature verification failed') + + alg = key['alg'] + rsakey = jwk.construct(key) + + try: + return jwt.decode( + user_token, + rsakey.to_pem().decode('utf-8'), + algorithms=[alg], + audience=client_id, + issuer=[self.strategy.session_get(self.name + '_agent'), + 'https://' + self.strategy.session_get(self.name + '_agent'), + self.strategy.session_get(self.name + '_authority').replace('https://', '')] + ) + except ExpiredSignatureError: + raise AuthTokenError(self, 'Signature has expired') + except JWTClaimsError as error: + raise AuthTokenError(self, str(error)) + except JWTError: + raise + + @handle_http_errors + def user_data(self, access_token, *args, **kwargs): + user_token = requests.get(self.userinfo_url(), headers={ + 'Authorization': 'Bearer {0}'.format(access_token) + }).text + return self.validate_and_return_user_token(user_token) + + def get_user_details(self, response): + data = { + self.setting('SOCIAL_AUTH_ID4ME_SCOPE_MAPPING', '')[key]: value for key, value in response.items() + if key in self.setting('SOCIAL_AUTH_ID4ME_SCOPE_MAPPING', '') + } + data.update(response.items()) + data['iss'] = self.strategy.session_get(self.name + '_authority') + data['clp'] = self.strategy.session_get(self.name + '_agent') + data['sub'] = response['sub'] + data['identity'] = self.strategy.session_get(self.name + '_identity') + return data