Cookies and Session Management for Sanic
Project description
Sanic Cookies
Much of the code here is borrowed from sanic_session.
I wanted to make some changes that would break a big part of sanic_session
's API, so I decided to create this repo instead.
Sanic cookies supports both client side and server side cookies.
Main deviations from sanic_session are
-
Interfaces are only responsible for reading/writing the
SessionDict
:Session management logic is handled by the
Session
object -
No race conditions:
By using:
async with request['session']: request['session']['foo'] = 'bar'
instead of:
request['session']['foo'] = 'bar'
It is still however possible to use the
session_dict
without a context manager, but it will raise some warnings, unless it's explicitly turned off (warn_lock=False)Note:
The locking mechanism used here only keeps track of locks on a thread-level, which means, an application that is horizontally scaled or one that runs on more than one process won't fully benefit from the locking mechanism that sanic-cookies currently has in place and might encounter some race conditions. I have plans to introduce a distributed locking mechanism. Probably using something like: Aioredlock. But for now, you should know that the locking mechanism that is currently in place will not work in a multi-process environment.
-
A simpler implementation of SessionDict that helps me sleep in peace at night. (Probably less performant)
-
In memory interface schedules cleanup to avoid running out of memory
-
Encrypted client side cookie interface
-
Ability to add more than one interface to the same session
-
Authenticated Session implementation
Setup ⚙️
$ pip install sanic_cookies
Quick Start
from sanic_cookies import Session, InMemory from sanic import Sanic app = Sanic() Session(app, master_interface=InMemory()) @app.route('/') async def handler(request): async with request['session'] as sess: sess['foo'] = 'bar'
Usage
Running multiple interfaces
from sanic_cookies import Session, InMemory, Aioredis from sanic import Sanic inmem = InMemory() aioredis = AioRedis(aioredis_pool_instance) app = Sanic() sess = Session(app, master_interface=inmem, session_name='my_1st_sess') sess.add_interface(aioredis) @app.route('/') async def index(request): async with request['my_1st_session'] as sess: sess['foo'] = 'bar' # At this point 'foo' = 'bar' is written both to the inmemory # interface and the aioredis interface async with request['my_1st_session'] as sess: # When reading, your session will always read from the "master_interface" # In that case it's the inmem interface assert sess['foo'] == 'bar' # Such pattern can be useful in many cases # e.g. you want to share your session information with an analytics team
Running multiple sessions
from sanic_cookies import Session, AuthSession, InMemory, InCookieEncrypted, AioRedis from sanic import Sanic inmem = InMemory() aioredis = Aioredis(aioredis_pool_instance) incookie = InCookieEncrypted(b'fernetsecretkey') app = Sanic() incookie_session = Session( app, master_interface=incookie, session_name='incookiesess', cookie_name='INCOOKIE' ) generic_session = Session( app, master_interface=inmem, session_name='session', cookie_name='SESSION' ) auth_session = AuthSession( app, master_interface=aioredis, session_name='auth_session', cookie_name='SECURE_SESSION' ) # for production (HTTPs) set `secure=True` in your auth_session, # but this will fail in local development @app.route('/') async def index(request): async with request['incookie_session'] as sess: sess['foo'] = 'bar' async with request['session'] as sess: sess['bar'] = 'baz' async with request['auth_session'] as sess: sess['baz'] = 'foo'
AuthSession
Following up on the previous example:
from sanic_cookies import login_required @app.route('/login') async def login(request): # 1. User verification logic # both will work (Whatever is json serializble will) # If you want to pickle an object simply change the default # encoder&decoder in the interfaces plugged in to your AuthSession authorized_user = 123 authorized_user = {'user_id': 123, 'email': 'foo@bar.baz'} # 2. Login user # Here we access the session object # (not the session dict that is accessible from the request) from the app await request.app.exts.auth_session.login_user(request, authorized_user) # 3. Use the session dict safely and exclusively for the logged in user async with request['auth_session'] as sess: sess['foo'] = 'bar' current_user = sess['current_user'] assert current_user == await request.app.exts.auth_session.current_user() @app.route('/logout') async def logout(request): async with request['auth_session'] as sess: assert sess['foo'] == 'bar' # From before await request.app.exts.auth_session.logout_user(request) # Resets the session async with request['auth_session'] as sess: assert sess.get('foo') is None # should never fail assert sess.get('current_user') is None # should never fail @app.route('/protected') @login_required() async def protected(request): assert await request.app.exts.auth_session.current_user() is not None # should never fail
Interfaces available
-
In memory
from sanic_cookies import Session, InMemory from sanic import Sanic interface = InMemory() app = Sanic() Session(app, master_interface=interface) # You can skip this part if you don't want scheduled interface cleanup @app.listener('before_server_start') def init_inmemory(app, loop): interface.init() @app.listener('after_server_stop') def kill_inmemory(app, loop): interface.kill() @app.route('/') async def handler(request): async with request['session'] as sess: sess['foo'] = 'bar'
-
Aioredis
from aioredis import Aioredis from sanic_cookies import Aioredis as AioredisInterface from sanic import Sanic app = Sanic() aioredis_pool_instance = Aioredis() aioredis = AioredisInterface(aioredis_pool_instance) Session(app, master_interface=interface) @app.route('/') async def handler(request): async with request['session'] as sess: sess['foo'] = 'bar'
-
Encrypted in-cookie (using the amazing cryptography.Fernet library)
i. Open a Python terminal and generate a new Fernet key:
>>> from cryptography.fernet import Fernet >>> SESSION_KEY = Fernet.generate_key() >>> print(SESSION_KEY) b'copy me to your sanic app and keep me really secure'
ii. Write your app
from sanic import Sanic from sanic_cookies import Session, InCookieEncrypted app = Sanic() app.config.SESSION_KEY = SESSION_KEY Session( app, master_interface=InCookieEncrypted(app.config.SESSION_KEY), ) @app.route('/') async def handler(request): async with request['session'] as sess: sess['foo'] = 'bar'
-
Gino-AsyncPG (Postgres 9.5+):
i. Manually create a table:
CREATE TABLE IF NOT EXISTS sessions ( created_at timestamp without time zone NOT NULL, expires_at timestamp without time zone, sid character varying, val character varying, CONSTRAINT sessions_pkey PRIMARY KEY (sid) );
ii. Add the interface:
from sanic import Sanic from gino.ext.sanic import Gino from sanic_cookies import GinoAsyncPG from something_secure import DB_SETTINGS app = Sanic() app.config.update(DB_SETTINGS) db = Gino() db.init_app(app) interface = GinoAsyncPG(client=db) auth_session = AuthSession(app, master_interface=interface) if __name__ == '__main__': app.run(host='127.0.0.1', port='8080')
Sessions available
- Session (A generic session interface)
- AuthSession (A session interface with login_user, logout_user, current_user logic)
Other pluggable parts
- Encoders and Decoders (Default to ujson)
- SID factory (Default to uuid.uuid4)
- Session dict implementation
Contact 📧
I currently work as a freelance software devloper. Like my work and got a gig for me?
Want to hire me fulltime? Send me an email @ omarryhan@gmail.com
Buy me a coffee ☕
Bitcoin: 3NmywNKr1Lzo8gyNXFUnzvboziACpEa31z
Ethereum: 0x1E1400C31Cd813685FE0f6D29E0F91c1Da4675aE
Bitcoin Cash: qqzn7rsav6hr3zqcp4829s48hvsvjat4zq7j42wkxd
Litecoin: MB5M3cE3jE4E8NwGCWoFjLvGqjDqPyyEJp
Paypal: https://paypal.me/omarryhan
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Filename, size | File type | Python version | Upload date | Hashes |
---|---|---|---|---|
Filename, size sanic_cookies-0.4.3.tar.gz (19.0 kB) | File type Source | Python version None | Upload date | Hashes View |