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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ and this project adheres to

- ✨(tools) extract domain from email address #2
- ✨(oidc) add the authentication backends #2
- ✨(oidc) add refresh token tools #3

[unreleased]: https://github.com/suitenumerique/django-lasuite/commits/main/
109 changes: 109 additions & 0 deletions documentation/how-to-use-oidc-call-to-resource-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Using the OIDC Authentication Backend to request a resource server

Once your project is configured with the OIDC authentication backend, you can use it to request resources from a resource server. This guide will help you set up and use the `ResourceServerBackend` for token introspection and secure API access.

## Configuration

You need to follow the steps from [how-to-use-oidc-backend.md](how-to-use-oidc-backend.md)

## Additional Settings for Resource Server Communication

To enable your application to communicate with protected resource servers, you'll need to configure token storage in your Django settings:

```python
# Store OIDC tokens in the session
OIDC_STORE_ACCESS_TOKEN = True # Store the access token in the session
OIDC_STORE_REFRESH_TOKEN = True # Store the encrypted refresh token in the session

# Required for refresh token encryption
OIDC_STORE_REFRESH_TOKEN_KEY = "your-32-byte-encryption-key==" # Must be a valid Fernet key (32 url-safe base64-encoded bytes)
```

### Purpose of Each Setting

1. **`OIDC_STORE_ACCESS_TOKEN`**: When set to `True`, the access token received from the OIDC provider will be stored in the user's session. This token is required for making authenticated requests to protected resource servers.

2. **`OIDC_STORE_REFRESH_TOKEN`**: When set to `True`, enables storing the refresh token in the user's session. The refresh token allows your application to request a new access token when the current one expires without requiring user re-authentication.

3. **`OIDC_STORE_REFRESH_TOKEN_KEY`**: This is a cryptographic key used to encrypt the refresh token before storing it in the session. This provides an additional layer of security since refresh tokens are sensitive credentials that can be used to obtain new access tokens.

## Generating a Secure Refresh Token Key

You can generate a secure Fernet key using Python:

```python
from cryptography.fernet import Fernet
key = Fernet.generate_key()
print(key.decode()) # Add this value to your settings
```

## Using the Stored Tokens

Once you have configured these settings, your application can use the stored tokens to make authenticated requests to resource servers:

```python
import requests
from django.http import JsonResponse

def call_resource_server(request):
# Get the access token from the session
access_token = request.session.get('oidc_access_token')

if not access_token:
return JsonResponse({'error': 'Not authenticated'}, status=401)

# Make an authenticated request to the resource server
response = requests.get(
'https://resource-server.example.com/api/resource',
headers={'Authorization': f'Bearer {access_token}'},
)

return JsonResponse(response.json())
```

## Token Refresh management

### View Based Token Refresh (via decorator)

Request the access token refresh only on specific views using the `refresh_oidc_access_token` decorator:

```python
from lasuite.oidc_login.decorators import refresh_oidc_access_token

class SomeViewSet(GenericViewSet):

@method_decorator(refresh_oidc_access_token)
def some_action(self, request):
# Your action logic here

# The call to the resource server
access_token = request.session.get('oidc_access_token')
requests.get(
'https://resource-server.example.com/api/resource',
headers={'Authorization': f'Bearer {access_token}'},
)
```

This will trigger the token refresh process only when the `some_action` method is called.
If the access token is expired, it will attempt to refresh it using the stored refresh token.

### Automatic Token Refresh (via middleware)

You can also use the `RefreshOIDCAccessToken` middleware to automatically refresh expired tokens:

```python
# Add to your MIDDLEWARE setting
MIDDLEWARE = [
# Other middleware...
'lasuite.oidc_login.middleware.RefreshOIDCAccessToken',
]
```

This middleware will:
1. Check if the current access token is expired
2. Use the stored refresh token to obtain a new access token
3. Update the session with the new token
4. Continue processing the request with the fresh token

If token refresh fails, the middleware will return a 401 response with a `refresh_url` header to redirect the user to re-authenticate.

68 changes: 68 additions & 0 deletions src/lasuite/oidc_login/backends.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""Authentication Backends for OIDC."""

import logging
from functools import lru_cache

import requests
from cryptography.fernet import Fernet
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
from django.utils.translation import gettext_lazy as _
from mozilla_django_oidc.auth import (
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
)
from mozilla_django_oidc.utils import import_from_settings

logger = logging.getLogger(__name__)

Expand All @@ -20,6 +23,41 @@
) # Default to 'sub' if not set in settings


@lru_cache(maxsize=1)
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 encrypted OIDC refresh token in the session if enabled in settings."""
if import_from_settings("OIDC_STORE_REFRESH_TOKEN", False):
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):
"""Store tokens in the session if enabled in settings."""
if import_from_settings("OIDC_STORE_ACCESS_TOKEN", False):
session["oidc_access_token"] = access_token

if import_from_settings("OIDC_STORE_ID_TOKEN", False):
session["oidc_id_token"] = id_token

store_oidc_refresh_token(session, refresh_token)


class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
"""
Custom OpenID Connect (OIDC) Authentication Backend.
Expand All @@ -28,6 +66,36 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
in the User model, and handles signed and/or encrypted UserInfo response.
"""

def __init__(self, *args, **kwargs):
"""
Initialize the OIDC Authentication Backend.
Adds an internal attribute to store the token_info dictionary.
The purpose of `self._token_info` is to not duplicate code from
the original `authenticate` method.
This won't be needed after https://github.com/mozilla/mozilla-django-oidc/pull/377
is merged.
"""
super().__init__(*args, **kwargs)
self._token_info = None

def get_token(self, payload):
"""
Return token object as a dictionary.
Store the value to extract the refresh token in the `authenticate` method.
"""
self._token_info = super().get_token(payload)
return self._token_info

def authenticate(self, request, **kwargs):
"""Authenticate a user based on the OIDC code flow."""
user = super().authenticate(request, **kwargs)

if user is not None:
# Then the user successfully authenticated
store_oidc_refresh_token(request.session, self._token_info.get("refresh_token"))

return user

def get_extra_claims(self, user_info):
"""
Return extra claims from user_info.
Expand Down
11 changes: 11 additions & 0 deletions src/lasuite/oidc_login/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
Decorators for the authentication app.
We don't want (yet) to enforce the OIDC access token to be "fresh" for all
views, so we provide a decorator to refresh the access token only when needed.
"""

from django.utils.decorators import decorator_from_middleware

from .middleware import RefreshOIDCAccessToken

refresh_oidc_access_token = decorator_from_middleware(RefreshOIDCAccessToken)
Loading