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.
Links¶
Prerequisites¶
To enable CSRF protection, a session is required, Sanic-WTF expects
request.ctx.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.
-
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
sanic_wtf.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
sanic_wtf.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.ctx.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.ctx.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', []))
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.ctx.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.ctx.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.6.0
backward incompatible upgrade
Supporting python 3.6, 2.7, 3.8, 3.9, and Sanic 20.3.0
0.5.0
Added file upload support and filefield validators
FileAllowed
andFileRequired
.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 callingSanicWTF.init_app()
0.1.0
First public release.