Skip to content

Commit a27e93c

Browse files
committed
✨(oidc) encrypt the refresh token in session
Enforce refresh token encryption for the session storage.
1 parent e993a50 commit a27e93c

File tree

6 files changed

+61
-5
lines changed

6 files changed

+61
-5
lines changed

CHANGELOG.md

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

1111
## Added
1212

13+
- ✨(oidc) add refresh token tools #584
1314
- ✨(frontend) add pinning on doc detail #711
1415
- 🚩(frontend) feature flag analytic on copy as html #649
1516
- ✨(frontend) Custom block divider with export #698

env.d/development/common.e2e.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ BURST_THROTTLE_RATES="200/minute"
44
DJANGO_SERVER_TO_SERVER_API_TOKENS=test-e2e
55
Y_PROVIDER_API_KEY=yprovider-api-key
66
Y_PROVIDER_API_BASE_URL=http://y-provider:4444/api/
7+
8+
# - add a key to store the refresh token in tests
9+
OIDC_STORE_REFRESH_TOKEN_KEY=qnw7gZrOFLkLuZIixzuxksNORFJyjWyi5ACugNchKJY=

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
@@ -493,6 +493,11 @@ class Base(Configuration):
493493
OIDC_STORE_REFRESH_TOKEN = values.BooleanValue(
494494
default=True, environ_name="OIDC_STORE_REFRESH_TOKEN", environ_prefix=None
495495
)
496+
OIDC_STORE_REFRESH_TOKEN_KEY = values.Value(
497+
default=None,
498+
environ_name="OIDC_STORE_REFRESH_TOKEN_KEY",
499+
environ_prefix=None,
500+
)
496501

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

0 commit comments

Comments
 (0)