Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions blurb_it/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import base64
import json
import logging
import os

import aiohttp
Expand Down Expand Up @@ -308,14 +309,36 @@ async def handle_add_blurb_post(request: Request) -> Response:
raise web.HTTPFound(location=request.app.router["add_blurb"].url_for())


if __name__ == "__main__": # pragma: no cover
fernet_key = fernet.Fernet.generate_key()
secret_key = base64.urlsafe_b64decode(fernet_key)
def get_session_key() -> str:
"""Return the Fernet key that encrypts the session cookie.

The key comes from the ``SESSION_KEY`` environment variable so that it
stays the same across restarts and deploys. Without it a random key is
generated, which is fine for local development, but then every restart
invalidates all existing sessions and signs everyone out.
"""
session_key = os.environ.get("SESSION_KEY")
if session_key:
try:
fernet.Fernet(session_key)
except ValueError as exc:
raise ValueError(
"SESSION_KEY is not a valid session key. "
"See the Deploy section of the readme."
) from exc
return session_key
logging.warning(
"SESSION_KEY is not set; using a random session key, so sessions "
"will not survive a restart. See the Deploy section of the readme."
)
return fernet.Fernet.generate_key().decode()


if __name__ == "__main__": # pragma: no cover
app = web.Application(
middlewares=[
middleware.error_middleware,
session_middleware(EncryptedCookieStorage(secret_key)),
session_middleware(EncryptedCookieStorage(get_session_key())),
]
)

Expand Down
17 changes: 17 additions & 0 deletions readme.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,25 @@ Configuration variables, set in the cabotage UI:
- ``GH_APP_ID``, ``GH_PRIVATE_KEY``: the GitHub App ID and private key.
- ``APP_URL``: the public hostname, without scheme. Must match the OAuth App
callback URL.
- ``SESSION_KEY``: the key that encrypts the session cookie. See
`Generating the session key`_ below.
- ``SENTRY_DSN``: where errors are reported.

Generating the session key
~~~~~~~~~~~~~~~~~~~~~~~~~~

``SESSION_KEY`` is a Fernet key. Generate it once, in any environment that
has the ``cryptography`` package installed:

.. code-block:: console

$ python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

Copy the whole printed line into ``SESSION_KEY``. Keep it the same across
deploys: changing it signs everyone out. Without it the app generates a
random key at startup, with the same effect on every restart. If the value is
not a valid Fernet key, the app refuses to start.


Requirements and dependencies
=============================
Expand Down
36 changes: 36 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
import base64
import logging
import pathlib

import aiohttp
Expand Down Expand Up @@ -613,3 +614,38 @@ async def get_installation(gh, jwt, username):
# Assert
assert resp.status == 503
assert "GitHub is not responding right now" in await resp.text()


def test_get_session_key_from_env(monkeypatch, caplog) -> None:
expected = fernet.Fernet.generate_key().decode()
monkeypatch.setenv("SESSION_KEY", expected)

session_key = __main__.get_session_key()

assert session_key == expected
assert "SESSION_KEY is not set" not in caplog.text
# The key must be usable as-is by the cookie storage.
EncryptedCookieStorage(session_key)


def test_get_session_key_random_when_unset(monkeypatch, caplog) -> None:
monkeypatch.delenv("SESSION_KEY", raising=False)

with caplog.at_level(logging.WARNING):
session_key = __main__.get_session_key()

assert session_key != __main__.get_session_key()
assert "SESSION_KEY is not set" in caplog.text
assert "See the Deploy section of the readme." in caplog.text
EncryptedCookieStorage(session_key)


def test_get_session_key_rejects_invalid_value(monkeypatch) -> None:
monkeypatch.setenv("SESSION_KEY", "not-a-fernet-key")

with pytest.raises(
ValueError,
match=r"SESSION_KEY is not a valid session key\. "
r"See the Deploy section of the readme\.",
):
__main__.get_session_key()
Loading