Flask-User icon indicating copy to clipboard operation
Flask-User copied to clipboard

Make Invitation token invalid after used once.

Open egglet opened this issue 8 years ago • 5 comments

If USER_REQUIRE_INVITATION = True then the invitation token should only be valid for the invited email, but the user is able to change this and register with any email.

This also means if the token isn't removed after registration, this token can be used over and over.

PS. @lingthio and @neurosnap thanks for all the awesome work!

egglet avatar Oct 15 '15 12:10 egglet

We tried to make the invited email optional, allowing users to choose a different email if they desire, but you have brought up some very interesting points. I'd like @lingthio to weigh in on this problem.

A simple solution would be to expire the token after successful registration. We could also set a flag to disable users from changing the email.

neurosnap avatar Oct 15 '15 12:10 neurosnap

How would I expire the token after successful registration?

marcuskelly avatar Oct 19 '17 17:10 marcuskelly

@marcuskelly, PR https://github.com/lingthio/Flask-User/pull/94 seems to do what you're looking for, though it was never merged. I'm not sure how it'll play with the existing code.

@lingthio and @neurosnap, I second @egglet's ideas about restricting the registration to only the recipient email and expiring the token after one use. Was there a reason https://github.com/lingthio/Flask-User/pull/94 wasn't pulled in, or did it just slip between the cracks? Alternatively, any plans to implement similar functionality in upcoming releases? Thanks for all the work you've put into this!

mgd722 avatar Dec 04 '17 20:12 mgd722

I agree with @neurosnap , that the invitation token is sent to a specific address, but that that invitee may use a different email address to register.

Also note that the USER_INVITE_EXPIRATION setting will expire the invitation token after a specified amount of time (specified in seconds).

It does make sense, however to mark the UserInvitation object after it has been used once. Marking it (as opposed of deleting it) would allow websites to show a list of UserInvitations and its state. It also would allow the invitation to be re-sent.

The PR #94, unfortunately tries to implement two features, and it deleted the UserInvitation object rather than mark it.

lingthio avatar Apr 19 '18 20:04 lingthio

Here is my solution to allowing an invitation to be used only once:

In my models file (last line is relevant):

class UserInvitation(db.Model):
	__tablename__ = 'user_invite'
	id = db.Column(db.Integer, primary_key=True)
	email = db.Column(db.String(255), nullable=False)

	# save the user of the invitee
	invited_by_user_id = db.Column(db.Integer, db.ForeignKey('users.id'))

	# token used for registration page to identify user registering
	token = db.Column(db.String(100), nullable=False, server_default='')

	# has token been used?
	token_used = db.Column(db.Boolean(), server_default="0")

Customise the UserManager


from flask_user import UserManager

from datetime import datetime
from flask import current_app, flash, redirect, render_template, request, url_for
from flask_login import current_user, login_user, logout_user
from flask_user import signals


''' Customization: Invite tokens can only be used once

See the following links:
	https://github.com/lingthio/Flask-User/issues/98
	https://github.com/lingthio/Flask-User/pull/94
	https://flask-user.readthedocs.io/en/latest/customizing_forms.html#customizing-form-views
	https://github.com/lingthio/Flask-User/blob/5c652e6479036c3d33aa1626524e4e65bd3b961e/flask_user/user_manager__views.py#L428

Need to change:
	- UserInvitation has a 'token_used' column
	- when an invite token is used, mark 'token_used' as True
	- when verifying an invite token (in view function) - if it is marked as used, it is no longer valid

Original register_view code is pasted here, and then adapted. See lines appended with '#CB was here'.
'''

class CustomUserManager(UserManager):

	def register_view(self):
		""" Display registration form and create new User."""

		safe_next_url = self._get_safe_next_url('next', self.USER_AFTER_LOGIN_ENDPOINT)
		safe_reg_next_url = self._get_safe_next_url('reg_next', self.USER_AFTER_REGISTER_ENDPOINT)

		# Initialize form
		login_form = self.LoginFormClass()  # for login_or_register.html
		register_form = self.RegisterFormClass(request.form)  # for register.html

		# invite token used to determine validity of registeree
		invite_token = request.values.get("token")

		# require invite without a token should disallow the user from registering
		if self.USER_REQUIRE_INVITATION and not invite_token:
			flash("Registration is invite only", "error")
			return redirect(url_for('user.login'))

		user_invitation = None
		if invite_token and self.db_manager.UserInvitationClass:
			data_items = self.token_manager.verify_token(invite_token, self.USER_INVITE_EXPIRATION)
			if data_items:
				user_invitation_id = data_items[0]
				user_invitation = self.db_manager.get_user_invitation_by_id(user_invitation_id)
			flash(user_invitation.token_used)
			if not user_invitation:
				flash("Invalid invitation token", "error")
				return redirect(url_for('user.login'))

			if user_invitation.token_used:			        #CB was here
				flash("Invitation already used", "error")   #CB was here
				return redirect(url_for('user.login'))		#CB was here

			register_form.invite_token.data = invite_token

		if request.method != 'POST':
			login_form.next.data = register_form.next.data = safe_next_url
			login_form.reg_next.data = register_form.reg_next.data = safe_reg_next_url
			if user_invitation:
				register_form.email.data = user_invitation.email

		# Process valid POST
		if request.method == 'POST' and register_form.validate():
			user = self.db_manager.add_user()
			register_form.populate_obj(user)
			user_email = self.db_manager.add_user_email(user=user, is_primary=True)
			register_form.populate_obj(user_email)

			# Store password hash instead of password
			user.password = self.hash_password(user.password)

			# Email confirmation depends on the USER_ENABLE_CONFIRM_EMAIL setting
			request_email_confirmation = self.USER_ENABLE_CONFIRM_EMAIL
			# Users that register through an invitation, can skip this process
			# but only when they register with an email that matches their invitation.
			if user_invitation:
				if user_invitation.email.lower() == register_form.email.data.lower():
					user_email.email_confirmed_at=datetime.utcnow()
					request_email_confirmation = False

				# mark the UserInvitation as used, note there is no "save_invitation" method in db_manager	      #CB was here
				user_invitation.token_used = True 			                         #CB was here
				self.db_manager.save_object(user_invitation)                        #CB was here

			self.db_manager.save_user_and_user_email(user, user_email)
			self.db_manager.commit()

			# Send 'registered' email and delete new User object if send fails
			if self.USER_SEND_REGISTERED_EMAIL:
				try:
					# Send 'confirm email' or 'registered' email
					self._send_registered_email(user, user_email, request_email_confirmation)
				except Exception as e:
					# delete new User object if send  fails
					self.db_manager.delete_object(user)
					self.db_manager.commit()
					raise

			# Send user_registered signal
			signals.user_registered.send(current_app._get_current_object(),
										 user=user,
										 user_invitation=user_invitation)

			# Redirect if USER_ENABLE_CONFIRM_EMAIL is set
			if self.USER_ENABLE_CONFIRM_EMAIL and request_email_confirmation:
				safe_reg_next_url = self.make_safe_url(register_form.reg_next.data)
				return redirect(safe_reg_next_url)

			# Auto-login after register or redirect to login page
			if 'reg_next' in request.args:
				safe_reg_next_url = self.make_safe_url(register_form.reg_next.data)
			else:
				safe_reg_next_url = self._endpoint_url(self.USER_AFTER_CONFIRM_ENDPOINT)
			if self.USER_AUTO_LOGIN_AFTER_REGISTER:
				return self._do_login_user(user, safe_reg_next_url)  # auto-login
			else:
				return redirect(url_for('user.login') + '?next=' + quote(safe_reg_next_url))  # redirect to login page

		# Render form
		self.prepare_domain_translations()
		return render_template(self.USER_REGISTER_TEMPLATE,
					  form=register_form,
					  login_form=login_form,
					  register_form=register_form)

carissableker avatar Sep 16 '21 09:09 carissableker