django-duo-universal-auth 0.2.0
A simple Django middleware for Duo Universal 2-factor authentication.
License: BSD 3-clause

# django-duo-universal-auth

A lightweight middleware application that adds a layer on top of any number of existing authentication backends, enabling 2FA with the user's Duo account using the [Universal Prompt](https://guide.duo.com/universal-prompt) after signing in with your Django application.

**Note:** In order to interface this middleware with Duo, you must create a new [Duo Web SDK](https://duo.com/docs/duoweb) application from within your organization's Duo Admin Portal and enable the "Show new Universal Prompt" setting. You will acquire a Client ID, Client Secret, and API Hostname, of which you will include in your `settings.py` file in the format listed below. It is strongly recommended not to hardcode these values in the settings file itself.

From [Duo's documentation for protecting applications](https://duo.com/docs/protecting-applications):
 > **Treat your Secret key or Client ID like a password**
The security of your Duo application is tied to the security of your Secret key (skey) or Client secret (client_secret). Secure it as you would any sensitive credential. Don't share it with unauthorized individuals or email it to anyone under any circumstances!

## Installation

To install the middleware application, use the following `pip` command (or equivalent for your package manager application):

```sh
pip install django-duo-universal-auth
```

### Sample Configuration (in your `settings.py` file)

First, add the package to your `INSTALLED_APPS` list variable:

```python
INSTALLED_APPS = [
    # ...
    'duo_universal_auth', # Add this!
]
```

Next, add the path for the middleware application to the `MIDDLEWARE` list variable:

```python
MIDDLEWARE = [
    # ...
    'duo_universal_auth.middleware.DuoUniversalAuthMiddleware', # Add this!
]
```

Then, add a new `DUO_UNIVERSAL_AUTH` configuration variable:

```python
DUO_UNIVERSAL_AUTH = {
    'MAIN': {
        'DUO_HOST': '<api_hostname>',
        'CLIENT_ID': '<client_id>',
        'CLIENT_SECRET': '<client_secret>',
        'AUTH_BACKENDS': [
            'django.contrib.auth.backends.ModelBackend',
        ],
        'FAIL_ACTION': 'CLOSED'
    }
}
```

## Duo API Callback Setup

**Note:** This step allows the application to communicate with Duo. If the view is not registered, the application will raise a `NoReverseMatch` error upon starting the Duo authentication flow.

To create the callback for the API to communicate with, you must add an entry to your `urlpatterns` variable from within your application's `urls.py` file (with any prepending path you choose):

```python
from django.urls import path, include

urlpatterns = [
    # ...
    path('duo/', include('duo_universal_auth.urls')), # Add this!
]
```

## Configuration Docs

Configurations for each Duo application are specified within individual dictionary objects inside a parent `DUO_UNIVERSAL_AUTH` dictionary each containing the following values:

#### `DUO_HOST`
##### Required: True

Represents the API Hostname for your organization's Duo API.
```python
'DUO_HOST': 'api-XXXXXXX.duosecurity.com'
```

#### `CLIENT_ID`
##### Required: True

Represents the Client ID for your application registered from within the Duo Admin Portal.
```python
'CLIENT_ID': 'DIXXXXXXXXXXXXXXXXXX'
```

#### `CLIENT_SECRET`
##### Required: True

Represents the Client Secret for your application registered from within the Duo Admin Portal.
```python
'CLIENT_SECRET': 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef'
```

#### `AUTH_BACKENDS`
##### Required: True

A list of authentication backends that the middleware will work with for the specific application. The Duo authentication middleware will only execute upon a successful authentication result from one of these backends.
```python
'AUTH_BACKENDS': [
    'django.contrib.auth.backends.ModelBackend',
]
```

#### `FAIL_ACTION`
##### Required: False (Default: `'CLOSED'`)

How the middleware should respond should the Duo authentication server be unavailable (from failing the preliminary health check).

 - `'CLOSED'`: Log out the user and return to the login page, disallowing any authentication while Duo servers are unavailable.
 - `'OPEN'`: Temporarily bypass Duo authentication until the Duo servers become available upon a future authentication attempt.
```python
'FAIL_ACTION': 'CLOSED'
```

#### `USERNAME_REMAP_FUNCTION`
##### Required: False

An optional one-argument function that takes in the current Django [`HttpRequest`](https://docs.djangoproject.com/en/4.0/ref/request-response/#httprequest-objects) object and returns the current authenticated user's username to send for Duo authentication. If unspecified, the username from [`HttpRequest.user`](https://docs.djangoproject.com/en/4.0/ref/request-response/#django.http.HttpRequest.user) will be used.

```python
'USERNAME_REMAP_FUNCTION': lambda r: r.user.username  # Mimics default behavior
```

## Post-Authentication Redirect
Once successfully authenticated with Duo, the middleware will automatically redirect the user to the path specified in the `DUO_NEXT_URL` session variable, falling back to the `LOGIN_REDIRECT_URL` settings variable if it is not present. This value is automatically assigned in the middleware before redirecting to Duo.