Sanic-WTF - Sanic meets WTForms

Sanic-WTF makes using WTForms with Sanic and CSRF (Cross-Site Request Forgery) protection a little bit easier.

Quick Start

Installation

pip install --upgrade Sanic-WTF

How to use it

Intialization (of Sanic)

from sanic import Sanic

app = Sanic(__name__)

# either WTF_CSRF_SECRET_KEY or SECRET_KEY should be set
app.config['WTF_CSRF_SECRET_KEY'] = 'top secret!'

@app.middleware('request')
async def add_session_to_request(request):
    # setup session

Defining Forms

from sanic_wtf import SanicForm
from wtforms import PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired

class LoginForm(SanicForm):
    name = StringField('Name', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    submit = SubmitField('Sign In')

That’s it, just subclass SanicForm and later on passing in the current request object when you instantiate the form class. Sanic-WTF will do the trick.

Form Validation

from sanic import response

@app.route('/', methods=['GET', 'POST'])
async def index(request):
    form = LoginForm(request)
    if request.method == 'POST' and form.validate():
        name = form.name.data
        password = form.password.data
        # check user password, log in user, etc.
        return response.redirect('/profile')
    # here, render_template is a function that render template with context
    return response.html(await render_template('index.html', form=form))

Note

For WTForms users: please note that SanicForm requires the whole request object instead of some sort of MultiDict.

For more details, please see documentation.

License

BSD New, see LICENSE for details.

Prerequisites

To enable CSRF protection, a session is required, Sanic-WTF expects request['session'] is available in this case. For a simple client side only, cookie-based session, similar to Flask’s built-in session, you might want to try Sanic-CookieSession.

Configuration

Option Description
WTF_CSRF_ENABLED If True, CSRF protection is enabled. Default is True
WTF_CSRF_FIELD_NAME The field name used in the form and session to store the CSRF token. Default is csrf_token
WTF_CSRF_SECRET_KEY bytes used for CSRF token generation. If it is unset, SECRET_KEY will be used instead. Either one of these have to be set to enable CSRF protection.
WTF_CSRF_TIME_LIMIT How long CSRF tokens are valid for, in seconds. Default is 1800. (Half an hour)

API

Note

For users of versions prior to 0.3.0, there is backward incompatible changes in API. The module-level helper object is not longer required, the new form SanicForm is smart enough to figure out how to get user defined settings.

class sanic_wtf.SanicForm(request=None, *args, meta=None, **kwargs)[source]

Form with session-based CSRF Protection.

Upon initialization, the form instance will setup CSRF protection with settings fetched from provided Sanic style request object. With no request object provided, CSRF protection will be disabled.

validate_on_submit()[source]

Return True if this form is submited and all fields verified

class sanic_wtf.FileAllowed(extensions, message=None)[source]

Validate that the file (by extention) is one of the listed types

sanic_wtf.file_allowed

alias of FileAllowed

class sanic_wtf.FileRequired(message=None)[source]

Validate that the data is a non-empty sanic.request.File object

sanic_wtf.file_required

alias of FileRequired

Full Example

Guest Book

from datetime import datetime
from sanic import Sanic, response
from sanic_wtf import SanicForm
from wtforms import SubmitField, TextField
from wtforms.validators import DataRequired, Length


app = Sanic(__name__)
app.config['SECRET_KEY'] = 'top secret !!!'


# NOTE
# session should be setup somewhere, SanicWTF expects request['session'] is a
# dict like session object.
# For demonstration purpose, we use a mock-up globally-shared session object.
session = {}
@app.middleware('request')
async def add_session(request):
    request['session'] = session


class FeedbackForm(SanicForm):
    note = TextField('Note', validators=[DataRequired(), Length(max=40)])
    submit = SubmitField('Submit')


@app.route('/', methods=['GET', 'POST'])
async def index(request):
    form = FeedbackForm(request)
    if request.method == 'POST' and form.validate():
        note = form.note.data
        msg = '{} - {}'.format(datetime.now(), note)
        session.setdefault('fb', []).append(msg)
        return response.redirect('/')
    # NOTE: trusting user input here, never do that in production
    feedback = ''.join('<p>{}</p>'.format(m) for m in session.get('fb', []))
    # Ah, f string, so, python 3.6, what do you expect from someone brave
    # enough to use sanic... :)
    content = f"""
    <h1>Form with CSRF Validation</h1>
    <p>Try <a href="/fail">form</a> that fails CSRF validation</p>
    {feedback}
    <form action="" method="POST">
      {'<br>'.join(form.csrf_token.errors)}
      {form.csrf_token}
      {'<br>'.join(form.note.errors)}
      <br>
      {form.note(size=40, placeholder="say something..")}
      {form.submit}
    </form>
    """
    return response.html(content)


@app.route('/fail', methods=['GET', 'POST'])
async def fail(request):
    form = FeedbackForm(request)
    if request.method == 'POST' and form.validate():
        note = form.note.data
        msg = '{} - {}'.format(datetime.now(), note)
        session.setdefault('fb', []).append(msg)
        return response.redirect('/fail')
    feedback = ''.join('<p>{}</p>'.format(m) for m in session.get('fb', []))
    content = f"""
    <h1>Form which fails CSRF Validation</h1>
    <p>This is the same as this <a href="/">form</a> except that CSRF
    validation always fail because we did not render the hidden csrf token</p>
    {feedback}
    <form action="" method="POST">
      {'<br>'.join(form.csrf_token.errors)}
      {'<br>'.join(form.note.errors)}
      <br>
      {form.note(size=40, placeholder="say something..")}
      {form.submit}
    </form>
    """
    return response.html(content)


if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8000, debug=True)

File Upload

from pathlib import Path
from sanic import Sanic, response
from sanic_wtf import FileAllowed, FileRequired, SanicForm
from wtforms import FileField, SubmitField, StringField
from wtforms.validators import Length


app = Sanic(__name__)
app.config['SECRET_KEY'] = 'top secret !!!'
app.config['UPLOAD_DIR'] = './uploaded.tmp'


# NOTE
# session should be setup somewhere, SanicWTF expects request['session'] is a
# dict like session object.
# For demonstration purpose, we use a mock-up globally-shared session object.
session = {}


@app.middleware('request')
async def add_session(request):
    request['session'] = session


class UploadForm(SanicForm):
    image = FileField('Image', validators=[
        FileRequired(), FileAllowed('bmp gif jpg jpeg png'.split())])
    description = StringField('Description', validators=[Length(max=20)])
    submit = SubmitField('Upload')


app.static('/img', app.config.UPLOAD_DIR)


@app.listener('after_server_start')
async def make_upload_dir(app, loop):
    Path(app.config.UPLOAD_DIR).mkdir(parents=True, exist_ok=True)


@app.route('/', methods=['GET', 'POST'])
async def index(request):
    form = UploadForm(request)
    if form.validate_on_submit():
        image = form.image.data
        # NOTE: trusting user submitted file names here, the name should be
        # sanitized in production.
        uploaded_file = Path(request.app.config.UPLOAD_DIR) / image.name
        uploaded_file.write_bytes(image.body)
        description = form.description.data or 'no description'
        session.setdefault('files', []).append((image.name, description))
        return response.redirect('/')
    img = '<section><img src="/img/{}"><p>{}</p><hr></section>'
    images = ''.join(img.format(i, d) for i, d in session.get('files', []))
    content = f"""
    <h1>Sanic-WTF file field validators example</h1>
    {images}
    <form action="" method="POST" enctype="multipart/form-data">
      {'<br>'.join(form.csrf_token.errors)}
      {form.csrf_token}
      {'<br>'.join(form.image.errors)}
      {'<br>'.join(form.description.errors)}
      <br> {form.image.label}
      <br> {form.image}
      <br> {form.description.label}
      <br> {form.description(size=20, placeholder="description")}
      <br> {form.submit}
    </form>
    """
    return response.html(content)


if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8000, debug=True)

Changelog

  • 0.5.0

    Added file upload support and filefield validators FileAllowed and FileRequired.

  • 0.4.0

    backward incompatible upgrade

    Removed property hidden_tag

  • 0.3.0

    backward incompatible upgrade

    Re-designed the API to fixed #6 - possible race condition. The new API is much simplified, easier to use, while still getting things done, and more.

    Added new setting: WTF_CSRF_TIME_LIMIT Added new method validate_on_submit() in the style of Flask-WTF.

  • 0.2.0

    Made SanicWTF.Form always available so that one can create the form classes before calling SanicWTF.init_app()

  • 0.1.0

    First public release.

Indices and tables