Skip to main content

flask-session.manager.sk

Version Python License: MIT

Support me on Ko-fi

Flask companion package for react-session.manager.sk.

Cookie-driven JWT session management for Flask backends, designed to pair with the React session manager's HttpOnly-cookie transport.

Install

# uv (recommended)
uv add flask-session.manager.sk

# pip
pip install flask-session.manager.sk

Quick Start

from flask import Flask, jsonify
from flask_jwt_extended import create_access_token
from flask_session_manager_sk import (
    SessionManager,
    SessionManagerCallbacks,
    verify_session_token_record,
)

app = Flask(__name__)
app.config.update(
    {
        "SECRET_KEY": "your-secret-key",
        "JWT_TOKEN_LOCATION": ["cookies"],
        "JWT_COOKIE_SECURE": True,  # require HTTPS in production
        "JWT_COOKIE_CSRF_PROTECT": True,
        "JWT_ACCESS_COOKIE_NAME": "access_token_cookie",
        "FRONTEND_URL": "https://myapp.example.com",
        "CORS_ORIGINS": ["https://myapp.example.com"],
    }
)

manager = SessionManager()


def verify_user_token(user, agent, device_uid, token):
    record = user.find_session_record(agent=agent, device_uid=device_uid)
    if verify_session_token_record(token, record):
        return record
    return None


callbacks = SessionManagerCallbacks(
    user_lookup=lambda identity: get_user_by_id(identity),
    refresh_user_token=lambda user, agent, device_uid: create_and_store_token(
        user, agent, device_uid
    ),
    verify_user_token=verify_user_token,
    is_user_active=lambda user: user.is_active,
    is_session_persistent=lambda user, agent, device_uid, record=None: bool(
        record and record.persistent
    ),
)

manager.init_app(app, callbacks=callbacks)


@app.route("/auth/who")
@jwt_required(optional=True)
def whoami():
    from flask_jwt_extended import current_user

    if current_user:
        return jsonify(logged_in=True, user_id=current_user.id)
    return jsonify(logged_in=False)

SessionManager.init_app() automatically registers the package CSRF/origin guard for cookie authentication. Browser unsafe requests using cookies must come from a configured FRONTEND_URL or CORS_ORIGINS origin unless you deliberately opt out with FSM_CSRF_ORIGIN_CHECK=False.

Public API

Only these names are part of the stable surface:

Name Description
SessionManager Flask extension that wires JWT callbacks
SessionManagerCallbacks Frozen dataclass of application hooks
token_response Create a JSON response with an HttpOnly JWT cookie
session_response Create an authenticated bootstrap response that exposes only the current CSRF claim
clear_token_response Create a response that clears JWT cookies
verify_session_token_record Verify a presented JWT against a server-side token record

Everything else in the package is internal and may change without notice.

import flask_session_manager_sk

print(flask_session_manager_sk.__version__)  # e.g. "1.0.0"

SessionManagerCallbacks

@dataclass(frozen=True)
class SessionManagerCallbacks:
    # Required
    user_lookup: Callable[[str], Any | None]
    refresh_user_token: Callable[[Any, str, str | None], str | None]

    # Optional
    verify_user_token: (
        Callable[[Any, str | None, str | None, str | None], Any | None] | None
    ) = None
    is_user_active: Callable[[Any], bool] | None = None
    is_session_persistent: (
        Callable[[Any, str | None, str | None, Any | None], bool] | None
    ) = None

verify_user_token receives (user, agent, device_uid, token). Agent and device UID are identifiers/signals only. They are not secrets and must never be enough to authenticate a session by themselves. Resolve the relevant server-side session record with metadata, then compare the presented JWT against the record's stored hash with verify_session_token_record() or equivalent constant-time logic.

refresh_user_token may persist the replacement JWT. Before invoking it, the extension validates all package-controlled persistent and Partitioned cookie configuration so a known configuration failure cannot leave server and browser token state out of sync.

is_session_persistent receives (user, agent, device_uid, token_record) during expired-token refresh. Return True only when the remembered-session policy is still valid for that server-side record. This lets the package reissue persistent cookies without owning your database schema or lifetime policy.

Flask Configuration Reference

These values are read at runtime via current_app.config. No configuration is stored in the package.

Key Required Default Description
SECRET_KEY Yes - Flask secret, must be >=32 bytes for HMAC-SHA256
JWT_TOKEN_LOCATION Yes - Include "cookies" for browser cookie auth; bearer-only clients can use "headers"
JWT_ACCESS_COOKIE_NAME Yes - Cookie name; react-session.manager.sk expects "access_token_cookie"
JWT_COOKIE_CSRF_PROTECT Yes - Must be True for recommended browser cookie auth. May be set to False when FSM_CSRF_ORIGIN_CHECK=True as an explicit reduced-defense fallback.
JWT_COOKIE_SECURE Yes - True in production and always required when JWT_COOKIE_SAMESITE="None"
JWT_COOKIE_SAMESITE No - "Lax" or "Strict"; use "None" only with Secure=True for cross-site cookies
FSM_CSRF_ORIGIN_CHECK No True Automatically reject unsafe cookie-auth requests from unconfigured or missing origins. Set False only for deliberate, security-reviewed opt-out.
FSM_PERSISTENT_MAX_AGE For persistent=True - timedelta or int seconds for remembered-session cookie Max-Age
FSM_COOKIE_PARTITIONED No False Append ; Partitioned (CHIPS) to all auth cookies. Requires JWT_COOKIE_SAMESITE="None" and JWT_COOKIE_SECURE=True. Helps cross-site cookie auth on supported iOS WebKit/Safari ITP and other third-party-cookie blockers.
FRONTEND_URL For cookie origin checks - Canonical URL of the SPA
CORS_ORIGINS For cookie origin checks - Allowed browser origins as a string or list

When cookie auth is enabled, initialization validates the security-sensitive combinations above and raises RuntimeError with an actionable message if the configuration is unsafe or missing required browser origins. Cookie auth requires at least one of JWT_COOKIE_CSRF_PROTECT=True or FSM_CSRF_ORIGIN_CHECK=True; disabling both is rejected. Bearer-only configurations are not blocked by these cookie checks.

CSRF and Origin Policy

Flask-JWT-Extended's double-submit CSRF token and this package's origin check are complementary defenses:

  • safe methods (GET, HEAD, OPTIONS) are not blocked by the package origin guard
  • unsafe methods (POST, PUT, PATCH, DELETE) using cookie auth require an allowed Origin or Referer
  • if both Origin and Referer are missing on an unsafe cookie-auth request, the guard fails closed with 403
  • bearer-authenticated requests skip the browser cookie origin guard
  • if both a cookie and a bearer authorization header are present, bearer mode wins for the package origin guard

Same-site readable-cookie CSRF

When the SPA and API share a registrable domain (e.g. app.example.com and api.example.com), the browser can read the csrf_access_token cookie that token_response() sets. Configure the frontend Axios instance with:

axios.defaults.withCredentials = true;
axios.defaults.withXSRFToken = true;
axios.defaults.xsrfCookieName = "csrf_access_token";
axios.defaults.xsrfHeaderName = "X-CSRF-TOKEN";

Axios then reads the cookie and sends it as X-CSRF-TOKEN automatically.

Genuinely cross-site SPA/API CSRF

When the SPA and API live on different registrable domains (e.g. https://portal.example.com and https://api.herokuapp.com), the browser cannot read the API-scoped csrf_access_token cookie from the SPA's origin. In this case token_response(), session_response(), and the expired-token refresh path return the current CSRF value in the response header:

X-CSRF-TOKEN: <current csrf claim>
Access-Control-Expose-Headers: X-CSRF-TOKEN

The access JWT itself remains HttpOnly and is never exposed to JavaScript.

Use session_response() from the authenticated route supplied as the React companion's userLoader. This restores in-memory CSRF state after a hard reload without rotating or exposing the access JWT:

from flask_jwt_extended import current_user, jwt_required
from flask_session_manager_sk import session_response


@app.get("/auth/who")
@jwt_required(optional=True)
def who_am_i():
    if current_user:
        return session_response({"logged_in": True, "Info": current_user.to_dict()})
    return session_response({"logged_in": False})

Required CORS configuration for cross-site deployments:

CORS(
    app,
    supports_credentials=True,
    origins=app.config["CORS_ORIGINS"],  # must not be "*" with credentials
    allow_headers=["Content-Type", "appVersion", "deviceUID", "X-CSRF-TOKEN"],
    expose_headers=["X-CSRF-TOKEN"],
)

The frontend companion react-session.manager.sk (v4.2+) captures this header in memory and attaches it to unsafe requests automatically. Do not store the CSRF value in localStorage or sessionStorage.

Required request header

Unsafe cookie-auth requests must include:

X-CSRF-TOKEN: <current csrf claim>

token_response() emits both the readable csrf_access_token cookie and the X-CSRF-TOKEN response header. session_response() re-emits the authenticated JWT's existing CSRF claim during session bootstrap, so both topologies continue working after a hard reload.

Origin-only fallback (reduced defense)

The recommended configuration enables both protections:

JWT_COOKIE_CSRF_PROTECT = True
FSM_CSRF_ORIGIN_CHECK = True

The header transport above is the preferred cross-site solution and is now implemented by this package (v1.3+) paired with react-session.manager.sk v4.2+. For consumers that cannot yet adopt it, an explicit origin-only fallback remains supported:

JWT_COOKIE_CSRF_PROTECT = False
FSM_CSRF_ORIGIN_CHECK = True

This disables Flask-JWT-Extended's double-submit CSRF check while keeping the strict Origin/Referer guard active for unsafe cookie-authenticated requests. It is a reduced-defense fallback, not a permanent configuration.

The package fails closed when both protections are disabled together (JWT_COOKIE_CSRF_PROTECT=False + FSM_CSRF_ORIGIN_CHECK=False) for cookie authentication.

Session Lifetime and Remember Me

JWT expiry, browser cookie lifetime, and remembered-session lifetime are separate concepts:

  • JWT_ACCESS_TOKEN_EXPIRES controls how long an individual JWT is valid.
  • Session cookies (persistent=False) are browser-session cookies and do not receive Max-Age.
  • Remembered cookies (persistent=True) receive Max-Age from FSM_PERSISTENT_MAX_AGE and can survive browser restarts.
  • Expired cookie JWTs are refreshed through replacement HttpOnly cookies. The refresh response is signal-only ({"refreshed": true}) and does not expose a new raw access token to JavaScript.
  • The refresh response also emits a new X-CSRF-TOKEN header and exposes it, so cross-site SPAs can update their in-memory CSRF value.
  • is_session_persistent(...) decides whether refresh should preserve persistent cookie attributes for a remembered session.
  • Absolute versus sliding remembered-session lifetime is application policy. Store fields such as created_at, expires_at, last_seen, or revoked_at on your own session record and return False from callbacks when the remembered session is no longer valid.
  • Logout, revocation, inactive users, and failed token-hash verification override persistence and must reject refresh.

Token Verification and Revocation

Use token hashes as the proof of possession. Device UID, User-Agent, IP address, and similar metadata are useful for finding a candidate session record, but they are not authentication secrets.

Recommended consumer pattern:

def verify_user_token(user, agent, device_uid, token):
    record = user.find_session_record(agent=agent, device_uid=device_uid)
    if verify_session_token_record(token, record):
        return record
    return None

verify_token_hash() uses a constant-time digest comparison and fails closed for missing values. clear_session_token_value() preserves the registered-device row while clearing active token state (token_hash = None, empty hint, no plaintext token). Legacy rows containing the old SHA-256 hash of an empty string are treated as revoked and never verify.

Cross-Site Cookies and iOS WebKit (CHIPS)

Browser auth uses an HttpOnly cookie. When the SPA and the API live on different registrable domains (e.g. app.example.com to api.herokuapp.com), the cookie is a third-party cookie. Desktop Chromium accepts SameSite=None; Secure third-party cookies, but iOS WebKit (Safari and every iOS browser, including Brave, which is WebKit under the hood) blocks them by default via Intelligent Tracking Prevention. Result: login succeeds, the browser never stores the cookie, and the next request 401s.

Set FSM_COOKIE_PARTITIONED=True to emit ; Partitioned (CHIPS) on all auth cookies. Partitioned cookies are stored in a per-top-level-site partition, so WebKit keeps them even with third-party cookies blocked. Requirements:

  • JWT_COOKIE_SAMESITE="None" and JWT_COOKIE_SECURE=True (enforced; a misconfigured combination raises RuntimeError at cookie-set time).
  • Browser support: iOS 16.4+ / Safari 16.4+, Chrome 114+. Older iOS remains affected; the durable fix is serving the API from the same site as the SPA.

React Companion Contract

The frontend companion react-session.manager.sk (v4.2+) uses this transport contract:

  • Axios configured with withCredentials: true, withXSRFToken: true
  • Cookies expected at default Flask-JWT-Extended names:
    • access_token_cookie (JWT)
    • csrf_access_token (CSRF double-submit)
  • CSRF header sent as X-CSRF-TOKEN
  • deviceUID header sent on every request
  • appVersion header sent on every request
  • No Authorization bearer header for browser requests
  • For cross-site deployments, the CSRF value is captured from the X-CSRF-TOKEN response header (in memory only) and attached to unsafe requests automatically
  • Legacy localStorage/sessionStorage bearer tokens are automatically cleared

Internal Modules

Not part of the stable API. Import at your own risk.

Module Content
flask_session_manager_sk.cookies CSRF origin checks, cookie auth detection, clear_session_token_value
flask_session_manager_sk.request get_agent, get_ip, get_token, get_dets_from_request
flask_session_manager_sk.tokens create_token_hash, token_hint, verify_token_hash, verify_session_token_record, update_token_record
flask_session_manager_sk.extension SessionManager implementation details

Migrating from Bearer Tokens

If your backend currently returns access tokens as JSON payloads (e.g. {"access_token": "..."}) and stores them in localStorage:

  1. Package adoption: Wire SessionManager with your user-lookup, token-refresh, token-verification, and optional persistence callbacks.
  2. Login endpoint: Call token_response(payload, status, access_token) instead of jsonify(payload). This sets the HttpOnly cookie automatically and also emits the X-CSRF-TOKEN header for cross-site SPAs. The payload must not contain an access_token field or the JWT value; attempts fail loudly instead of exposing the credential to JavaScript.
  3. Session endpoint: Return authenticated userLoader/whoami payloads through session_response() so a hard reload restores cross-site CSRF state.
  4. CSRF/origin checks: Remove custom boilerplate that manually registers reject_cookie_csrf() as a before_request hook. SessionManager.init_app() registers it automatically for cookie auth by default.
  5. Token verification: Make sure your verify_user_token callback compares the presented JWT against the stored token hash. Matching device metadata alone is insecure.
  6. Frontend: Upgrade react-session.manager.sk to the companion version documented in its release notes. It removes bearer-token browser storage, keeps same-site XSRF support, and adds cross-site CSRF header capture for SPAs on a different registrable domain.
  7. Backwards compatibility: Non-browser clients (API scripts, scheduled tasks) can still send valid bearer authorization when JWT_TOKEN_LOCATION includes "headers". The package origin guard skips bearer-authenticated requests. Expired bearer tokens are not auto-refreshed by this package because refreshed JWTs are only issued through HttpOnly cookies; bearer clients should use the application's explicit token-renewal flow.

Development

git clone https://github.com/Skulldorom/flask-session.manager.sk.git
cd flask-session.manager.sk

# Install deps + editable package
uv sync --dev

# Run checks
uv run ruff check .
uv run ruff format --check .
uv run pytest -v

# Build
uv build

License

MIT - see LICENSE.

Release files for flask-session.manager.sk 1.3.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for flask-session.manager.sk 1.3.2
File Size Uploaded
flask_session_manager_sk-1.3.2.tar.gz 39.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for flask-session.manager.sk 1.3.2
File Interpreter ABI Platform
flask_session_manager_sk-1.3.2-py3-none-any.whl Python 3 none any Details

Total release size: 56.4 kB

Release files / flask_session_manager_sk-1.3.2.tar.gz

Download URL flask_session_manager_sk-1.3.2.tar.gz
Size 39.5 kB
Tags Source
SHA-256 checksum
How to use checksums
a259d95304e030ecb7adcb0d8d0394245bc390d06f783298e52f992e00277dfd
BLAKE2b-256 checksum
How to use checksums
2a6fcc9a75dc64642b0f7bc49512a20db07f94c3caa74d31dec24460516b2ca3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.

Transparency log

Release files / flask_session_manager_sk-1.3.2-py3-none-any.whl

Download URL flask_session_manager_sk-1.3.2-py3-none-any.whl
Size 16.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ccc61426ad78463ebc992c55d5809ea9d463bedb531bfd5ede35bb065cf85198
BLAKE2b-256 checksum
How to use checksums
d41bda9cbb9d41b5ac09b1c9bcff83ca106230762563c0884a01c3cb1c3bf9c8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.3.2 This release

2 release files

1.3.1

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.2

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.9

2 release files

1.0.8

2 release files

1.0.7

2 release files

1.0.6

2 release files

1.0.4

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.0

2 release files

0.1.4

2 release files

0.1.2

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page