docs: Add documentation for the new custom auth wrapper setting.

This commit is contained in:
Mateusz Mandera 2024-01-24 00:16:06 +01:00 committed by Tim Abbott
parent f6ef2d860b
commit 8ab0296a6e
1 changed files with 93 additions and 0 deletions

View File

@ -1069,6 +1069,99 @@ configure the JWT secret and algorithm via `JWT_AUTH_KEYS` in
`/etc/zulip/settings.py`; see the inline comment documentation in that
file for details.
## Configuring a custom Python wrapper around the `authenticate` mechanism
Zulip supports configuring a custom authentication function that will
work as a wrapper around every login attempt to Zulip, enabling custom
logging, additional authentication checks, and more.
This mechanism protects the web login and the mobile login process
used to obtain an API key, but **will not be called** when processing
API requests by the Zulip mobile apps or other API clients that have
already obtained an API key (a step that typically happens once per
device during first-time login).
:::{note}
Knowledge of [how authentication backends work in Django][django-authenticate-details]
as well as some familiarity with Zulip's authentication implementation in
`zproject/backends.py` are required.
This feature is beta and has some rough edges as well as requiring
significantly more expertise than other authentication features; we do
not recommend using it without specific advice from Zulip support, but
we document it here for completeness.
:::
You can write custom Python logic that will wrap such `authenticate()`
calls by specifying in `/etc/zulip/settings.py`:
```python3
def custom_auth_wrapper(
auth_func, *args, **kwargs
):
from zerver.lib.exceptions import JsonableError
backend = args[0]
backend_name = backend.name
request = args[1]
ip_address = request.META["REMOTE_ADDR"]
backend.logger.info("%s backend. ip is %s", backend_name, ip_address)
user_profile = auth_func(*args, **kwargs)
if user_profile is not None and user_profile.delivery_email == "protecteduser@example.com":
if backend_name == "email" and ip_address != "x.x.x.x":
raise JsonableError("Your IP address is not allowed to log in as this user.")
return user_profile
# We need to actually specify to use this function defined above as the
# custom authentication wrapper:
CUSTOM_AUTHENTICATION_WRAPPER_FUNCTION = custom_auth_wrapper
```
`auth_func` is the underlying `authenticate` function belonging to the
authentication backend class currently being processed. If you have
more than one backend enabled, this will be executed multiple times -
each time with `auth_func` being the `authenticate` function of the
backend class currently being processed.
Therefore, your `custom_auth_wrapper` can inspect the received
arguments, process them as desired and eventually call the original
`auth_func` to obtain the underlying authentication result, to analyze
and potentially return it. The simple code demonstrated above checks
whether `auth_func` succeeds, and if so, whether the resulting user
account belongs to `protecteduser@example.com`. Such authentication,
if it's using the `EmailAuthBackend` should only be allowed if made
from a pre-defined IP address `x.x.x.x`, so if these restrictions are
violated, a JSON error response will be generated.
The example demonstrates the possibility of making the logic dependent
on the specific authentication backend being used, but unless you're
very familiar with the various backends used by Zulip, a safer
approach is to keep things general.
:::{important}
Using `CUSTOM_AUTHENTICATION_WRAPPER_FUNCTION` with social
authentication backends (Google, GitHub, and anything else that
subclasses `SocialAuthMixin` in `zproject/backends.py`) is not
recommended.
Due to the different way that social authentication backends process
authentication attempts using a 3rd party site that provides the
user's identity, it is **not** a good approach to modify their own
`authenticate` calls directly.
If you need to use this feature in combination with those backends,
you should make your logic be applied when processing the
`ZulipDummyBackend` - which is the final layer of the authentication
checks for whether authentication should succeed. If you want to
reject authentication requests e.g. based on IP address of the
request, this is where it should happen.
:::
[django-authenticate-details]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#writing-an-authentication-backend
## Adding more authentication backends
Adding an integration with any of the more than 100 authentication