-
Notifications
You must be signed in to change notification settings - Fork 293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make Invitation token invalid after used once. #98
Comments
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. |
How would I expire the token after successful registration? |
@marcuskelly, PR #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 #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! |
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. |
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) |
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!
The text was updated successfully, but these errors were encountered: