I am currently learning Flask through Miguel Grinberg's book. If you are familiar you might know Flasky (An application that Miguel use during the book)
I am currently in section 8, dealing with Password Reset, here the original code (you can also find it on the repo. it's tag 8g):
models.py
class User(UserMixin, db.Model):
__tablename__ = 'users'
...
def generate_reset_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'reset': self.id})
def reset_password(self, token, new_password):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('reset') != self.id:
return False
self.password = new_password
db.session.add(self)
return True
auth/views.py
#auth.route('/reset/<token>', methods=['GET', 'POST'])
def password_reset(token):
if not current_user.is_anonymous:
return redirect(url_for('main.index'))
form = PasswordResetForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is None:
return redirect(url_for('main.index'))
if user.reset_password(token, form.password.data):
flash('Your password has been updated.')
return redirect(url_for('auth.login'))
else:
return redirect(url_for('main.index'))
return render_template('auth/reset_password.html', form=form)
auth/forms.py
class PasswordResetForm(Form):
email = StringField('Email', validators=[Required(), Length(1, 64),
Email()])
password = PasswordField('New Password', validators=[
Required(), EqualTo('password2', message='Passwords must match')])
password2 = PasswordField('Confirm password', validators=[Required()])
submit = SubmitField('Reset Password')
def validate_email(self, field):
if User.query.filter_by(email=field.data).first() is None:
raise ValidationError('Unknown email address.')
My question:
I don't want to ask the user for their email again since they are changing the password through an email they have received. Is there a way to get the user or user's email from that token?
For 1— On a security level, it can be nice advantage to your users to hide who has accounts with your site. for instance, lets say it was a Addicts Anonymous site, if I wanted to see if alice#example.com was a member, I could simply try a password reset to confirm she's a member.
Alternatively if you had a large collection of email addresses, you could use that password reset form to narrow down the list to active members for use in a more targeted social engineering attack, or at least narrow the list down if you're aiming to brute-force them.
Alright so in case this could be useful for someone else. The user info is already in the token, in the {'reset': user_id}.
The problem is that token management logic is in User model. So having an email field in the form for later finding that user in the view does the trick in the current version
Since you get the token in this view we can move that logic to the view:
auth/views.py
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
#auth.route('/reset/<token>', methods=['GET', 'POST'])
def reset_password(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
raise ValidationError()
user_id = data['reset']
....
And in User model, we need to modify the reset_password() method:
models.py
class User(UserMixin, db.Model):
__tablename__ = 'users'
...
def reset_password(self, new_password):
self.password = new_password
db.session.add(self)
return True
Related
In my twitter-like app, I want to allow users to edit their profiles. If the password the user submits in the edit form is not the valid password, my app should flash a message ("Wrong password") and redirect to the homepage.
What I'm having trouble with is the form ensuring that the submitted password is the valid password. Here's what I have so far:
class EditProfileForm(FlaskForm):
"""Edit form"""
username = StringField('Username', validators=[DataRequired()])
email = StringField('E-mail', validators=[DataRequired(), Email()])
image_url = StringField('(Optional) Image URL')
header_image_url = StringField('(Optional) Image URL')
bio = StringField('(Optional) Bio')
password = PasswordField('Password')
def __init__(self, user, *args, **kwargs):
super(EditProfileForm, self).__init__(*args, **kwargs)
self.user = user
def validate_password_proof(self, field):
if form.data != self.user.password:
raise ValidationError('Wrong password.')
I found this validator method from another post, but I can't figure out how to properly initialize the form ('user' is not defined):
#app.route('/users/profile', methods=["GET", "POST"])
def profile():
"""Update profile for current user."""
if not g.user:
flash("Access unauthorized.", "danger")
return redirect("/")
form = EditProfileForm(user)
return render_template()
If anyone has another solution to ensure the password is the correct password or can point out how I'm misusing the above I'd appreciate it!
Do this to the validator:
def validate_password(self, password):
if password.data != get_user_byEmail(self.email.data):
raise ValidationError('Wrong password.')
of course you have to implement get_user_byEmail(email: str)
in the route pass the form to the template
return render_template('templateName')
Edit (trying to clarify the problem):
The custom validator I created for my flask app to check for duplicate usernames and emails does not work for admin users to modify other user accounts. When an admin user edits another user's username or email, the validator indicates it is a duplicate because it is trying to compare the current (admin) user's username and email, not the username and email of the user being edited. How do I write a custom validator to use the user object passed to the form in the view function below to compare username and email for the user being edited and not the logged in user?
Original post:
I am working on a Flask app that includes the ability for users with admin privileges to modify user account information. Currently, I am using Flask-WTF and WTForms to create a form on a user profile page that the account owner or a user with admin privileges (through role assignment in the user model) can edit user information. In the view function, I pass the user information to the form to pre-populate the form fields with the user information. When I made the profile editing form for the account owner, when the form is submitted I have a custom validator that compares the submitted data to the data for the currently logged in user to detect if there were any fields edited if then check if some of those fields (like username, email, etc.) are already in the database to avoid duplicates. For the form that admin users can edit other users, this validator keeps preventing writing new values to the database because the validator is comparing the logged in user information to the database and not the user information belonging to the account that is being edited. I want to know how to pass the user object that I use to populate the form fields to the form validation when the form is submitted? The Flask app is using a blueprint structure. I am pretty new to this so I hope the question makes sense. Here is some code that I think is helpful.
Here is the view function:
# Edit account view function for admin users
#users.route('/edit-account/<int:user_id>', methods=['GET', 'POST'])
#login_required
def edit_account(user_id):
# Get the info for the user being edited
user = User.query.get_or_404(user_id)
# Check to be sure user has admin privileges
if current_user.role != 'admin' and current_user.role != 'agent':
abort(403)
form = AccountEditForm()
if form.validate_on_submit():
# Update the profile picture, if a file is submitted
if form.profile_pic.data:
picture_file = save_picture(form.profile_pic.data)
user.profile_pic = picture_file
# Update the database entries for the user
user.username = form.username.data
user.first_name = form.first_name.data
user.last_name = form.last_name.data
user.email = form.email.data
user.role = form.role.data
db.session.commit()
flash(f'The account for {user.first_name} {user.last_name} has been updated.', 'success')
return redirect(url_for('users.users_admin'))
# Pre-populate the form with existing data
elif request.method == 'GET':
form.username.data = user.username
form.first_name.data = user.first_name
form.last_name.data = user.last_name
form.email.data = user.email
form.role.data = user.role
image_file = url_for('static', filename=f'profile_pics/{user.profile_pic}')
return render_template('account.html', title='account',
image_file=image_file, form=form, user=user)
Here is the form class that doesn’t work for the validator when an admin user is trying to edit another user’s account:
class AccountEditForm(FlaskForm):
username = StringField('Username',
validators=[DataRequired(), Length(min=2, max=20)])
first_name = StringField('First Name',
validators=[DataRequired(), Length(min=2, max=32)])
last_name = StringField('Last Name',
validators=[DataRequired(), Length(min=2, max=32)])
email = StringField('Email',
validators=[DataRequired(), Email()])
profile_pic = FileField('Update Profile Picture',
validators=[FileAllowed(['jpg', 'png'])])
role = SelectField('Role', validators=[DataRequired()],
choices=[('admin', 'Admin'), ('agent', 'Agent'), ('faculty', 'Faculty'),
('staff', 'Staff'), ('student', 'Student')])
submit = SubmitField('Update')
# Custom validation to check for duplicate usernames
def validate_username(self, username):
# Check to see if the form data is different than the current db entry
if username.data != username:
# Query db for existing username matching the one submitted on the form
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('Username is taken, please choose another.')
# Custom validation to check for duplicate email
def validate_email(self, email):
# Check to see if the form data is different than the current db entry
if email.data != user.email:
# Query db for existing email matching the one submitted on the form
user = User.query.filter_by(email=email.data).first()
if user:
raise ValidationError('Email is already used, please choose another or login.')
Here is the custom validator that works when the user is editing their own account from a different form class:
# Custom validation to check for duplicate usernames
def validate_username(self, username):
# Check to see if the form data is different than the current db entry
if username.data != current_user.username:
# Query db for existing username matching the one submitted on the form
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('Username is taken, please choose another.')
# Custom validation to check for duplicate email
def validate_email(self, email):
# Check to see if the form data is different than the current db entry
if email.data != current_user.email:
# Query db for existing email matching the one submitted on the form
user = User.query.filter_by(email=email.data).first()
if user:
raise ValidationError('Email is already used, please choose another or login.')
here is an example of passing object to the custom validator, and the scenario is exactly like yours:
class EditProfileAdminForm(EditProfileForm):
email = StringField('Email', validators=[DataRequired(), Length(1, 254), Email()])
role = SelectField('Role', coerce=int)
active = BooleanField('Active')
confirmed = BooleanField('Confirmed')
submit = SubmitField()
def __init__(self, user, *args, **kwargs): # accept the object
super(EditProfileAdminForm, self).__init__(*args, **kwargs)
self.role.choices = [(role.id, role.name)
for role in Role.query.order_by(Role.name).all()]
self.user = user # set the object as class attr
def validate_username(self, field):
# use self.user.username to get the user's username
if field.data != self.user.username and User.query.filter_by(username=field.data).first():
raise ValidationError('The username is already in use.')
def validate_email(self, field):
if field.data != self.user.email and User.query.filter_by(email=field.data.lower()).first():
raise ValidationError('The email is already in use.')
# view function
#admin_bp.route('/profile/<int:user_id>', methods=['GET', 'POST'])
#login_required
#admin_required
def edit_profile_admin(user_id):
user = User.query.get_or_404(user_id)
form = EditProfileAdminForm(user=user) # pass the object
# ...
Example codes come from a photo-sharing application.
While i am authenticating the login form using authenticate function i am getting user as none eventhough getting the username and password.
settings.py:
---------
AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",)
forms.py:
----------
class UserRegistrationForm(forms.Form):
fname = forms.CharField(required=True,label='FirstName',max_length=32)
lname = forms.CharField(required=True,label='LastName',max_length=32)
username = forms.CharField(required = True,label = 'Username',max_length = 32)
password = forms.CharField(required = True,label = 'Password',max_length = 32,min_length=8)
class login_form(forms.Form):
username = forms.CharField()
password1 = forms.CharField(widget=forms.PasswordInput)
views.py:
--------
def signup(request):
if request.method == 'POST':
form = UserRegistrationForm(request.POST)
if form.is_valid():
userObj = form.cleaned_data
username = userObj['username']
password = userObj['password']
fname = userObj['fname']
lname = userObj['lname']
print (username,password,fname,lname)
if(len(password)<8):
messages.error(request,"This password length should be minimum 8 characters")
# raise ValidationError("This password length should be minimum 8 characters ")
if not (User.objects.filter(username=username).exists()):
p = Event(fname=fname, lname=lname, username=username, password=password)
p.save()
# return HttpResponseRedirect('Login.html')
return redirect('/Login/')
else:
raise forms.ValidationError('Looks like a username with that username or password already exists')
else:
form = UserRegistrationForm()
return render(request, 'signup.html', {'form':form})
def Login(request):
form = login_form(request.POST or None)
if form.is_valid():
username = form.cleaned_data.get("username")
password = form.cleaned_data.get("password1")
print (username,password)
user = authenticate(username=username, password=password)
print('user is', user)
models.py
:--------
class MyUserManager(BaseUserManager):
def create_user(self, fname,lname,username, password):
"""
Creates and saves a User with the given username, date of
birth and password.
"""
if not username:
raise ValueError('Users must have an username')
user = self.model(username=username,fname=fname,lname=lname)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, fname,lname,username, password,email=None):
"""
Creates and saves a superuser with the given username and password.
"""
user = self.create_user(
fname=fname,
lname=lname,
username=username,
password=password,
)
user.is_admin = True
user.is_superuser = True
user.save(using=self._db)
return user
class Event(AbstractBaseUser):
fname = models.CharField('fname', max_length=120)
lname = models.CharField('lname',max_length=120)
username = models.CharField('username',max_length = 60,unique=True)
password = models.CharField('password',max_length=120,default='pavi#2789')
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['fname','lname']
objects = MyUserManager()
def __unicode__(self):
return self.username
class Meta:
# managed = False
db_table = "user"
# fields = ('username', 'fname', 'lname', 'password', 'password2')
In database side the login credentials are saved.I dont know what is going wrong here.
Here Event is nothing but the model which i have created .
I have updated my models.py with the AbstractBaseUser, BaseUserManager and login using the superuser credentials it is working but when i am creating the user with the singup form the login is not working and throwing me the error as Manager isn't available; 'auth.User' has been swapped for 'Provisioning.Event'
This isn't right at all. You can't just declare a random model and expect it to work for authentication. You need to subclass AbstractBaseUser and add your fields, declare your model in the AUTH_USER_MODEL setting, and set the password appropriately on save.
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
class Event(AbstractBaseUser):
...
And in settings.py:
AUTH_USER_MODEL = 'myapp.Event'
Now, when you create the user in the view, you need to use set_password to hash the password:
p = Event(fname=fname, lname=lname, username=username)
p.set_password(password)
p.save()
Also, note that the checking of existing usernames should be taken care of in the view - which would happen automatically if you used a ModelForm. Even better, use the built-in UserCreationForm from django.contrib.auth.forms. (But whatever you do, note that it makes no sense at all to filter on User, since you aren't using that model at all.)
The problem is not with the call to authenticate, but probably with how you are implementing the custom user model.
Using a custom user model is totally fine, and is very useful, but if you want to keep things easy for yourself, let Django handle the password part.
There's a great guide on how to write your own user model and still have it play nicely with built-in Django functionality, like authentication: https://docs.djangoproject.com/en/2.1/topics/auth/customizing/#specifying-a-custom-user-model
The reason for this is because Django hashes passwords for storage in the database for security. There's a lot of nuance to how Django handles passwords. It's quite fascinating if you're interested: https://docs.djangoproject.com/en/2.1/topics/auth/passwords/#how-django-stores-passwords
This will be an easy question for someone out there. I searched around quite a bit and if there's a thread that addresses this perfectly, please direct me and close this out. I'm building a very simple web form within our organization's website using Django.
In forms.py I've got a class for the login page:
class userLogin(forms.Form):
user = forms.CharField(label = 'Username ', max_length = 25)
pwd = forms.CharField(label = 'Password ', widget = forms.PasswordInput)
def clean(self):
cleaned_data = super(userLogin, self).clean()
user = cleaned_data.get("user")
pwd = cleaned_data.get("pwd")
if not fs.authenticateUser(user, pwd):
raise forms.ValidationError("Invalid password!")
I have to authenticate against our website's API, which is what the fs.authenticate bit is about; it just returns True or False. In views.py I do this:
if request.method == 'POST':
user_form = userLogin(data = request.POST)
if user_form.is_valid():
return redirect('PickupAuthorization/main')
else:
pass
The authentication and redirection to the main page works splendidly, and the login form throws an invalid password message as expected when the credential is incorrect. What I want to know is how to make "user" available to views.py. If I try to reference user_form.user in the view, Python tells me that user is not an attribute of userLogin!
I haven't done much with OOP, so I'm imagining that this has a simple answer, but I can't freakin' find it. Thanks!
EDIT: I have had to shelve this project for the moment in favor of more pressing matters, but I will update it when I have a solution that works properly.
You need to use authenticate method provided by Django in order to get the user. The authenticate method returns a user if it finds one.
To authenticate a given username and password, use authenticate(). It takes two keyword arguments, username and password, and it returns a User object if the password is valid for the given username. If the password is invalid, authenticate() returns None:
Generally the flow using authenticate goes like this:
forms.py
class userLogin(forms.Form):
user = forms.CharField(label = 'Username ', max_length = 25)
pwd = forms.CharField(label = 'Password ', widget = forms.PasswordInput)
def __init__(self, *args, **kwargs):
self.user_cache = None # You need to make the user as None initially
super(LoginForm, self).__init__(*args, **kwargs)
def clean(self):
cleaned_data = super(userLogin, self).clean()
user = cleaned_data.get("user")
pwd = cleaned_data.get("pwd")
# Now you get the user
self.user_cache = authenticate(username=user, password=pwd)
# Do other stuff
return self.cleaned_data
# Function to return user in views
def get_user(self):
return self.user_cache
Views.py
if request.method == 'POST':
user_form = userLogin(data = request.POST)
# You can now get user using the get_user method
user = user_form.get_user()
# Do other stuff
Another thing I'd like to add is if your user has already logged in you can simply do request.user.username in your views.py to get the current user.
I am trying to create a simple login using Django authentication. All the code here is working but I am thinking that it is violating DRY principles. I will explain:
In my forms.py I have a simple login form:
class LoginForm(forms.Form):
email = forms.CharField()
password = forms.PasswordField()
def clean(self):
email = self.cleaned_data.get('email', None)
password = self.cleaned_data.get('password', None)
u = authenticate(email, password)
if u is None:
raise forms.ValidationError(ERROR_MSG)
if not u.is_active:
raise forms.ValidationError(ERROR_MSG)
so I am doing a check of User here already. However, in my views.py:
def login(request):
login_form = LoginForm(request or None)
if login_form.is_valid():
#This part is repeated
email = request.POST.get('email')
password = request.POST.get('password')
u = authenticate(email, password)
login(request, u)
return render(request, 'home.html', {})
I am querying the database again, which to me seems to violate DRY. Does anybody have a better way of doing this? I want to reuse the LoginForm for other uses, but also want to do it cleanly.
Any ideas?
Instead of doing the authenticate() twice, you can set the user on the form object and then use this user in your view.
forms.py
class LoginForm(forms.Form):
email = forms.CharField()
password = forms.PasswordField()
def clean(self):
email = self.cleaned_data.get('email', None)
password = self.cleaned_data.get('password', None)
self.u = authenticate(email, password) # set the user as an instance variable
if self.u is None:
raise forms.ValidationError(ERROR_MSG)
if not self.u.is_active:
raise forms.ValidationError(ERROR_MSG)
views.py
def login(request):
login_form = LoginForm(request or None)
if login_form.is_valid():
login(request, login_form.u) # directly pass the user by accessing from login_form object
return render(request, 'home.html', {})