def test_create_token(self) -> None: """Basic usage of create_token.""" token_1 = token.create_token('*****@*****.**', 'login') self.assertTrue(token_1) token_2 = token.create_token('*****@*****.**', 'unsubscribe') self.assertTrue(token_2) self.assertNotEqual(token_1, token_2) token_3 = token.create_token('*****@*****.**', 'login') self.assertTrue(token_3) self.assertNotEqual(token_1, token_3)
def get_default_coaching_email_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, Any]: """Compute default variables used in all coaching emails.""" return get_default_vars(user) | { 'changeEmailSettingsUrl': get_bob_link( 'unsubscribe.html', { 'auth': auth_token.create_token(user.user_id, role='settings'), 'coachingEmailFrequency': email_pb2.EmailFrequency.Name( user.profile.coaching_email_frequency), 'hl': user.profile.locale, 'user': user.user_id, }), 'firstName': french.cleanup_firstname(user.profile.name), 'gender': user_profile_pb2.Gender.Name(user.profile.gender), # TODO(pascal): Harmonize use of URL suffix (instead of link). 'statusUpdateUrl': get_status_update_link(user), }
def test_auth_user_id_token_corrupted_data( self, mock_warning: mock.MagicMock) -> None: """Authenticate using the user ID and a token but data is corrupted.""" # Create password. user_id = self.authenticate_new_user(email='*****@*****.**', password='******', first_name='Pascal', last_name='Corpet') timed_token = token.create_token(user_id, is_using_timestamp=True) # Screw the data in Mongo. self._user_db.user.update_one({}, {'$set': {'revision': {'hack': 1}}}) response = self.app.post( '/api/user/authenticate', data=f'{{"userId": "{user_id}", "authToken": "{timed_token}"}}', content_type='application/json') self.assertEqual(500, response.status_code) self.assertIn('Les données utilisateur sont corrompues', response.get_data(as_text=True)) mock_warning.assert_called_once() self.assertIn( 'Failed to parse revision', mock_warning.call_args[0][0] % mock_warning.call_args[0][1:])
def test_auth_user_id_token_outdated(self, mock_time: mock.MagicMock) -> None: """Authenticate using the user ID and a very old token.""" now = time.time() mock_time.time.return_value = now # Create password. user_id = self.authenticate_new_user(email='*****@*****.**', password='******', first_name='Pascal', last_name='Corpet') timed_token = token.create_token(user_id, is_using_timestamp=True) # 10 days later… mock_time.time.return_value = now + 86400 * 10 response = self.app.post( '/api/user/authenticate', data=f'{{"userId": "{user_id}", "authToken": "{timed_token}"}}', content_type='application/json') self.assertEqual(498, response.status_code) self.assertIn("Token d'authentification périmé", response.get_data(as_text=True))
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 test_auth_user_id_token(self, mock_time: mock.MagicMock) -> None: """Authenticate using the user ID and a token.""" now = time.time() mock_time.time.return_value = now # Create password. user_id = self.authenticate_new_user(email='*****@*****.**', password='******', first_name='Pascal', last_name='Corpet') timed_token = token.create_token(user_id, is_using_timestamp=True) # 2 days later… mock_time.time.return_value = now + 86400 * 2 response = self.app.post( '/api/user/authenticate', data=f'{{"userId": "{user_id}", "authToken": "{timed_token}"}}', content_type='application/json') auth_response = self.json_from_response(response) self.assertEqual( 'Pascal', auth_response.get('authenticatedUser', {}).get('profile', {}).get('name')) self.assertTrue(auth_response.get('lastAccessAt'))
def setUp(self) -> None: """Create a user and get its FFS auth token.""" super().setUp() self.user_id, self.auth_token = self.create_user_with_token() self.ffs_auth_token = auth_token.create_token( self.user_id, role='first-followup-survey')
def get_default_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, Any]: """Compute default variables used in all emails: firstName, gender and unsubscribeLink.""" return { 'areEmailsAnswerRead': product.bob.are_email_answers_read, 'baseUrl': product.bob.base_url, 'firstName': french.cleanup_firstname(user.profile.name), 'gender': user_profile_pb2.Gender.Name(user.profile.gender), 'highlightColor': product.bob.get_config('highlightColor', '#faf453'), 'productLogoUrl': product.bob.get_config( 'productLogoUrl', 'https://t.bob-emploi.fr/tplimg/6u2u/b/oirn/2ugx1.png'), 'productName': product.bob.name, 'unsubscribeLink': get_bob_link( 'unsubscribe.html', { 'auth': auth_token.create_token(user.profile.email, role='unsubscribe'), 'hl': user.profile.locale, 'user': user.user_id, }), }
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 test_delete_url(self) -> None: """User can be deleted using a simple GET URL and an 'unsubscribe' auth token.""" unsub_token = auth_token.create_token(self.user_id, role='unsubscribe') response = self.app.get( f'/api/upskilling/user/delete/{self.user_id}?token={unsub_token}') self.assertLess(response.status_code, 400, msg=response.get_data(as_text=True)) self.assertIn('adresse email sera supprimée', response.get_data(as_text=True))
def test_auth_user_id_token_unknown_user(self) -> None: """Authenticate using a token but the user ID does not correspond to anything.""" user_id = str(objectid.ObjectId()) timed_token = token.create_token(user_id, is_using_timestamp=True) response = self.app.post( '/api/user/authenticate', data=f'{{"userId": "{user_id}", "authToken": "{timed_token}"}}', content_type='application/json') self.assertEqual(404, response.status_code) self.assertIn('Utilisateur inconnu', response.get_data(as_text=True))
def _token_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) self._load_user_with_token(auth_request, response.authenticated_user) if response.authenticated_user.HasField('deleted_at'): flask.abort(404, i18n.flask_translate('Compte supprimé')) response.auth_token = token.create_token(auth_request.user_id, 'auth') self._handle_returning_user(response) return response
def _email_register( self, auth_request: auth_pb2.AuthRequest, response: auth_pb2.AuthResponse) \ -> auth_pb2.AuthResponse: """Registers a new user using an email address.""" is_existing_user = self._load_user_with_token( auth_request, response.authenticated_user, is_timestamp_required=False) force_update = False if not is_existing_user and auth_request.hashed_password: if not auth_request.first_name: flask.abort( 422, i18n.flask_translate('Le champ first_name est nécessaire')) should_create_account = not is_existing_user and auth_request.hashed_password if is_existing_user or should_create_account: force_update |= response.authenticated_user.profile.email != auth_request.email response.authenticated_user.profile.email = auth_request.email response.authenticated_user.hashed_email = hash_user_email( auth_request.email) response.is_new_user = not response.authenticated_user.has_account force_update |= response.is_new_user response.authenticated_user.has_account = True force_update |= \ response.authenticated_user.has_password != bool(auth_request.hashed_password) response.authenticated_user.has_password = bool( auth_request.hashed_password) if is_existing_user: self._handle_returning_user(response, force_update=force_update) elif should_create_account: response.authenticated_user.profile.name = auth_request.first_name response.authenticated_user.profile.last_name = auth_request.last_name self.save_new_user(response.authenticated_user, auth_request.user_data) if auth_request.hashed_password: object_id = objectid.ObjectId(response.authenticated_user.user_id) self._user_db.user_auth.replace_one( {'_id': object_id}, {'hashedPassword': auth_request.hashed_password}, upsert=True) response.is_password_updated = True response.is_new_user = not is_existing_user # TODO(cyrille): Consider dropping if there's no user_id. response.auth_token = token.create_token( response.authenticated_user.user_id, 'auth') return response
def _get_nps_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, str]: user_id = user.user_id return campaign.get_default_vars(user) | { 'npsFormUrl': campaign.get_bob_link( '/api/nps', { 'user': user_id, 'token': auth_token.create_token(user_id, 'nps'), 'redirect': campaign.get_bob_link('/retours', {'hl': user.profile.locale}), }), }
def _employment_vars( user: user_pb2.User, *, now: datetime.datetime, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any, ) -> dict[str, str]: """Computes vars for a given user for the employment survey. Returns a dict with all vars required for the template. """ num_months_ago = round((now - user.registered_at.ToDatetime()).days / 30.5) if num_months_ago <= 0 and not user.features_enabled.alpha: raise campaign.DoNotSend( f'User registered only recently ({user.registered_at})') scoring_project = scoring.ScoringProject(project_pb2.Project(), user, database) registered_since = scoring_project.get_several_months_text(num_months_ago) for status in user.employment_status: if status.created_at.ToDatetime() > _ONE_MONTH_AGO: raise campaign.DoNotSend( 'User has already updated their employment status less than one month ago.' ) base_params = { 'user': user.user_id, 'token': parse.quote( auth_token.create_token(user.user_id, role='employment-status')), } return campaign.get_default_vars(user) | { 'registeredSince': registered_since, 'seekingUrl': campaign.get_bob_link( '/api/employment-status', base_params | { 'seeking': user_pb2.SeekingStatus.Name(user_pb2.STILL_SEEKING), 'redirect': campaign.get_bob_link('/statut/en-recherche'), }), 'stopSeekingUrl': campaign.get_bob_link( '/api/employment-status', base_params | { 'seeking': user_pb2.SeekingStatus.Name(user_pb2.STOP_SEEKING), 'redirect': campaign.get_bob_link('/statut/ne-recherche-plus'), }), }
def get_status_update_link(user: user_pb2.User) -> str: """Make link with token from user ID for RER status update.""" user_id = user.user_id profile = user.profile was_employed = any( [p.kind == project_pb2.FIND_ANOTHER_JOB for p in user.projects]) return get_bob_link( 'statut/mise-a-jour', { 'employed': str(was_employed), 'gender': user_profile_pb2.Gender.Name(profile.gender), 'hl': profile.locale or 'fr', 'token': auth_token.create_token(user_id, role='employment-status'), 'user': user_id, })
def test_auth_user_id_token_wrong_format_id(self) -> None: """Authenticate using a token but a wrongly formatted user ID.""" # Create password. user_id = self.authenticate_new_user(email='*****@*****.**', password='******', first_name='Pascal', last_name='Corpet') timed_token = token.create_token(user_id, is_using_timestamp=True) response = self.app.post( '/api/user/authenticate', data=f'{{"userId": "aaa", "authToken": "{timed_token}"}}', content_type='application/json') self.assertEqual(400, response.status_code) self.assertIn( 'L'identifiant utilisateur "aaa" n'a pas le bon format', response.get_data(as_text=True))
def _get_ffs_vars( user: user_pb2.User, *, now: datetime.datetime, **unused_kwargs: Any, ) -> dict[str, str]: user_id = user.user_id days_since_registered = (now - user.registered_at.ToDatetime()).days if user.net_promoter_score_survey_response.score: raise campaign.DoNotSend('User already answered the NPS survey') is_alpha = user.features_enabled.alpha if (days_since_registered < 6 or days_since_registered > 13) and not is_alpha: raise campaign.DoNotSend( 'User registered too long ago or too recently') main_challenge_id = user.projects[ 0].diagnostic.category_id if user.projects else '' return campaign.get_default_vars(user) | { 'buttonBackgroundColor': '#58bbfb', 'buttonTextColor': '#ffffff', 'ffsFormUrl': campaign.get_bob_link( '/api/first-followup-survey', { 'user': user_id, 'token': auth_token.create_token(user_id, 'first-followup-survey'), 'redirect': campaign.get_bob_link( '/first-followup-survey', { 'hl': user.profile.locale, 'gender': user_profile_pb2.Gender.Name( user.profile.gender), 'mainChallenge': main_challenge_id, }), }), }
def test_get_user(self) -> None: """Get user info.""" user_id = self.create_user_with_token( data={ 'profile': { 'name': 'Pascale', 'gender': 'FEMININE' }, 'projects': [{ 'city': { 'name': 'Lyon' }, 'createdAt': '2017-05-31T19:25:01Z', 'targetJob': { 'name': 'CTO' } }] })[0] nps_auth_token = auth_token.create_token(user_id, role='nps') response = self.app.get( f'/api/nps/user/{user_id}', headers={'Authorization': 'Bearer ' + nps_auth_token}) user = self.json_from_response(response) self.assertEqual( { 'profile': { 'gender': 'FEMININE' }, 'projects': [{ 'city': { 'name': 'Lyon' }, 'targetJob': { 'name': 'CTO' }, }], }, user)
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 = 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) 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 = token.create_token( response.authenticated_user.user_id, 'auth') return response
def get_default_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, Any]: """Default template variables for a Jobflix campaign.""" return { 'productName': product.bob.get_plugin_config('jobflix', 'productName', _PRODUCT), 'productUrl': parse.urljoin( product.bob.base_url, product.bob.get_plugin_config('jobflix', 'productUrl', 'https://www.jobflix.app')), 'senderFirstName': _COACH_FIRST_NAME, 'unsubscribeLink': campaign.get_bob_link( f'/api/upskilling/user/delete/{user.user_id}', { 'hl': user.profile.locale, 'token': parse.quote( auth_token.create_token(user.user_id, role='unsubscribe')), }), }
def test_check_token_wrong_role(self) -> None: """check_token fails if wrong role.""" login_token = token.create_token('*****@*****.**', 'login') with self.assertRaises(ValueError): token.check_token('*****@*****.**', login_token, 'unsubscribe')
def _get_vars(user: user_pb2.User, *, now: datetime.datetime, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any) -> dict[str, str]: if not user.projects or not user.projects[0].actions: raise campaign.DoNotSend('User has no project or no actions yet') project = user.projects[0] most_recent_date = max(d.ToDatetime() for d in (user.registered_at, project.created_at, project.action_plan_started_at)) if (now - most_recent_date).days > 7: raise campaign.DoNotSend( 'User has registered a while ago, too late to send the activation') # Set locale. user_locale = user.profile.locale.split('@', 1)[0] date_format = '%d %B %Y' if user_locale == 'fr' or not user_locale: locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8') elif user_locale == 'en_UK': locale.setlocale(locale.LC_ALL, 'en_GB.UTF-8') date_format = '%B %d %Y' elif user_locale == 'en': locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') date_format = '%B %d %Y' else: logging.exception('Sending an email with an unknown locale: %s', user_locale) scoring_project = scoring.ScoringProject(project, user, database, now=now) auth_token = parse.quote( token.create_token(user.user_id, is_using_timestamp=True)) settings_token = parse.quote( token.create_token(user.user_id, role='settings')) coaching_email_frequency_name = \ email_pb2.EmailFrequency.Name(user.profile.coaching_email_frequency) # This uses tutoiement by default, because its content adressed from the user to a third party # (that we assume the user is familiar enough with), not from Bob to the user. virality_template = parse.urlencode({ 'body': scoring_project.translate_static_string( 'Salut,\n\n' "Est-ce que tu connais Bob\u00A0? C'est un site qui propose de t'aider dans ta " "recherche d'emploi en te proposant un diagnostic et des conseils personnalisés. " 'Tu verras, ça vaut le coup\u00A0: en 15 minutes, tu en sauras plus sur où tu en es, ' 'et ce que tu peux faire pour avancer plus efficacement. ' "Et en plus, c'est gratuit\u00A0!\n\n" '{invite_url}\n\n' 'En tous cas, bon courage pour la suite,\n\n' '{first_name}', ).format(invite_url=parse.urljoin( product.bob.base_url, 'invite#vm2m'), first_name=user.profile.name), 'subject': scoring_project.translate_static_string("Ça m'a fait penser à toi"), }) change_email_settings_url = parse.urljoin( product.bob.base_url, 'unsubscribe.html?' + parse.urlencode({ 'user': user.user_id, 'auth': settings_token, 'coachingEmailFrequency': coaching_email_frequency_name, 'hl': user.profile.locale, })) team_members = ( 'Tabitha', 'Paul', 'John', 'Pascal', 'Sil', 'Cyrille', 'Flo', 'Nicolas', 'Florian', 'Lillie', 'Benjamin', 'Émilie', ) # Selected and pending actions. highlighted_actions = [ action for action in project.actions if action.status == action_pb2.ACTION_CURRENT ] if len(highlighted_actions) < 2: highlighted_actions.extend( action for action in project.actions if action.status == action_pb2.ACTION_UNREAD) else: highlighted_actions = sorted( highlighted_actions, key=lambda action: action.expected_completion_at.ToDatetime()) actions = [{ 'title': action.title, 'url': parse.urljoin( product.bob.base_url, f'/projet/{project.project_id}/action/{action.action_id}' f'?userId={user.user_id}&authToken={auth_token}') } for action in highlighted_actions[:2]] data: dict[str, Any] = campaign.get_default_vars(user) data |= { 'actions': actions, 'changeEmailSettingsUrl': change_email_settings_url, 'coachingEmailFrequency': email_pb2.EmailFrequency.Name(user.profile.coaching_email_frequency) if user.profile.coaching_email_frequency and user.profile.coaching_email_frequency != email_pb2.EMAIL_NONE else '', 'date': now.strftime(date_format), 'firstTeamMember': team_members[0], 'isActionPlanCompleted': project.HasField('action_plan_started_at'), 'isCoachingEnabled': 'True' if user.profile.coaching_email_frequency and user.profile.coaching_email_frequency != email_pb2.EMAIL_NONE else '', 'loginUrl': parse.urljoin(product.bob.base_url, f'?userId={user.user_id}&authToken={auth_token}'), 'numActions': len(actions), 'numberUsers': '270\u00A0000', 'numberTeamMembers': len(team_members), 'ofJob': scoring_project.populate_template('%ofJobName', raise_on_missing_var=True) if project.target_job.name else '', 'teamMembers': ', '.join(team_members[1:]), 'viralityTemplate': f'mailto:?{virality_template}' } return data
def test_check_token(self) -> None: """Basic usage of check_token (round trip with create_token).""" login_token = token.create_token('*****@*****.**', 'login') token.check_token('*****@*****.**', login_token, 'login')
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 = 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 setUp(self) -> None: """Create a user and get its nps auth token.""" super().setUp() self.user_id, self.auth_token = self.create_user_with_token() self.nps_auth_token = auth_token.create_token(self.user_id, role='nps')