Skip to content

Store and manage lifecycle of access tokens beyond authentication #362

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed
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
2 changes: 1 addition & 1 deletion django_auth_adfs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
Adding imports here will break setup.py
"""

__version__ = '1.15.0'
__version__ = "1.16.0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to be incremented. We'll do that when we cut a release.

10 changes: 7 additions & 3 deletions django_auth_adfs/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def validate_access_token(self, access_token):
logger.info(str(error))
raise PermissionDenied

def process_access_token(self, access_token, adfs_response=None):
def process_access_token(self, access_token, adfs_response=None, request=None):
if not access_token:
raise PermissionDenied

Expand All @@ -197,6 +197,10 @@ def process_access_token(self, access_token, adfs_response=None):
if not claims:
raise PermissionDenied

# Store tokens in session if middleware is enabled
if request and adfs_response and hasattr(request, "token_storage"):
request.token_storage.store_tokens(request, access_token, adfs_response)
Comment on lines +200 to +202
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to pull this out into a separate method that's called after process_access_token? It would make it easier for someone to customize what they want to do with the access token at that moment.


groups = self.process_user_groups(claims, access_token)
user = self.create_user(claims)
self.update_user_attributes(user, claims)
Expand Down Expand Up @@ -420,7 +424,7 @@ def authenticate(self, request=None, authorization_code=None, **kwargs):

adfs_response = self.exchange_auth_code(authorization_code, request)
access_token = adfs_response["access_token"]
user = self.process_access_token(access_token, adfs_response)
user = self.process_access_token(access_token, adfs_response, request)
return user


Expand All @@ -440,7 +444,7 @@ def authenticate(self, request=None, access_token=None, **kwargs):
return

access_token = access_token.decode()
user = self.process_access_token(access_token)
user = self.process_access_token(access_token, request=request)
return user


Expand Down
6 changes: 6 additions & 0 deletions django_auth_adfs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ def __init__(self):
)
self.PROXIES = None

# Token Lifecycle Middleware settings
self.TOKEN_REFRESH_THRESHOLD = 300 # 5 minutes
self.STORE_OBO_TOKEN = True
self.TOKEN_ENCRYPTION_SALT = b"django_auth_adfs_token_encryption"
self.LOGOUT_ON_TOKEN_REFRESH_FAILURE = False

self.VERSION = 'v1.0'
self.SCOPES = []

Expand Down
50 changes: 50 additions & 0 deletions django_auth_adfs/middleware.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
Based on https://djangosnippets.org/snippets/1179/
"""

import logging
from re import compile

from django.conf import settings as django_settings
Expand All @@ -9,6 +11,7 @@

from django_auth_adfs.exceptions import MFARequired
from django_auth_adfs.config import settings
from django_auth_adfs.token_manager import token_manager

LOGIN_EXEMPT_URLS = [
compile(django_settings.LOGIN_URL.lstrip('/')),
Expand All @@ -19,6 +22,8 @@
if hasattr(settings, 'LOGIN_EXEMPT_URLS'):
LOGIN_EXEMPT_URLS += [compile(expr) for expr in settings.LOGIN_EXEMPT_URLS]

logger = logging.getLogger("django_auth_adfs")


class LoginRequiredMiddleware:
"""
Expand All @@ -30,6 +35,7 @@ class LoginRequiredMiddleware:
Requires authentication middleware and template context processors to be
loaded. You'll get an error if they aren't.
"""

def __init__(self, get_response):
self.get_response = get_response

Expand All @@ -49,3 +55,47 @@ def __call__(self, request):
return redirect_to_login('django_auth_adfs:login-force-mfa')

return self.get_response(request)


class TokenLifecycleMiddleware:
"""
Middleware that handles the lifecycle of ADFS access and refresh tokens.

This middleware will:
1. Check if the access token is about to expire
2. Use the refresh token to get a new access token if needed
3. Update the tokens in the session
4. Handle OBO (On-Behalf-Of) tokens for Microsoft Graph API

Token storage during authentication is handled by the backend when this middleware is enabled.

To enable this middleware, add it to your MIDDLEWARE setting:
'django_auth_adfs.middleware.TokenLifecycleMiddleware'

You can configure the token refresh behavior with these settings:

TOKEN_REFRESH_THRESHOLD: Number of seconds before expiration to refresh (default: 300)
STORE_OBO_TOKEN: Boolean to enable/disable OBO token storage (default: True)
LOGOUT_ON_TOKEN_REFRESH_FAILURE: Whether to log out the user if token refresh fails (default: False)
"""

def __init__(self, get_response):
self.get_response = get_response
self.token_manager = token_manager
# Log warning if using signed cookies
if token_manager.using_signed_cookies:
logger.warning(
"TokenLifecycleMiddleware is enabled but you are using the signed_cookies session backend. "
"Storing tokens in signed cookies is not recommended for security reasons and cookie size limitations. "
"The middleware will not store tokens in the session. "
"Consider using database or cache-based sessions instead."
)

def __call__(self, request):
if hasattr(request, "session") and not self.token_manager.using_signed_cookies:
request.token_storage = self.token_manager

if hasattr(request, "user") and request.user.is_authenticated:
self.token_manager.check_token_expiration(request)

return self.get_response(request)
Loading
Loading