You can override the login form when you are setting up Flask-Security:
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore, login_form=CustomLoginForm)
Then create your custom login form class that extends the default LoginForm. And override the validate function to do stuff before or after the login attempt.
from flask_security.forms import LoginForm
class CustomLoginForm(LoginForm):
def validate(self):
# Put code here if you want to do stuff before login attempt
response = super(CustomLoginForm, self).validate()
# Put code here if you want to do stuff after login attempt
return response
"reponse" will be True or False based on if the login was successful or not. If you want to check possible errors that occurred during the login attempt see "self.errors"
Greetings, Kevin ;)
Answer from Kevin on Stack OverflowYou can override the login form when you are setting up Flask-Security:
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore, login_form=CustomLoginForm)
Then create your custom login form class that extends the default LoginForm. And override the validate function to do stuff before or after the login attempt.
from flask_security.forms import LoginForm
class CustomLoginForm(LoginForm):
def validate(self):
# Put code here if you want to do stuff before login attempt
response = super(CustomLoginForm, self).validate()
# Put code here if you want to do stuff after login attempt
return response
"reponse" will be True or False based on if the login was successful or not. If you want to check possible errors that occurred during the login attempt see "self.errors"
Greetings, Kevin ;)
after registering the flask security extension, you can create an endpoint with the exact same name/route and it should override the one registered by Flask-security.
If you are using blueprints, make sure you register your blueprint before registering Flask-security.
python - How do I embed a Flask-Security login form on my page? - Stack Overflow
How to customize flask.ext.security.forms.LoginForm to prompt and use username instead of email? - Stack Overflow
Flask-Security with Flask-Admin, how to use two different login forms?
I think your best bet is just to copy flask_security's login form and view into your project, modify as necessary so you get the behaviour you want specific to the admin, and then register it as a separate endpoint/view. It can share the same blueprint you're using for your other admin views, and you can register your admin-specific context processor with that blueprint, so it only gets included where its needed. So all of flask_security continues to use the security.login endpoint, as well as the "public facing" login. Your admins, however, use your custom endpoint.
Example code (untested, there might be bugs, but hopefully it should get you close):
make a custom login form for the admin:
from flask_security.forms import LoginForm
class AdminLoginForm(LoginForm):
"""
extend/override stuffs as necessary. this is a stock Flask-WTF form
"""use it in your custom admin login view:
from flask import redirect, render_template, request
from flask_security.utils import login_user
from flask_security.views import after_this_request, _commit
@bp.route('/admin/login', endpoint='admin.login', methods=['GET', 'POST'])
def admin_login():
form = AdminLoginForm(request.form)
if form.validate_on_submit():
login_user(form.user, remember=form.remember.data)
# only necessay if you have `SECURITY_TRACKABLE = True` in your config
after_this_request(_commit)
return redirect(form.next.data or '/admin')
return render_template('admin/login.html', form=form)configure your admins to use it:
from flask import abort, redirect, request, url_for
from flask_admin import AdminIndexView, expose
from flask_admin.contrib.sqla import ModelView as BaseModelView
from flask_security import current_user
from http import HTTPStatus
class AdminSecurityMixin:
def is_accessible(self):
if (current_user.is_active # is current_user account enabled?
# are they logged in?
and current_user.is_authenticated
# do they have the correct roles to login to the admin?
and current_user.has_role('ROLE_ADMIN')):
return True
return False
def _handle_view(self, name, **kwargs):
if not self.is_accessible():
# if user is logged in, but can't view the admin, reject access
if current_user.is_authenticated:
abort(HTTPStatus.FORBIDDEN)
# otherwise redirect to the admin login view
# the next parameter is so we can redirect them after they'ved
# logged in to where they wanted to go originally
return redirect(url_for('admin.login', next=request.url))
class ModelView(AdminSecurityMixin, BaseModelView):
"""
all your model admins should extend this base class, so that they are secured
this is a great place to put other stuffs common to all your admins too
"""
class AdminDashboard(AdminSecurityMixin, AdminIndexView):
@expose('/')
def index(self):
return self.render('admin/dashboard.html')(probably those three admin classes shouldn't be in the same file, but, that's up to you. also, obviously I've left out the templates, but you'll need those too)
More on reddit.compython - Flask-Security: Customizing Registration - Stack Overflow
security/login_user.html is the full page template for the login form. It's not what you would want to embed on each page because it deals with other things like flashed messages, errors, and layout.
Write your own template to render just the form and include that, or add it to your base template.
<form action="{{ url_for_security('login') }}" method="POST">
{{ login_user_form.hidden_tag() }}
{{ login_user_form.email(placeholder='Email') }}
{{ login_user_form.password(placeholder='Password') }}
{{ login_user_form.remember.label }} {{ login_user_form.remember }}
{{ login_user_form.submit }}
</form>
(This is just an example, you'll want to style it to fit your page.)
None of your views will deal with the login form directly, so login_form and url_for_security aren't available while rendering most templates (this was causing the original issue you observed). Inject them for each request using app.context_processor.
from flask_security import LoginForm, url_for_security
@app.context_processor
def login_context():
return {
'url_for_security': url_for_security,
'login_user_form': LoginForm(),
}
Your stack trace reads:
jinja2.exceptions.UndefinedError: 'login_user_form' is undefined
This error message is coming from Jinja, which is saying that login_user_form is undefined.
To solve this, if this was your base.html,
...
{% block content %}
<form action="" method="post" name="post">
{{form.hidden_tag()}}
...
you've to pass login_user_form as a context variable to your render template. So, in your /login route, you should return something like this:
return render_template("base.html",
title = 'Home',
form = login_user_form)
To login with a name instead of an email address (using Flask-Security 1.7.0 or higher), you can replace the email field with a name field in the User model
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), unique=True, index=True)
password = db.Column(db.String(255))
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
and update the app configuration.
app.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = 'name'
Next, to allow users to login using a name instead of an email, we will use the fact that the LoginForm validation method assumes the user identity attribute is in the email form field.
from flask_security.forms import LoginForm
from wtforms import StringField
from wtforms.validators import InputRequired
class ExtendedLoginForm(LoginForm):
email = StringField('Username', [InputRequired()])
# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore,
login_form=ExtendedLoginForm)
This way, we can login using a name without rewriting the validation method or the login template. Of course, this is a hack and the more correct approach would be to add a custom validate method (which checks a name form field) to the ExtendedLoginForm class and to update the login template accordingly (as done by Raja R above). Or even better, to replace the 'email' form field in 'LoginForm' with a 'user_identity' field and rewrite the validate method to accommodate any user identity field.
However, the approach above makes it easy to login with a name or an email address. To do this, define a user model with both a name and email field.
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
name = db.Column(db.String(255), unique=True, index=True)
password = db.Column(db.String(255))
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
and update the app configuration.
app.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = ('name','email')
Finally, create the custom login form.
from flask_security.forms import LoginForm
from wtforms import StringField
from wtforms.validators import InputRequired
class ExtendedLoginForm(LoginForm):
email = StringField('Username or Email Address', [InputRequired()])
# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore,
login_form=ExtendedLoginForm)
Now, when logging in, Flask-Security will accept an email or name in the email form field.
In earlier versions of flask-security, the unique identifier of the user was the email property. You could extend models by adding your own properties, but the email field still served to uniquely identify the user (so the username had to be the email field). From version 1.7.0 it is possible to identify user with a different property. In the config file you should add the
app.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = 'name'
But you still have to have the email property in your model. There was a issue reported 3 days on git, for exactly what you are looking for https://github.com/mattupstate/flask-security/issues/396
I am building a flask boilerplate site and I am using flask admin so I can create users (its an "internal" site with no registrations).
So I have a login form where the admin can login and create user, but I also want to have a regular login where users can log into the site.
They're pretty much the same login just a different form and look? Is that possible or should I have them route to the same form and then have non admin redirect to the home page and the admin can redirect to the admin backend?
Right now I am using a context processor to hook into flask-admin:
# define a context processor for merging flask-admin's template context into the
# flask-security views.
@security.context_processor
def security_context_processor():
return dict(
admin_base_template=admin.base_template,
admin_view=admin.index_view,
h=admin_helpers,
get_url=url_for
)So that the url_for('security.login') will direct you to /mysite/admin/login? Is there a way to break it so that, url_for('security.admin.login') directs to /mysite/admin/login and url_for('security.login') directs to /mysite/login?
Something along those lines.
To login with a username instead of an email address (using Flask-Security 1.7.0 or higher), you can replace the email field with a username field in the User model
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(255), unique=True, index=True)
password = db.Column(db.String(255))
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
and update the app configuration.
app.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = 'username'
Next, to allow users to login using a username instead of an email, we will use the fact that the LoginForm validation method assumes the user identity attribute is in the email form field.
from flask_security.forms import LoginForm
from wtforms import StringField
from wtforms.validators import InputRequired
class ExtendedLoginForm(LoginForm):
email = StringField('Username', [InputRequired()])
# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore,
login_form=ExtendedLoginForm)
This way, we can login using a username without rewriting the validation method or the login template. Of course, this is a hack and the more correct approach would be to add a custom validate method, which checks a username form field, to the ExtendedLoginForm class and to update the login template accordingly.
However, the approach above makes it easy to login with a username or an email address. To do this, define a user model with both a username and email field.
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
username = db.Column(db.String(255), unique=True, index=True)
password = db.Column(db.String(255))
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
and update the app configuration.
app.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = ('username','email')
Finally, create the custom login form.
from flask_security.forms import LoginForm
from wtforms import StringField
from wtforms.validators import InputRequired
class ExtendedLoginForm(LoginForm):
email = StringField('Username or Email Address', [InputRequired()])
# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore,
login_form=ExtendedLoginForm)
Now, when logging in, Flask-Security will accept an email or username in the email form field.
I managed to implement login using either username or password by overwriting the login form:
class ExtendedLoginForm(LoginForm):
email = StringField('Username or Email Address')
username = StringField("Username")
def validate(self):
from flask_security.utils import (
_datastore,
get_message,
hash_password,
)
from flask_security.confirmable import requires_confirmation
if not super(LoginForm, self).validate():
return False
# try login using email
self.user = _datastore.get_user(self.email.data)
if self.user is None:
self.user = _datastore.get_user(self.username.data)
if self.user is None:
self.email.errors.append(get_message("USER_DOES_NOT_EXIST")[0])
# Reduce timing variation between existing and non-existing users
hash_password(self.password.data)
return False
if not self.user.password:
self.password.errors.append(get_message("PASSWORD_NOT_SET")[0])
# Reduce timing variation between existing and non-existing users
hash_password(self.password.data)
return False
if not self.user.verify_and_update_password(self.password.data):
self.password.errors.append(get_message("INVALID_PASSWORD")[0])
return False
if requires_confirmation(self.user):
self.email.errors.append(get_message("CONFIRMATION_REQUIRED")[0])
return False
if not self.user.is_active:
self.email.errors.append(get_message("DISABLED_ACCOUNT")[0])
return False
return True
and register it as described in other posts:
# Setup Flask-Security
app.config['SECURITY_USER_IDENTITY_ATTRIBUTES'] = ('username','email')
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore,
login_form=ExtendedLoginForm)
Since email and username are optional now one of them can be used to login. But make sure that both fields are set unique in the DB model.