Metadata-Version: 2.4
Name: django-saml2-auth-community
Version: 3.22.0
Summary: Django SAML2 Authentication Made Easy.
Project-URL: Homepage, https://github.com/mostafa/django-saml2-auth
Project-URL: Issues, https://github.com/mostafa/django-saml2-auth/issues
Author-email: Mostafa Moradian <mstfmoradian@gmail.com>
License-Expression: Apache-2.0
License-File: AUTHORS.md
License-File: LICENSE
Keywords: authentication,django,saml,saml2,sso
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: pyjwt==2.13.0
Requires-Dist: pysaml2==7.5.4
Description-Content-Type: text/markdown

# Django SAML2 Authentication

[![PyPI](https://img.shields.io/pypi/v/django-saml2-auth-community?label=version&logo=pypi)](https://pypi.org/project/django-saml2-auth-community/) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/mostafa/django-saml2-auth/ci.yml?branch=main&logo=github)](https://github.com/mostafa/django-saml2-auth/actions) [![Coveralls](https://img.shields.io/coveralls/github/mostafa/django-saml2-auth?logo=coveralls)](https://coveralls.io/github/mostafa/django-saml2-auth) [![Downloads (new)](https://pepy.tech/badge/django-saml2-auth-community)](https://pepy.tech/project/django-saml2-auth-community) [![Downloads (legacy)](https://pepy.tech/badge/grafana-django-saml2-auth)](https://pepy.tech/project/grafana-django-saml2-auth)

> [!NOTE]
> To learn more about SAML SSO in Django, read the "SAML SSO in Django" series: [Part 1: Introduction to SAML SSO](https://mostafa.dev/saml-sso-in-django-734e0f871f22) and [Part 2: Integrating SAML SSO into a Django app with Okta](https://mostafa.dev/saml-sso-in-django-1a681123e801).

> [!IMPORTANT]
> This project is a fork of [grafana/django-saml2-auth](https://github.com/grafana/django-saml2-auth), itself forked from [fangli/django-saml2-auth](https://github.com/fangli/django-saml2-auth). As I stated [in this comment](https://github.com/grafana/django-saml2-auth/issues/396#issuecomment-4204617928), I no longer work for Grafana and no longer have access to that repository, so ongoing work continues here: [mostafa/django-saml2-auth](https://github.com/mostafa/django-saml2-auth).
>
> **PyPI:** **`django-saml2-auth-community`** is the community-maintained line, not a Grafana product. Keep **`import django_saml2_auth`**; only the **`pip`**/**`lockfile`** install name changes from **`grafana-django-saml2-auth`**. The legacy package **`grafana-django-saml2-auth`** now acts as a compatibility shim that depends on this package.

This plugin integrates SAML2 authentication into Django apps. SAML2 is a standard; most SAML2 identity providers work with it.

**IdP-initiated vs SP-initiated SSO**

- **IdP-initiated:** The user starts at the identity provider (e.g. Okta), opens your application there, and the IdP sends a SAML assertion to your app (the service provider).
- **SP-initiated:** The user starts at your site (for example `/accounts/login/`), is redirected to the IdP to sign in, then returns with an assertion.

With **`CREATE_USER`** enabled, new users can be created in Django when they complete SSO, according to your attribute mapping and hooks and not only for IdP-initiated flows.

## Project Information

- Original Author: Fang Li ([@fangli](https://github.com/fangli))
- Maintainer: Mostafa Moradian ([@mostafa](https://github.com/mostafa))
- Version support matrix:

    | **Python**                             | **Django**     | **django-saml2-auth** | **End of extended support<br/>(Django)** |
    | -------------------------------------- | -------------- | --------------------- | ---------------------------------------- |
    | 3.10.x, 3.11.x, 3.12.x, 3.13.x, 3.14.x | 5.2.x (≥5.2.8) | >3.12.0               | April 2028                               |
    | 3.12.x, 3.13.x, 3.14.x                 | 6.0.x          | >3.12.0               | April 2027                               |

  **Django 5.2.8+** is the minimum supported Django release. Python 3.14 is supported with **Django 5.2.8+** or **Django 6.0** ([5.2](https://docs.djangoproject.com/en/5.2/releases/5.2/#python-compatibility), [6.0](https://docs.djangoproject.com/en/6.0/releases/6.0/#python-compatibility)). **Django 6.0** supports **Python 3.12, 3.13, and 3.14** only; use Django 5.2 for Python 3.10 or 3.11.

- Release notes are on [GitHub Releases](https://github.com/mostafa/django-saml2-auth/releases).

- To contribute, read the [contributing guide](CONTRIBUTING.md). To report security issues privately, see [SECURITY.md](SECURITY.md).

## CycloneDX SBOM

Each release includes a CycloneDX SBOM (JSON) as a workflow artifact, built with [cyclonedx-python](https://github.com/CycloneDX/cyclonedx-python) in [.github/workflows/release.yml](.github/workflows/release.yml).

## Installation

Install from PyPI:

```bash
pip install django-saml2-auth-community
```

Or install from a Git clone (use a virtual environment):

```bash
git clone https://github.com/mostafa/django-saml2-auth
cd django-saml2-auth
pip install .
```

`pysaml2` also needs **xmlsec** on the system:

```bash
# RPM-based (e.g. yum/dnf)
# yum install xmlsec1

# Debian/Ubuntu
# apt-get install xmlsec1

# macOS (Homebrew)
# brew install xmlsec1
```

[Windows binaries](https://www.zlatkovic.com/projects/libxml/index.html) are also available.

## Usage

1. Once you have the library installed or in your `requirements.txt`, import the views module in your root `urls.py`:

    ```python
    import django_saml2_auth.views
    ```

2. Override the default login page in the root `urls.py` file, by adding these lines **BEFORE** any `urlpatterns`:

    ```python
    # These are the SAML2 related URLs. (required)
    re_path(r'^sso/', include('django_saml2_auth.urls')),

    # The following line will replace the default user login with SAML2 (optional)
    # To set the after-login redirect, use "?next=/the/path/you/want" on this view.
    re_path(r'^accounts/login/$', django_saml2_auth.views.signin),

    # The following line will replace the admin login with SAML2 (optional)
    # To set the after-login redirect, use "?next=/the/path/you/want"
    # with this view.
    re_path(r'^admin/login/$', django_saml2_auth.views.signin),
    ```

3. Add `'django_saml2_auth'` to `INSTALLED_APPS` in your Django `settings.py`:

    ```python
    INSTALLED_APPS = [
        '...',
        'django_saml2_auth',
    ]
    ```

4. In `settings.py`, add the SAML2 related configuration:

    Please note, the only required setting is **METADATA\_AUTO\_CONF\_URL** or the existence of a **GET\_METADATA\_AUTO\_CONF\_URLS** trigger function. The following block shows all required and optional configuration settings and their default values.

    <details>
        <summary>Click to see the entire settings block</summary>

    ```python
    SAML2_AUTH = {
        # Metadata is required, choose either remote url or local file path
        'METADATA_AUTO_CONF_URL': '[The auto(dynamic) metadata configuration URL of SAML2]',
        'METADATA_LOCAL_FILE_PATH': '[The metadata configuration file path]',
        'KEY_FILE': '[The key file path]',
        'CERT_FILE': '[The certificate file path]',

        # If both `KEY_FILE` and `CERT_FILE` are provided, `ENCRYPTION_KEYPAIRS` will be added automatically. There is no need to provide it unless you wish to override the default value.
        'ENCRYPTION_KEYPAIRS': [
            {
                "key_file": '[The key file path]',
                "cert_file": '[The certificate file path]',
            }
        ],

        'DEBUG': False,  # Send debug information to a log file
        # Optional logging configuration.
        # By default, it won't log anything.
        # The following configuration is an example of how to configure the logger,
        # which can be used together with the DEBUG option above. Please note that
        # the logger configuration follows the Python's logging configuration schema:
        # https://docs.python.org/3/library/logging.config.html#logging-config-dictschema
        'LOGGING': {
            'version': 1,
            'formatters': {
                'simple': {
                    'format': '[%(asctime)s] [%(levelname)s] [%(name)s.%(funcName)s] %(message)s',
                },
            },
            'handlers': {
                'stdout': {
                    'class': 'logging.StreamHandler',
                    'stream': 'ext://sys.stdout',
                    'level': 'DEBUG',
                    'formatter': 'simple',
                },
            },
            'loggers': {
                'saml2': {
                    'level': 'DEBUG'
                },
            },
            'root': {
                'level': 'DEBUG',
                'handlers': [
                    'stdout',
                ],
            },
        },

        # Optional settings below
        'DEFAULT_NEXT_URL': '/admin',  # Redirect after login. If omitted, defaults to the Django admin index. Overridden by ?next= on the login URL.
        'CREATE_USER': True,  # Create a new Django user when a new user logs in. Defaults to True.
        'NEW_USER_PROFILE': {
            'USER_GROUPS': [],  # The default group name when a new user logs in
            'ACTIVE_STATUS': True,  # The default active status for new users
            'STAFF_STATUS': False,  # The staff status for new users
            'SUPERUSER_STATUS': False,  # The superuser status for new users
        },
        'ATTRIBUTES_MAP': {  # Change Email/UserName/FirstName/LastName to corresponding SAML2 userprofile attributes.
            'email': 'user.email',
            'username': 'user.username',
            'first_name': 'user.first_name',
            'last_name': 'user.last_name',
            'token': 'Token',  # Required in the map unless TOKEN_REQUIRED is False
            'groups': 'Groups',  # Optional
        },
        'GROUPS_MAP': {  # Optionally allow mapping SAML2 Groups to Django Groups
            'SAML Group Name': 'Django Group Name',
        },
        'TRIGGER': {
            'EXTRACT_USER_IDENTITY': 'path.to.your.extract.user.identity.hook.method',
            # Optional: needs to return a User Model instance or None
            'GET_USER': 'path.to.your.get.user.hook.method',
            'CREATE_USER': 'path.to.your.new.user.hook.method',
            'BEFORE_LOGIN': 'path.to.your.login.hook.method',
            'AFTER_LOGIN': 'path.to.your.after.login.hook.method',
            # Optional. This is executed right before METADATA_AUTO_CONF_URL.
            # For systems with many metadata files registered allows to narrow the search scope.
            'GET_USER_ID_FROM_SAML_RESPONSE': 'path.to.your.get.user.from.saml.hook.method',
            # This can override the METADATA_AUTO_CONF_URL to enumerate all existing metadata autoconf URLs
            'GET_METADATA_AUTO_CONF_URLS': 'path.to.your.get.metadata.conf.hook.method',
            # This will override ASSERTION_URL to allow more dynamic assertion URLs
            'GET_CUSTOM_ASSERTION_URL': 'path.to.your.get.custom.assertion.url.hook.method',
            # This will override FRONTEND_URL for more dynamic URLs
            'GET_CUSTOM_FRONTEND_URL': 'path.to.your.get.custom.frontend.url.hook.method',
        },
        'ASSERTION_URL': 'https://mysite.com',  # Custom URL to validate incoming SAML requests against
        'ENTITY_ID': 'https://mysite.com/sso/acs/',  # Populates the Issuer element in authn request
        'NAME_ID_FORMAT': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',  # Match your IdP / NameID policy
        'USE_JWT': True,  # Set this to True if you are running a Single Page Application (SPA) with Django Rest Framework (DRF), and are using JWT authentication to authorize client users
        'JWT_ALGORITHM': 'HS256',  # JWT algorithm to sign the message with
        'JWT_SECRET': 'your.jwt.secret',  # JWT secret to sign the message with
        'JWT_PRIVATE_KEY': '--- YOUR PRIVATE KEY ---',  # Private key for asymmetric JWT; use an RS* or ES* algorithm
        'JWT_PRIVATE_KEY_PASSPHRASE': 'your.passphrase',  # If your private key is encrypted, you might need to provide a passphrase for decryption
        'JWT_PUBLIC_KEY': '--- YOUR PUBLIC KEY ---',  # Public key to decode the signed JWT token
        'JWT_EXP': 60,  # JWT expiry time in seconds
        'FRONTEND_URL': 'https://myfrontendclient.com',  # Redirect URL for the client if you are using JWT auth with DRF. See explanation below
        'LOGIN_CASE_SENSITIVE': True,  # Match USERNAME_FIELD with exact case when True
        'AUTHN_REQUESTS_SIGNED': True, # Require each authentication request to be signed
        'LOGOUT_REQUESTS_SIGNED': True,  # Require each logout request to be signed
        'WANT_ASSERTIONS_SIGNED': True,  # Require each assertion to be signed
        'WANT_RESPONSE_SIGNED': True,  # Require response to be signed
        'FORCE_AUTHN': False, # Forces the user to re-authenticate with each authentication request
        'ACCEPTED_TIME_DIFF': None,  # Accepted time difference between your server and the Identity Provider
        # Hostnames only (no `https://` prefix), per Django's url_has_allowed_host_and_scheme.
        # Used for `?next` (signin), post-login ACS redirects (including RelayState), and USE_JWT targets.
        'ALLOWED_REDIRECT_HOSTS': ["myfrontendclient.com"],
        'TOKEN_REQUIRED': True,  # Whether or not to require the token parameter in the SAML assertion
        'DISABLE_EXCEPTION_HANDLER': True,  # Whether the custom exception handler should be used
    }

    ```

    </details>

5. In your SAML2 SSO identity provider, set the Single-sign-on URL and Audience URI (SP Entity ID) to <http://your-domain/sso/acs/>

## Debugging

To inspect SAML traffic between the identity provider and Django, use SAML-tracer for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/saml-tracer/) or [Chrome](https://chrome.google.com/webstore/detail/saml-tracer/mpdajninpobndbfcldcmbpnnbhibjmch?hl=en).

Also, you can enable the debug mode in the `settings.py` file by setting the `DEBUG` flag to `True` and enabling the `LOGGING` configuration. See above for configuration examples.

*Note:* Don't forget to disable the debug mode in production and also remove the logging configuration if you don't want to see internal logs of pysaml2 library.

## Module Settings

Some settings control this module; others are forwarded to **pysaml2**. See the [pysaml2 configuration docs](https://pysaml2.readthedocs.io/en/latest/howto/config.html) for the full set—not every pysaml2 option is wired through here.

<details>
    <summary>Click to see the module settings</summary>

| **Field name**                              | **Description**                                                                                                                                                                                                                                                                                                                                                                                                                                           | **Data type(s)** | **Default value(s)**                                                                                                                     | **Example**                                                                             |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| **METADATA\_AUTO\_CONF\_URL**               | Auto SAML2 metadata configuration URL                                                                                                                                                                                                                                                                                                                                                                                                                     | `str`            | `None`                                                                                                                                   | `https://ORG.okta.com/app/APP-ID/sso/saml/metadata`                                     |
| **METADATA\_LOCAL\_FILE\_PATH**             | SAML2 metadata configuration file path                                                                                                                                                                                                                                                                                                                                                                                                                    | `str`            | `None`                                                                                                                                   | `/path/to/the/metadata.xml`                                                             |
| **KEY_FILE**                                | SAML2 private key path. Required when `AUTHN_REQUESTS_SIGNED` is `True`.                                                                                                                                                                                                                                                                                                                                                                                  | `str`            | `None`                                                                                                                                   | `/path/to/the/key.pem`                                                                  |
| **CERT_FILE**                               | SAML2 public certificate file path                                                                                                                                                                                                                                                                                                                                                                                                                        | `str`            | `None`                                                                                                                                   | `/path/to/the/cert.pem`                                                                 |
| **ENCRYPTION_KEYPAIRS**                     | Required for handling encrypted assertions. Will be automatically set if both `KEY_FILE` and `CERT_FILE` are set.                                                                                                                                                                                                                                                                                                                                         | `list`           | Not set.                                                                                                                                 | `[ { 'key_file': '[The key file path]', 'cert_file': '[The certificate file path]' } ]` |
| **DEBUG**                                   | Send debug information to a log file                                                                                                                                                                                                                                                                                                                                                                                                                      | `bool`           | `False`                                                                                                                                  |                                                                                         |
| **LOGGING**                                 | Logging configuration dictionary                                                                                                                                                                                                                                                                                                                                                                                                                          | `dict`           | Not set.                                                                                                                                 |                                                                                         |
| **DEFAULT\_NEXT\_URL**                      | Post-login redirect if `?next=` is not used. If unset, resolves to the Django admin index (`admin:index`).                                                                                                                                                                                                                                                                                                                                                | `str`            | `None` (→ `admin:index`)                                                                                                                 | `https://app.example.com/account/login`                                                 |
| **CREATE\_USER**                            | Determines if a new Django user should be created for new users                                                                                                                                                                                                                                                                                                                                                                                           | `bool`           | `True`                                                                                                                                   |                                                                                         |
| **CREATE\_GROUPS**                          | Determines if a new Django group should be created if the SAML2 Group does not exist                                                                                                                                                                                                                                                                                                                                                                      | `bool`           | `False`                                                                                                                                  |                                                                                         |
| **NEW\_USER\_PROFILE**                      | Default settings for newly created users                                                                                                                                                                                                                                                                                                                                                                                                                  | `dict`           | `{'USER_GROUPS': [], 'ACTIVE_STATUS': True, 'STAFF_STATUS': False, 'SUPERUSER_STATUS': False}`                                           |                                                                                         |
| **ATTRIBUTES\_MAP**                         | Mapping of Django user attributes to SAML2 user attributes                                                                                                                                                                                                                                                                                                                                                                                                | `dict`           | `{'email': 'user.email', 'username': 'user.username', 'first_name': 'user.first_name', 'last_name': 'user.last_name', 'token': 'token'}` | `{'your.field': 'SAML.field'}`                                                          |
| **TOKEN\_REQUIRED**                         | Set this to `False` if you don't require the token parameter in the SAML assertion (in the attributes map)                                                                                                                                                                                                                                                                                                                                                | `bool`           | `True`                                                                                                                                   |                                                                                         |
| **TRIGGER**                                 | Optional hooks (dotted paths to callables) for login and user provisioning. Arguments vary by hook; see each row below.                                                                                                                                                                                                                                                                                                                                   | `dict`           | `{}`                                                                                                                                     |                                                                                         |
| **TRIGGER.EXTRACT\_USER\_IDENTITY**         | Called when extracting the user identity from the SAML2 response. Should accept `(user_dict, authn_response)` and may return an enriched `user_dict`.                                                                                                                                                                                                                                                                                                     | `str`            | `None`                                                                                                                                   | `my_app.models.users.extract_user_identity`                                             |
| **TRIGGER.GET\_USER**                       | Custom lookup for an existing user before login. Should accept the user dict and return a `User` instance or `None`.                                                                                                                                                                                                                                                                                                                                      | `str`            | `None`                                                                                                                                   | `my_app.models.users.get`                                                               |
| **TRIGGER.CREATE\_USER**                    | Called after a new user row is created, before login. Accepts the user dict.                                                                                                                                                                                                                                                                                                                                                                              | `str`            | `None`                                                                                                                                   | `my_app.models.users.create`                                                            |
| **TRIGGER.BEFORE\_LOGIN**                   | A method to be called when an existing user logs in. This method will be called before the user is logged in and after the SAML2 identity provider returns user attributes. This method should accept ONE parameter of user dict.                                                                                                                                                                                                                         | `str`            | `None`                                                                                                                                   | `my_app.models.users.before_login`                                                      |
| **TRIGGER.AFTER\_LOGIN**                    | A method to be called when an existing user logs in. This method will be called after the user is logged in and after the SAML2 identity provider returns user attributes. This method should accept TWO parameters of session and user dict.                                                                                                                                                                                                             | `str`            | `None`                                                                                                                                   | `my_app.models.users.after_login`                                                       |
| **TRIGGER.GET\_METADATA\_AUTO\_CONF\_URLS** | A hook function that returns a list of metadata Autoconf URLs as dictionary, where the key is `"url"` and the value is the corresponding metadata Autoconf URL. (e.g., `[{"url": METADATA_URL1}, {"url": METADATA_URL2}]`). This can override the `METADATA_AUTO_CONF_URL` to enumerate all existing metadata autoconf URLs.                                                                                                                              | `str`            | `None`                                                                                                                                   | `my_app.models.users.get_metadata_autoconf_urls`                                        |
| **TRIGGER.GET\_CUSTOM\_METADATA**           | Custom SAML metadata loader. Should return metadata as `Mapping[str, Any]` and overrides other metadata settings. Same arguments as `django_saml2_auth.saml.get_metadata` (`user_id`, `domain`, `saml_response`). See `tests.test_saml.get_custom_metadata_example`.                                                                                                                                                                                      | `str`            | `None`                                                                                                                                   | `my_app.utils.get_custom_saml_metadata`                                                 |
| **TRIGGER.CUSTOM\_DECODE\_JWT**             | A hook function to decode the user JWT. This method will be called instead of the `decode_jwt_token` default function and should return the user_model.USERNAME_FIELD. This method accepts one parameter: `token`.                                                                                                                                                                                                                                        | `str`            | `None`                                                                                                                                   | `my_app.models.users.decode_custom_token`                                               |
| **TRIGGER.CUSTOM\_CREATE\_JWT**             | A hook function to create a custom JWT for the user. This method will be called instead of the `create_jwt_token` default function and should return the token. This method accepts one parameter: `user`.                                                                                                                                                                                                                                                | `str`            | `None`                                                                                                                                   | `my_app.models.users.create_custom_token`                                               |
| **TRIGGER.CUSTOM\_TOKEN\_QUERY**            | A hook function to create a custom query params with the JWT for the user. This method will be called after `CUSTOM_CREATE_JWT` to populate a query and attach it to a URL; should return the query params containing the token (e.g., `?token=encoded.jwt.token`). This method accepts one parameter: `token`.                                                                                                                                           | `str`            | `None`                                                                                                                                   | `my_app.models.users.get_custom_token_query`                                            |
| **TRIGGER.GET\_CUSTOM\_ASSERTION\_URL**     | A hook function to get the assertion URL dynamically. Useful when you have dynamic routing, multi-tenant setup and etc. Overrides `ASSERTION_URL`.                                                                                                                                                                                                                                                                                                        | `str`            | `None`                                                                                                                                   | `my_app.utils.get_custom_assertion_url`                                                 |
| **TRIGGER.GET\_CUSTOM\_FRONTEND\_URL**      | Dynamic `FRONTEND_URL` when using JWT (overrides `FRONTEND_URL`). Accepts `relay_state`.                                                                                                                                                                                                                                                                                                                                                                  | `str`            | `None`                                                                                                                                   | `my_app.utils.get_custom_frontend_url`                                                  |
| **ASSERTION\_URL**                          | A URL to validate incoming SAML responses against. By default, `django-saml2-auth` will validate the SAML response's Service Provider address against the actual HTTP request's host and scheme. If this value is set, it will validate against `ASSERTION_URL` instead - perfect for when Django is running behind a reverse proxy. This will only allow to customize the domain part of the URL, for more customization use `GET_CUSTOM_ASSERTION_URL`. | `str`            | `None`                                                                                                                                   | `https://example.com`                                                                   |
| **ENTITY\_ID**                              | Optional entity ID for the `Issuer` element in the authentication request, if required by the IdP.                                                                                                                                                                                                                                                                                                                                                        | `str`            | `None`                                                                                                                                   | `https://example.com/sso/acs/`                                                          |
| **NAME\_ID\_FORMAT**                        | The optional value of the `'Format'` property of the `'NameIDPolicy'` element in authentication requests.                                                                                                                                                                                                                                                                                                                                                 | `str`            | `None`                                                                                                                                   | `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent`                                  |
| **USE\_JWT**                                | Set this to the boolean `True` if you are using Django with JWT authentication                                                                                                                                                                                                                                                                                                                                                                            | `bool`           | `False`                                                                                                                                  |                                                                                         |
| **JWT\_ALGORITHM**                          | Algorithm for signing JWTs ([PyJWT algorithms](https://pyjwt.readthedocs.io/en/stable/algorithms.html)). Must match `JWT_SECRET` (HMAC) or key pair (RSA/EC).                                                                                                                                                                                                                                                                                             | `str`            | `None`                                                                                                                                   | `HS256`, `RS256`                                                                        |
| **JWT\_SECRET**                             | JWT secret to sign the message if an HMAC is used with the SHA hash algorithm (`HS*`).                                                                                                                                                                                                                                                                                                                                                                    | `str`            | `None`                                                                                                                                   |                                                                                         |
| **JWT\_PRIVATE\_KEY**                       | Private key for asymmetric JWT signing (`RS*`, `ES*`, etc.).                                                                                                                                                                                                                                                                                                                                                                                              | `str` or `bytes` | `None`                                                                                                                                   |                                                                                         |
| **JWT\_PRIVATE\_KEY\_PASSPHRASE**           | If your private key is encrypted, you must provide a passphrase for decryption.                                                                                                                                                                                                                                                                                                                                                                           | `str` or `bytes` | `None`                                                                                                                                   |                                                                                         |
| **JWT\_PUBLIC\_KEY**                        | Public key to decode the signed JWT token.                                                                                                                                                                                                                                                                                                                                                                                                                | `str` or `bytes` | `'--- YOUR PUBLIC KEY ---'`                                                                                                              |                                                                                         |
| **JWT\_EXP**                                | JWT expiry time in seconds                                                                                                                                                                                                                                                                                                                                                                                                                                | `int`            | 60                                                                                                                                       |                                                                                         |
| **FRONTEND\_URL**                           | When `USE_JWT` is `True`, redirect target for the browser after SSO; JWT is appended as `?token=...`. Defaults to `DEFAULT_NEXT_URL` / admin index if unset. Example: `https://app.example.com/?token=<jwt>`.                                                                                                                                                                                                                                             | `str`            | `None` (→ same as default next URL)                                                                                                      | `https://app.example.com/`                                                              |
| **AUTHN\_REQUESTS\_SIGNED**                 | Set this to `False` if your provider doesn't sign each authorization request.  KEY_FILE is required if this is set True.                                                                                                                                                                                                                                                                                                                                  | `bool`           | `True`                                                                                                                                   |
| **LOGOUT\_REQUESTS\_SIGNED**                | Set this to `False` if your provider doesn't sign each logout request.                                                                                                                                                                                                                                                                                                                                                                                    | `bool`           | `True`                                                                                                                                   |                                                                                         |
| **WANT\_ASSERTIONS\_SIGNED**                | Set this to `False` if your provider doesn't sign each assertion.                                                                                                                                                                                                                                                                                                                                                                                         | `bool`           | `True`                                                                                                                                   |                                                                                         |
| **WANT\_RESPONSE\_SIGNED**                  | Set this to `False` if you don't want your provider to sign the response.                                                                                                                                                                                                                                                                                                                                                                                 | `bool`           | `True`                                                                                                                                   |                                                                                         |
| **FORCE\_AUTHN**                            | SAML2 request attribute that forces the user to re-authenticate with the Identity Provider (IdP), even if they already have an active session.                                                                                                                                                                                                                                                                                                            | `bool`           | `False`                                                                                                                                  |                                                                                         |
| **ACCEPTED\_TIME\_DIFF**                    | Sets the [accepted time diff](https://pysaml2.readthedocs.io/en/latest/howto/config.html#accepted-time-diff) in seconds                                                                                                                                                                                                                                                                                                                                   | `int` or `None`  | `None`                                                                                                                                   |                                                                                         |
| **ALLOWED\_REDIRECT\_HOSTS**                | Hostnames allowed for `?next=` and post-login redirects (no scheme; see Django’s `url_has_allowed_host_and_scheme`).                                                                                                                                                                                                                                                                                                                                      | `list`           | `[]`                                                                                                                                     | `['app.example.com', 'api.example.com']`                                                |
| **DISABLE\_EXCEPTION\_HANDLER**             | Set this to `True` if you want to disable the exception handler. Make sure to handle the `SAMLAuthError`s and other exceptions.                                                                                                                                                                                                                                                                                                                           | `bool`           | `False`                                                                                                                                  |                                                                                         |

### Triggers

| **Setting name**                       | **Description**                                                                                                                             | **Interface**                                                                                |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| **GET\_METADATA\_AUTO\_CONF\_URLS**    | Return candidate metadata URLs (see trigger implementation).                                                                                | get_metadata_auto_conf_urls(user_id: Optional[str] = None) -> Optional[List[Dict[str, str]]] |
| **GET\_USER_ID\_FROM\_SAML\_RESPONSE** | Allows retrieving a user ID before GET_METADATA_AUTO_CONF_URLS gets triggered. Warning: SAML response still not verified. Use with caution! | get_user_id_from_saml_response(saml_response: str, user_id: Optional[str]) -> Optional[str]  |

</details>

## JWT Signing Algorithm and Settings

Both symmetric and asymmetric signing functions are [supported](https://pyjwt.readthedocs.io/en/stable/algorithms.html). If you want to use symmetric signing using a secret key, use either of the following algorithms plus a secret key:

- HS256
- HS384
- HS512

```python
{
    ...
    'USE_JWT': True,
    'JWT_ALGORITHM': 'HS256',
    'JWT_SECRET': 'YOU.ULTRA.SECURE.SECRET',
    ...
}
```

Otherwise if you want to use your PKI key-pair to sign JWT tokens, use either of the following algorithms and then set the following fields:

- RS256
- RS384
- RS512
- ES256
- ES256K
- ES384
- ES521
- ES512
- PS256
- PS384
- PS512
- EdDSA

```python
{
    ...
    'USE_JWT': True,
    'JWT_ALGORITHM': 'RS256',
    'JWT_PRIVATE_KEY': '--- YOUR PRIVATE KEY ---',
    'JWT_PRIVATE_KEY_PASSPHRASE': 'your.passphrase',  # Optional, if your private key is encrypted
    'JWT_PUBLIC_KEY': '--- YOUR PUBLIC KEY ---',
    ...
}
```

> [!NOTE]
> If both PKI fields and `JWT_SECRET` are defined, the `JWT_ALGORITHM` decides which method to use for signing tokens.

### Custom token triggers

This is an example of the functions that could be passed to the `TRIGGER.CUSTOM_CREATE_JWT` (it uses the [DRF Simple JWT library](https://github.com/jazzband/djangorestframework-simplejwt/blob/master/docs/index.rst)) and to `TRIGGER.CUSTOM_TOKEN_QUERY`:

``` python
from rest_framework_simplejwt.tokens import RefreshToken


def get_custom_jwt(user):
    """Create token for user and return it"""
    return RefreshToken.for_user(user)


def get_custom_token_query(refresh):
    """Create url query with refresh and access token"""
    return "?%s%s%s%s%s" % ("refresh=", str(refresh), "&", "access=", str(refresh.access_token))

```

## Exception Handling

By default, errors render a built-in template. Customize templates in the next section, or set `DISABLE_EXCEPTION_HANDLER` to `True` to let `SAMLAuthError` (and other exceptions) propagate—useful for API-only integrations where you handle errors yourself.

## Customize Error Messages and Templates

The default permission `denied`, `error` and user `welcome` page can be overridden.

To override these pages, add templates named `django_saml2_auth/error.html`, `django_saml2_auth/welcome.html`, or `django_saml2_auth/denied.html` under your project’s template directories.

> [!NOTE]
> If you set `DISABLE_EXCEPTION_HANDLER` to `True`, the custom error pages will not be displayed.

If a `django_saml2_auth/welcome.html` template exists, it is shown after login instead of redirecting to the previous page. The [user object](https://docs.djangoproject.com/en/stable/ref/contrib/auth/#user-model) is available as `user` in the template context.

To use the bundled sign-out view, add these **before** your `urlpatterns`:

```python
from django.urls import re_path

import django_saml2_auth.views

# Optional: replace default logout with SAML2 sign-out
re_path(r"^accounts/logout/$", django_saml2_auth.views.signout),
re_path(r"^admin/logout/$", django_saml2_auth.views.signout),
```

Override sign-out UI with `django_saml2_auth/signout.html` if needed.

If your SAML2 identity provider uses user attribute names other than the defaults listed in the `settings.py` `ATTRIBUTES_MAP`, update them in `settings.py`.

## For Okta Users

The `METADATA_AUTO_CONF_URL` for `settings.py` appears in the Okta admin UI under the SAML2 app **Sign On** tab. In **Settings**, look for:

    Identity Provider metadata is available if this application supports dynamic configuration.

The `Identity Provider metadata` link is the `METADATA_AUTO_CONF_URL`.

More information can be found in the [Okta Developer Documentation](https://developer.okta.com/docs/guides/saml-application-setup/overview/).

## Release Process

Releases are mostly automated; you still configure GitHub once, then tag and push:

1. In the GitHub repository, create an Actions **environment** named `pypi` (Settings → Environments) and store the **`PYPI_API_TOKEN`** secret there for PyPI uploads. The release workflow attaches the tag build job to this environment.
2. Tag the `main` branch locally with `vSEMVER`, e.g. `v3.9.0`, and push the tag.
3. Pushing the tag triggers the workflow, which will:
   1. run the linters and tests.
   2. build the binary and source package.
   3. publish the package to PyPI.
   4. create a new release with auto-generated release notes on the tag.
   5. upload the SBOM artifacts and build artifacts to the release.
