Skip to content

Commit 35a2425

Browse files
committed
✨(oidc) encrypt the refresh token in session
Enforce refresh token encryption for the session storage.
1 parent 78e5688 commit 35a2425

File tree

5 files changed

+58
-5
lines changed

5 files changed

+58
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to
1111

1212
## Added
1313

14+
- ✨(oidc) add refresh token tools #584
1415
- ✨(backend) add soft delete and restore API endpoints to documents #516
1516
- ✨(backend) allow organizing documents in a tree structure #516
1617
- ✨(backend) add "excerpt" field to document list serializer #516

src/backend/core/authentication/backends.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"""Authentication Backends for the Impress core app."""
22

33
import logging
4+
from functools import lru_cache
45

56
from django.conf import settings
67
from django.core.exceptions import SuspiciousOperation
78
from django.utils.translation import gettext_lazy as _
89

910
import requests
11+
from cryptography.fernet import Fernet
1012
from mozilla_django_oidc.auth import (
1113
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
1214
)
@@ -17,10 +19,28 @@
1719
logger = logging.getLogger(__name__)
1820

1921

22+
@lru_cache(maxsize=0)
23+
def get_cipher_suite():
24+
"""Return a Fernet cipher suite."""
25+
key = import_from_settings("OIDC_STORE_REFRESH_TOKEN_KEY", None)
26+
if not key:
27+
raise ValueError("OIDC_STORE_REFRESH_TOKEN_KEY setting is required.")
28+
return Fernet(key)
29+
30+
2031
def store_oidc_refresh_token(session, refresh_token):
21-
"""Store the OIDC refresh token in the session if enabled in settings."""
32+
"""Store the encrypted OIDC refresh token in the session if enabled in settings."""
2233
if import_from_settings("OIDC_STORE_REFRESH_TOKEN", False):
23-
session["oidc_refresh_token"] = refresh_token
34+
encrypted_token = get_cipher_suite().encrypt(refresh_token.encode())
35+
session["oidc_refresh_token"] = encrypted_token.decode()
36+
37+
38+
def get_oidc_refresh_token(session):
39+
"""Retrieve and decrypt the OIDC refresh token from the session."""
40+
encrypted_token = session.get("oidc_refresh_token")
41+
if encrypted_token:
42+
return get_cipher_suite().decrypt(encrypted_token.encode()).decode()
43+
return None
2444

2545

2646
def store_tokens(session, access_token, id_token, refresh_token):

src/backend/core/tests/authentication/test_backends.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,37 @@
1010

1111
import pytest
1212
import responses
13+
from cryptography.fernet import Fernet
1314

1415
from core import models
15-
from core.authentication.backends import OIDCAuthenticationBackend
16+
from core.authentication.backends import (
17+
OIDCAuthenticationBackend,
18+
get_oidc_refresh_token,
19+
store_oidc_refresh_token,
20+
)
1621
from core.factories import UserFactory
1722

1823
pytestmark = pytest.mark.django_db
1924

2025

26+
def test_oidc_refresh_token_session_store(settings):
27+
"""Test that the OIDC refresh token is stored and retrieved from the session."""
28+
session = {}
29+
30+
with pytest.raises(
31+
ValueError, match="OIDC_STORE_REFRESH_TOKEN_KEY setting is required."
32+
):
33+
store_oidc_refresh_token(session, "test-refresh-token")
34+
35+
settings.OIDC_STORE_REFRESH_TOKEN_KEY = Fernet.generate_key()
36+
37+
store_oidc_refresh_token(session, "test-refresh-token")
38+
assert session["oidc_refresh_token"] is not None
39+
assert session["oidc_refresh_token"] != "test-refresh-token"
40+
41+
assert get_oidc_refresh_token(session) == "test-refresh-token"
42+
43+
2144
def test_authentication_getter_existing_user_no_email(
2245
django_assert_num_queries, monkeypatch
2346
):
@@ -561,6 +584,7 @@ def test_authentication_session_tokens(
561584
settings.OIDC_OP_JWKS_ENDPOINT = "http://oidc.endpoint.test/jwks"
562585
settings.OIDC_STORE_ACCESS_TOKEN = True
563586
settings.OIDC_STORE_REFRESH_TOKEN = True
587+
settings.OIDC_STORE_REFRESH_TOKEN_KEY = Fernet.generate_key()
564588

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

599623
assert user is not None
600624
assert request.session["oidc_access_token"] == "test-access-token"
601-
assert request.session["oidc_refresh_token"] == "test-refresh-token"
625+
assert get_oidc_refresh_token(request.session) == "test-refresh-token"

src/backend/core/tests/authentication/test_middleware.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
import pytest
1212
import requests.exceptions
1313
import responses
14+
from cryptography.fernet import Fernet
1415

1516
from core import factories
17+
from core.authentication.backends import get_oidc_refresh_token
1618
from core.authentication.middleware import RefreshOIDCAccessToken
1719

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

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

136139
assert response is None
137140
assert request.session["oidc_access_token"] == "new_token"
138-
assert request.session["oidc_refresh_token"] == "new_refresh_token"
141+
assert get_oidc_refresh_token(request.session) == "new_refresh_token"
139142

140143

141144
def test_non_expired_token(oidc_settings): # pylint: disable=unused-argument

src/backend/impress/settings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,11 @@ class Base(Configuration):
490490
OIDC_STORE_REFRESH_TOKEN = values.BooleanValue(
491491
default=True, environ_name="OIDC_STORE_REFRESH_TOKEN", environ_prefix=None
492492
)
493+
OIDC_STORE_REFRESH_TOKEN_KEY = values.Value(
494+
default=None,
495+
environ_name="OIDC_STORE_REFRESH_TOKEN_KEY",
496+
environ_prefix=None,
497+
)
493498

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

0 commit comments

Comments
 (0)