def reset_password_get(auth, uid=None, token=None): """ View for user to land on the reset password page. HTTp Method: GET :param auth: the authentication state :param uid: the user id :param token: the token in verification key :return :raises: HTTPError(http.BAD_REQUEST) if verification key for the user is invalid, has expired or was used """ # if users are logged in, log them out and redirect back to this page if auth.logged_in: return auth_logout(redirect_url=request.url) # Check if request bears a valid pair of `uid` and `token` user_obj = User.load(uid) if not (user_obj and user_obj.verify_password_token(token=token)): error_data = { 'message_short': 'Invalid Request.', 'message_long': 'The requested URL is invalid, has expired, or was already used', } raise HTTPError(http.BAD_REQUEST, data=error_data) # refresh the verification key (v2) user_obj.verification_key_v2 = generate_verification_key(verification_type='password') user_obj.save() return { 'uid': user_obj._id, 'token': user_obj.verification_key_v2['token'], }
def form_valid(self, form): email = form.cleaned_data.get('emails') user = get_user(email) if user is None or user._id != self.kwargs.get('guid'): return HttpResponse( '{} with id "{}" and email "{}" not found.'.format( self.context_object_name.title(), self.kwargs.get('guid'), email ), status=409 ) reset_abs_url = furl(DOMAIN) user.verification_key_v2 = generate_verification_key(verification_type='password') user.save() reset_abs_url.path.add(('resetpassword/{}/{}'.format(user._id, user.verification_key_v2['token']))) send_mail( subject='Reset OSF Password', message='Follow this link to reset your password: {}'.format( reset_abs_url.url ), from_email=OSF_SUPPORT_EMAIL, recipient_list=[email] ) update_admin_log( user_id=self.request.user.id, object_id=user.pk, object_repr='User', message='Emailed user {} a reset link.'.format(user.pk), action_flag=USER_EMAILED ) return super(ResetPasswordView, self).form_valid(form)
def make_response_from_ticket(ticket, service_url): """ Given a CAS ticket and service URL, attempt to validate the user and return a proper redirect response. :param str ticket: CAS service ticket :param str service_url: Service URL from which the authentication request originates :return: redirect response """ service_furl = furl.furl(service_url) if 'ticket' in service_furl.args: service_furl.args.pop('ticket') client = get_client() cas_resp = client.service_validate(ticket, service_furl.url) if cas_resp.authenticated: user, external_credential, action = get_user_from_cas_resp(cas_resp) # user found and authenticated if user and action == 'authenticate': # if we successfully authenticate and a verification key is present, invalidate it if user.verification_key: user.verification_key = None user.save() # if user is authenticated by external IDP, ask CAS to authenticate user for a second time # this extra step will guarantee that 2FA are enforced # current CAS session created by external login must be cleared first before authentication if external_credential: user.verification_key = generate_verification_key() user.save() return redirect(get_logout_url(get_login_url( service_url, username=user.username, verification_key=user.verification_key ))) # if user is authenticated by CAS return authenticate( user, cas_resp.attributes['accessToken'], redirect(service_furl.url) ) # first time login from external identity provider if not user and external_credential and action == 'external_first_login': from website.util import web_url_for # orcid attributes can be marked private and not shared, default to orcid otherwise fullname = '{} {}'.format(cas_resp.attributes.get('given-names', ''), cas_resp.attributes.get('family-name', '')).strip() if not fullname: fullname = external_credential['id'] user = { 'external_id_provider': external_credential['provider'], 'external_id': external_credential['id'], 'fullname': fullname, 'access_token': cas_resp.attributes['accessToken'], } return external_first_login_authenticate( user, redirect(web_url_for('external_login_email_get')) ) # Unauthorized: ticket could not be validated, or user does not exist. return redirect(service_furl.url)
def send_claim_registered_email(claimer, unclaimed_user, node, throttle=24 * 3600): """ A registered user claiming the unclaimed user account as an contributor to a project. Send an email for claiming the account to the referrer and notify the claimer. :param claimer: the claimer :param unclaimed_user: the user account to claim :param node: the project node where the user account is claimed :param throttle: the time period in seconds before another claim for the account can be made :return: :raise: http.BAD_REQUEST """ unclaimed_record = unclaimed_user.get_unclaimed_record(node._primary_key) # check throttle timestamp = unclaimed_record.get('last_sent') if not throttle_period_expired(timestamp, throttle): raise HTTPError(http.BAD_REQUEST, data=dict( message_long='User account can only be claimed with an existing user once every 24 hours' )) # roll the valid token for each email, thus user cannot change email and approve a different email address verification_key = generate_verification_key(verification_type='claim') unclaimed_record['token'] = verification_key['token'] unclaimed_record['expires'] = verification_key['expires'] unclaimed_record['claimer_email'] = claimer.username unclaimed_user.save() referrer = User.load(unclaimed_record['referrer_id']) claim_url = web_url_for( 'claim_user_registered', uid=unclaimed_user._primary_key, pid=node._primary_key, token=unclaimed_record['token'], _external=True, ) # Send mail to referrer, telling them to forward verification link to claimer mails.send_mail( referrer.username, mails.FORWARD_INVITE_REGISTERED, user=unclaimed_user, referrer=referrer, node=node, claim_url=claim_url, fullname=unclaimed_record['name'], ) unclaimed_record['last_sent'] = get_timestamp() unclaimed_user.save() # Send mail to claimer, telling them to wait for referrer mails.send_mail( claimer.username, mails.PENDING_VERIFICATION_REGISTERED, fullname=claimer.fullname, referrer=referrer, node=node, )
def get_link(self, user): user.verification_key_v2 = generate_verification_key(verification_type='password') user.verification_key_v2['expires'] = datetime.utcnow().replace(tzinfo=pytz.utc) + timedelta(hours=48) user.save() reset_abs_url = furl(DOMAIN) reset_abs_url.path.add(('resetpassword/{}/{}'.format(user._id, user.verification_key_v2['token']))) return reset_abs_url
def forgot_password_post(): """ View for user to submit forgot password form. HTTP Method: POST :return {} """ form = ForgotPasswordForm(request.form, prefix='forgot_password') if not form.validate(): # Don't go anywhere forms.push_errors_to_status(form.errors) else: email = form.email.data status_message = ('If there is an OSF account associated with {0}, an email with instructions on how to ' 'reset the OSF password has been sent to {0}. If you do not receive an email and believe ' 'you should have, please contact OSF Support. ').format(email) kind = 'success' # check if the user exists user_obj = get_user(email=email) if user_obj: # rate limit forgot_password_post if not throttle_period_expired(user_obj.email_last_sent, settings.SEND_EMAIL_THROTTLE): status_message = 'You have recently requested to change your password. Please wait a few minutes ' \ 'before trying again.' kind = 'error' else: # TODO [OSF-6673]: Use the feature in [OSF-6998] for user to resend claim email. # if the user account is not claimed yet if (user_obj.is_invited and user_obj.unclaimed_records and not user_obj.date_last_login and not user_obj.is_claimed and not user_obj.is_registered): status_message = 'You cannot reset password on this account. Please contact OSF Support.' kind = 'error' else: # new random verification key (v2) user_obj.verification_key_v2 = generate_verification_key(verification_type='password') user_obj.email_last_sent = datetime.datetime.utcnow() user_obj.save() reset_link = furl.urljoin( settings.DOMAIN, web_url_for( 'reset_password_get', uid=user_obj._id, token=user_obj.verification_key_v2['token'] ) ) mails.send_mail( to_addr=email, mail=mails.FORGOT_PASSWORD, reset_link=reset_link ) status.push_status_message(status_message, kind=kind, trust=False) return {}
def claim_user_form(auth, **kwargs): """ View for rendering the set password page for a claimed user. Must have ``token`` as a querystring argument. Renders the set password form, validates it, and sets the user's password. HTTP Method: GET, POST """ uid, pid = kwargs['uid'], kwargs['pid'] token = request.form.get('token') or request.args.get('token') user = User.load(uid) # If unregistered user is not in database, or url bears an invalid token raise HTTP 400 error if not user or not verify_claim_token(user, token, pid): error_data = { 'message_short': 'Invalid url.', 'message_long': 'Claim user does not exists, the token in the URL is invalid or has expired.' } raise HTTPError(http.BAD_REQUEST, data=error_data) # If user is logged in, redirect to 're-enter password' page if auth.logged_in: return redirect(web_url_for('claim_user_registered', uid=uid, pid=pid, token=token)) unclaimed_record = user.unclaimed_records[pid] user.fullname = unclaimed_record['name'] user.update_guessed_names() # The email can be the original referrer email if no claimer email has been specified. claimer_email = unclaimed_record.get('claimer_email') or unclaimed_record.get('email') form = SetEmailAndPasswordForm(request.form, token=token) if request.method == 'POST': if form.validate(): username, password = claimer_email, form.password.data user.register(username=username, password=password) # Clear unclaimed records user.unclaimed_records = {} user.verification_key = generate_verification_key() user.save() # Authenticate user and redirect to project page status.push_status_message(language.CLAIMED_CONTRIBUTOR, kind='success', trust=True) # Redirect to CAS and authenticate the user with a verification key. return redirect(cas.get_login_url( web_url_for('view_project', pid=pid, _absolute=True), username=user.username, verification_key=user.verification_key )) else: forms.push_errors_to_status(form.errors) return { 'firstname': user.given_name, 'email': claimer_email if claimer_email else '', 'fullname': user.fullname, 'form': forms.utils.jsonify(form) if is_json_request() else form, }
def setUp(self): super(TestResetPassword, self).setUp() self.user = AuthUserFactory() self.another_user = AuthUserFactory() self.osf_key = generate_verification_key() self.user.verification_key = self.osf_key self.user.save() self.cas_key = None self.get_url = web_url_for('reset_password_get', verification_key=self.osf_key) self.get_url_invalid_key = web_url_for('reset_password_get', verification_key=generate_verification_key())
def get_link(self, user): user.verification_key_v2 = generate_verification_key( verification_type='password') user.verification_key_v2['expires'] = datetime.utcnow().replace( tzinfo=pytz.utc) + timedelta(hours=48) user.save() reset_abs_url = furl(DOMAIN) reset_abs_url.path.add( ('resetpassword/{}/{}'.format(user._id, user.verification_key_v2['token']))) return reset_abs_url
def main(): dry = '--dry' in sys.argv if not dry: # If we're not running in dry mode log everything to a file script_utils.add_file_logger(logger, __file__) with transaction.atomic(): qs = Registration.objects.filter(_contributors__is_registered=False, is_deleted=False) logger.info('Found {} registrations with unregistered contributors'.format(qs.count())) for registration in qs: registration_id = registration._id logger.info('Adding unclaimed_records for unregistered contributors on {}'.format(registration_id)) registered_from_id = registration.registered_from._id # Update unclaimed records for all unregistered contributors in the registration for contributor in registration.contributors.filter(is_registered=False): contrib_id = contributor._id # Most unregistered users will have a record for the registration's node record = contributor.unclaimed_records.get(registered_from_id) if not record: # Old unregistered contributors that have been removed from the original node will not have a record logger.info('No record for node {} for user {}, inferring from other data'.format(registered_from_id, contrib_id)) # Get referrer id from logs for log in registration.logs.filter(action='contributor_added').order_by('date'): if contrib_id in log.params['contributors']: referrer_id = str(OSFUser.objects.get(id=log.user_id)._id) break else: # This should not get hit. Worst outcome is that resent claim emails will fail to send via admin for this record logger.info('No record of {} in {}\'s logs.'.format(contrib_id, registration_id)) referrer_id = None verification_key = generate_verification_key(verification_type='claim') # name defaults back to name given in first unclaimed record record = { 'name': contributor.given_name, 'referrer_id': referrer_id, 'token': verification_key['token'], 'expires': verification_key['expires'], 'email': None, } logger.info('Writing new unclaimed_record entry for user {} for registration {}.'.format(contrib_id, registration_id)) contributor.unclaimed_records[registration_id] = record contributor.save() if dry: raise Exception('Abort Transaction - Dry Run') print('Done')
def forgot_password_post(auth, **kwargs): """ View for user to submit forgot password form. HTTP Method: POST """ # If user is already logged in, redirect to dashboard page. if auth.logged_in: return redirect(web_url_for('dashboard')) form = ForgotPasswordForm(request.form, prefix='forgot_password') if form.validate(): email = form.email.data status_message = ('If there is an OSF account associated with {0}, an email with instructions on how to ' 'reset the OSF password has been sent to {0}. If you do not receive an email and believe ' 'you should have, please contact OSF Support. ').format(email) # check if the user exists user_obj = get_user(email=email) if user_obj: # check forgot_password rate limit if throttle_period_expired(user_obj.email_last_sent, settings.SEND_EMAIL_THROTTLE): # new random verification key, allows OSF to check whether the reset_password request is valid, # this verification key is used twice, one for GET reset_password and one for POST reset_password # and it will be destroyed when POST reset_password succeeds user_obj.verification_key = generate_verification_key() user_obj.email_last_sent = datetime.datetime.utcnow() user_obj.save() reset_link = furl.urljoin( settings.DOMAIN, web_url_for( 'reset_password_get', verification_key=user_obj.verification_key ) ) mails.send_mail( to_addr=email, mail=mails.FORGOT_PASSWORD, reset_link=reset_link ) status.push_status_message(status_message, kind='success', trust=False) else: status.push_status_message('You have recently requested to change your password. Please wait a ' 'few minutes before trying again.', kind='error', trust=False) else: status.push_status_message(status_message, kind='success', trust=False) else: forms.push_errors_to_status(form.errors) # Don't go anywhere return {}
def setUp(self): super(TestResetPassword, self).setUp() self.user = AuthUserFactory() self.another_user = AuthUserFactory() self.osf_key_v2 = generate_verification_key(verification_type='password') self.user.verification_key_v2 = self.osf_key_v2 self.user.verification_key = None self.user.save() self.get_url = web_url_for( 'reset_password_get', uid=self.user._id, token=self.osf_key_v2['token'] ) self.get_url_invalid_key = web_url_for( 'reset_password_get', uid=self.user._id, token=generate_verification_key() ) self.get_url_invalid_user = web_url_for( 'reset_password_get', uid=self.another_user._id, token=self.osf_key_v2['token'] )
def forgot_password_post(): """ View for user to submit forgot password form. HTTP Method: POST :return {} """ form = ForgotPasswordForm(request.form, prefix='forgot_password') if not form.validate(): # Don't go anywhere forms.push_errors_to_status(form.errors) else: email = form.email.data status_message = ( 'If there is an OSF account associated with {0}, an email with instructions on how to ' 'reset the OSF password has been sent to {0}. If you do not receive an email and believe ' 'you should have, please contact OSF Support. ').format(email) kind = 'success' # check if the user exists user_obj = get_user(email=email) if user_obj: # rate limit forgot_password_post if not throttle_period_expired(user_obj.email_last_sent, settings.SEND_EMAIL_THROTTLE): status_message = 'You have recently requested to change your password. Please wait a few minutes ' \ 'before trying again.' kind = 'error' # TODO [OSF-6673]: Use the feature in [OSF-6998] for user to resend claim email. elif user_obj.is_active: # new random verification key (v2) user_obj.verification_key_v2 = generate_verification_key( verification_type='password') user_obj.email_last_sent = timezone.now() user_obj.save() reset_link = furl.urljoin( settings.DOMAIN, web_url_for('reset_password_get', uid=user_obj._id, token=user_obj.verification_key_v2['token'])) mails.send_mail( to_addr=email, mail=mails.FORGOT_PASSWORD, reset_link=reset_link, can_change_preferences=False, ) status.push_status_message(status_message, kind=kind, trust=False) return {}
def reset_password_post(uid=None, token=None): """ View for user to submit reset password form. HTTP Method: POST :param uid: the user id :param token: the token in verification key :return: :raises: HTTPError(http.BAD_REQUEST) if verification key for the user is invalid, has expired or was used """ form = ResetPasswordForm(request.form) # Check if request bears a valid pair of `uid` and `token` user_obj = User.load(uid) if not (user_obj and user_obj.verify_password_token(token=token)): error_data = { 'message_short': 'Invalid Request.', 'message_long': 'The requested URL is invalid, has expired, or was already used', } raise HTTPError(http.BAD_REQUEST, data=error_data) if not form.validate(): # Don't go anywhere forms.push_errors_to_status(form.errors) else: # clear verification key (v2) user_obj.verification_key_v2 = {} # new verification key (v1) for CAS user_obj.verification_key = generate_verification_key(verification_type=None) try: user_obj.set_password(form.password.data) user_obj.save() except exceptions.ChangePasswordError as error: for message in error.messages: status.push_status_message(message, kind='warning', trust=False) else: status.push_status_message('Password reset', kind='success', trust=False) # redirect to CAS and authenticate the user automatically with one-time verification key. return redirect(cas.get_login_url( web_url_for('user_account', _absolute=True), username=user_obj.username, verification_key=user_obj.verification_key )) return { 'uid': user_obj._id, 'token': user_obj.verification_key_v2['token'], }
def create_fake_user(): email = fake.email() name = fake.name() parsed = impute_names(name) user = UserFactory(username=email, fullname=name, is_registered=True, is_claimed=True, verification_key=generate_verification_key(), date_registered=fake.date_time(), emails=[email], **parsed) user.set_password('faker123') user.save() return user
def create_fake_user(): email = fake.email() name = fake.name() parsed = utils.impute_names(name) user = UserFactory(username=email, fullname=name, is_registered=True, is_claimed=True, verification_key=generate_verification_key(), date_registered=fake.date_time(), emails=[email], **parsed ) user.set_password('faker123') user.save() logger.info('Created user: {0} <{1}>'.format(user.fullname, user.username)) return user
def create_fake_user(): email = fake.email() name = fake.name() parsed = utils.impute_names(name) user = UserFactory(username=email, fullname=name, is_registered=True, is_claimed=True, verification_key=generate_verification_key(), date_registered=fake.date_time(), emails=[email], **parsed) user.set_password('faker123') user.save() logger.info('Created user: {0} <{1}>'.format(user.fullname, user.username)) return user
def reset_password_post(auth, verification_key=None, **kwargs): """ View for user to submit reset password form. HTTP Method: POST :raises: HTTPError(http.BAD_REQUEST) if verification_key is invalid """ # If user is already logged in, log user out if auth.logged_in: return auth_logout(redirect_url=request.url) form = ResetPasswordForm(request.form) # Check if request bears a valid verification_key user_obj = get_user(verification_key=verification_key) if not user_obj: error_data = { 'message_short': 'Invalid url.', 'message_long': 'The verification key in the URL is invalid or has expired.' } raise HTTPError(400, data=error_data) if form.validate(): # new random verification key, allows CAS to authenticate the user w/o password, one-time only. # this overwrite also invalidates the verification key generated by forgot_password_post user_obj.verification_key = generate_verification_key() try: user_obj.set_password(form.password.data) user_obj.save() except exceptions.ChangePasswordError as error: for message in error.messages: status.push_status_message(message, kind='warning', trust=False) else: status.push_status_message('Password reset', kind='success', trust=False) # redirect to CAS and authenticate the user with the one-time verification key. return redirect(cas.get_login_url( web_url_for('user_account', _absolute=True), username=user_obj.username, verification_key=user_obj.verification_key )) else: forms.push_errors_to_status(form.errors) # Don't go anywhere return { 'verification_key': verification_key }, 400
def create_fake_user(): email = fake.email() name = fake.name() parsed = impute_names(name) user = UserFactory( username=email, fullname=name, is_registered=True, is_claimed=True, verification_key=generate_verification_key(), date_registered=fake.date_time(), emails=[email], **parsed ) user.set_password('faker123') user.save() return user
def _reset_password_get(auth, uid=None, token=None, institutional=False): """ View for user to land on the reset password page. Takes a unique token generated by the forgot-password page and (if valid) grants the user a new temporary token to allow them to reset their password. User is redirected to the reset password page. If token is not valid, returns an error page and 400 Bad Request status code. HTTP Method: GET :param auth: the authentication state :param uid: the user id :param token: the token in verification key :param institutional: is this the institutional reset password page? :return :raises: HTTPError(http_status.HTTP_400_BAD_REQUEST) if verification key for the user is invalid, has expired or was used """ # if users are logged in, log them out and redirect back to this page if auth.logged_in: return auth_logout(redirect_url=request.url) # Check if request bears a valid pair of `uid` and `token` user_obj = OSFUser.load(uid) if not (user_obj and user_obj.verify_password_token(token=token)): error_data = { 'message_short': 'Invalid Request.', 'message_long': 'The requested URL is invalid, has expired, or was already used', } raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=error_data) # refresh the verification key (v2) user_obj.verification_key_v2 = generate_verification_key(verification_type='password') user_obj.save() # override routes.py login_url to redirect to dashboard service_url = web_url_for('dashboard', _absolute=True) return { 'uid': user_obj._id, 'token': user_obj.verification_key_v2['token'], 'login_url': service_url, 'isInstitutional': institutional, # view knows paths better than template 'resetPath': 'resetpassword-institution' if institutional else 'resetpassword', }
def get_or_create_user(fullname, address, is_spam=False): """Get or create user by email address. :param str fullname: User full name :param str address: User email address :param bool is_spam: User flagged as potential spam :return: Tuple of (user, created) """ user = get_user(email=address) if user: return user, False else: password = str(uuid.uuid4()) user = User.create_confirmed(address, password, fullname) user.verification_key = generate_verification_key() if is_spam: user.system_tags.append('is_spam') return user, True
def get_or_create_user(fullname, address, reset_password=True, is_spam=False): """ Get or create user by fullname and email address. :param str fullname: user full name :param str address: user email address :param boolean reset_password: ask user to reset their password :param bool is_spam: user flagged as potential spam :return: tuple of (user, created) """ user = get_user(email=address) if user: return user, False else: password = str(uuid.uuid4()) user = User.create_confirmed(address, password, fullname) if password: user.verification_key_v2 = generate_verification_key(verification_type='password') if is_spam: user.system_tags.append('is_spam') return user, True
def reset_password_get(auth, uid=None, token=None): """ View for user to land on the reset password page. HTTp Method: GET :param auth: the authentication state :param uid: the user id :param token: the token in verification key :return :raises: HTTPError(http_status.HTTP_400_BAD_REQUEST) if verification key for the user is invalid, has expired or was used """ # if users are logged in, log them out and redirect back to this page if auth.logged_in: return auth_logout(redirect_url=request.url) # Check if request bears a valid pair of `uid` and `token` user_obj = OSFUser.load(uid) if not (user_obj and user_obj.verify_password_token(token=token)): error_data = { 'message_short': 'Invalid Request.', 'message_long': 'The requested URL is invalid, has expired, or was already used', } raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=error_data) # refresh the verification key (v2) user_obj.verification_key_v2 = generate_verification_key( verification_type='password') user_obj.save() #override routes.py login_url to redirect to dashboard service_url = web_url_for('dashboard', _absolute=True) return { 'uid': user_obj._id, 'token': user_obj.verification_key_v2['token'], 'login_url': service_url, }
def get_or_create_user(fullname, address, reset_password=True, is_spam=False): """ Get or create user by fullname and email address. :param str fullname: user full name :param str address: user email address :param boolean reset_password: ask user to reset their password :param bool is_spam: user flagged as potential spam :return: tuple of (user, created) """ user = get_user(email=address) if user: return user, False else: password = str(uuid.uuid4()) user = User.create_confirmed(address, password, fullname) if password: user.verification_key_v2 = generate_verification_key( verification_type='password') if is_spam: user.system_tags.append('is_spam') return user, True
def get_or_create_user(fullname, address, reset_password=True, is_spam=False): """ Get or create user by fullname and email address. :param str fullname: user full name :param str address: user email address :param boolean reset_password: ask user to reset their password :param bool is_spam: user flagged as potential spam :return: tuple of (user, created) """ from osf.models import OSFUser user = get_user(email=address) if user: return user, False else: password = str(uuid.uuid4()) user = OSFUser.create_confirmed(address, password, fullname) if reset_password: user.verification_key_v2 = generate_verification_key(verification_type='password') if is_spam: user.save() # need to save in order to add a tag user.add_system_tag('is_spam') return user, True
def post(self, request, *args, **kwargs): email = self.request.POST['emails'] user = get_user(email) url = furl(DOMAIN) user.verification_key_v2 = generate_verification_key( verification_type='password') user.save() url.path.add( f'resetpassword/{user._id}/{user.verification_key_v2["token"]}') send_mail( subject='Reset OSF Password', message=f'Follow this link to reset your password: {url.url}', from_email=OSF_SUPPORT_EMAIL, recipient_list=[email]) update_admin_log(user_id=self.request.user.id, object_id=user.pk, object_repr='User', message=f'Emailed user {user.pk} a reset link.', action_flag=USER_EMAILED) return redirect(self.get_success_url())
def external_login_confirm_email_get(auth, uid, token): """ View for email confirmation links when user first login through external identity provider. HTTP Method: GET When users click the confirm link, they are expected not to be logged in. If not, they will be logged out first and redirected back to this view. After OSF verifies the link and performs all actions, they will be automatically logged in through CAS and redirected back to this view again being authenticated. :param auth: the auth context :param uid: the user's primary key :param token: the verification token """ user = OSFUser.load(uid) if not user: raise HTTPError(http.BAD_REQUEST) destination = request.args.get('destination') if not destination: raise HTTPError(http.BAD_REQUEST) # if user is already logged in if auth and auth.user: # if it is a wrong user if auth.user._id != user._id: return auth_logout(redirect_url=request.url) # if it is the expected user new = request.args.get('new', None) if destination in campaigns.get_campaigns(): # external domain takes priority campaign_url = campaigns.external_campaign_url_for(destination) if not campaign_url: campaign_url = campaigns.campaign_url_for(destination) return redirect(campaign_url) if new: status.push_status_message(language.WELCOME_MESSAGE, kind='default', jumbotron=True, trust=True) return redirect(web_url_for('dashboard')) # token is invalid if token not in user.email_verifications: raise HTTPError(http.BAD_REQUEST) verification = user.email_verifications[token] email = verification['email'] provider = verification['external_identity'].keys()[0] provider_id = verification['external_identity'][provider].keys()[0] # wrong provider if provider not in user.external_identity: raise HTTPError(http.BAD_REQUEST) external_status = user.external_identity[provider][provider_id] try: ensure_external_identity_uniqueness(provider, provider_id, user) except ValidationError as e: raise HTTPError(http.FORBIDDEN, e.message) if not user.is_registered: user.register(email) if not user.emails.filter(address=email.lower()): user.emails.create(address=email.lower()) user.date_last_logged_in = timezone.now() user.external_identity[provider][provider_id] = 'VERIFIED' user.social[provider.lower()] = provider_id del user.email_verifications[token] user.verification_key = generate_verification_key() user.save() service_url = request.url if external_status == 'CREATE': mails.send_mail(to_addr=user.username, mail=mails.WELCOME, mimetype='html', user=user) service_url += '&{}'.format(urllib.urlencode({'new': 'true'})) elif external_status == 'LINK': mails.send_mail( user=user, to_addr=user.username, mail=mails.EXTERNAL_LOGIN_LINK_SUCCESS, external_id_provider=provider, ) # redirect to CAS and authenticate the user with the verification key return redirect( cas.get_login_url(service_url, username=user.username, verification_key=user.verification_key))
def send_claim_email(email, unclaimed_user, node, notify=True, throttle=24 * 3600, email_template="default"): """ Unregistered user claiming a user account as an contributor to a project. Send an email for claiming the account. Either sends to the given email or the referrer's email, depending on the email address provided. :param str email: The address given in the claim user form :param User unclaimed_user: The User record to claim. :param Node node: The node where the user claimed their account. :param bool notify: If True and an email is sent to the referrer, an email will also be sent to the invited user about their pending verification. :param int throttle: Time period (in seconds) after the referrer is emailed during which the referrer will not be emailed again. :param str email_template: the email template to use :return :raise http.BAD_REQUEST """ claimer_email = email.lower().strip() unclaimed_record = unclaimed_user.get_unclaimed_record(node._primary_key) referrer = User.load(unclaimed_record["referrer_id"]) claim_url = unclaimed_user.get_claim_url(node._primary_key, external=True) # When adding the contributor, the referrer provides both name and email. # The given email is the same provided by user, just send to that email. if unclaimed_record.get("email") == claimer_email: mail_tpl = getattr(mails, "INVITE_{}".format(email_template.upper())) to_addr = claimer_email unclaimed_record["claimer_email"] = claimer_email unclaimed_user.save() # When adding the contributor, the referred only provides the name. # The account is later claimed by some one who provides the email. # Send email to the referrer and ask her/him to forward the email to the user. else: # check throttle timestamp = unclaimed_record.get("last_sent") if not throttle_period_expired(timestamp, throttle): raise HTTPError( http.BAD_REQUEST, data=dict(message_long="User account can only be claimed with an existing user once every 24 hours"), ) # roll the valid token for each email, thus user cannot change email and approve a different email address verification_key = generate_verification_key(verification_type="claim") unclaimed_record["last_sent"] = get_timestamp() unclaimed_record["token"] = verification_key["token"] unclaimed_record["expires"] = verification_key["expires"] unclaimed_record["claimer_email"] = claimer_email unclaimed_user.save() claim_url = unclaimed_user.get_claim_url(node._primary_key, external=True) # send an email to the invited user without `claim_url` if notify: pending_mail = mails.PENDING_VERIFICATION mails.send_mail( claimer_email, pending_mail, user=unclaimed_user, referrer=referrer, fullname=unclaimed_record["name"], node=node, ) mail_tpl = mails.FORWARD_INVITE to_addr = referrer.username # send an email to the referrer with `claim_url` mails.send_mail( to_addr, mail_tpl, user=unclaimed_user, referrer=referrer, node=node, claim_url=claim_url, email=claimer_email, fullname=unclaimed_record["name"], ) return to_addr
def make_response_from_ticket(ticket, service_url): """ Given a CAS ticket and service URL, attempt to validate the user and return a proper redirect response. :param str ticket: CAS service ticket :param str service_url: Service URL from which the authentication request originates :return: redirect response """ service_furl = furl.furl(service_url) # `service_url` is guaranteed to be removed of `ticket` parameter, which has been pulled off in # `framework.sessions.before_request()`. if 'ticket' in service_furl.args: service_furl.args.pop('ticket') client = get_client() cas_resp = client.service_validate(ticket, service_furl.url) if cas_resp.authenticated: user, external_credential, action = get_user_from_cas_resp(cas_resp) user_updates = {} # serialize updates to user to be applied async # user found and authenticated if user and action == 'authenticate': print_cas_log( f'CAS response - authenticating user: user=[{user._id}], ' f'external=[{external_credential}], action=[{action}]', LogLevel.INFO, ) # If users check the TOS consent checkbox via CAS, CAS sets the attribute `termsOfServiceChecked` to `true` # and then release it to OSF among other authentication attributes. When OSF receives it, it trusts CAS and # updates the user object if this is THE FINAL STEP of the login flow. DON'T update TOS consent status when # `external_credential == true` (i.e. w/ `action == 'authenticate'` or `action == 'external_first_login'`) # since neither is the final step of a login flow. tos_checked_via_cas = cas_resp.attributes.get( 'termsOfServiceChecked', 'false') == 'true' if tos_checked_via_cas: user_updates['accepted_terms_of_service'] = timezone.now() print_cas_log( f'CAS TOS consent checked: {user.guids.first()._id}, {user.username}', LogLevel.INFO) # if we successfully authenticate and a verification key is present, invalidate it if user.verification_key: user_updates['verification_key'] = None # if user is authenticated by external IDP, ask CAS to authenticate user for a second time # this extra step will guarantee that 2FA are enforced # current CAS session created by external login must be cleared first before authentication if external_credential: user.verification_key = generate_verification_key() user.save() print_cas_log( f'CAS response - redirect existing external IdP login to verification key login: user=[{user._id}]', LogLevel.INFO) return redirect( get_logout_url( get_login_url(service_url, username=user.username, verification_key=user.verification_key))) # if user is authenticated by CAS # TODO [CAS-27]: Remove Access Token From Service Validation print_cas_log( f'CAS response - finalizing authentication: user=[{user._id}]', LogLevel.INFO) return authenticate(user, cas_resp.attributes.get('accessToken', ''), redirect(service_furl.url), user_updates) # first time login from external identity provider if not user and external_credential and action == 'external_first_login': print_cas_log( f'CAS response - first login from external IdP: ' f'external=[{external_credential}], action=[{action}]', LogLevel.INFO, ) from website.util import web_url_for # orcid attributes can be marked private and not shared, default to orcid otherwise fullname = u'{} {}'.format( cas_resp.attributes.get('given-names', ''), cas_resp.attributes.get('family-name', '')).strip() # TODO [CAS-27]: Remove Access Token From Service Validation user = { 'external_id_provider': external_credential['provider'], 'external_id': external_credential['id'], 'fullname': fullname, 'access_token': cas_resp.attributes.get('accessToken', ''), 'service_url': service_furl.url, } print_cas_log( f'CAS response - creating anonymous session: external=[{external_credential}]', LogLevel.INFO) return external_first_login_authenticate( user, redirect(web_url_for('external_login_email_get'))) # Unauthorized: ticket could not be validated, or user does not exist. print_cas_log( 'Ticket validation failed or user does not exist. Redirect back to service URL (logged out).', LogLevel.ERROR) return redirect(service_furl.url)
def external_login_confirm_email_get(auth, uid, token): """ View for email confirmation links when user first login through external identity provider. HTTP Method: GET When users click the confirm link, they are expected not to be logged in. If not, they will be logged out first and redirected back to this view. After OSF verifies the link and performs all actions, they will be automatically logged in through CAS and redirected back to this view again being authenticated. :param auth: the auth context :param uid: the user's primary key :param token: the verification token """ user = User.load(uid) if not user: raise HTTPError(http.BAD_REQUEST) destination = request.args.get('destination') if not destination: raise HTTPError(http.BAD_REQUEST) # if user is already logged in if auth and auth.user: # if it is a wrong user if auth.user._id != user._id: return auth_logout(redirect_url=request.url) # if it is the expected user new = request.args.get('new', None) if destination in campaigns.get_campaigns(): return redirect(campaigns.campaign_url_for(destination)) if new: status.push_status_message(language.WELCOME_MESSAGE, kind='default', jumbotron=True, trust=True) return redirect(web_url_for('dashboard')) # token is invalid if token not in user.email_verifications: raise HTTPError(http.BAD_REQUEST) verification = user.email_verifications[token] email = verification['email'] provider = verification['external_identity'].keys()[0] provider_id = verification['external_identity'][provider].keys()[0] # wrong provider if provider not in user.external_identity: raise HTTPError(http.BAD_REQUEST) external_status = user.external_identity[provider][provider_id] try: ensure_external_identity_uniqueness(provider, provider_id, user) except ValidationError as e: raise HTTPError(http.FORBIDDEN, e.message) if not user.is_registered: user.set_password(uuid.uuid4(), notify=False) user.register(email) if email.lower() not in user.emails: user.emails.append(email.lower()) user.date_last_logged_in = datetime.datetime.utcnow() user.external_identity[provider][provider_id] = 'VERIFIED' user.social[provider.lower()] = provider_id del user.email_verifications[token] user.verification_key = generate_verification_key() user.save() service_url = request.url if external_status == 'CREATE': mails.send_mail( to_addr=user.username, mail=mails.WELCOME, mimetype='html', user=user ) service_url += '&{}'.format(urllib.urlencode({'new': 'true'})) elif external_status == 'LINK': mails.send_mail( user=user, to_addr=user.username, mail=mails.EXTERNAL_LOGIN_LINK_SUCCESS, external_id_provider=provider, ) # redirect to CAS and authenticate the user with the verification key return redirect(cas.get_login_url( service_url, username=user.username, verification_key=user.verification_key ))
def confirm_email_get(token, auth=None, **kwargs): """ View for email confirmation links. Authenticates and redirects to user settings page if confirmation is successful, otherwise shows an "Expired Link" error. HTTP Method: GET """ user = User.load(kwargs['uid']) is_merge = 'confirm_merge' in request.args is_initial_confirmation = not user.date_confirmed log_out = request.args.get('logout', None) if user is None: raise HTTPError(http.NOT_FOUND) # if the user is merging or adding an email (they already are an osf user) if log_out: return auth_email_logout(token, user) if auth and auth.user and (auth.user._id == user._id or auth.user._id == user.merged_by._id): if not is_merge: # determine if the user registered through a campaign campaign = campaigns.campaign_for_user(user) if campaign: return redirect(campaigns.campaign_url_for(campaign)) # go to home page with push notification if len(auth.user.emails) == 1 and len(auth.user.email_verifications) == 0: status.push_status_message(language.WELCOME_MESSAGE, kind='default', jumbotron=True, trust=True) if token in auth.user.email_verifications: status.push_status_message(language.CONFIRM_ALTERNATE_EMAIL_ERROR, kind='danger', trust=True) return redirect(web_url_for('index')) status.push_status_message(language.MERGE_COMPLETE, kind='success', trust=False) return redirect(web_url_for('user_account')) try: user.confirm_email(token, merge=is_merge) except exceptions.EmailConfirmTokenError as e: raise HTTPError(http.BAD_REQUEST, data={ 'message_short': e.message_short, 'message_long': e.message_long }) if is_initial_confirmation: user.date_last_login = datetime.datetime.utcnow() user.save() # send out our welcome message mails.send_mail( to_addr=user.username, mail=mails.WELCOME, mimetype='html', user=user ) # new random verification key, allows CAS to authenticate the user w/o password one-time only. user.verification_key = generate_verification_key() user.save() # redirect to CAS and authenticate the user with a verification key. return redirect(cas.get_login_url( request.url, username=user.username, verification_key=user.verification_key ))
def main(): dry = '--dry' in sys.argv if not dry: # If we're not running in dry mode log everything to a file script_utils.add_file_logger(logger, __file__) with transaction.atomic(): qs = Registration.objects.filter(_contributors__is_registered=False, is_deleted=False) logger.info( 'Found {} registrations with unregistered contributors'.format( qs.count())) for registration in qs: registration_id = registration._id logger.info( 'Adding unclaimed_records for unregistered contributors on {}'. format(registration_id)) registered_from_id = registration.registered_from._id # Update unclaimed records for all unregistered contributors in the registration for contributor in registration.contributors.filter( is_registered=False): contrib_id = contributor._id # Most unregistered users will have a record for the registration's node record = contributor.unclaimed_records.get(registered_from_id) if not record: # Old unregistered contributors that have been removed from the original node will not have a record logger.info( 'No record for node {} for user {}, inferring from other data' .format(registered_from_id, contrib_id)) # Get referrer id from logs for log in registration.logs.filter( action='contributor_added').order_by('date'): if contrib_id in log.params['contributors']: referrer_id = str( OSFUser.objects.get(id=log.user_id)._id) break else: # This should not get hit. Worst outcome is that resent claim emails will fail to send via admin for this record logger.info('No record of {} in {}\'s logs.'.format( contrib_id, registration_id)) referrer_id = None verification_key = generate_verification_key( verification_type='claim') # name defaults back to name given in first unclaimed record record = { 'name': contributor.given_name, 'referrer_id': referrer_id, 'token': verification_key['token'], 'expires': verification_key['expires'], 'email': None, } logger.info( 'Writing new unclaimed_record entry for user {} for registration {}.' .format(contrib_id, registration_id)) contributor.unclaimed_records[registration_id] = record contributor.save() if dry: raise Exception('Abort Transaction - Dry Run') print('Done')
def claim_user_form(auth, **kwargs): """ View for rendering the set password page for a claimed user. Must have ``token`` as a querystring argument. Renders the set password form, validates it, and sets the user's password. HTTP Method: GET, POST """ uid, pid = kwargs["uid"], kwargs["pid"] token = request.form.get("token") or request.args.get("token") user = User.load(uid) # If unregistered user is not in database, or url bears an invalid token raise HTTP 400 error if not user or not verify_claim_token(user, token, pid): error_data = { "message_short": "Invalid url.", "message_long": "Claim user does not exists, the token in the URL is invalid or has expired.", } raise HTTPError(http.BAD_REQUEST, data=error_data) # If user is logged in, redirect to 're-enter password' page if auth.logged_in: return redirect(web_url_for("claim_user_registered", uid=uid, pid=pid, token=token)) unclaimed_record = user.unclaimed_records[pid] user.fullname = unclaimed_record["name"] user.update_guessed_names() # The email can be the original referrer email if no claimer email has been specified. claimer_email = unclaimed_record.get("claimer_email") or unclaimed_record.get("email") # If there is a registered user with this email, redirect to 're-enter password' page found_by_email = User.find_by_email(claimer_email) user_from_email = found_by_email[0] if found_by_email else None if user_from_email and user_from_email.is_registered: return redirect(web_url_for("claim_user_registered", uid=uid, pid=pid, token=token)) form = SetEmailAndPasswordForm(request.form, token=token) if request.method == "POST": if not form.validate(): forms.push_errors_to_status(form.errors) elif settings.RECAPTCHA_SITE_KEY and not validate_recaptcha( request.form.get("g-recaptcha-response"), remote_ip=request.remote_addr ): status.push_status_message("Invalid captcha supplied.", kind="error") else: username, password = claimer_email, form.password.data if not username: raise HTTPError( http.BAD_REQUEST, data=dict( message_long="No email associated with this account. Please claim this " "account on the project to which you were invited." ), ) user.register(username=username, password=password) # Clear unclaimed records user.unclaimed_records = {} user.verification_key = generate_verification_key() user.save() # Authenticate user and redirect to project page status.push_status_message(language.CLAIMED_CONTRIBUTOR, kind="success", trust=True) # Redirect to CAS and authenticate the user with a verification key. return redirect( cas.get_login_url( web_url_for("view_project", pid=pid, _absolute=True), username=user.username, verification_key=user.verification_key, ) ) return { "firstname": user.given_name, "email": claimer_email if claimer_email else "", "fullname": user.fullname, "form": forms.utils.jsonify(form) if is_json_request() else form, }
def send_claim_email(email, unclaimed_user, node, notify=True, throttle=24 * 3600, email_template='default'): """ Unregistered user claiming a user account as an contributor to a project. Send an email for claiming the account. Either sends to the given email or the referrer's email, depending on the email address provided. :param str email: The address given in the claim user form :param User unclaimed_user: The User record to claim. :param Node node: The node where the user claimed their account. :param bool notify: If True and an email is sent to the referrer, an email will also be sent to the invited user about their pending verification. :param int throttle: Time period (in seconds) after the referrer is emailed during which the referrer will not be emailed again. :param str email_template: the email template to use :return :raise http.BAD_REQUEST """ claimer_email = email.lower().strip() unclaimed_record = unclaimed_user.get_unclaimed_record(node._primary_key) referrer = OSFUser.load(unclaimed_record['referrer_id']) claim_url = unclaimed_user.get_claim_url(node._primary_key, external=True) # Option 1: # When adding the contributor, the referrer provides both name and email. # The given email is the same provided by user, just send to that email. preprint_provider = None logo = None if unclaimed_record.get('email') == claimer_email: # check email template for branded preprints if email_template == 'preprint': email_template, preprint_provider = find_preprint_provider(node) if not email_template or not preprint_provider: return mail_tpl = getattr(mails, 'INVITE_PREPRINT')(email_template, preprint_provider) if preprint_provider._id == 'osf': logo = settings.OSF_PREPRINTS_LOGO else: logo = preprint_provider._id else: mail_tpl = getattr(mails, 'INVITE_DEFAULT'.format(email_template.upper())) to_addr = claimer_email unclaimed_record['claimer_email'] = claimer_email unclaimed_user.save() # Option 2: # TODO: [new improvement ticket] this option is disabled from preprint but still available on the project page # When adding the contributor, the referred only provides the name. # The account is later claimed by some one who provides the email. # Send email to the referrer and ask her/him to forward the email to the user. else: # check throttle timestamp = unclaimed_record.get('last_sent') if not throttle_period_expired(timestamp, throttle): raise HTTPError(http.BAD_REQUEST, data=dict( message_long='User account can only be claimed with an existing user once every 24 hours' )) # roll the valid token for each email, thus user cannot change email and approve a different email address verification_key = generate_verification_key(verification_type='claim') unclaimed_record['last_sent'] = get_timestamp() unclaimed_record['token'] = verification_key['token'] unclaimed_record['expires'] = verification_key['expires'] unclaimed_record['claimer_email'] = claimer_email unclaimed_user.save() claim_url = unclaimed_user.get_claim_url(node._primary_key, external=True) # send an email to the invited user without `claim_url` if notify: pending_mail = mails.PENDING_VERIFICATION mails.send_mail( claimer_email, pending_mail, user=unclaimed_user, referrer=referrer, fullname=unclaimed_record['name'], node=node, can_change_preferences=False, osf_contact_email=settings.OSF_CONTACT_EMAIL, ) mail_tpl = mails.FORWARD_INVITE to_addr = referrer.username # Send an email to the claimer (Option 1) or to the referrer (Option 2) with `claim_url` mails.send_mail( to_addr, mail_tpl, user=unclaimed_user, referrer=referrer, node=node, claim_url=claim_url, email=claimer_email, fullname=unclaimed_record['name'], branded_service=preprint_provider, can_change_preferences=False, logo=logo if logo else settings.OSF_LOGO, osf_contact_email=settings.OSF_CONTACT_EMAIL, ) return to_addr
def claim_user_form(auth, **kwargs): """ View for rendering the set password page for a claimed user. Must have ``token`` as a querystring argument. Renders the set password form, validates it, and sets the user's password. HTTP Method: GET, POST """ uid, pid = kwargs['uid'], kwargs['pid'] token = request.form.get('token') or request.args.get('token') user = OSFUser.load(uid) # If unregistered user is not in database, or url bears an invalid token raise HTTP 400 error if not user or not verify_claim_token(user, token, pid): error_data = { 'message_short': 'Invalid url.', 'message_long': 'Claim user does not exists, the token in the URL is invalid or has expired.' } raise HTTPError(http.BAD_REQUEST, data=error_data) # If user is logged in, redirect to 're-enter password' page if auth.logged_in: return redirect(web_url_for('claim_user_registered', uid=uid, pid=pid, token=token)) unclaimed_record = user.unclaimed_records[pid] user.fullname = unclaimed_record['name'] user.update_guessed_names() # The email can be the original referrer email if no claimer email has been specified. claimer_email = unclaimed_record.get('claimer_email') or unclaimed_record.get('email') # If there is a registered user with this email, redirect to 're-enter password' page try: user_from_email = OSFUser.objects.get(emails__address=claimer_email.lower().strip()) if claimer_email else None except OSFUser.DoesNotExist: user_from_email = None if user_from_email and user_from_email.is_registered: return redirect(web_url_for('claim_user_registered', uid=uid, pid=pid, token=token)) form = SetEmailAndPasswordForm(request.form, token=token) if request.method == 'POST': if not form.validate(): forms.push_errors_to_status(form.errors) elif settings.RECAPTCHA_SITE_KEY and not validate_recaptcha(request.form.get('g-recaptcha-response'), remote_ip=request.remote_addr): status.push_status_message('Invalid captcha supplied.', kind='error') else: username, password = claimer_email, form.password.data if not username: raise HTTPError(http.BAD_REQUEST, data=dict( message_long='No email associated with this account. Please claim this ' 'account on the project to which you were invited.' )) user.register(username=username, password=password, accepted_terms_of_service=form.accepted_terms_of_service.data) # Clear unclaimed records user.unclaimed_records = {} user.verification_key = generate_verification_key() user.save() # Authenticate user and redirect to project page status.push_status_message(language.CLAIMED_CONTRIBUTOR, kind='success', trust=True) # Redirect to CAS and authenticate the user with a verification key. provider = PreprintProvider.load(pid) redirect_url = None if provider: redirect_url = web_url_for('auth_login', next=provider.landing_url, _absolute=True) else: redirect_url = web_url_for('resolve_guid', guid=pid, _absolute=True) return redirect(cas.get_login_url( redirect_url, username=user.username, verification_key=user.verification_key )) return { 'firstname': user.given_name, 'email': claimer_email if claimer_email else '', 'fullname': user.fullname, 'form': forms.utils.jsonify(form) if is_json_request() else form, 'osf_contact_email': settings.OSF_CONTACT_EMAIL, }
def _forgot_password_post(mail_template, reset_route, institutional=False): """ View for user to submit forgot password form (standard or institutional). Validates submitted form and sends reset-password link via email if valid. If user has submitted another password reset request recently, declines to create a new one and asks the user to not submit again for awhile. Standard and institutional forgot-password requests behave similarly but use slightly different language and interfaces. When an institution is deactivated, the user should be given the opportunity to reclaim their account. CAS co-ops the forgot-password functionality to send a "set a new password" email link to the institutional user. The language of the email has been adjusted from the standard context, the response html the status message from the reset action is displayed as regular text, and the password form is not shown. HTTP Method: POST :return {} """ form = ForgotPasswordForm(request.form, prefix='forgot_password') if not form.validate(): # Don't go anywhere forms.push_errors_to_status(form.errors) else: email = form.email.data status_message = ('If there is an OSF account associated with {0}, an email with instructions on how to ' 'reset the OSF password has been sent to {0}. If you do not receive an email and believe ' 'you should have, please contact OSF Support. ').format(email) kind = 'success' # check if the user exists user_obj = get_user(email=email) if user_obj: # rate limit forgot_password_post if not throttle_period_expired(user_obj.email_last_sent, settings.SEND_EMAIL_THROTTLE): status_message = 'You have recently requested to change your password. Please wait a few minutes ' \ 'before trying again.' kind = 'error' # TODO [OSF-6673]: Use the feature in [OSF-6998] for user to resend claim email. elif user_obj.is_active: # new random verification key (v2) user_obj.verification_key_v2 = generate_verification_key(verification_type='password') user_obj.email_last_sent = timezone.now() user_obj.save() reset_link = furl.urljoin( settings.DOMAIN, web_url_for( reset_route, uid=user_obj._id, token=user_obj.verification_key_v2['token'] ) ) mails.send_mail( to_addr=email, mail=mail_template, reset_link=reset_link, can_change_preferences=False, ) # institutional forgot password page displays the message as main text, not as an alert if institutional: # pass isError instead of kind to template to decouple python error flag from template's # css class return {'message': status_message, 'isError': (kind == 'error'), 'institutional': institutional} status.push_status_message(status_message, kind=kind, trust=False) return {}
def external_login_confirm_email_get(auth, uid, token): """ View for email confirmation links when user first login through external identity provider. HTTP Method: GET :param auth: the auth context :param uid: the user's primary key :param token: the verification token """ user = User.load(uid) if not user: raise HTTPError(http.BAD_REQUEST) if auth and auth.user: if auth.user._id == user._id: new = request.args.get('new', None) if new: status.push_status_message(language.WELCOME_MESSAGE, kind='default', jumbotron=True, trust=True) return redirect(web_url_for('index')) # If user is already logged in, log user out and retry request return auth_logout(redirect_url=request.url) # token is invalid if token not in user.email_verifications: raise HTTPError(http.BAD_REQUEST) verification = user.email_verifications[token] email = verification['email'] provider = verification['external_identity'].keys()[0] provider_id = verification['external_identity'][provider].keys()[0] # wrong provider if provider not in user.external_identity: raise HTTPError(http.BAD_REQUEST) external_status = user.external_identity[provider][provider_id] try: ensure_external_identity_uniqueness(provider, provider_id, user) except ValidationError as e: raise HTTPError(http.FORBIDDEN, e.message) if not user.is_registered: user.set_password(uuid.uuid4(), notify=False) user.register(email) if email.lower() not in user.emails: user.emails.append(email.lower()) user.date_last_logged_in = datetime.datetime.utcnow() user.external_identity[provider][provider_id] = 'VERIFIED' user.social[provider.lower()] = provider_id del user.email_verifications[token] user.verification_key = generate_verification_key() user.save() service_url = request.url if external_status == 'CREATE': mails.send_mail( to_addr=user.username, mail=mails.WELCOME, mimetype='html', user=user ) service_url = service_url + '?new=true' elif external_status == 'LINK': mails.send_mail( user=user, to_addr=user.username, mail=mails.EXTERNAL_LOGIN_LINK_SUCCESS, external_id_provider=provider, ) # redirect to CAS and authenticate the user with the verification key return redirect(cas.get_login_url( service_url, username=user.username, verification_key=user.verification_key ))
def claim_user_form(auth, **kwargs): """ View for rendering the set password page for a claimed user. Must have ``token`` as a querystring argument. Renders the set password form, validates it, and sets the user's password. HTTP Method: GET, POST """ uid, pid = kwargs['uid'], kwargs['pid'] token = request.form.get('token') or request.args.get('token') user = OSFUser.load(uid) # If unregistered user is not in database, or url bears an invalid token raise HTTP 400 error if not user or not verify_claim_token(user, token, pid): error_data = { 'message_short': 'Invalid url.', 'message_long': 'Claim user does not exists, the token in the URL is invalid or has expired.' } raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=error_data) # If user is logged in, redirect to 're-enter password' page if auth.logged_in: return redirect( web_url_for('claim_user_registered', uid=uid, pid=pid, token=token)) unclaimed_record = user.unclaimed_records[pid] user.fullname = unclaimed_record['name'] user.update_guessed_names() # The email can be the original referrer email if no claimer email has been specified. claimer_email = unclaimed_record.get( 'claimer_email') or unclaimed_record.get('email') # If there is a registered user with this email, redirect to 're-enter password' page try: user_from_email = OSFUser.objects.get( emails__address=claimer_email.lower().strip( )) if claimer_email else None except OSFUser.DoesNotExist: user_from_email = None if user_from_email and user_from_email.is_registered: return redirect( web_url_for('claim_user_registered', uid=uid, pid=pid, token=token)) form = SetEmailAndPasswordForm(request.form, token=token) if request.method == 'POST': if not form.validate(): forms.push_errors_to_status(form.errors) elif settings.RECAPTCHA_SITE_KEY and not validate_recaptcha( request.form.get('g-recaptcha-response'), remote_ip=request.remote_addr): status.push_status_message('Invalid captcha supplied.', kind='error') else: username, password = claimer_email, form.password.data if not username: raise HTTPError( http_status.HTTP_400_BAD_REQUEST, data=dict( message_long= 'No email associated with this account. Please claim this ' 'account on the project to which you were invited.')) user.register( username=username, password=password, accepted_terms_of_service=form.accepted_terms_of_service.data) # Clear unclaimed records user.unclaimed_records = {} user.verification_key = generate_verification_key() user.save() # Authenticate user and redirect to project page status.push_status_message(language.CLAIMED_CONTRIBUTOR, kind='success', trust=True) # Redirect to CAS and authenticate the user with a verification key. provider = PreprintProvider.load(pid) redirect_url = None if provider: redirect_url = web_url_for('auth_login', next=provider.landing_url, _absolute=True) else: redirect_url = web_url_for('resolve_guid', guid=pid, _absolute=True) return redirect( cas.get_login_url(redirect_url, username=user.username, verification_key=user.verification_key)) return { 'firstname': user.given_name, 'email': claimer_email if claimer_email else '', 'fullname': user.fullname, 'form': forms.utils.jsonify(form) if is_json_request() else form, 'osf_contact_email': settings.OSF_CONTACT_EMAIL, }
def send_claim_email(email, unclaimed_user, node, notify=True, throttle=24 * 3600, email_template='default'): """ Unregistered user claiming a user account as an contributor to a project. Send an email for claiming the account. Either sends to the given email or the referrer's email, depending on the email address provided. :param str email: The address given in the claim user form :param User unclaimed_user: The User record to claim. :param Node node: The node where the user claimed their account. :param bool notify: If True and an email is sent to the referrer, an email will also be sent to the invited user about their pending verification. :param int throttle: Time period (in seconds) after the referrer is emailed during which the referrer will not be emailed again. :param str email_template: the email template to use :return :raise http_status.HTTP_400_BAD_REQUEST """ claimer_email = email.lower().strip() unclaimed_record = unclaimed_user.get_unclaimed_record(node._primary_key) referrer = OSFUser.load(unclaimed_record['referrer_id']) claim_url = unclaimed_user.get_claim_url(node._primary_key, external=True) # Option 1: # When adding the contributor, the referrer provides both name and email. # The given email is the same provided by user, just send to that email. preprint_provider = None logo = None if unclaimed_record.get('email') == claimer_email: # check email template for branded preprints if email_template == 'preprint': email_template, preprint_provider = find_preprint_provider(node) if not email_template or not preprint_provider: return mail_tpl = getattr(mails, 'INVITE_PREPRINT')(email_template, preprint_provider) if preprint_provider._id == 'osf': logo = settings.OSF_PREPRINTS_LOGO else: logo = preprint_provider._id else: mail_tpl = getattr(mails, 'INVITE_DEFAULT'.format(email_template.upper())) to_addr = claimer_email unclaimed_record['claimer_email'] = claimer_email unclaimed_user.save() # Option 2: # TODO: [new improvement ticket] this option is disabled from preprint but still available on the project page # When adding the contributor, the referred only provides the name. # The account is later claimed by some one who provides the email. # Send email to the referrer and ask her/him to forward the email to the user. else: # check throttle timestamp = unclaimed_record.get('last_sent') if not throttle_period_expired(timestamp, throttle): raise HTTPError( http_status.HTTP_400_BAD_REQUEST, data=dict( message_long= 'User account can only be claimed with an existing user once every 24 hours' )) # roll the valid token for each email, thus user cannot change email and approve a different email address verification_key = generate_verification_key(verification_type='claim') unclaimed_record['last_sent'] = get_timestamp() unclaimed_record['token'] = verification_key['token'] unclaimed_record['expires'] = verification_key['expires'] unclaimed_record['claimer_email'] = claimer_email unclaimed_user.save() claim_url = unclaimed_user.get_claim_url(node._primary_key, external=True) # send an email to the invited user without `claim_url` if notify: pending_mail = mails.PENDING_VERIFICATION mails.send_mail( claimer_email, pending_mail, user=unclaimed_user, referrer=referrer, fullname=unclaimed_record['name'], node=node, can_change_preferences=False, osf_contact_email=settings.OSF_CONTACT_EMAIL, ) mail_tpl = mails.FORWARD_INVITE to_addr = referrer.username # Send an email to the claimer (Option 1) or to the referrer (Option 2) with `claim_url` mails.send_mail( to_addr, mail_tpl, user=unclaimed_user, referrer=referrer, node=node, claim_url=claim_url, email=claimer_email, fullname=unclaimed_record['name'], branded_service=preprint_provider, can_change_preferences=False, logo=logo if logo else settings.OSF_LOGO, osf_contact_email=settings.OSF_CONTACT_EMAIL, ) return to_addr
def send_claim_registered_email(claimer, unclaimed_user, node, throttle=24 * 3600): """ A registered user claiming the unclaimed user account as an contributor to a project. Send an email for claiming the account to the referrer and notify the claimer. :param claimer: the claimer :param unclaimed_user: the user account to claim :param node: the project node where the user account is claimed :param throttle: the time period in seconds before another claim for the account can be made :return: :raise: http_status.HTTP_400_BAD_REQUEST """ unclaimed_record = unclaimed_user.get_unclaimed_record(node._primary_key) # check throttle timestamp = unclaimed_record.get('last_sent') if not throttle_period_expired(timestamp, throttle): raise HTTPError( http_status.HTTP_400_BAD_REQUEST, data=dict( message_long= 'User account can only be claimed with an existing user once every 24 hours' )) # roll the valid token for each email, thus user cannot change email and approve a different email address verification_key = generate_verification_key(verification_type='claim') unclaimed_record['token'] = verification_key['token'] unclaimed_record['expires'] = verification_key['expires'] unclaimed_record['claimer_email'] = claimer.username unclaimed_user.save() referrer = OSFUser.load(unclaimed_record['referrer_id']) claim_url = web_url_for( 'claim_user_registered', uid=unclaimed_user._primary_key, pid=node._primary_key, token=unclaimed_record['token'], _absolute=True, ) # Send mail to referrer, telling them to forward verification link to claimer mails.send_mail( referrer.username, mails.FORWARD_INVITE_REGISTERED, user=unclaimed_user, referrer=referrer, node=node, claim_url=claim_url, fullname=unclaimed_record['name'], can_change_preferences=False, osf_contact_email=settings.OSF_CONTACT_EMAIL, ) unclaimed_record['last_sent'] = get_timestamp() unclaimed_user.save() # Send mail to claimer, telling them to wait for referrer mails.send_mail( claimer.username, mails.PENDING_VERIFICATION_REGISTERED, fullname=claimer.fullname, referrer=referrer, node=node, can_change_preferences=False, osf_contact_email=settings.OSF_CONTACT_EMAIL, )
def make_response_from_ticket(ticket, service_url): """ Given a CAS ticket and service URL, attempt to validate the user and return a proper redirect response. :param str ticket: CAS service ticket :param str service_url: Service URL from which the authentication request originates :return: redirect response """ service_furl = furl.furl(service_url) # `service_url` is guaranteed to be removed of `ticket` parameter, which has been pulled off in # `framework.sessions.before_request()`. if 'ticket' in service_furl.args: service_furl.args.pop('ticket') client = get_client() cas_resp = client.service_validate(ticket, service_furl.url) if cas_resp.authenticated: user, external_credential, action = get_user_from_cas_resp(cas_resp) # user found and authenticated if user and action == 'authenticate': # if we successfully authenticate and a verification key is present, invalidate it if user.verification_key: user.verification_key = None user.save() # if user is authenticated by external IDP, ask CAS to authenticate user for a second time # this extra step will guarantee that 2FA are enforced # current CAS session created by external login must be cleared first before authentication if external_credential: user.verification_key = generate_verification_key() user.save() return redirect( get_logout_url( get_login_url(service_url, username=user.username, verification_key=user.verification_key))) # if user is authenticated by CAS # TODO [CAS-27]: Remove Access Token From Service Validation return authenticate(user, cas_resp.attributes.get('accessToken', ''), redirect(service_furl.url)) # first time login from external identity provider if not user and external_credential and action == 'external_first_login': from website.util import web_url_for # orcid attributes can be marked private and not shared, default to orcid otherwise fullname = u'{} {}'.format( cas_resp.attributes.get('given-names', ''), cas_resp.attributes.get('family-name', '')).strip() # TODO [CAS-27]: Remove Access Token From Service Validation user = { 'external_id_provider': external_credential['provider'], 'external_id': external_credential['id'], 'fullname': fullname, 'access_token': cas_resp.attributes.get('accessToken', ''), 'service_url': service_furl.url, } return external_first_login_authenticate( user, redirect(web_url_for('external_login_email_get'))) # Unauthorized: ticket could not be validated, or user does not exist. return redirect(service_furl.url)
def confirm_email_get(token, auth=None, **kwargs): """ View for email confirmation links. Authenticates and redirects to user settings page if confirmation is successful, otherwise shows an "Expired Link" error. HTTP Method: GET """ is_merge = 'confirm_merge' in request.args try: if not is_merge or not check_select_for_update(): user = OSFUser.objects.get(guids___id=kwargs['uid']) else: user = OSFUser.objects.filter( guids___id=kwargs['uid']).select_for_update().get() except OSFUser.DoesNotExist: raise HTTPError(http.NOT_FOUND) is_initial_confirmation = not user.date_confirmed log_out = request.args.get('logout', None) # if the user is merging or adding an email (they already are an osf user) if log_out: return auth_email_logout(token, user) if auth and auth.user and (auth.user._id == user._id or auth.user._id == user.merged_by._id): if not is_merge: # determine if the user registered through a campaign campaign = campaigns.campaign_for_user(user) if campaign: return redirect(campaigns.campaign_url_for(campaign)) # go to home page with push notification if auth.user.emails.count() == 1 and len( auth.user.email_verifications) == 0: status.push_status_message(language.WELCOME_MESSAGE, kind='default', jumbotron=True, trust=True) if token in auth.user.email_verifications: status.push_status_message( language.CONFIRM_ALTERNATE_EMAIL_ERROR, kind='danger', trust=True) return redirect(web_url_for('index')) status.push_status_message(language.MERGE_COMPLETE, kind='success', trust=False) return redirect(web_url_for('user_account')) try: user.confirm_email(token, merge=is_merge) except exceptions.EmailConfirmTokenError as e: raise HTTPError(http.BAD_REQUEST, data={ 'message_short': e.message_short, 'message_long': e.message_long }) if is_initial_confirmation: user.update_date_last_login() user.save() # send out our welcome message mails.send_mail(to_addr=user.username, mail=mails.WELCOME, mimetype='html', user=user) # new random verification key, allows CAS to authenticate the user w/o password one-time only. user.verification_key = generate_verification_key() user.save() # redirect to CAS and authenticate the user with a verification key. return redirect( cas.get_login_url(request.url, username=user.username, verification_key=user.verification_key))
def send_claim_email(email, unclaimed_user, node, notify=True, throttle=24 * 3600, email_template='default'): """ Unregistered user claiming a user account as an contributor to a project. Send an email for claiming the account. Either sends to the given email or the referrer's email, depending on the email address provided. :param str email: The address given in the claim user form :param User unclaimed_user: The User record to claim. :param Node node: The node where the user claimed their account. :param bool notify: If True and an email is sent to the referrer, an email will also be sent to the invited user about their pending verification. :param int throttle: Time period (in seconds) after the referrer is emailed during which the referrer will not be emailed again. :param str email_template: the email template to use :return :raise http.BAD_REQUEST """ claimer_email = email.lower().strip() unclaimed_record = unclaimed_user.get_unclaimed_record(node._primary_key) referrer = User.load(unclaimed_record['referrer_id']) claim_url = unclaimed_user.get_claim_url(node._primary_key, external=True) # When adding the contributor, the referrer provides both name and email. # The given email is the same provided by user, just send to that email. if unclaimed_record.get('email') == claimer_email: mail_tpl = getattr(mails, 'INVITE_{}'.format(email_template.upper())) to_addr = claimer_email unclaimed_record['claimer_email'] = claimer_email unclaimed_user.save() # When adding the contributor, the referred only provides the name. # The account is later claimed by some one who provides the email. # Send email to the referrer and ask her/him to forward the email to the user. else: # check throttle timestamp = unclaimed_record.get('last_sent') if not throttle_period_expired(timestamp, throttle): raise HTTPError( http.BAD_REQUEST, data=dict( message_long= 'User account can only be claimed with an existing user once every 24 hours' )) # roll the valid token for each email, thus user cannot change email and approve a different email address verification_key = generate_verification_key(verification_type='claim') unclaimed_record['last_sent'] = get_timestamp() unclaimed_record['token'] = verification_key['token'] unclaimed_record['expires'] = verification_key['expires'] unclaimed_record['claimer_email'] = claimer_email unclaimed_user.save() claim_url = unclaimed_user.get_claim_url(node._primary_key, external=True) # send an email to the invited user without `claim_url` if notify: pending_mail = mails.PENDING_VERIFICATION mails.send_mail(claimer_email, pending_mail, user=unclaimed_user, referrer=referrer, fullname=unclaimed_record['name'], node=node) mail_tpl = mails.FORWARD_INVITE to_addr = referrer.username # send an email to the referrer with `claim_url` mails.send_mail(to_addr, mail_tpl, user=unclaimed_user, referrer=referrer, node=node, claim_url=claim_url, email=claimer_email, fullname=unclaimed_record['name']) return to_addr
def external_login_confirm_email_get(auth, uid, token): """ View for email confirmation links when user first login through external identity provider. HTTP Method: GET :param auth: the auth context :param uid: the user's primary key :param token: the verification token """ user = User.load(uid) if not user: raise HTTPError(http.BAD_REQUEST) if auth and auth.user and auth.user._id == user._id: new = request.args.get('new', None) if new: status.push_status_message(language.WELCOME_MESSAGE, kind='default', jumbotron=True, trust=True) return redirect(web_url_for('index')) # token is invalid if token not in user.email_verifications: raise HTTPError(http.BAD_REQUEST) verification = user.email_verifications[token] email = verification['email'] provider = verification['external_identity'].keys()[0] provider_id = verification['external_identity'][provider].keys()[0] # wrong provider if provider not in user.external_identity: raise HTTPError(http.BAD_REQUEST) external_status = user.external_identity[provider][provider_id] try: ensure_external_identity_uniqueness(provider, provider_id, user) except ValidationError as e: raise HTTPError(http.FORBIDDEN, e.message) if not user.is_registered: user.register(email) if email.lower() not in user.emails: user.emails.append(email.lower()) user.date_last_logged_in = datetime.datetime.utcnow() user.external_identity[provider][provider_id] = 'VERIFIED' user.social[provider.lower()] = provider_id del user.email_verifications[token] user.verification_key = generate_verification_key() user.save() service_url = request.url if external_status == 'CREATE': mails.send_mail( to_addr=user.username, mail=mails.WELCOME, mimetype='html', user=user ) service_url = service_url + '?new=true' elif external_status == 'LINK': mails.send_mail( user=user, to_addr=user.username, mail=mails.EXTERNAL_LOGIN_LINK_SUCCESS, external_id_provider=provider, ) # redirect to CAS and authenticate the user with the verification key return redirect(cas.get_login_url( service_url, username=user.username, verification_key=user.verification_key ))