Metadata-Version: 2.4
Name: datapunt-authorization-django
Version: 2.0.2
Summary: Datapunt authorization check for Django
Home-page: https://github.com/Amsterdam/authorization_django
Author: Amsterdam Datapunt
Author-email: datapunt@amsterdam.nl
License: Mozilla Public License Version 2.0
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
Classifier: Programming Language :: Python
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
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=4.2
Requires-Dist: requests>=2.32.3
Requires-Dist: jwcrypto>=1.5.6
Provides-Extra: tests
Requires-Dist: pytest>=8.3.5; extra == "tests"
Requires-Dist: pytest-cov>=6.0.0; extra == "tests"
Requires-Dist: pytest-django>=4.10.0; extra == "tests"
Requires-Dist: requests_mock; extra == "tests"
Provides-Extra: extended
Requires-Dist: djangorestframework>=3.15.2; extra == "extended"
Requires-Dist: drf-spectacular>=0.28.0; extra == "extended"
Requires-Dist: pytest>=8.3.5; extra == "extended"
Requires-Dist: pytest-cov>=6.0.0; extra == "extended"
Requires-Dist: pytest-django>=4.10.0; extra == "extended"
Requires-Dist: requests_mock; extra == "extended"
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: summary

Datapunt Django Authorization
=============================

![python 3.4 onward](https://img.shields.io/badge/python-3.4%2C%203.5%2C%203.6-blue.svg)
![Mozilla Public License Version 2.0](https://img.shields.io/badge/license-MPLv2.0-blue.svg)

Django middleware that adds functionality to check authorization, based on JSON Web Tokens.

Unlike many Django OAuth2/OIDC libraries, this middleware does **NOT** interact with Django User objects.
It only validates the JSON Web Token, and exposes its scopes in the request object.
This allows backends to operate based on the token scope.

---------------------

Install
-------

Install the Django middleware:

```
pip install datapunt-authorization-django
```

Add `authorization_django.authorization_middleware` to the list of middlewares
in `settings.py`, and configure either a JWKS as json or an url to a JWKS.

```
MIDDLEWARE = (
   ...
   'authorization_django.middleware.AuthorizationMiddleware',
)
```

The old-style of using `authorization_django.authorization_middleware` is still supported,
but no longer recommended.

Settings
--------

The following settings are used by the middleware, and can be configured in
your ``settings.py`` in the ``DATAPUNT_AUTHZ`` dictionary.

| Setting                    | Description                                                                    | Default value                                          |
|----------------------------|--------------------------------------------------------------------------------|--------------------------------------------------------|
| JWKS                       | A valid JWKS as json, to validate tokens. See RFC 7517 and 7518 for details    | ""                                                     |
| JWKS_URL                   | A url to a valid JWKS, to validate tokens                                      | ""                                                     |
| JWKS_URLS                  | A list of URLs to a valid JWKS, to validate tokens                             | ""                                                     |
| CHECK_CLAIMS               | Which claims to check, e.g. `{"iss": "...", "aud": "..."}`                     | {}                                                     |
| MIN_INTERVAL_KEYSET_UPDATE | Minimal interval in secs between two checks for keyset update                  | 30                                                     |
| MIN_SCOPE                  | Minimum needed scope(s) to view non-whitelisted urls                           | empty tuple                                            |
| FORCED_ANONYMOUS_ROUTES    | Routes for which not to check for authorization (whitelist)                    | empty tuple                                            |
| PROTECTED                  | Routes which require scopes for access. Optionally with distinction of methods | empty list                                             |
| ALWAYS_OK                  | Disable any authorization checks, use only for local development               | False                                                  |
| ALLOWED_SIGNING_ALGORITHMS | List of allowed algorithms for signing web tokens                              | ['ES256', 'ES384', 'ES512', 'RS256', 'RS384', 'RS512'] |
| EXCEPTION_HANDLER          | Custom function to handle a raised exception  function                         | None                                                   |

The possible values for `CHECK_CLAIMS` are the RFC 7519 defined claims.
The relevant values are:

* **iss** (`str` issuer): Identifies the principal that issued the token.
* **aud** (`str`/`list` audience): Identifies the intended audience. This can be a list too.

Usage
-----

#### Scope notation

Beware of the scope notation! All scopes that are read from the token are converted using [scope.upper().replace("_", "/")](https://github.com/Amsterdam/authorization_django/blob/d702ea2a78b994d3e38ed576d309658f04820fa0/authorization_django/middleware.py#L184).

All scopes are transformed to uppercase, and underscores `_` are replaced by slashes `/`. So a scope `read_only` in keycloak should be defined as `READ/ONLY` in the settings.

The middleware provides different ways to add authorization to the application:

#### Define a minimal scope that is required for access

With the MIN_SCOPE setting you can define a tuple of scopes that are required to access the application. An exception is made for the routes defined in FORCED_ANONYMOUS_ROUTES, which is basically a whitelist, and for the OPTIONS method, which is always allowed. It is also allowed to configure a single scope as a string.

```
# Require 'EMPLOYEE' scope for access, except for /status route
'MIN_SCOPE': 'EMPLOYEE'
'FORCED_ANONYMOUS_ROUTES': '/status'
```

or e.g.

```
# Require 'EMPLOYEE' and 'HR' scope for access
'MIN_SCOPE': ('EMPLOYEE', 'HR')
```

#### Define protected routes

With the PROTECTED setting you can define routes that require certain scopes for access. A distinction can be made between HTTP methods. An exception is made for the OPTIONS method, which is always allowed.

```
# Require 'EMPLOYEE' scope for access to /api/secure route
'PROTECTED': [
  ('/api/secure', ['*'], ['EMPLOYEE'])
]
```

```
# Require 'EMPLOYEE' scope for read access to /private route
# Require 'ADMIN' scope for write access to /private route
'PROTECTED': [
  ('/private', ['GET', 'HEAD'], ['EMPLOYEE'])
  ('/private', ['POST', 'PUT', 'PATCH', 'DELETE'], ['ADMIN'])
]
```

**Note:** the FORCED_ANONYMOUS_ROUTES setting takes precedence over the routes defined in PROTECTED, so if a route in PROTECTED starts with a route set in FORCED_ANONYMOUS_ROUTES, this will lead to a ProtectedRouteConflictError

#### A method to check for authorization is added to the request object

It will add a callable `request.is_authorized_for(scope)`
that can tell you whether the current request is authorized for the given
scope:

```
if request.is_authorized_for('ADMIN'):
  ...  # do admin things
elif request.is_authorized_for('EMPLOYEE'):
  ...  # do employee level things
else:
  ...  # only the public stuff
```

### Django REST Framework Extensions

To enforce JWT authentication for REST views, add the following code.
This will also update the generated OpenAPI specification when drf-spectacular is used in the project.

```python
from rest_framework.views import APIView
from authorization_django.extensions.drf import JWTAuthentication


class CustomAPIView(APIView):
    authentication_classes = [JWTAuthentication]
```

Or configure this globally:

```python
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "authorization_django.extensions.drf.JWTAuthentication",
    ],
}
```

Additional token-scopes can also be verified for specific view:

```python
from rest_framework.views import APIView
from authorization_django.extensions.drf import HasTokenScopes


class CustomAPIView(APIView):
    permission_classes = [HasTokenScopes("extra-scope1", "extra-scope2")]
```

Contribute
----------

Activate your virtualenv, install the egg in `editable` mode, and start coding:

```
pip install -e .[extended]
```

Testing:

```
make test
```

Doing a release
----------

(This is for authorization_django developers.)

We use GitHub pull requests. If your PR should produce a new release of authorization_django, make sure one of the commits 1) increments the version number in setup.cfg appropriately and 2) adds a description of the release to the changelog below in this README. Then,

1. Merge the commit in GitHub, after review;
2. Pull the code from GitHub and merge it into the master branch: `git checkout main && git fetch origin && git merge --ff-only origin/main`
3. Tag the release `vX.Y.Z` with `git tag -a vX.Y.Z -m "Bump to vX.Y.Z"`
4. Push the tag to GitHub with `git push origin --tags`
5. The `publish-to-pypi`-workflow will automatically publish the release

Changelog
---------

* v2.0.2
  * Ensure thread safety by encapsulating jwks logic in wrapper class.
* v2.0.1
  * Fix allow custom responses and preserve original responses
* v2.0.0
  * Allow for custom (JSON) response after raised exception instead of direct 4** response.
* v1.8.0
  * Add support for apps that use both Entra ID and Keycloak
* v1.7.0
  * Support Python 3.13 and 3.14
* v1.6.2
  * Add appid to Entra token claims
* v1.6.1
  * Fix reading subject claim for Entra ID SPN tokens.
* v1.6.0
  * Added claim checking using `CHECK_CLAIMS`, and enforce it for Microsoft Entra ID.
* v1.5.0
  * Add authentication class for django rest framework and drf-spectacular
* v1.4.0
  * Support Microsoft Entra ID token structure
  * Added `JWKS_URLS` setting to authenticate against multiple backends
* v1.3.3
  * Bump jwcrypto requirement to 1.4.2
* v1.3.2
  * Stopped logging entire Authorization headers in case of a parse error
* v1.3.1
  * Extended support for Microsoft Azure AD JWT Token structure
  * Improved tests for Expired token logic
* v1.3.0
  * Support Microsoft Azure AD JWT Token structure
* v1.2.0:
  * expose claims via get_token_claims
  * Expose scopes via get_token_scopes
  * Fix SyntaxWarning in middleware
* v1.1.0
  * Add option to require authorization for specific routes
  * Fix MIN_SCOPE as tuple bug
* v1.0.0
  * By default do not allow symmetric signing algoritms
* v0.3.1
  * Bugfix for token with empty scopes claim
  * Lowered version requirement for requests module
* v0.3
  * Use jwcrypto module to verify tokens
  * Add support to load JWKS from public url
  * Remove support for custom logger settings
* v0.2.3
  * Settings are now grouped in settings.py (see Settings section above)
  * Middleware now creates audit logs
