import uuid from urllib.parse import urlencode import jwt from jwt.exceptions import InvalidTokenError from esia_connector.exceptions import IncorrectMarkerError from esia_connector.utils import get_timestamp, sign_params, make_request class EsiaSettings: def __init__(self, esia_client_id, redirect_uri, certificate_file, private_key_file, esia_service_url, esia_scope, esia_token_check_key=None): """ Esia settings class :param str esia_client_id: client system id at ESIA :param str redirect_uri: uri, where browser will be redirected after authorization :param str certificate_file: path to client system certificate file :param str private_key_file: path to client system private key :param str esia_service_url: url of ESIA service :param str esia_scope: scopes keywords in single string, divided with space. :param str or None esia_token_check_key: path to ESIA key to verify access token with """ self.esia_client_id = esia_client_id self.redirect_uri = redirect_uri self.certificate_file = certificate_file self.private_key_file = private_key_file self.esia_service_url = esia_service_url self.esia_scope = esia_scope self.esia_token_check_key = esia_token_check_key class EsiaAuth: """ Esia authentication connector """ _ESIA_ISSUER_NAME = 'http://esia.gosuslugi.ru/' _AUTHORIZATION_URL = '/aas/oauth2/ac' _TOKEN_EXCHANGE_URL = '/aas/oauth2/te' def __init__(self, settings): """ :param EsiaSettings settings: connector settings """ self.settings = settings def get_auth_url(self, state=None, redirect_uri=None): """ Return url which end-user should visit to authorize at ESIA. :param str or None state: identifier, will be returned as GET parameter in redirected request after auth. :param str or None redirect_uri: uri, where browser will be redirected after authorization. :return: url :rtype: str """ params = { 'client_id': self.settings.esia_client_id, 'client_secret': '', 'redirect_uri': redirect_uri or self.settings.redirect_uri, 'scope': self.settings.esia_scope, 'response_type': 'code', 'state': state or str(uuid.uuid4()), 'timestamp': get_timestamp(), 'access_type': 'offline' } params = sign_params(params, certificate_file=self.settings.certificate_file, private_key_file=self.settings.private_key_file) params = urlencode(sorted(params.items())) # sorted needed to make uri deterministic for tests. return '{base_url}{auth_url}?{params}'.format(base_url=self.settings.esia_service_url, auth_url=self._AUTHORIZATION_URL, params=params) def complete_authorization(self, code, state, validate_token=True, redirect_uri=None): """ Exchanges received code and state to access token, validates token (optionally), extracts ESIA user id from token and returns ESIAInformationConnector instance. :type code: str :type state: str :param boolean validate_token: perform token validation :param str or None redirect_uri: uri, where browser will be redirected after authorization. :rtype: EsiaInformationConnector :raises IncorrectJsonError: if response contains invalid json body :raises HttpError: if response status code is not 2XX :raises IncorrectMarkerError: if validate_token set to True and received token cannot be validated """ params = { 'client_id': self.settings.esia_client_id, 'code': code, 'grant_type': 'authorization_code', 'redirect_uri': redirect_uri or self.settings.redirect_uri, 'timestamp': get_timestamp(), 'token_type': 'Bearer', 'scope': self.settings.esia_scope, 'state': state, } params = sign_params(params, certificate_file=self.settings.certificate_file, private_key_file=self.settings.private_key_file) url = '{base_url}{token_url}'.format(base_url=self.settings.esia_service_url, token_url=self._TOKEN_EXCHANGE_URL) response_json = make_request(url=url, method='POST', data=params) id_token = response_json['id_token'] if validate_token: payload = self._validate_token(id_token) else: payload = self._parse_token(id_token) return EsiaInformationConnector(access_token=response_json['access_token'], oid=self._get_user_id(payload), settings=self.settings) @staticmethod def _parse_token(token): """ :rtype: dict """ return jwt.decode(token, verify=False) @staticmethod def _get_user_id(payload): """ :param dict payload: token payload """ return payload.get('urn:esia:sbj', {}).get('urn:esia:sbj:oid') def _validate_token(self, token): """ :param str token: token to validate """ if self.settings.esia_token_check_key is None: raise ValueError("To validate token you need to specify `esia_token_check_key` in settings!") with open(self.settings.esia_token_check_key, 'r') as f: data = f.read() try: return jwt.decode(token, key=data, audience=self.settings.esia_client_id, issuer=self._ESIA_ISSUER_NAME) except InvalidTokenError as e: raise IncorrectMarkerError(e) class EsiaInformationConnector: """ Connector for fetching information from ESIA REST services. """ def __init__(self, access_token, oid, settings): """ :param str access_token: access token :param int oid: ESIA object id :param EsiaSettings settings: connector settings """ self.token = access_token self.oid = oid self.settings = settings self._rest_base_url = '%s/rs' % settings.esia_service_url def esia_request(self, endpoint_url, accept_schema=None): """ Makes request to ESIA REST service and returns response JSON data. :param str endpoint_url: endpoint url :param str or None accept_schema: optional schema (version) for response data format :rtype: dict :raises IncorrectJsonError: if response contains invalid json body :raises HttpError: if response status code is not 2XX """ headers = { 'Authorization': "Bearer %s" % self.token } if accept_schema: headers['Accept'] = 'application/json; schema="%s"' % accept_schema else: headers['Accept'] = 'application/json' return make_request(url=endpoint_url, headers=headers) def get_person_main_info(self, accept_schema=None): url = '{base}/prns/{oid}'.format(base=self._rest_base_url, oid=self.oid) return self.esia_request(endpoint_url=url, accept_schema=accept_schema) def get_person_addresses(self, accept_schema=None): url = '{base}/prns/{oid}/addrs?embed=(elements)'.format(base=self._rest_base_url, oid=self.oid) return self.esia_request(endpoint_url=url, accept_schema=accept_schema) def get_person_contacts(self, accept_schema=None): url = '{base}/prns/{oid}/ctts?embed=(elements)'.format(base=self._rest_base_url, oid=self.oid) return self.esia_request(endpoint_url=url, accept_schema=accept_schema) def get_person_documents(self, accept_schema=None): url = '{base}/prns/{oid}/docs?embed=(elements)'.format(base=self._rest_base_url, oid=self.oid) return self.esia_request(endpoint_url=url, accept_schema=accept_schema)