Beispiel #1
0
    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
Beispiel #2
0
    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
Beispiel #3
0
 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
Beispiel #4
0
    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
Beispiel #5
0
    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
Beispiel #6
0
    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
Beispiel #7
0
    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
Beispiel #8
0
    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
Beispiel #9
0
    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