Skip to content

Commit

Permalink
✨(oidc) encrypt the refresh token in session
Browse files Browse the repository at this point in the history
Enforce refresh token encryption for the session storage.
  • Loading branch information
qbey committed Jan 28, 2025
1 parent d3253ab commit 97db241
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to

## Added

- ✨(oidc) add refresh token tools #584
- github actions to managed Crowdin workflow
- 📈Integrate Posthog #540
- 🏷️(backend) add content-type to uploaded files #552
Expand Down
24 changes: 22 additions & 2 deletions src/backend/core/authentication/backends.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""Authentication Backends for the Impress core app."""

import logging
from functools import lru_cache

from django.conf import settings
from django.core.exceptions import SuspiciousOperation
from django.utils.translation import gettext_lazy as _

import requests
from cryptography.fernet import Fernet
from mozilla_django_oidc.auth import (
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
)
Expand All @@ -17,10 +19,28 @@
logger = logging.getLogger(__name__)


@lru_cache(maxsize=0)
def get_cipher_suite():
"""Return a Fernet cipher suite."""
key = import_from_settings("OIDC_STORE_REFRESH_TOKEN_KEY", None)
if not key:
raise ValueError("OIDC_STORE_REFRESH_TOKEN_KEY setting is required.")
return Fernet(key)


def store_oidc_refresh_token(session, refresh_token):
"""Store the OIDC refresh token in the session if enabled in settings."""
"""Store the encrypted OIDC refresh token in the session if enabled in settings."""
if import_from_settings("OIDC_STORE_REFRESH_TOKEN", False):
session["oidc_refresh_token"] = refresh_token
encrypted_token = get_cipher_suite().encrypt(refresh_token.encode())
session["oidc_refresh_token"] = encrypted_token.decode()


def get_oidc_refresh_token(session):
"""Retrieve and decrypt the OIDC refresh token from the session."""
encrypted_token = session.get("oidc_refresh_token")
if encrypted_token:
return get_cipher_suite().decrypt(encrypted_token.encode()).decode()
return None


def store_tokens(session, access_token, id_token, refresh_token):
Expand Down
28 changes: 26 additions & 2 deletions src/backend/core/tests/authentication/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,37 @@

import pytest
import responses
from cryptography.fernet import Fernet

from core import models
from core.authentication.backends import OIDCAuthenticationBackend
from core.authentication.backends import (
OIDCAuthenticationBackend,
get_oidc_refresh_token,
store_oidc_refresh_token,
)
from core.factories import UserFactory

pytestmark = pytest.mark.django_db


def test_oidc_refresh_token_session_store(settings):
"""Test that the OIDC refresh token is stored and retrieved from the session."""
session = {}

with pytest.raises(
ValueError, match="OIDC_STORE_REFRESH_TOKEN_KEY setting is required."
):
store_oidc_refresh_token(session, "test-refresh-token")

settings.OIDC_STORE_REFRESH_TOKEN_KEY = Fernet.generate_key()

store_oidc_refresh_token(session, "test-refresh-token")
assert session["oidc_refresh_token"] is not None
assert session["oidc_refresh_token"] != "test-refresh-token"

assert get_oidc_refresh_token(session) == "test-refresh-token"


def test_authentication_getter_existing_user_no_email(
django_assert_num_queries, monkeypatch
):
Expand Down Expand Up @@ -561,6 +584,7 @@ def test_authentication_session_tokens(
settings.OIDC_OP_JWKS_ENDPOINT = "http://oidc.endpoint.test/jwks"
settings.OIDC_STORE_ACCESS_TOKEN = True
settings.OIDC_STORE_REFRESH_TOKEN = True
settings.OIDC_STORE_REFRESH_TOKEN_KEY = Fernet.generate_key()

klass = OIDCAuthenticationBackend()
request = rf.get("/some-url", {"state": "test-state", "code": "test-code"})
Expand Down Expand Up @@ -598,4 +622,4 @@ def verify_token_mocked(*args, **kwargs):

assert user is not None
assert request.session["oidc_access_token"] == "test-access-token"
assert request.session["oidc_refresh_token"] == "test-refresh-token"
assert get_oidc_refresh_token(request.session) == "test-refresh-token"
5 changes: 4 additions & 1 deletion src/backend/core/tests/authentication/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
import pytest
import requests.exceptions
import responses
from cryptography.fernet import Fernet

from core import factories
from core.authentication.backends import get_oidc_refresh_token
from core.authentication.middleware import RefreshOIDCAccessToken

pytestmark = pytest.mark.django_db
Expand Down Expand Up @@ -108,6 +110,7 @@ def test_basic_auth_disabled(oidc_settings): # pylint: disable=unused-argument
@responses.activate
def test_successful_token_refresh(oidc_settings): # pylint: disable=unused-argument
"""Test that the middleware successfully refreshes the token."""
oidc_settings.OIDC_STORE_REFRESH_TOKEN_KEY = Fernet.generate_key()
user = factories.UserFactory()

request = RequestFactory().get("/test")
Expand Down Expand Up @@ -135,7 +138,7 @@ def test_successful_token_refresh(oidc_settings): # pylint: disable=unused-argu

assert response is None
assert request.session["oidc_access_token"] == "new_token"
assert request.session["oidc_refresh_token"] == "new_refresh_token"
assert get_oidc_refresh_token(request.session) == "new_refresh_token"


def test_non_expired_token(oidc_settings): # pylint: disable=unused-argument
Expand Down
5 changes: 5 additions & 0 deletions src/backend/impress/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,11 @@ class Base(Configuration):
OIDC_STORE_REFRESH_TOKEN = values.BooleanValue(
default=True, environ_name="OIDC_STORE_REFRESH_TOKEN", environ_prefix=None
)
OIDC_STORE_REFRESH_TOKEN_KEY = values.Value(
default=None,
environ_name="OIDC_STORE_REFRESH_TOKEN_KEY",
environ_prefix=None,
)

# WARNING: Enabling this setting allows multiple user accounts to share the same email
# address. This may cause security issues and is not recommended for production use when
Expand Down

0 comments on commit 97db241

Please sign in to comment.