diff --git a/docs/index.rst b/docs/index.rst index 0991e12..970e134 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -34,13 +34,13 @@ your Flask application. They include: Many of these features are made possible by integrating various Flask extensions and libraries. They include: -1. Flask-Login -2. Flask-Mail -3. Flask-Principal -4. Flask-Script -5. Flask-WTF -6. itsdangerous -7. passlib +1. `Flask-Login `_ +2. `Flask-Mail `_ +3. `Flask-Principal `_ +4. `Flask-Script `_ +5. `Flask-WTF `_ +6. `itsdangerous `_ +7. `passlib `_ Additionally, it assumes you'll be using a common library for your database connections and model definitions. Flask-Security thus supports SQLAlchemy and diff --git a/example/security.py b/example/security.py deleted file mode 100644 index fe684d8..0000000 --- a/example/security.py +++ /dev/null @@ -1,316 +0,0 @@ -# -*- coding: utf-8 -*- - -""" - Flask-Security - ~~~~~~~~~~~~~~ - - This module was generated by Flask-Security to give developers greater - control over the various security mechanisms. For more information about - using this feature see: - - TODO: Documentation URL -""" - - -from datetime import datetime - -from flask import current_app as app, redirect, request, session, \ - render_template, jsonify, Blueprint -from flask.ext.login import login_user, logout_user -from flask.ext.principal import Identity, AnonymousIdentity, identity_changed -from werkzeug.datastructures import MultiDict -from werkzeug.local import LocalProxy - -from flask_security.confirmable import confirm_by_token, reset_confirmation_token -from flask_security.decorators import login_required -from flask_security.exceptions import ConfirmationError, BadCredentialsError, \ - ResetPasswordError -from flask_security.forms import LoginForm, RegisterForm, ForgotPasswordForm, \ - ResetPasswordForm, ResendConfirmationForm -from flask_security.recoverable import reset_by_token, \ - reset_password_reset_token -from flask_security.signals import user_registered -from flask_security.tokens import generate_authentication_token -from flask_security.utils import get_url, get_post_login_redirect, do_flash, \ - get_remember_token, get_message, config_value - - -# Convenient references -_security = LocalProxy(lambda: app.security) - -_datastore = LocalProxy(lambda: app.security.datastore) - -_logger = LocalProxy(lambda: app.logger) - - -def _do_login(user, remember=True): - """Performs the login and sends the appropriate signal.""" - - if not login_user(user, remember): - return False - - if user.authentication_token is None: - user.authentication_token = generate_authentication_token(user) - - if remember: - user.remember_token = get_remember_token(user.email, user.password) - - if _security.trackable: - old_current, new_current = user.current_login_at, datetime.utcnow() - user.last_login_at = old_current or new_current - user.current_login_at = new_current - - old_current, new_current = user.current_login_ip, request.remote_addr - user.last_login_ip = old_current or new_current - user.current_login_ip = new_current - - user.login_count = user.login_count + 1 if user.login_count else 0 - - _datastore._save_model(user) - - identity_changed.send(app._get_current_object(), - identity=Identity(user.id)) - - _logger.debug('User %s logged in' % user) - return True - - -def _json_auth_ok(user): - return jsonify({ - "meta": { - "code": 200 - }, - "response": { - "user": { - "id": str(user.id), - "authentication_token": user.authentication_token - } - } - }) - - -def _json_auth_error(msg): - resp = jsonify({ - "meta": { - "code": 400 - }, - "response": { - "error": msg - } - }) - resp.status_code = 400 - return resp - - -def authenticate(): - """View function which handles an authentication request.""" - - form = LoginForm(MultiDict(request.json) if request.json else request.form) - - try: - user = _security.auth_provider.authenticate(form) - - if _do_login(user, remember=form.remember.data): - if request.json: - return _json_auth_ok(user) - - return redirect(get_post_login_redirect()) - - raise BadCredentialsError('Account is disabled') - - except BadCredentialsError, e: - msg = str(e) - - _logger.debug('Unsuccessful authentication attempt: %s' % msg) - - if request.json: - return _json_auth_error(msg) - - do_flash(msg, 'error') - - return redirect(request.referrer or _security.login_manager.login_view) - - -def logout(): - """View function which handles a logout request.""" - - for key in ('identity.name', 'identity.auth_type'): - session.pop(key, None) - - identity_changed.send(app._get_current_object(), - identity=AnonymousIdentity()) - - logout_user() - _logger.debug('User logged out') - - return redirect(request.args.get('next', None) or - _security.post_logout_view) - - -def register_user(): - """View function which handles a registration request.""" - - form = RegisterForm(csrf_enabled=not app.testing) - - if form.validate_on_submit(): - # Create user - u = _datastore.create_user(**form.to_dict()) - - # Send confirmation instructions if necessary - t = reset_confirmation_token(u) if _security.confirmable else None - - data = dict(user=u, confirm_token=t) - user_registered.send(data, app=app._get_current_object()) - - _logger.debug('User %s registered' % u) - - # Login the user if allowed - if not _security.confirmable or _security.login_without_confirmation: - _do_login(u) - - return redirect(_security.post_register_view or - _security.post_login_view) - - return render_template('security/registrations/new.html', - register_user_form=form) - - -def send_confirmation(): - form = ResendConfirmationForm() - - if form.validate_on_submit(): - user = _datastore.find_user(email=form.email.data) - - reset_confirmation_token(user) - - _logger.debug('%s request confirmation instructions' % user) - - msg, cat = get_message('CONFIRMATION_REQUEST', email=user.email) - - do_flash(msg, cat) - - else: - for key, value in form.errors.items(): - do_flash(value[0], 'error') - - return render_template('security/confirmations/new.html', - reset_confirmation_form=form) - - -def confirm_account(token): - """View function which handles a account confirmation request.""" - try: - user = confirm_by_token(token) - _logger.debug('%s confirmed their account' % user) - - except ConfirmationError, e: - msg, cat = str(e), 'error' - - _logger.debug('Confirmation error: ' + msg) - - if e.user: - reset_confirmation_token(e.user) - - msg, cat = get_message('CONFIRMATION_EXPIRED', - within=_security.confirm_email_within, - email=e.user.email) - - do_flash(msg, cat) - - return redirect(get_url(_security.confirm_error_view)) - - do_flash(get_message('ACCOUNT_CONFIRMED')) - - return redirect(_security.post_confirm_view or _security.post_login_view) - - -def forgot_password(): - """View function that handles a forgotten password request.""" - - form = ForgotPasswordForm(csrf_enabled=not app.testing) - - if form.validate_on_submit(): - user = _datastore.find_user(**form.to_dict()) - - reset_password_reset_token(user) - - _logger.debug('%s requested to reset their password' % user) - - msg, cat = get_message('PASSWORD_RESET_REQUEST', email=user.email) - - do_flash(msg, cat) - - return redirect(_security.post_forgot_view) - - else: - _logger.debug('A reset password request was made for %s but ' - 'that email does not exist.' % form.email.data) - - for key, value in form.errors.items(): - do_flash(value[0], 'error') - - return render_template('security/passwords/new.html', - forgot_password_form=form) - - -def reset_password(token): - """View function that handles a reset password request.""" - - form = ResetPasswordForm(csrf_enabled=not app.testing) - - if form.validate_on_submit(): - try: - user = reset_by_token(token=token, **form.to_dict()) - _logger.debug('%s reset their password' % user) - - except ResetPasswordError, e: - msg, cat = str(e), 'error' - - _logger.debug('Password reset error: ' + msg) - - if e.user: - reset_password_reset_token(e.user) - - msg, cat = get_message('PASSWORD_RESET_EXPIRED', - within=_security.reset_password_within, - email=e.user.email) - - do_flash(msg, cat) - - return render_template('security/passwords/edit.html', - reset_password_form=form, - password_reset_token=token) - - -def create_blueprint(app, name, import_name, **kwargs): - bp = Blueprint(name, import_name, **kwargs) - - bp.route(config_value('AUTH_URL', app=app), - methods=['POST'], - endpoint='authenticate')(authenticate) - - bp.route(config_value('LOGOUT_URL', app=app), - endpoint='logout')(login_required(logout)) - - if config_value('REGISTERABLE', app=app): - bp.route(config_value('REGISTER_URL', app=app), - methods=['GET', 'POST'], - endpoint='register')(register_user) - - if config_value('RECOVERABLE', app=app): - bp.route(config_value('RESET_URL', app=app), - methods=['GET', 'POST'], - endpoint='forgot_password')(forgot_password) - bp.route(config_value('RESET_URL', app=app) + '/', - methods=['GET', 'POST'], - endpoint='reset_password')(reset_password) - - if config_value('CONFIRMABLE', app=app): - bp.route(config_value('CONFIRM_URL', app=app), - methods=['GET', 'POST'], - endpoint='send_confirmation')(send_confirmation) - bp.route(config_value('CONFIRM_URL', app=app) + '/', - methods=['GET', 'POST'], - endpoint='confirm_account')(confirm_account) - - return bp diff --git a/flask_security/__init__.py b/flask_security/__init__.py index cca9f3e..ceedf93 100644 --- a/flask_security/__init__.py +++ b/flask_security/__init__.py @@ -10,8 +10,6 @@ :license: MIT, see LICENSE for more details. """ -from flask.ext.login import login_user, logout_user - from .core import Security, RoleMixin, UserMixin, AnonymousUser, \ AuthenticationProvider, current_user from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore @@ -22,3 +20,4 @@ from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \ from .signals import confirm_instructions_sent, password_reset, \ password_reset_requested, reset_instructions_sent, user_confirmed, \ user_registered +from .utils import login_user, logout_user diff --git a/flask_security/tokens.py b/flask_security/tokens.py deleted file mode 100644 index f896469..0000000 --- a/flask_security/tokens.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.tokens - ~~~~~~~~~~~~~~~~~~~~~~~~~ - - Flask-Security tokens module - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -from flask import current_app as app -from werkzeug.local import LocalProxy - -from .utils import md5 - - -# Convenient references -_security = LocalProxy(lambda: app.extensions['security']) - -_datastore = LocalProxy(lambda: _security.datastore) - - -def generate_authentication_token(user): - """Generates a unique authentication token for the specified user. - - :param user: The user to work with - """ - data = [str(user.id), md5(user.email)] - return _security.token_auth_serializer.dumps(data) - - -def reset_authentication_token(user): - """Resets a user's authentication token and returns the new token value. - - :param user: The user to work with - """ - token = generate_authentication_token(user) - user.authentication_token = token - _datastore._save_model(user) - return token - - -def ensure_authentication_token(user): - """Ensures that a user has an authentication token. If the user has an - authentication token already, nothing is performed. - - :param user: The user to work with - """ - if not user.authentication_token: - return reset_authentication_token(user) diff --git a/flask_security/utils.py b/flask_security/utils.py index 24a54b9..f8a095e 100644 --- a/flask_security/utils.py +++ b/flask_security/utils.py @@ -17,7 +17,9 @@ from contextlib import contextmanager from datetime import datetime, timedelta from flask import url_for, flash, current_app, request, session, render_template -from flask.ext.login import make_secure_token +from flask.ext.login import make_secure_token, login_user as _login_user, \ + logout_user as _logout_user +from flask.ext.principal import Identity, AnonymousIdentity, identity_changed from werkzeug.local import LocalProxy from .signals import user_registered, password_reset_requested @@ -26,8 +28,56 @@ from .signals import user_registered, password_reset_requested # Convenient references _security = LocalProxy(lambda: current_app.extensions['security']) +_datastore = LocalProxy(lambda: _security.datastore) + _pwd_context = LocalProxy(lambda: _security.pwd_context) +_logger = LocalProxy(lambda: current_app.logger) + + +def login_user(user, remember=True): + """Performs the login and sends the appropriate signal.""" + + if not _login_user(user, remember): + return False + + if user.authentication_token is None: + from .tokens import generate_authentication_token + + user.authentication_token = generate_authentication_token(user) + + if remember: + user.remember_token = get_remember_token(user.email, user.password) + + if _security.trackable: + old_current, new_current = user.current_login_at, datetime.utcnow() + user.last_login_at = old_current or new_current + user.current_login_at = new_current + + old_current, new_current = user.current_login_ip, request.remote_addr + user.last_login_ip = old_current or new_current + user.current_login_ip = new_current + + user.login_count = user.login_count + 1 if user.login_count else 0 + + _datastore._save_model(user) + + identity_changed.send(current_app._get_current_object(), + identity=Identity(user.id)) + + _logger.debug('User %s logged in' % user) + return True + + +def logout_user(): + for key in ('identity.name', 'identity.auth_type'): + session.pop(key, None) + + identity_changed.send(current_app._get_current_object(), + identity=AnonymousIdentity()) + + _logout_user() + def get_hmac(msg, salt=None, digestmod=None): digestmod = digestmod or hashlib.sha512 @@ -44,6 +94,15 @@ def encrypt_password(password, salt=None, use_hmac=False): return _pwd_context.encrypt(hmac_value) +def generate_authentication_token(user): + """Generates a unique authentication token for the specified user. + + :param user: The user to work with + """ + data = [str(user.id), md5(user.email)] + return _security.token_auth_serializer.dumps(data) + + def md5(data): return hashlib.md5(data).hexdigest() diff --git a/flask_security/views.py b/flask_security/views.py index f1d534c..a0eb415 100644 --- a/flask_security/views.py +++ b/flask_security/views.py @@ -9,12 +9,9 @@ :license: MIT, see LICENSE for more details. """ -from datetime import datetime - from flask import current_app as app, redirect, request, session, \ render_template, jsonify, Blueprint -from flask.ext.login import login_user, logout_user -from flask.ext.principal import Identity, AnonymousIdentity, identity_changed +from flask.ext.principal import AnonymousIdentity, identity_changed from werkzeug.datastructures import MultiDict from werkzeug.local import LocalProxy @@ -27,9 +24,8 @@ from flask_security.forms import LoginForm, RegisterForm, ForgotPasswordForm, \ from flask_security.recoverable import reset_by_token, \ reset_password_reset_token from flask_security.signals import user_registered -from flask_security.tokens import generate_authentication_token from flask_security.utils import get_url, get_post_login_redirect, do_flash, \ - get_remember_token, get_message, config_value + get_message, config_value, login_user, logout_user # Convenient references @@ -40,38 +36,6 @@ _datastore = LocalProxy(lambda: _security.datastore) _logger = LocalProxy(lambda: app.logger) -def _do_login(user, remember=True): - """Performs the login and sends the appropriate signal.""" - - if not login_user(user, remember): - return False - - if user.authentication_token is None: - user.authentication_token = generate_authentication_token(user) - - if remember: - user.remember_token = get_remember_token(user.email, user.password) - - if _security.trackable: - old_current, new_current = user.current_login_at, datetime.utcnow() - user.last_login_at = old_current or new_current - user.current_login_at = new_current - - old_current, new_current = user.current_login_ip, request.remote_addr - user.last_login_ip = old_current or new_current - user.current_login_ip = new_current - - user.login_count = user.login_count + 1 if user.login_count else 0 - - _datastore._save_model(user) - - identity_changed.send(app._get_current_object(), - identity=Identity(user.id)) - - _logger.debug('User %s logged in' % user) - return True - - def _json_auth_ok(user): return jsonify({ "meta": { @@ -107,7 +71,7 @@ def authenticate(): try: user = _security.auth_provider.authenticate(form) - if _do_login(user, remember=form.remember.data): + if login_user(user, remember=form.remember.data): if request.json: return _json_auth_ok(user) @@ -131,13 +95,8 @@ def authenticate(): def logout(): """View function which handles a logout request.""" - for key in ('identity.name', 'identity.auth_type'): - session.pop(key, None) - - identity_changed.send(app._get_current_object(), - identity=AnonymousIdentity()) - logout_user() + _logger.debug('User logged out') return redirect(request.args.get('next', None) or @@ -163,7 +122,7 @@ def register_user(): # Login the user if allowed if not _security.confirmable or _security.login_without_confirmation: - _do_login(u) + login_user(u) return redirect(_security.post_register_view or _security.post_login_view)