Skip to content

Commit a358e08

Browse files
ADR 024: mTLS for 2FA (#1025)
* ADR 024: mTLS for 2FA This PR enables using a client certificate (also known as mutual TLS) for 2-factor-authentication. Signed-off-by: Rouven Bauer <[email protected]> Co-authored-by: Steve Cathcart <[email protected]>
1 parent 02dfe9f commit a358e08

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+2470
-733
lines changed

docs/source/api.rst

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ Additional configuration can be provided via the :class:`neo4j.Driver` construct
403403
+ :ref:`trust-ref`
404404
+ :ref:`ssl-context-ref`
405405
+ :ref:`trusted-certificates-ref`
406+
+ :ref:`client-certificate-ref`
406407
+ :ref:`user-agent-ref`
407408
+ :ref:`driver-notifications-min-severity-ref`
408409
+ :ref:`driver-notifications-disabled-categories-ref`
@@ -573,7 +574,8 @@ Specify how to determine the authenticity of encryption certificates provided by
573574

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

576-
This setting does not have any effect if ``encrypted`` is set to ``False``.
577+
This setting does not have any effect if ``encrypted`` is set to ``False`` or a
578+
custom ``ssl_context`` is configured.
577579

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

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

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

608-
If given, ``encrypted`` and ``trusted_certificates`` have no effect.
610+
If given, ``encrypted``, ``trusted_certificates``, and ``client_certificate`` have no effect.
609611

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

637+
.. versionadded:: 5.0
638+
635639
.. autoclass:: neo4j.TrustSystemCAs
636640

637641
.. autoclass:: neo4j.TrustAll
638642

639643
.. autoclass:: neo4j.TrustCustomCAs
640644

641-
.. versionadded:: 5.0
645+
646+
.. _client-certificate-ref:
647+
648+
``client_certificate``
649+
----------------------
650+
Specify a client certificate or certificate provider for mutual TLS (mTLS) authentication.
651+
652+
This setting does not have any effect if ``encrypted`` is set to ``False``
653+
(and the URI scheme is ``bolt://`` or ``neo4j://``) or a custom ``ssl_context`` is configured.
654+
655+
**This is a preview** (see :ref:`filter-warnings-ref`).
656+
It might be changed without following the deprecation policy.
657+
See also
658+
https://github.com/neo4j/neo4j-python-driver/wiki/preview-features
659+
660+
:Type: :class:`.ClientCertificate`, :class:`.ClientCertificateProvider` or :data:`None`.
661+
:Default: :data:`None`
662+
663+
.. versionadded:: 5.19
664+
665+
.. autoclass:: neo4j.auth_management.ClientCertificate
666+
667+
.. autoclass:: neo4j.auth_management.ClientCertificateProvider
642668

643669

644670
.. _user-agent-ref:

docs/source/async_api.rst

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,8 @@ Async Driver Configuration
381381
driver accepts
382382

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

386387

387388
.. _async-resolver-ref:
@@ -436,6 +437,28 @@ For example:
436437
:Default: :data:`None`
437438

438439

440+
.. _async-client-certificate-ref:
441+
442+
``client_certificate``
443+
----------------------
444+
Specify a client certificate or certificate provider for mutual TLS (mTLS) authentication.
445+
446+
This setting does not have any effect if ``encrypted`` is set to ``False``
447+
(and the URI scheme is ``bolt://`` or ``neo4j://``) or a custom ``ssl_context`` is configured.
448+
449+
**This is a preview** (see :ref:`filter-warnings-ref`).
450+
It might be changed without following the deprecation policy.
451+
See also
452+
https://github.com/neo4j/neo4j-python-driver/wiki/preview-features
453+
454+
:Type: :class:`.ClientCertificate`, :class:`.AsyncClientCertificateProvider` or :data:`None`.
455+
:Default: :data:`None`
456+
457+
.. versionadded:: 5.19
458+
459+
.. autoclass:: neo4j.auth_management.AsyncClientCertificateProvider
460+
461+
439462

440463
Driver Object Lifetime
441464
======================

src/neo4j/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
)
3838
from ._conf import (
3939
Config as _Config,
40-
PoolConfig as _PoolConfig,
4140
SessionConfig as _SessionConfig,
4241
TrustAll,
4342
TrustCustomCAs,
@@ -53,6 +52,7 @@
5352
PreviewWarning,
5453
version as __version__,
5554
)
55+
from ._sync.config import PoolConfig as _PoolConfig
5656
from ._sync.driver import (
5757
BoltDriver,
5858
Driver,

src/neo4j/_api.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ class NotificationMinimumSeverity(str, Enum):
4848
>>> NotificationMinimumSeverity.INFORMATION == "INFORMATION"
4949
True
5050
51-
.. versionadded:: 5.7
52-
5351
.. seealso::
5452
driver config :ref:`driver-notifications-min-severity-ref`,
5553
session config :ref:`session-notifications-min-severity-ref`
54+
55+
.. versionadded:: 5.7
5656
"""
5757

5858
OFF = "OFF"
@@ -111,9 +111,9 @@ class NotificationSeverity(str, Enum):
111111
# or severity_level == "UNKNOWN"
112112
log.debug("%r", notification)
113113
114-
.. versionadded:: 5.7
115-
116114
.. seealso:: :attr:`SummaryNotification.severity_level`
115+
116+
.. versionadded:: 5.7
117117
"""
118118

119119
WARNING = "WARNING"
@@ -137,14 +137,14 @@ class NotificationDisabledCategory(str, Enum):
137137
>>> NotificationDisabledCategory.DEPRECATION == "DEPRECATION"
138138
True
139139
140+
.. seealso::
141+
driver config :ref:`driver-notifications-disabled-categories-ref`,
142+
session config :ref:`session-notifications-disabled-categories-ref`
143+
140144
.. versionadded:: 5.7
141145
142146
.. versionchanged:: 5.14
143147
Added categories :attr:`.SECURITY` and :attr:`.TOPOLOGY`.
144-
145-
.. seealso::
146-
driver config :ref:`driver-notifications-disabled-categories-ref`,
147-
session config :ref:`session-notifications-disabled-categories-ref`
148148
"""
149149

150150
HINT = "HINT"
@@ -188,12 +188,12 @@ class NotificationCategory(str, Enum):
188188
>>> NotificationCategory.UNKNOWN == "UNKNOWN"
189189
True
190190
191+
.. seealso:: :attr:`SummaryNotification.category`
192+
191193
.. versionadded:: 5.7
192194
193195
.. versionchanged:: 5.14
194196
Added categories :attr:`.SECURITY` and :attr:`.TOPOLOGY`.
195-
196-
.. seealso:: :attr:`SummaryNotification.category`
197197
"""
198198

199199
HINT = "HINT"

src/neo4j/_async/auth_management.py

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@
1919
import typing as t
2020
from logging import getLogger
2121

22-
from .._async_compat.concurrency import AsyncLock
22+
from .._async_compat.concurrency import (
23+
AsyncCooperativeLock,
24+
AsyncLock,
25+
)
2326
from .._auth_management import (
2427
AsyncAuthManager,
28+
AsyncClientCertificateProvider,
29+
ClientCertificate,
2530
expiring_auth_has_expired,
2631
ExpiringAuth,
2732
)
33+
from .._meta import preview
2834

2935

3036
if t.TYPE_CHECKING:
@@ -285,3 +291,127 @@ async def auth_provider():
285291
"Neo.ClientError.Security.Unauthorized",
286292
))
287293
return AsyncNeo4jAuthTokenManager(provider, handled_codes)
294+
295+
296+
class _AsyncStaticClientCertificateProvider(AsyncClientCertificateProvider):
297+
_cert: t.Optional[ClientCertificate]
298+
299+
def __init__(self, cert: ClientCertificate) -> None:
300+
self._cert = cert
301+
302+
async def get_certificate(self) -> t.Optional[ClientCertificate]:
303+
cert, self._cert = self._cert, None
304+
return cert
305+
306+
307+
@preview("Mutual TLS is a preview feature.")
308+
class AsyncRotatingClientCertificateProvider(AsyncClientCertificateProvider):
309+
"""
310+
Implementation of a certificate provider that can rotate certificates.
311+
312+
The provider will make the driver use the initial certificate for all
313+
connections until the certificate is updated using the
314+
:meth:`update_certificate` method.
315+
From that point on, the new certificate will be used for all new
316+
connections until :meth:`update_certificate` is called again and so on.
317+
318+
**This is a preview** (see :ref:`filter-warnings-ref`).
319+
It might be changed without following the deprecation policy.
320+
See also
321+
https://github.com/neo4j/neo4j-python-driver/wiki/preview-features
322+
323+
Example::
324+
325+
from neo4j import AsyncGraphDatabase
326+
from neo4j.auth_management import (
327+
ClientCertificate,
328+
AsyncClientCertificateProviders,
329+
)
330+
331+
332+
provider = AsyncClientCertificateProviders.rotating(
333+
ClientCertificate(
334+
certfile="path/to/certfile.pem",
335+
keyfile="path/to/keyfile.pem",
336+
password=lambda: "super_secret_password"
337+
)
338+
)
339+
driver = AsyncGraphDatabase.driver(
340+
# secure driver must be configured for client certificate
341+
# to be used: (...+s[sc] scheme or encrypted=True)
342+
"neo4j+s://example.com:7687",
343+
# auth still required as before, unless server is configured to not
344+
# use authentication
345+
auth=("neo4j", "password"),
346+
client_certificate=provider
347+
)
348+
349+
# do work with the driver, until the certificate needs to be rotated
350+
...
351+
352+
await provider.update_certificate(
353+
ClientCertificate(
354+
certfile="path/to/new/certfile.pem",
355+
keyfile="path/to/new/keyfile.pem",
356+
password=lambda: "new_super_secret_password"
357+
)
358+
)
359+
360+
# do more work with the driver, until the certificate needs to be
361+
# rotated again
362+
...
363+
364+
:param initial_cert: The certificate to use initially.
365+
366+
.. versionadded:: 5.19
367+
368+
"""
369+
def __init__(self, initial_cert: ClientCertificate) -> None:
370+
self._cert: t.Optional[ClientCertificate] = initial_cert
371+
self._lock = AsyncCooperativeLock()
372+
373+
async def get_certificate(self) -> t.Optional[ClientCertificate]:
374+
async with self._lock:
375+
cert, self._cert = self._cert, None
376+
return cert
377+
378+
async def update_certificate(self, cert: ClientCertificate) -> None:
379+
"""
380+
Update the certificate to use for new connections.
381+
"""
382+
async with self._lock:
383+
self._cert = cert
384+
385+
386+
class AsyncClientCertificateProviders:
387+
"""A collection of :class:`.AsyncClientCertificateProvider` factories.
388+
389+
**This is a preview** (see :ref:`filter-warnings-ref`).
390+
It might be changed without following the deprecation policy.
391+
See also
392+
https://github.com/neo4j/neo4j-python-driver/wiki/preview-features
393+
394+
.. versionadded:: 5.19
395+
"""
396+
@staticmethod
397+
@preview("Mutual TLS is a preview feature.")
398+
def static(cert: ClientCertificate) -> AsyncClientCertificateProvider:
399+
"""
400+
Create a static client certificate provider.
401+
402+
The provider simply makes the driver use the given certificate for all
403+
connections.
404+
"""
405+
return _AsyncStaticClientCertificateProvider(cert)
406+
407+
@staticmethod
408+
@preview("Mutual TLS is a preview feature.")
409+
def rotating(
410+
initial_cert: ClientCertificate
411+
) -> AsyncRotatingClientCertificateProvider:
412+
"""
413+
Create certificate provider that allows for rotating certificates.
414+
415+
.. seealso:: :class:`.AsyncRotatingClientCertificateProvider`
416+
"""
417+
return AsyncRotatingClientCertificateProvider(initial_cert)

0 commit comments

Comments
 (0)