Skip to content

ADR 024: mTLS for 2FA #1025

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 7, 2024
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
32 changes: 29 additions & 3 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ Additional configuration can be provided via the :class:`neo4j.Driver` construct
+ :ref:`trust-ref`
+ :ref:`ssl-context-ref`
+ :ref:`trusted-certificates-ref`
+ :ref:`client-certificate-ref`
+ :ref:`user-agent-ref`
+ :ref:`driver-notifications-min-severity-ref`
+ :ref:`driver-notifications-disabled-categories-ref`
Expand Down Expand Up @@ -573,7 +574,8 @@ Specify how to determine the authenticity of encryption certificates provided by

This setting is only available for URI schemes ``bolt://`` and ``neo4j://`` (:ref:`uri-ref`).

This setting does not have any effect if ``encrypted`` is set to ``False``.
This setting does not have any effect if ``encrypted`` is set to ``False`` or a
custom ``ssl_context`` is configured.

:Type: ``neo4j.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES``, ``neo4j.TRUST_ALL_CERTIFICATES``

Expand Down Expand Up @@ -605,7 +607,7 @@ Specify a custom SSL context to use for wrapping connections.

This setting is only available for URI schemes ``bolt://`` and ``neo4j://`` (:ref:`uri-ref`).

If given, ``encrypted`` and ``trusted_certificates`` have no effect.
If given, ``encrypted``, ``trusted_certificates``, and ``client_certificate`` have no effect.

.. warning::
This option may compromise your application's security if used improperly.
Expand All @@ -632,13 +634,37 @@ custom ``ssl_context`` is configured.
:Type: :class:`.TrustSystemCAs`, :class:`.TrustAll`, or :class:`.TrustCustomCAs`
:Default: :const:`neo4j.TrustSystemCAs()`

.. versionadded:: 5.0

.. autoclass:: neo4j.TrustSystemCAs

.. autoclass:: neo4j.TrustAll

.. autoclass:: neo4j.TrustCustomCAs

.. versionadded:: 5.0

.. _client-certificate-ref:

``client_certificate``
----------------------
Specify a client certificate or certificate provider for mutual TLS (mTLS) authentication.

This setting does not have any effect if ``encrypted`` is set to ``False``
(and the URI scheme is ``bolt://`` or ``neo4j://``) or a custom ``ssl_context`` is configured.

**This is a preview** (see :ref:`filter-warnings-ref`).
It might be changed without following the deprecation policy.
See also
https://github.com/neo4j/neo4j-python-driver/wiki/preview-features

:Type: :class:`.ClientCertificate`, :class:`.ClientCertificateProvider` or :data:`None`.
:Default: :data:`None`

.. versionadded:: 5.19

.. autoclass:: neo4j.auth_management.ClientCertificate

.. autoclass:: neo4j.auth_management.ClientCertificateProvider


.. _user-agent-ref:
Expand Down
25 changes: 24 additions & 1 deletion docs/source/async_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,8 @@ Async Driver Configuration
driver accepts

* a sync as well as an async custom resolver function (see :ref:`async-resolver-ref`)
* as sync as well as an async auth token manager (see :class:`.AsyncAuthManager`).
* a sync as well as an async auth token manager (see :class:`.AsyncAuthManager`).
* an async client certificate provider (see :ref:`async-client-certificate-ref`).


.. _async-resolver-ref:
Expand Down Expand Up @@ -436,6 +437,28 @@ For example:
:Default: :data:`None`


.. _async-client-certificate-ref:

``client_certificate``
----------------------
Specify a client certificate or certificate provider for mutual TLS (mTLS) authentication.

This setting does not have any effect if ``encrypted`` is set to ``False``
(and the URI scheme is ``bolt://`` or ``neo4j://``) or a custom ``ssl_context`` is configured.

**This is a preview** (see :ref:`filter-warnings-ref`).
It might be changed without following the deprecation policy.
See also
https://github.com/neo4j/neo4j-python-driver/wiki/preview-features

:Type: :class:`.ClientCertificate`, :class:`.AsyncClientCertificateProvider` or :data:`None`.
:Default: :data:`None`

.. versionadded:: 5.19

.. autoclass:: neo4j.auth_management.AsyncClientCertificateProvider



Driver Object Lifetime
======================
Expand Down
2 changes: 1 addition & 1 deletion src/neo4j/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
)
from ._conf import (
Config as _Config,
PoolConfig as _PoolConfig,
SessionConfig as _SessionConfig,
TrustAll,
TrustCustomCAs,
Expand All @@ -53,6 +52,7 @@
PreviewWarning,
version as __version__,
)
from ._sync.config import PoolConfig as _PoolConfig
from ._sync.driver import (
BoltDriver,
Driver,
Expand Down
20 changes: 10 additions & 10 deletions src/neo4j/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ class NotificationMinimumSeverity(str, Enum):
>>> NotificationMinimumSeverity.INFORMATION == "INFORMATION"
True

.. versionadded:: 5.7

.. seealso::
driver config :ref:`driver-notifications-min-severity-ref`,
session config :ref:`session-notifications-min-severity-ref`

.. versionadded:: 5.7
"""

OFF = "OFF"
Expand Down Expand Up @@ -111,9 +111,9 @@ class NotificationSeverity(str, Enum):
# or severity_level == "UNKNOWN"
log.debug("%r", notification)

.. versionadded:: 5.7

.. seealso:: :attr:`SummaryNotification.severity_level`

.. versionadded:: 5.7
"""

WARNING = "WARNING"
Expand All @@ -137,14 +137,14 @@ class NotificationDisabledCategory(str, Enum):
>>> NotificationDisabledCategory.DEPRECATION == "DEPRECATION"
True

.. seealso::
driver config :ref:`driver-notifications-disabled-categories-ref`,
session config :ref:`session-notifications-disabled-categories-ref`

.. versionadded:: 5.7

.. versionchanged:: 5.14
Added categories :attr:`.SECURITY` and :attr:`.TOPOLOGY`.

.. seealso::
driver config :ref:`driver-notifications-disabled-categories-ref`,
session config :ref:`session-notifications-disabled-categories-ref`
"""

HINT = "HINT"
Expand Down Expand Up @@ -188,12 +188,12 @@ class NotificationCategory(str, Enum):
>>> NotificationCategory.UNKNOWN == "UNKNOWN"
True

.. seealso:: :attr:`SummaryNotification.category`

.. versionadded:: 5.7

.. versionchanged:: 5.14
Added categories :attr:`.SECURITY` and :attr:`.TOPOLOGY`.

.. seealso:: :attr:`SummaryNotification.category`
"""

HINT = "HINT"
Expand Down
132 changes: 131 additions & 1 deletion src/neo4j/_async/auth_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@
import typing as t
from logging import getLogger

from .._async_compat.concurrency import AsyncLock
from .._async_compat.concurrency import (
AsyncCooperativeLock,
AsyncLock,
)
from .._auth_management import (
AsyncAuthManager,
AsyncClientCertificateProvider,
ClientCertificate,
expiring_auth_has_expired,
ExpiringAuth,
)
from .._meta import preview


if t.TYPE_CHECKING:
Expand Down Expand Up @@ -285,3 +291,127 @@ async def auth_provider():
"Neo.ClientError.Security.Unauthorized",
))
return AsyncNeo4jAuthTokenManager(provider, handled_codes)


class _AsyncStaticClientCertificateProvider(AsyncClientCertificateProvider):
_cert: t.Optional[ClientCertificate]

def __init__(self, cert: ClientCertificate) -> None:
self._cert = cert

async def get_certificate(self) -> t.Optional[ClientCertificate]:
cert, self._cert = self._cert, None
return cert


@preview("Mutual TLS is a preview feature.")
class AsyncRotatingClientCertificateProvider(AsyncClientCertificateProvider):
"""
Implementation of a certificate provider that can rotate certificates.

The provider will make the driver use the initial certificate for all
connections until the certificate is updated using the
:meth:`update_certificate` method.
From that point on, the new certificate will be used for all new
connections until :meth:`update_certificate` is called again and so on.

**This is a preview** (see :ref:`filter-warnings-ref`).
It might be changed without following the deprecation policy.
See also
https://github.com/neo4j/neo4j-python-driver/wiki/preview-features

Example::

from neo4j import AsyncGraphDatabase
from neo4j.auth_management import (
ClientCertificate,
AsyncClientCertificateProviders,
)


provider = AsyncClientCertificateProviders.rotating(
ClientCertificate(
certfile="path/to/certfile.pem",
keyfile="path/to/keyfile.pem",
password=lambda: "super_secret_password"
)
)
driver = AsyncGraphDatabase.driver(
# secure driver must be configured for client certificate
# to be used: (...+s[sc] scheme or encrypted=True)
"neo4j+s://example.com:7687",
# auth still required as before, unless server is configured to not
# use authentication
auth=("neo4j", "password"),
client_certificate=provider
)

# do work with the driver, until the certificate needs to be rotated
...

await provider.update_certificate(
ClientCertificate(
certfile="path/to/new/certfile.pem",
keyfile="path/to/new/keyfile.pem",
password=lambda: "new_super_secret_password"
)
)

# do more work with the driver, until the certificate needs to be
# rotated again
...

:param initial_cert: The certificate to use initially.

.. versionadded:: 5.19

"""
def __init__(self, initial_cert: ClientCertificate) -> None:
self._cert: t.Optional[ClientCertificate] = initial_cert
self._lock = AsyncCooperativeLock()

async def get_certificate(self) -> t.Optional[ClientCertificate]:
async with self._lock:
cert, self._cert = self._cert, None
return cert

async def update_certificate(self, cert: ClientCertificate) -> None:
"""
Update the certificate to use for new connections.
"""
async with self._lock:
self._cert = cert


class AsyncClientCertificateProviders:
"""A collection of :class:`.AsyncClientCertificateProvider` factories.

**This is a preview** (see :ref:`filter-warnings-ref`).
It might be changed without following the deprecation policy.
See also
https://github.com/neo4j/neo4j-python-driver/wiki/preview-features

.. versionadded:: 5.19
"""
@staticmethod
@preview("Mutual TLS is a preview feature.")
def static(cert: ClientCertificate) -> AsyncClientCertificateProvider:
"""
Create a static client certificate provider.

The provider simply makes the driver use the given certificate for all
connections.
"""
return _AsyncStaticClientCertificateProvider(cert)

@staticmethod
@preview("Mutual TLS is a preview feature.")
def rotating(
initial_cert: ClientCertificate
) -> AsyncRotatingClientCertificateProvider:
"""
Create certificate provider that allows for rotating certificates.

.. seealso:: :class:`.AsyncRotatingClientCertificateProvider`
"""
return AsyncRotatingClientCertificateProvider(initial_cert)
Loading