1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
|
Two-factor Configurations
=========================
Two-factor authentication provides a second layer of security to any type of
login, requiring extra information or a secondary device to log in, in addition
to ones login credentials. The added feature includes the ability to add a
secondary authentication method using either via email, sms message, or an
Authenticator app such as Google, Lastpass, or Authy.
The following code sample illustrates how to get started as quickly as
possible using SQLAlchemy and two-factor feature:
Basic SQLAlchemy Two-Factor Application
+++++++++++++++++++++++++++++++++++++++
SQLAlchemy Install requirements
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
::
$ mkvirtualenv <your-app-name>
$ pip install flask-security-too flask-sqlalchemy cryptography pyqrcode
Two-factor Application
~~~~~~~~~~~~~~~~~~~~~~
The following code sample illustrates how to get started as quickly as
possible using SQLAlchemy:
::
from flask import Flask, current_app, render_template
from flask_sqlalchemy import SQLAlchemy
from flask_security import Security, SQLAlchemyUserDatastore, \
UserMixin, RoleMixin, login_required
# At top of file
from flask_mail import Mail
# Convenient references
from werkzeug.datastructures import MultiDict
from werkzeug.local import LocalProxy
_security = LocalProxy(lambda: current_app.extensions['security'])
_datastore = LocalProxy(lambda: _security.datastore)
# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
# Generate a nice key using secrets.token_urlsafe()
app.config['SECRET_KEY'] = os.environ.get("SECRET_KEY", 'pf9Wkove4IKEAXvy-cQkeDPhv9Cb3Ag-wyJILbq_dFw')
# Bcrypt is set as default SECURITY_PASSWORD_HASH, which requires a salt
# Generate a good salt using: secrets.SystemRandom().getrandbits(128)
app.config['SECURITY_PASSWORD_SALT'] = os.environ.get("SECURITY_PASSWORD_SALT", '146585145368132386173505678016728509634')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
app.config['SECURITY_TWO_FACTOR_ENABLED_METHODS'] = ['email',
'authenticator'] # 'sms' also valid but requires an sms provider
app.config['SECURITY_TWO_FACTOR'] = True
app.config['SECURITY_TWO_FACTOR_RESCUE_MAIL'] = 'put_your_mail@gmail.com'
app.config['SECURITY_TWO_FACTOR_ALWAYS_VALIDATE']=False
app.config['SECURITY_TWO_FACTOR_LOGIN_VALIDITY']='1 week'
# Generate a good totp secret using: passlib.totp.generate_secret()
app.config['SECURITY_TOTP_SECRETS'] = {"1": "TjQ9Qa31VOrfEzuPy4VHQWPCTmRzCnFzMKLxXYiZu9B"}
app.config['SECURITY_TOTP_ISSUER'] = 'put_your_app_name'
# Create database connection object
db = SQLAlchemy(app)
# Define models
roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=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'))
tf_phone_number = db.Column(db.String(64))
tf_primary_method = db.Column(db.String(140))
tf_totp_secret = db.Column(db.String(255))
# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)
mail = Mail(app)
# Create a user to test with
@app.before_first_request
def create_user():
db.create_all()
user_datastore.create_user(email='gal@lp.com', password='password', username='gal',
tf_totp_secret=None, tf_primary_method=None)
db.session.commit()
# Views
@app.route('/')
@login_required
def home():
return render_template('index.html')
if __name__ == '__main__':
app.run()
.. _2fa_theory_of_operation:
Theory of Operation
+++++++++++++++++++++
.. note::
The Two-factor feature requires that session cookies be received and sent as part of the API.
This is true regardless of if the application uses forms or JSON.
The Two-factor (2FA) API has four paths:
- Normal login once everything set up
- Changing 2FA setup
- Initial login/registration when 2FA is required
- Rescue
When using forms, the flow from one state to the next is handled by the forms themselves. When using JSON
the application must of course explicitly access the appropriate endpoints. The descriptions below describe the JSON access pattern.
Normal Login
~~~~~~~~~~~~
In the normal case, when the user has already setup their preferred 2FA method (e.g. email, SMS, authenticator app),
then the flow starts with the authentication process using the ``/login`` or ``/us-signin`` endpoints, providing
their identity and password. If 2FA is required, the response will indicate that. Then, the application must POST to the ``/tf-validate``
with the correct code.
Changing 2FA Setup
~~~~~~~~~~~~~~~~~~~
An authenticated user can change their 2FA configuration (primary_method, phone number, etc.). In order to prevent a user from being
locked out, the new configuration must be validated before it is stored permanently. The user starts with a GET on ``/tf-setup``. This will return
a list of configured 2FA methods the user can choose from, and the existing configuration. This must be followed with a POST on ``/tf-setup`` with the new primary
method (and phone number if SMS). In the case of SMS, a code will be sent to the phone/device and again use ``/tf-validate`` to confirm code.
In the case of setting up an authenticator app, the response to the POST will contain the QRcode image as well
as the required information for manual entry.
Once the code has been successfully
entered, the new configuration will be permanently stored.
Initial login/registration
~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is basically a combination of the above two - initial POST to ``/login`` will return indicating that 2FA is required. The user must then POST to ``/tf_setup`` to setup
the desired 2FA method, and finally have the user enter the code and POST to ``/tf-validate``.
Rescue
~~~~~~
Life happens - if the user doesn't have their mobile devices (SMS) or authenticator app, then they can request using ``/tf-rescue`` endpoint to have the code sent to their email.
If they have lost access to their email, they can request an email be sent to the application administrators.
Validity
~~~~~~~~
Sometimes it can be preferrable to enter the 2FA code once a day/week/month, especially if a user logs in and out of a website multiple times. This allows the
security of a two factor authentication but with a slightly better user experience. This can be achevied by setting ``SECURITY_TWO_FACTOR_ALWAYS_VALIDATE`` to ``False``,
and clicking the 'Remember' button on the login form. Once the two factor code is validated, a cookie is set to allow skipping the validation step. The cookie is named
``tf_validity`` and contains the signed token containing the user's ``fs_uniquifier``. The cookie and token are both set to expire after the time delta given in
``SECURITY_TWO_FACTOR_LOGIN_VALIDITY``. Note that setting ``SECURITY_TWO_FACTOR_LOGIN_VALIDITY`` to 0 is equivalent to ``SECURITY_TWO_FACTOR_ALWAYS_VALIDATE`` being ``True``.
|