Skip to main content

Cookies and Session Management for Sanic

Project description

Logo

Software License Build Status Code style: black Downloads Monthly Downloads

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

  1. 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'
    
  2. 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'
    
  3. 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'
    
  4. 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

  1. Session (A generic session interface)
  2. AuthSession (A session interface with login_user, logout_user, current_user logic)

Other pluggable parts

  1. Encoders and Decoders (Default to ujson)
  2. SID factory (Default to uuid.uuid4)
  3. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sanic_cookies-0.4.3.tar.gz (19.0 kB view details)

Uploaded Source

File details

Details for the file sanic_cookies-0.4.3.tar.gz.

File metadata

  • Download URL: sanic_cookies-0.4.3.tar.gz
  • Upload date:
  • Size: 19.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.22.0 setuptools/45.2.0 requests-toolbelt/0.9.1 tqdm/4.42.1 CPython/3.6.9

File hashes

Hashes for sanic_cookies-0.4.3.tar.gz
Algorithm Hash digest
SHA256 4f4b5019bb37c57a432cdaf4e45a171419bccbcf1a25a4b4f7cd86b3a6b3d4cf
MD5 a8ef71f6fcadf40ffa9bf18a5d2e56cb
BLAKE2b-256 db0ba503a143ff62835982c280b71c632f4e0233d3c5d533e46a26223ad4c6dc

See more details on using hashes here.

Provenance

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page