
basic_auth branch that openid is forked from Commits squashed together (in reverse chronological order): - do the label thing only for boolean fields - made edit_account to autofocus on the first field - added feature to render_divs where if field.label == '' then it will render form.description the same a render_label - added allow_registration check - refactored create_user - removed verification_key from create_user - removed get_user from openid - cleanup after removing openid from template_env.globals - fix for werkzueg 0.9.1 - cleanup after merge - more tests - restored openid extra_validation just for safety - tests for openid - deleted openid extra_validation - passed next parameter in session for openid - fixed a bug that was deleting the messages - implemented openid store using sqlalchemy - ask openid provider for 'nickname' to prefill username in registration form - refactored delete openid url to work with generic urls such as google and to not allow a user to delete a url if it is there only one and they don't have a pw - refactored login to register user workflow, which fixed a problem where the 'or register with a password link' wasn't showing up when the finish_login view called the register view because there wasn't any redirect. - added the ability to remove openid's - added the ability to add openids to an existing account - refactored start_login and finish_login views - modified edit_account.html to use render_divs - modified gmg/edit/views to behave appropriatly if no password authentication is enabled. moved the update email stuff to it's own funtion to make edit_account view cleaner. edit_account now modifies the form depending on the plugins. - minor typos - added retrieving email from openid provider - moved allow_registration check to a decorator - moved check if auth is enabled to a decorator - changed openid user registration to go through login first - cleanup after merge - modified verification emails to use itsdangerous tokens - added error handling on bad token, fixed route, and added tests - added support for user to change email address - added link to login view openid/password in login template - updated openid get_user function - modified get_user function to take kwargs instead of username - no need for user might be email kwarg in check_login_simple - added gen_password_hash and check_password functions to auth/__init__ - added focus to form input - made imports fully qualified - modified basic_auth.check_login to check that the user has a pw_hash first - changed occurances of form.data['whatever'] to form.whatever.data - convert tabs to spaces in register template, remove unsed templates, and fixed trans tags in templates - in process of openid login. it works, but needs major imporvements - make password field required in basic_auth form - check if password field present in basic_auth create_user - modified openid create_user function - modified models based on Elronds suggestions - changed register form action to a variable to be passed in by the view using the template - openid plugin v0, still need to authenticate via openid. - added a register_user function to be able to use in a plugin's register view, and modified auth/views.register to redirect to openid/register if appropriate. - Modified basic_auth plugin to work with modified auth plugin hooks. Added context variables. Removed basic_auth/tools which was previously renamed to basic_auth/lib. - modified auth/__init__ hooks to work better with multiple plugins. Removed auth/lib.py. And added a basic_extra_verification function that all plugins will use. - added models and migrations for openid plugin
153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
# GNU MediaGoblin -- federated, autonomous media hosting
|
|
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import random
|
|
import logging
|
|
|
|
from werkzeug.exceptions import Forbidden
|
|
from wtforms import Form, HiddenField, validators
|
|
|
|
from mediagoblin import mg_globals
|
|
from mediagoblin.meddleware import BaseMeddleware
|
|
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
|
|
|
|
_log = logging.getLogger(__name__)
|
|
|
|
# Use the system (hardware-based) random number generator if it exists.
|
|
# -- this optimization is lifted from Django
|
|
if hasattr(random, 'SystemRandom'):
|
|
getrandbits = random.SystemRandom().getrandbits
|
|
else:
|
|
getrandbits = random.getrandbits
|
|
|
|
|
|
def csrf_exempt(func):
|
|
"""Decorate a Controller to exempt it from CSRF protection."""
|
|
|
|
func.csrf_enabled = False
|
|
return func
|
|
|
|
|
|
class CsrfForm(Form):
|
|
"""Simple form to handle rendering a CSRF token and confirming it
|
|
is included in the POST."""
|
|
|
|
csrf_token = HiddenField("",
|
|
[validators.Required()])
|
|
|
|
|
|
def render_csrf_form_token(request):
|
|
"""Render the CSRF token in a format suitable for inclusion in a
|
|
form."""
|
|
|
|
if 'CSRF_TOKEN' not in request.environ:
|
|
return None
|
|
|
|
form = CsrfForm(csrf_token=request.environ['CSRF_TOKEN'])
|
|
|
|
return form.csrf_token
|
|
|
|
|
|
class CsrfMeddleware(BaseMeddleware):
|
|
"""CSRF Protection Meddleware
|
|
|
|
Adds a CSRF Cookie to responses and verifies that it is present
|
|
and matches the form token for non-safe requests.
|
|
"""
|
|
|
|
CSRF_KEYLEN = 64
|
|
SAFE_HTTP_METHODS = ("GET", "HEAD", "OPTIONS", "TRACE")
|
|
|
|
def process_request(self, request, controller):
|
|
"""For non-safe requests, confirm that the tokens are present
|
|
and match.
|
|
"""
|
|
|
|
# get the token from the cookie
|
|
try:
|
|
request.environ['CSRF_TOKEN'] = \
|
|
request.cookies[mg_globals.app_config['csrf_cookie_name']]
|
|
|
|
except KeyError:
|
|
# if it doesn't exist, make a new one
|
|
request.environ['CSRF_TOKEN'] = self._make_token(request)
|
|
|
|
# if this is a non-"safe" request (ie, one that could have
|
|
# side effects), confirm that the CSRF tokens are present and
|
|
# valid
|
|
if (getattr(controller, 'csrf_enabled', True) and
|
|
request.method not in self.SAFE_HTTP_METHODS and
|
|
('gmg.verify_csrf' in request.environ or
|
|
'paste.testing' not in request.environ)
|
|
):
|
|
|
|
return self.verify_tokens(request)
|
|
|
|
def process_response(self, request, response):
|
|
"""Add the CSRF cookie to the response if needed and set Vary
|
|
headers.
|
|
"""
|
|
|
|
# set the CSRF cookie
|
|
response.set_cookie(
|
|
mg_globals.app_config['csrf_cookie_name'],
|
|
request.environ['CSRF_TOKEN'],
|
|
path=request.environ['SCRIPT_NAME'],
|
|
domain=mg_globals.app_config.get('csrf_cookie_domain'),
|
|
secure=(request.scheme.lower() == 'https'),
|
|
httponly=True)
|
|
|
|
# update the Vary header
|
|
response.vary = list(getattr(response, 'vary', None) or []) + ['Cookie']
|
|
|
|
def _make_token(self, request):
|
|
"""Generate a new token to use for CSRF protection."""
|
|
|
|
return "%s" % (getrandbits(self.CSRF_KEYLEN),)
|
|
|
|
def verify_tokens(self, request):
|
|
"""Verify that the CSRF Cookie exists and that it matches the
|
|
form value."""
|
|
|
|
# confirm the cookie token was presented
|
|
cookie_token = request.cookies.get(
|
|
mg_globals.app_config['csrf_cookie_name'],
|
|
None)
|
|
|
|
if cookie_token is None:
|
|
# the CSRF cookie must be present in the request, if not a
|
|
# cookie blocker might be in action (in the best case)
|
|
_log.error('CSRF cookie not present')
|
|
raise Forbidden(_('CSRF cookie not present. This is most likely '
|
|
'the result of a cookie blocker or somesuch.<br/>'
|
|
'Make sure to permit the settings of cookies for '
|
|
'this domain.'))
|
|
|
|
# get the form token and confirm it matches
|
|
form = CsrfForm(request.form)
|
|
if form.validate():
|
|
form_token = form.csrf_token.data
|
|
|
|
if form_token == cookie_token:
|
|
# all's well that ends well
|
|
return
|
|
|
|
# either the tokens didn't match or the form token wasn't
|
|
# present; either way, the request is denied
|
|
errstr = 'CSRF validation failed'
|
|
_log.error(errstr)
|
|
raise Forbidden(errstr)
|