def _google_authenticate( self, auth_request: auth_pb2.AuthRequest) -> auth_pb2.AuthResponse: try: id_info = token.decode_google_id_token( auth_request.google_token_id) except i18n.TranslatableException as error: flask.abort(401, error.flask_translate()) response = auth_pb2.AuthResponse() user_dict = self._user_collection.find_one( {'googleId': id_info['sub']}) if proto.parse_from_mongo(user_dict, response.authenticated_user, 'user_id'): self._handle_returning_user(response) else: is_existing_user = self._load_user_from_token_or_email( auth_request, response.authenticated_user, id_info['email']) response.authenticated_user.profile.picture_url = id_info.get( 'picture', '') response.authenticated_user.google_id = id_info['sub'] response.is_new_user = not response.authenticated_user.has_account response.authenticated_user.has_account = True if is_existing_user: self._handle_returning_user(response, force_update=True) else: self.save_new_user(response.authenticated_user, auth_request.user_data) response.auth_token = token.create_token( response.authenticated_user.user_id, 'auth') return response
def _pe_connect_authenticate( self, auth_request: auth_pb2.AuthRequest) -> auth_pb2.AuthResponse: token_data = _get_oauth2_access_token( 'https://authentification-candidat.pole-emploi.fr/connexion/oauth2/access_token?' 'realm=/individu', code=auth_request.pe_connect_code, client_id=_EMPLOI_STORE_CLIENT_ID or '', client_secret=_EMPLOI_STORE_CLIENT_SECRET or '', auth_name='PE Connect', ) if token_data.get('nonce') != auth_request.pe_connect_nonce: flask.abort(403, i18n.flask_translate('Mauvais paramètre nonce')) bearer = token_data.get('token_type', 'Bearer') access_token = token_data.get('access_token', '') authorization_header = f'{bearer} {access_token}' user_info_response = requests.get( 'https://api.emploi-store.fr/partenaire/peconnect-individu/v1/userinfo', headers={'Authorization': authorization_header}) if user_info_response.status_code < 200 or user_info_response.status_code >= 400: logging.warning('PE Connect fails (%d): "%s"', user_info_response.status_code, user_info_response.text) flask.abort(403, user_info_response.text) user_info = typing.cast(dict[str, str], user_info_response.json()) response = auth_pb2.AuthResponse() user_dict = self._user_collection.find_one( {'peConnectId': user_info['sub']}) if proto.parse_from_mongo(user_dict, response.authenticated_user, 'user_id'): self._handle_returning_user(response) else: user = response.authenticated_user is_existing_user = self._load_user_from_token_or_email( auth_request, user, user_info.get('email')) user.pe_connect_id = user_info['sub'] response.is_new_user = force_update = not user.has_account user.has_account = True if is_existing_user: self._handle_returning_user(response, force_update=force_update) else: # TODO(pascal): Handle the case where one of the name is missing. user.profile.name = french.cleanup_firstname( user_info.get('given_name', '')) user.profile.last_name = french.cleanup_firstname( user_info.get('family_name', '')) user.profile.gender = _PE_CONNECT_GENDER.get( user_info.get('gender', ''), user_profile_pb2.UNKNOWN_GENDER) self.save_new_user(user, auth_request.user_data) response.auth_token = token.create_token( response.authenticated_user.user_id, 'auth') return response
def _create_guest_user( self, auth_request: auth_pb2.AuthRequest) -> auth_pb2.AuthResponse: response = auth_pb2.AuthResponse() response.authenticated_user.profile.name = auth_request.first_name self.save_new_user(response.authenticated_user, auth_request.user_data) response.auth_token = token.create_token( response.authenticated_user.user_id, 'auth') response.is_new_user = True return response
def _token_authenticate( self, auth_request: auth_pb2.AuthRequest) -> auth_pb2.AuthResponse: instant = int(time.time()) response = auth_pb2.AuthResponse() response.hash_salt = _timestamped_hash(instant, auth_request.email) self._load_user_with_token(auth_request, response.authenticated_user) if response.authenticated_user.HasField('deleted_at'): flask.abort(404, 'Compte supprimé') response.auth_token = create_token(auth_request.user_id, 'auth') self._handle_returning_user(response) return response
def _facebook_authenticate( self, auth_request: auth_pb2.AuthRequest) -> auth_pb2.AuthResponse: user_info_response = requests.get( 'https://graph.facebook.com/v4.0/me', params=dict( fields='id,first_name,email', access_token=auth_request.facebook_access_token, )) if user_info_response.status_code < 200 or user_info_response.status_code >= 300: flask.abort( user_info_response.status_code, user_info_response.json().get('error', {}).get('message', '')) user_info = typing.cast(Dict[str, str], user_info_response.json()) response = auth_pb2.AuthResponse() user_dict = self._user_collection.find_one( {'facebookId': user_info['id']}) if proto.parse_from_mongo(user_dict, response.authenticated_user, 'user_id'): self._handle_returning_user(response) else: is_existing_user, had_email = self._load_user_from_token_or_email( auth_request, response.authenticated_user, user_info.get('email')) response.authenticated_user.facebook_id = user_info['id'] response.is_new_user = not response.authenticated_user.has_account response.authenticated_user.has_account = True if is_existing_user: self._handle_returning_user(response, force_update=True, had_email=had_email) else: response.authenticated_user.profile.name = user_info.get( 'first_name', '') self.save_new_user(response.authenticated_user, auth_request.user_data) response.auth_token = create_token(response.authenticated_user.user_id, 'auth') return response
def _email_authenticate( self, auth_request: auth_pb2.AuthRequest) -> auth_pb2.AuthResponse: instant = int(time.time()) response = auth_pb2.AuthResponse() response.hash_salt = token.timestamped_hash(instant, auth_request.email) user_dict = self._user_collection.find_one( {'hashedEmail': hash_user_email(auth_request.email)}) if not user_dict: return self._email_register(auth_request, response) user_object_id = user_dict['_id'] user_id = str(user_object_id) user_auth_dict = self._user_db.user_auth.find_one( {'_id': user_object_id}) if not auth_request.hashed_password: # User exists but did not send a password: probably just getting some fresh salt. return response if not user_auth_dict: if not auth_request.auth_token: # User is trying to connect with a password, but never created one. self.send_auth_token(user_dict) _abort_failed_login() try: token.check_token(user_id, auth_request.auth_token, role='auth') except ValueError as error: logging.info('Invalid token:\n %s', error) _abort_on_bad_auth() # User that already uses an SSO account is now trying to add a password. _parse_user_from_mongo(user_dict, response.authenticated_user) self._user_db.user_auth.insert_one({ '_id': user_object_id, 'hashedPassword': auth_request.hashed_password, }) response.auth_token = token.create_token(user_id, 'auth') response.authenticated_user.has_account = True response.authenticated_user.has_password = True response.is_password_updated = True self._handle_returning_user(response, force_update=True) return response if auth_request.user_id: # User is a guest using a pre-existing email account: # maybe they're using the same password. if auth_request.hashed_password != user_auth_dict.get( 'hashedPassword', ''): return response _parse_user_from_mongo(user_dict, response.authenticated_user) return response if auth_request.auth_token: self._reset_password(auth_request, user_id, user_auth_dict, user_dict) _parse_user_from_mongo(user_dict, response.authenticated_user) response.auth_token = token.create_token(user_id, 'auth') return response if not auth_request.hash_salt: # User exists but has not sent salt: probably just getting some fresh salt. return response # Check that salt is valid. salt = auth_request.hash_salt try: if not token.assert_valid_salt(salt, auth_request.email, instant): return response except ValueError as error: logging.info('Salt has not been generated by this server:\n %s', error) _abort_on_bad_auth() stored_hashed_password = user_auth_dict.get('hashedPassword', '') _check_password(stored_hashed_password, salt, auth_request.hashed_password) _parse_user_from_mongo(user_dict, response.authenticated_user) response.auth_token = token.create_token(user_id, 'auth') # Update the password. if auth_request.new_hashed_password: self._user_db.user_auth.replace_one( {'_id': user_object_id}, {'hashedPassword': auth_request.new_hashed_password}) response.is_password_updated = True user_dict['_id'] = user_id self.send_update_confirmation(user_dict) self._handle_returning_user(response) return response
def _linked_in_authenticate( self, auth_request: auth_pb2.AuthRequest) -> auth_pb2.AuthResponse: token_data = _get_oauth2_access_token( 'https://www.linkedin.com/oauth/v2/accessToken', code=auth_request.linked_in_code, client_id=_LINKED_IN_CLIENT_ID or '', client_secret=_LINKED_IN_CLIENT_SECRET or '', auth_name='LinkedIn Auth', ) bearer = token_data.get('token_type', 'Bearer') access_token = token_data.get('access_token', '') authorization_header = f'{bearer} {access_token}' user_info_response = requests.get( 'https://api.linkedin.com/v2/me', headers={'Authorization': authorization_header}) if user_info_response.status_code < 200 or user_info_response.status_code >= 400: logging.warning('LinkedIn Auth fails (%d): "%s"', user_info_response.status_code, user_info_response.text) flask.abort(403, user_info_response.text) user_info = typing.cast(dict[str, str], user_info_response.json()) response = auth_pb2.AuthResponse() # TODO(cyrille): Factorize with other 3rd party auth. user_dict = self._user_collection.find_one( {'linkedInId': user_info['id']}) if proto.parse_from_mongo(user_dict, response.authenticated_user, 'user_id'): self._handle_returning_user(response) else: email_response = requests.get( 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))', headers={'Authorization': authorization_header}) if email_response.status_code < 200 or email_response.status_code >= 400: logging.warning('LinkedIn Auth fails (%d): "%s"', email_response.status_code, email_response.text) flask.abort(403, email_response.text) email_response_json = typing.cast(_LinkedInEmailResponse, email_response.json()) email = email_response_json.get('handle~', {}).get('emailAddress') user = response.authenticated_user is_existing_user = self._load_user_from_token_or_email( auth_request, user, email) user.linked_in_id = user_info['id'] response.is_new_user = not user.has_account user.has_account = True if is_existing_user: self._handle_returning_user(response, force_update=True) else: # TODO(pascal): Handle the case where one of the name is missing. user.profile.name = user_info.get('localizedFirstName', '') user.profile.last_name = user_info.get('localizedLastName', '') self.save_new_user(user, auth_request.user_data) response.auth_token = token.create_token( response.authenticated_user.user_id, 'auth') return response
def _email_authenticate( self, auth_request: auth_pb2.AuthRequest) -> auth_pb2.AuthResponse: instant = int(time.time()) response = auth_pb2.AuthResponse() response.hash_salt = _timestamped_hash(instant, auth_request.email) user_dict = self._user_collection.find_one( {'hashedEmail': hash_user_email(auth_request.email)}) if not user_dict: return self._email_register(auth_request, response) user_object_id = user_dict['_id'] user_id = str(user_object_id) user_auth_dict = self._user_db.user_auth.find_one( {'_id': user_object_id}) if not auth_request.hashed_password: # User exists but did not sent a password: probably just getting some fresh salt. return response if not user_auth_dict: if not auth_request.auth_token: # User is trying to connect with a password, but never created one. self.send_auth_token(user_dict) flask.abort( 403, f'Nous avons envoyé un email à {auth_request.email} avec un lien pour se ' 'connecter', ) try: check_token(user_id, auth_request.auth_token, role='auth') except ValueError as error: flask.abort(403, f'Token invalide : {error}.') # User that already uses an SSO account is now trying to add a password. _parse_user_from_mongo(user_dict, response.authenticated_user) self._user_db.user_auth.insert_one({ '_id': user_object_id, 'hashedPassword': auth_request.hashed_password, }) response.auth_token = create_token(user_id, 'auth') response.authenticated_user.has_account = True response.authenticated_user.has_password = True response.is_password_updated = True self._handle_returning_user(response, force_update=True) return response if auth_request.user_id: # User is a guest using a pre-existing email account: # maybe they're using the same password. if auth_request.hashed_password != user_auth_dict.get( 'hashedPassword', ''): return response _parse_user_from_mongo(user_dict, response.authenticated_user) return response if auth_request.auth_token: self._reset_password(auth_request, user_id, user_auth_dict) _parse_user_from_mongo(user_dict, response.authenticated_user) response.auth_token = create_token(user_id, 'auth') return response if not auth_request.hash_salt: # User exists but has not sent salt: probably just getting some fresh salt. return response # Check that salt is valid. salt = auth_request.hash_salt try: if not _assert_valid_salt(salt, auth_request.email, instant): return response except ValueError as error: flask.abort( 403, f"Le sel n'a pas été généré par ce serveur : {error}.") stored_hashed_password = user_auth_dict.get('hashedPassword', '') hashed_password = hashlib.sha1() hashed_password.update(salt.encode('ascii')) hashed_password.update(stored_hashed_password.encode('ascii')) request_hashed_password = binascii.unhexlify( auth_request.hashed_password) if request_hashed_password != hashed_password.digest(): flask.abort(403, 'Mot de passe erroné.') _parse_user_from_mongo(user_dict, response.authenticated_user) response.auth_token = create_token(user_id, 'auth') # Update the password. if auth_request.new_hashed_password: self._user_db.user_auth.replace_one( {'_id': user_object_id}, {'hashedPassword': auth_request.new_hashed_password}) response.is_password_updated = True self._handle_returning_user(response) return response
def _pe_connect_authenticate( self, auth_request: auth_pb2.AuthRequest) -> auth_pb2.AuthResponse: token_data = _get_oauth2_access_token( 'https://authentification-candidat.pole-emploi.fr/connexion/oauth2/access_token?' 'realm=/individu', code=auth_request.pe_connect_code, client_id=_EMPLOI_STORE_CLIENT_ID or '', client_secret=_EMPLOI_STORE_CLIENT_SECRET or '', auth_name='PE Connect', ) if token_data.get('nonce') != auth_request.pe_connect_nonce: flask.abort(403, 'Mauvais paramètre nonce') bearer = token_data.get('token_type', 'Bearer') access_token = token_data.get('access_token', '') authorization_header = f'{bearer} {access_token}' scopes = token_data.get('scope', '').split(' ') user_info_response = requests.get( 'https://api.emploi-store.fr/partenaire/peconnect-individu/v1/userinfo', headers={'Authorization': authorization_header}) if user_info_response.status_code < 200 or user_info_response.status_code >= 400: logging.warning('PE Connect fails (%d): "%s"', user_info_response.status_code, user_info_response.text) flask.abort(403, user_info_response.text) user_info = typing.cast(Dict[str, str], user_info_response.json()) city = None if 'coordonnees' in scopes: coordinates_response = requests.get( 'https://api.emploi-store.fr/partenaire/peconnect-coordonnees/v1/coordonnees', headers={ 'Authorization': authorization_header, 'pe-nom-application': 'Bob Emploi', }) if coordinates_response.status_code >= 200 and coordinates_response.status_code < 400: coordinates = typing.cast(Dict[str, str], coordinates_response.json()) code_insee = coordinates.get('codeINSEE') if code_insee: clean_code_insee = _replace_arrondissement_insee_to_city( code_insee) city = geo.get_city_proto(clean_code_insee) job = None if 'competences' in scopes: competences_response = requests.get( 'https://api.emploi-store.fr/partenaire/peconnect-competences/v1/competences', headers={'Authorization': authorization_header}, ) if competences_response.status_code >= 200 and competences_response.status_code < 400: competences = typing.cast(List[Dict[str, str]], competences_response.json()) job_id, rome_id = next( ((c.get('codeAppellation'), c.get('codeRome')) for c in competences), (None, None)) if job_id and rome_id: job = jobs.get_job_proto(self._db, job_id, rome_id) response = auth_pb2.AuthResponse() user_dict = self._user_collection.find_one( {'peConnectId': user_info['sub']}) if proto.parse_from_mongo(user_dict, response.authenticated_user, 'user_id'): self._handle_returning_user(response) else: user = response.authenticated_user is_existing_user, had_email = self._load_user_from_token_or_email( auth_request, user, user_info.get('email')) user.pe_connect_id = user_info['sub'] response.is_new_user = force_update = not user.has_account user.has_account = True if city or job and not user.projects: force_update = True user.projects.add(is_incomplete=True, city=city or None, target_job=job) if is_existing_user: self._handle_returning_user(response, force_update=force_update, had_email=had_email) else: # TODO(pascal): Handle the case where one of the name is missing. user.profile.name = french.cleanup_firstname( user_info.get('given_name', '')) user.profile.last_name = french.cleanup_firstname( user_info.get('family_name', '')) user.profile.gender = \ _PE_CONNECT_GENDER.get(user_info.get('gender', ''), user_pb2.UNKNOWN_GENDER) self.save_new_user(user, auth_request.user_data) response.auth_token = create_token(response.authenticated_user.user_id, 'auth') return response