From b3c37cedba8abcee99da9163cd1c32e06b46edfc Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Sat, 24 May 2025 01:59:50 +0200 Subject: [PATCH] Add JWT token authentication --- CHANGES.rst | 1 + docs/connect.rst | 6 ++++++ src/crate/client/connection.py | 4 ++++ src/crate/client/http.py | 8 ++++++++ tests/client/test_http.py | 29 ++++++++++++++++++++++------- 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index db094c5e..4740ba06 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ Unreleased ========== - Modernize project definition to latest Python best practices. Thanks, @surister. - Exceptions: Exceptions from the BLOB API now include their full names. +- Added JWT token authentication 2025/01/30 2.0.0 ================ diff --git a/docs/connect.rst b/docs/connect.rst index 36e4dd54..fca3a667 100644 --- a/docs/connect.rst +++ b/docs/connect.rst @@ -241,6 +241,12 @@ and password. authenticate as the CrateDB superuser, which is ``crate``. The superuser does not have a password, so you can omit the ``password`` argument. +Alternatively, authenticate using a JWT token: + + >>> connection = client.connect(..., jwt_token="") + +Here, replace ```` with the appropriate JWT token. + .. _schema-selection: Schema selection diff --git a/src/crate/client/connection.py b/src/crate/client/connection.py index 0638a018..018aca1b 100644 --- a/src/crate/client/connection.py +++ b/src/crate/client/connection.py @@ -42,6 +42,7 @@ def __init__( ssl_relax_minimum_version=False, username=None, password=None, + jwt_token=None, schema=None, pool_size=None, socket_keepalive=True, @@ -81,6 +82,8 @@ def __init__( the username in the database. :param password: the password of the user in the database. + :param jwt_token: + the JWT token to authenticate with the server. :param pool_size: (optional) Number of connections to save that can be reused. @@ -148,6 +151,7 @@ def __init__( ssl_relax_minimum_version=ssl_relax_minimum_version, username=username, password=password, + jwt_token=jwt_token, schema=schema, pool_size=pool_size, socket_keepalive=socket_keepalive, diff --git a/src/crate/client/http.py b/src/crate/client/http.py index 0d9d6c9f..59bf3668 100644 --- a/src/crate/client/http.py +++ b/src/crate/client/http.py @@ -158,6 +158,7 @@ def request( headers=None, username=None, password=None, + jwt_token=None, schema=None, backoff_factor=0, **kwargs, @@ -173,6 +174,10 @@ def request( if length is not None: headers["Content-Length"] = length + # Authentication token + if jwt_token is not None and "Authorization" not in headers: + headers["Authorization"] = "Bearer %s" % jwt_token + # Authentication credentials if username is not None: if "Authorization" not in headers and username is not None: @@ -421,6 +426,7 @@ def __init__( ssl_relax_minimum_version=False, username=None, password=None, + jwt_token=None, schema=None, pool_size=None, socket_keepalive=True, @@ -477,6 +483,7 @@ def __init__( self._local = threading.local() self.username = username self.password = password + self.jwt_token = jwt_token self.schema = schema self.path = self.SQL_PATH @@ -593,6 +600,7 @@ def _request(self, method, path, server=None, **kwargs): path, username=self.username, password=self.password, + jwt_token=self.jwt_token, backoff_factor=self.backoff_factor, schema=self.schema, **kwargs, diff --git a/tests/client/test_http.py b/tests/client/test_http.py index 0292b661..7c5011f7 100644 --- a/tests/client/test_http.py +++ b/tests/client/test_http.py @@ -632,14 +632,22 @@ def do_POST(self): self.server.SHARED["schema"] = self.headers.get("Default-Schema") if self.headers.get("Authorization") is not None: - auth_header = self.headers["Authorization"].replace("Basic ", "") - credentials = b64decode(auth_header).decode("utf-8").split(":", 1) - self.server.SHARED["username"] = credentials[0] - if len(credentials) > 1 and credentials[1]: - self.server.SHARED["password"] = credentials[1] - else: - self.server.SHARED["password"] = None + auth_header = self.headers["Authorization"] + if "Basic" in auth_header: + auth_header = auth_header.replace("Basic ", "") + credentials = ( + b64decode(auth_header).decode("utf-8").split(":", 1) + ) + self.server.SHARED["username"] = credentials[0] + if len(credentials) > 1 and credentials[1]: + self.server.SHARED["password"] = credentials[1] + else: + self.server.SHARED["password"] = None + elif "Bearer" in auth_header: + jwt_token = auth_header.replace("Bearer ", "") + self.server.SHARED["jwt_token"] = jwt_token else: + self.server.SHARED["jwt_token"] = None self.server.SHARED["username"] = None if self.headers.get("X-User") is not None: @@ -705,3 +713,10 @@ def test_credentials(serve_http): assert server.SHARED["usernameFromXUser"] == username assert server.SHARED["username"] == username assert server.SHARED["password"] == password + + # Just a single token, most convenient. + jwt_token = "testJwtToken" + with connect(url, jwt_token=jwt_token) as conn: + assert conn.client.jwt_token == jwt_token + conn.client.sql("select 3;") + assert server.SHARED["jwt_token"] == jwt_token