This is an automated email from the ASF dual-hosted git repository.

jasonliu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 43b51932f90 Add JWT/OIDC authentication support to Hashicorp Vault 
provider (#61439)
43b51932f90 is described below

commit 43b51932f90d27d94c4e8d13fcb7ef85a97ebe98
Author: Piotr Klinski <[email protected]>
AuthorDate: Mon Feb 9 14:56:41 2026 +0100

    Add JWT/OIDC authentication support to Hashicorp Vault provider (#61439)
    
    * Add JWT/OIDC authentication support to Hashicorp Vault provider
    
    This adds JWT/OIDC authentication method support to the Hashicorp Vault
    provider, enabling token-less authentication through identity federation.
    
    Key features:
    - New 'jwt' auth_type for VaultClient, VaultHook, and VaultBackend
    - Support for jwt_token parameter or automatic token retrieval from jwt_path
    - Configurable jwt_role for Vault role binding
    - Full backwards compatibility with existing auth methods
    
    Use cases enabled:
    - Kubernetes workload identity with projected service account tokens
    - Cloud provider identity (AWS IAM roles, GCP Workload Identity, Azure AD)
    - CI/CD pipelines (GitHub Actions OIDC, GitLab CI)
    - External identity providers (Auth0, Okta, Keycloak)
    
    Co-Authored-By: Claude Opus 4.5 <[email protected]>
    
    * Update 
providers/hashicorp/src/airflow/providers/hashicorp/_internal_client/vault_client.py
    
    Co-authored-by: Wei Lee <[email protected]>
    
    * Update providers/hashicorp/src/airflow/providers/hashicorp/hooks/vault.py
    
    Co-authored-by: Wei Lee <[email protected]>
    
    * update the args order for methods
    
    * Update 
providers/hashicorp/src/airflow/providers/hashicorp/_internal_client/vault_client.py
    
    Co-authored-by: Wei Lee <[email protected]>
    
    * apply fixes for oorder in new jwt parameter for docsstring
    
    * Address PR review: use stricter mock assertions and inline kwargs
    
    Replace assert_called_with with call_args_list assertions in JWT tests
    to verify exact number of calls. Inline kwargs dicts directly into
    VaultHook() constructor calls where they are only used once.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * - remove jwt token defaults
    - fix documentaion
    - minor fiex
    
    * Remove DEFAULT_JWT_TOKEN_PATH constant and K8s fallback from JWT auth
    
    JWT is a general-purpose Vault auth method, not tied to Kubernetes.
    Remove the DEFAULT_JWT_TOKEN_PATH constant (which pointed to the K8s
    service account token path) and its fallback in VaultHook. Users must
    now explicitly provide either jwt_token or jwt_token_path when using
    JWT auth, otherwise _VaultClient raises a clear validation error.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 4.5 <[email protected]>
    Co-authored-by: Wei Lee <[email protected]>
---
 .../docs/secrets-backends/hashicorp-vault.rst      | 29 +++++++
 .../hashicorp/_internal_client/vault_client.py     | 37 +++++++-
 .../src/airflow/providers/hashicorp/hooks/vault.py | 31 ++++++-
 .../airflow/providers/hashicorp/secrets/vault.py   | 11 ++-
 .../_internal_client/test_vault_client.py          | 99 +++++++++++++++++++++-
 .../tests/unit/hashicorp/hooks/test_vault.py       | 79 +++++++++++++++++
 .../tests/unit/hashicorp/secrets/test_vault.py     | 51 +++++++++++
 7 files changed, 331 insertions(+), 6 deletions(-)

diff --git a/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst 
b/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst
index 7c6ad07c26d..faf4db70f01 100644
--- a/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst
+++ b/providers/hashicorp/docs/secrets-backends/hashicorp-vault.rst
@@ -230,6 +230,35 @@ For more details, please refer to the AWS Assume Role 
Authentication documentati
     backend = airflow.providers.hashicorp.secrets.vault.VaultBackend
     backend_kwargs = {"connections_path": "airflow-connections", 
"variables_path": null, "mount_point": "airflow", "url": 
"http://127.0.0.1:8200";, "auth_type": "aws_iam", "assume_role_kwargs": 
{"RoleArn":"arn:aws:iam::123456789000:role/hashicorp-aws-iam-role", 
"RoleSessionName": "Airflow"}}
 
+Vault authentication with JWT/OIDC
+""""""""""""""""""""""""""""""""""
+
+JWT/OIDC authentication is useful for:
+
+- Cloud provider identity (AWS IAM roles, GCP Workload Identity, Azure AD)
+- CI/CD pipelines (GitHub Actions OIDC, GitLab CI)
+- External identity providers (Auth0, Okta, Keycloak)
+- Kubernetes workload identity with projected service account tokens
+
+To use JWT authentication, set ``auth_type`` to ``jwt`` and provide 
``jwt_role``. You must also provide a JWT token
+via ``jwt_token`` (inline) or ``jwt_token_path`` (path to a file containing 
the token):
+
+.. code-block:: ini
+
+    [secrets]
+    backend = airflow.providers.hashicorp.secrets.vault.VaultBackend
+    backend_kwargs = {"connections_path": "connections", "variables_path": 
"variables", "mount_point": "airflow", "url": "http://127.0.0.1:8200";, 
"auth_type": "jwt", "jwt_role": "airflow-role", "jwt_token_path": 
"/var/run/secrets/tokens/vault-token"}
+
+You can also provide the token directly:
+
+.. code-block:: ini
+
+    [secrets]
+    backend = airflow.providers.hashicorp.secrets.vault.VaultBackend
+    backend_kwargs = {"connections_path": "connections", "variables_path": 
"variables", "mount_point": "airflow", "url": "http://127.0.0.1:8200";, 
"auth_type": "jwt", "jwt_role": "airflow-role", "jwt_token": 
"eyJhbGciOiJSUzI1NiIs..."}
+
+If you need to use a different mount point for the JWT auth method (default is 
``jwt``), you can specify it with ``auth_mount_point``.
+
 Using multiple mount points
 """""""""""""""""""""""""""
 
diff --git 
a/providers/hashicorp/src/airflow/providers/hashicorp/_internal_client/vault_client.py
 
b/providers/hashicorp/src/airflow/providers/hashicorp/_internal_client/vault_client.py
index e1e63d788c2..8ce9a735e89 100644
--- 
a/providers/hashicorp/src/airflow/providers/hashicorp/_internal_client/vault_client.py
+++ 
b/providers/hashicorp/src/airflow/providers/hashicorp/_internal_client/vault_client.py
@@ -39,6 +39,7 @@ VALID_AUTH_TYPES: list[str] = [
     "azure",
     "github",
     "gcp",
+    "jwt",
     "kubernetes",
     "ldap",
     "radius",
@@ -58,7 +59,7 @@ class _VaultClient(LoggingMixin):
 
     :param url: Base URL for the Vault instance being addressed.
     :param auth_type: Authentication Type for Vault. Default is ``token``. 
Available values are in
-        ('approle', 'aws_iam', 'azure', 'github', 'gcp', 'kubernetes', 'ldap', 
'radius', 'token', 'userpass')
+        ('approle', 'aws_iam', 'azure', 'github', 'gcp', 'jwt', 'kubernetes', 
'ldap', 'radius', 'token', 'userpass')
     :param auth_mount_point: It can be used to define mount_point for 
authentication chosen
           Default depends on the authentication method used.
     :param mount_point: The "path" the secret engine was mounted on. Default 
is "secret". Note that
@@ -93,6 +94,9 @@ class _VaultClient(LoggingMixin):
     :param radius_host: Host for radius (for ``radius`` auth_type).
     :param radius_secret: Secret for radius (for ``radius`` auth_type).
     :param radius_port: Port for radius (for ``radius`` auth_type).
+    :param jwt_role: Role for Authentication (for ``jwt`` auth_type).
+    :param jwt_token: JWT token for Authentication (for ``jwt`` auth_type).
+    :param jwt_token_path: Path to file containing JWT token for 
Authentication (for ``jwt`` auth_type).
     """
 
     def __init__(
@@ -112,7 +116,7 @@ class _VaultClient(LoggingMixin):
         role_id: str | None = None,
         region: str | None = None,
         kubernetes_role: str | None = None,
-        kubernetes_jwt_path: str | None = 
"/var/run/secrets/kubernetes.io/serviceaccount/token",
+        kubernetes_jwt_path: str | None = DEFAULT_KUBERNETES_JWT_PATH,
         gcp_key_path: str | None = None,
         gcp_keyfile_dict: dict | None = None,
         gcp_scopes: str | None = None,
@@ -121,6 +125,10 @@ class _VaultClient(LoggingMixin):
         radius_host: str | None = None,
         radius_secret: str | None = None,
         radius_port: int | None = None,
+        *,
+        jwt_role: str | None = None,
+        jwt_token: str | None = None,
+        jwt_token_path: str | None = None,
         **kwargs,
     ):
         super().__init__()
@@ -143,6 +151,11 @@ class _VaultClient(LoggingMixin):
                 raise VaultError("The 'kubernetes' authentication type 
requires 'kubernetes_role'")
             if not kubernetes_jwt_path:
                 raise VaultError("The 'kubernetes' authentication type 
requires 'kubernetes_jwt_path'")
+        if auth_type == "jwt":
+            if not jwt_role:
+                raise VaultError("The 'jwt' authentication type requires 
'jwt_role'")
+            if not jwt_token and not jwt_token_path:
+                raise VaultError("The 'jwt' authentication type requires 
'jwt_token' or 'jwt_token_path'")
         if auth_type == "azure":
             if not azure_resource:
                 raise VaultError("The 'azure' authentication type requires 
'azure_resource'")
@@ -188,6 +201,9 @@ class _VaultClient(LoggingMixin):
         self.radius_host = radius_host
         self.radius_secret = radius_secret
         self.radius_port = radius_port
+        self.jwt_role = jwt_role
+        self.jwt_token = jwt_token
+        self.jwt_token_path = jwt_token_path
 
     @property
     def client(self):
@@ -241,6 +257,8 @@ class _VaultClient(LoggingMixin):
             self._auth_gcp(_client)
         elif self.auth_type == "github":
             self._auth_github(_client)
+        elif self.auth_type == "jwt":
+            self._auth_jwt(_client)
         elif self.auth_type == "kubernetes":
             self._auth_kubernetes(_client)
         elif self.auth_type == "ldap":
@@ -299,6 +317,21 @@ class _VaultClient(LoggingMixin):
             else:
                 Kubernetes(_client.adapter).login(role=self.kubernetes_role, 
jwt=jwt)
 
+    def _auth_jwt(self, _client: hvac.Client) -> None:
+        """Authenticate using JWT auth method."""
+        if self.jwt_token:
+            jwt = self.jwt_token.strip()
+        elif self.jwt_token_path:
+            with open(self.jwt_token_path) as f:
+                jwt = f.read().strip()
+        else:
+            raise VaultError("The jwt_token or jwt_token_path should be set 
here. This should not happen.")
+
+        if self.auth_mount_point:
+            _client.auth.jwt.jwt_login(role=self.jwt_role, jwt=jwt, 
path=self.auth_mount_point)
+        else:
+            _client.auth.jwt.jwt_login(role=self.jwt_role, jwt=jwt)
+
     def _auth_github(self, _client: hvac.Client) -> None:
         if self.auth_mount_point:
             _client.auth.github.login(token=self.token, 
mount_point=self.auth_mount_point)
diff --git a/providers/hashicorp/src/airflow/providers/hashicorp/hooks/vault.py 
b/providers/hashicorp/src/airflow/providers/hashicorp/hooks/vault.py
index c84f9a70b02..05d14b9b4db 100644
--- a/providers/hashicorp/src/airflow/providers/hashicorp/hooks/vault.py
+++ b/providers/hashicorp/src/airflow/providers/hashicorp/hooks/vault.py
@@ -79,7 +79,7 @@ class VaultHook(BaseHook):
 
     :param vault_conn_id: The id of the connection to use
     :param auth_type: Authentication Type for the Vault. Default is ``token``. 
Available values are:
-        ('approle', 'github', 'gcp', 'kubernetes', 'ldap', 'token', 'userpass')
+        ('approle', 'github', 'gcp', 'jwt', 'kubernetes', 'ldap', 'token', 
'userpass')
     :param auth_mount_point: It can be used to define mount_point for 
authentication chosen
           Default depends on the authentication method used.
     :param kv_engine_version: Select the version of the engine to run (``1`` 
or ``2``). Defaults to
@@ -99,7 +99,9 @@ class VaultHook(BaseHook):
            (for ``azure`` auth_type)
     :param radius_host: Host for radius (for ``radius`` auth_type)
     :param radius_port: Port for radius (for ``radius`` auth_type)
-
+    :param jwt_role: Role for Authentication (for ``jwt`` auth_type)
+    :param jwt_token: JWT token for Authentication (for ``jwt`` auth_type)
+    :param jwt_token_path: Path to file containing JWT token for 
Authentication (for ``jwt`` auth_type).
     """
 
     conn_name_attr = "vault_conn_id"
@@ -124,6 +126,9 @@ class VaultHook(BaseHook):
         azure_resource: str | None = None,
         radius_host: str | None = None,
         radius_port: int | None = None,
+        jwt_role: str | None = None,
+        jwt_token: str | None = None,
+        jwt_token_path: str | None = None,
         **kwargs,
     ):
         super().__init__()
@@ -171,6 +176,11 @@ class VaultHook(BaseHook):
             if auth_type == "kubernetes"
             else (None, None)
         )
+        jwt_role, jwt_token, jwt_token_path = (
+            self._get_jwt_parameters_from_connection(jwt_role, jwt_token, 
jwt_token_path)
+            if auth_type == "jwt"
+            else (None, None, None)
+        )
         radius_host, radius_port = (
             self._get_radius_parameters_from_connection(radius_host, 
radius_port)
             if auth_type == "radius"
@@ -225,6 +235,9 @@ class VaultHook(BaseHook):
             radius_host=radius_host,
             radius_secret=self.connection.password,
             radius_port=radius_port,
+            jwt_role=jwt_role,
+            jwt_token=jwt_token,
+            jwt_token_path=jwt_token_path,
         )
 
         self.vault_client = _VaultClient(**client_kwargs)
@@ -240,6 +253,17 @@ class VaultHook(BaseHook):
             kubernetes_role = 
self.connection.extra_dejson.get("kubernetes_role")
         return kubernetes_jwt_path, kubernetes_role
 
+    def _get_jwt_parameters_from_connection(
+        self, jwt_role: str | None, jwt_token: str | None, jwt_token_path: str 
| None
+    ) -> tuple[str | None, str | None, str | None]:
+        if not jwt_role:
+            jwt_role = self.connection.extra_dejson.get("jwt_role")
+        if not jwt_token:
+            jwt_token = self.connection.extra_dejson.get("jwt_token")
+        if not jwt_token_path:
+            jwt_token_path = self.connection.extra_dejson.get("jwt_token_path")
+        return jwt_role, jwt_token, jwt_token_path
+
     def _get_gcp_parameters_from_connection(
         self,
         gcp_key_path: str | None,
@@ -376,6 +400,9 @@ class VaultHook(BaseHook):
             "kubernetes_jwt_path": StringField(
                 lazy_gettext("Kubernetes jwt path"), 
widget=BS3TextFieldWidget()
             ),
+            "jwt_role": StringField(lazy_gettext("JWT role"), 
widget=BS3TextFieldWidget()),
+            "jwt_token": StringField(lazy_gettext("JWT token"), 
widget=BS3TextFieldWidget()),
+            "jwt_token_path": StringField(lazy_gettext("JWT token path"), 
widget=BS3TextFieldWidget()),
             "token_path": StringField(lazy_gettext("Token path"), 
widget=BS3TextFieldWidget()),
             "gcp_key_path": StringField(lazy_gettext("GCP key path"), 
widget=BS3TextFieldWidget()),
             "gcp_scopes": StringField(lazy_gettext("GCP scopes"), 
widget=BS3TextFieldWidget()),
diff --git 
a/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py 
b/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py
index 40812a44830..b60e6238510 100644
--- a/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py
+++ b/providers/hashicorp/src/airflow/providers/hashicorp/secrets/vault.py
@@ -54,7 +54,7 @@ class VaultBackend(BaseSecretsBackend, LoggingMixin):
         (default: 'config'). If set to None (null), requests for 
configurations will not be sent to Vault.
     :param url: Base URL for the Vault instance being addressed.
     :param auth_type: Authentication Type for Vault. Default is ``token``. 
Available values are:
-        ('approle', 'aws_iam', 'azure', 'github', 'gcp', 'kubernetes', 'ldap', 
'radius', 'token', 'userpass')
+        ('approle', 'aws_iam', 'azure', 'github', 'gcp', 'jwt', 'kubernetes', 
'ldap', 'radius', 'token', 'userpass')
     :param auth_mount_point: It can be used to define mount_point for 
authentication chosen
           Default depends on the authentication method used.
     :param mount_point: The "path" the secret engine was mounted on. Default 
is "secret". Note that
@@ -89,6 +89,9 @@ class VaultBackend(BaseSecretsBackend, LoggingMixin):
     :param radius_host: Host for radius (for ``radius`` auth_type).
     :param radius_secret: Secret for radius (for ``radius`` auth_type).
     :param radius_port: Port for radius (for ``radius`` auth_type).
+    :param jwt_role: Role for Authentication (for ``jwt`` auth_type).
+    :param jwt_token: JWT token for Authentication (for ``jwt`` auth_type).
+    :param jwt_token_path: Path to file containing JWT token for 
Authentication (for ``jwt`` auth_type).
     """
 
     def __init__(
@@ -120,6 +123,9 @@ class VaultBackend(BaseSecretsBackend, LoggingMixin):
         radius_host: str | None = None,
         radius_secret: str | None = None,
         radius_port: int | None = None,
+        jwt_role: str | None = None,
+        jwt_token: str | None = None,
+        jwt_token_path: str | None = None,
         **kwargs,
     ):
         super().__init__()
@@ -153,6 +159,9 @@ class VaultBackend(BaseSecretsBackend, LoggingMixin):
             radius_host=radius_host,
             radius_secret=radius_secret,
             radius_port=radius_port,
+            jwt_role=jwt_role,
+            jwt_token=jwt_token,
+            jwt_token_path=jwt_token_path,
             **kwargs,
         )
 
diff --git 
a/providers/hashicorp/tests/unit/hashicorp/_internal_client/test_vault_client.py
 
b/providers/hashicorp/tests/unit/hashicorp/_internal_client/test_vault_client.py
index c9239b75a99..fe27763e60c 100644
--- 
a/providers/hashicorp/tests/unit/hashicorp/_internal_client/test_vault_client.py
+++ 
b/providers/hashicorp/tests/unit/hashicorp/_internal_client/test_vault_client.py
@@ -19,7 +19,7 @@ from __future__ import annotations
 import json
 import time
 from unittest import mock
-from unittest.mock import mock_open, patch
+from unittest.mock import call, mock_open, patch
 
 import pytest
 from hvac.exceptions import InvalidPath, VaultError
@@ -587,6 +587,103 @@ class TestVaultClient:
                 url="http://localhost:8180";,
             )
 
+    
@mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
+    def test_jwt_with_token(self, mock_hvac):
+        mock_client = mock.MagicMock()
+        mock_hvac.Client.return_value = mock_client
+        vault_client = _VaultClient(
+            auth_type="jwt",
+            jwt_role="my-role",
+            jwt_token="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.test",
+            url="http://localhost:8180";,
+            session=None,
+        )
+        client = vault_client.client
+        assert mock_hvac.Client.call_args_list == 
[call(url="http://localhost:8180";, session=None)]
+        assert client.auth.jwt.jwt_login.call_args_list == [
+            call(role="my-role", 
jwt="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.test")
+        ]
+        assert client.is_authenticated.call_args_list == [call(), call()]
+        assert vault_client.kv_engine_version == 2
+
+    
@mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
+    def test_jwt_with_token_path(self, mock_hvac):
+        mock_client = mock.MagicMock()
+        mock_hvac.Client.return_value = mock_client
+        vault_client = _VaultClient(
+            auth_type="jwt",
+            jwt_role="my-role",
+            jwt_token_path="path/to/jwt",
+            url="http://localhost:8180";,
+            session=None,
+        )
+        with patch("builtins.open", 
mock_open(read_data="eyJhbGciOiJSUzI1NiJ9.jwt-from-file")) as mock_file:
+            client = vault_client.client
+        assert mock_file.call_args_list == [call("path/to/jwt")]
+        assert mock_hvac.Client.call_args_list == 
[call(url="http://localhost:8180";, session=None)]
+        assert client.auth.jwt.jwt_login.call_args_list == [
+            call(role="my-role", jwt="eyJhbGciOiJSUzI1NiJ9.jwt-from-file")
+        ]
+        assert client.is_authenticated.call_args_list == [call(), call()]
+        assert vault_client.kv_engine_version == 2
+
+    
@mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
+    def test_jwt_with_token_strips_whitespace(self, mock_hvac):
+        mock_client = mock.MagicMock()
+        mock_hvac.Client.return_value = mock_client
+        vault_client = _VaultClient(
+            auth_type="jwt",
+            jwt_role="my-role",
+            jwt_token="  eyJhbGciOiJSUzI1NiJ9.test  \n",
+            url="http://localhost:8180";,
+            session=None,
+        )
+        client = vault_client.client
+        client.auth.jwt.jwt_login.assert_called_with(role="my-role", 
jwt="eyJhbGciOiJSUzI1NiJ9.test")
+
+    
@mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
+    def test_jwt_different_auth_mount_point(self, mock_hvac):
+        mock_client = mock.MagicMock()
+        mock_hvac.Client.return_value = mock_client
+        vault_client = _VaultClient(
+            auth_type="jwt",
+            jwt_role="my-role",
+            jwt_token="eyJhbGciOiJSUzI1NiJ9.test",
+            auth_mount_point="custom-jwt",
+            url="http://localhost:8180";,
+            session=None,
+        )
+        client = vault_client.client
+        assert mock_hvac.Client.call_args_list == 
[call(url="http://localhost:8180";, session=None)]
+        assert client.auth.jwt.jwt_login.call_args_list == [
+            call(role="my-role", jwt="eyJhbGciOiJSUzI1NiJ9.test", 
path="custom-jwt")
+        ]
+        assert client.is_authenticated.call_args_list == [call(), call()]
+        assert vault_client.kv_engine_version == 2
+
+    
@mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
+    def test_jwt_missing_role(self, mock_hvac):
+        mock_client = mock.MagicMock()
+        mock_hvac.Client.return_value = mock_client
+        with pytest.raises(VaultError, match="requires 'jwt_role'"):
+            _VaultClient(
+                auth_type="jwt",
+                jwt_token="eyJhbGciOiJSUzI1NiJ9.test",
+                url="http://localhost:8180";,
+            )
+
+    
@mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
+    def test_jwt_missing_token_and_path(self, mock_hvac):
+        mock_client = mock.MagicMock()
+        mock_hvac.Client.return_value = mock_client
+        with pytest.raises(VaultError, match="requires 'jwt_token' or 
'jwt_token_path'"):
+            _VaultClient(
+                auth_type="jwt",
+                jwt_role="my-role",
+                jwt_token_path=None,
+                url="http://localhost:8180";,
+            )
+
     
@mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
     def test_ldap(self, mock_hvac):
         mock_client = mock.MagicMock()
diff --git a/providers/hashicorp/tests/unit/hashicorp/hooks/test_vault.py 
b/providers/hashicorp/tests/unit/hashicorp/hooks/test_vault.py
index a072f373703..50b9f9e022c 100644
--- a/providers/hashicorp/tests/unit/hashicorp/hooks/test_vault.py
+++ b/providers/hashicorp/tests/unit/hashicorp/hooks/test_vault.py
@@ -733,6 +733,85 @@ class TestVaultHook:
         test_client.is_authenticated.assert_called_with()
         assert test_hook.vault_client.kv_engine_version == 2
 
+    
@mock.patch("airflow.providers.hashicorp.hooks.vault.VaultHook.get_connection")
+    
@mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
+    def test_jwt_init_params(self, mock_hvac, mock_get_connection):
+        mock_client = mock.MagicMock()
+        mock_hvac.Client.return_value = mock_client
+        mock_connection = self.get_mock_connection()
+        mock_get_connection.return_value = mock_connection
+
+        connection_dict = {}
+
+        mock_connection.extra_dejson.get.side_effect = connection_dict.get
+        test_hook = VaultHook(
+            auth_type="jwt",
+            jwt_role="my-role",
+            jwt_token="eyJhbGciOiJSUzI1NiJ9.test",
+            vault_conn_id="vault_conn_id",
+            session=None,
+        )
+        mock_get_connection.assert_called_with("vault_conn_id")
+        test_client = test_hook.get_conn()
+        mock_hvac.Client.assert_called_with(url="http://localhost:8180";, 
session=None)
+        test_client.auth.jwt.jwt_login.assert_called_with(role="my-role", 
jwt="eyJhbGciOiJSUzI1NiJ9.test")
+        test_client.is_authenticated.assert_called_with()
+        assert test_hook.vault_client.kv_engine_version == 2
+
+    
@mock.patch("airflow.providers.hashicorp.hooks.vault.VaultHook.get_connection")
+    
@mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
+    def test_jwt_dejson(self, mock_hvac, mock_get_connection):
+        mock_client = mock.MagicMock()
+        mock_hvac.Client.return_value = mock_client
+        mock_connection = self.get_mock_connection()
+        mock_get_connection.return_value = mock_connection
+
+        connection_dict = {
+            "auth_type": "jwt",
+            "jwt_role": "my-role",
+            "jwt_token": "eyJhbGciOiJSUzI1NiJ9.dejson-test",
+        }
+
+        mock_connection.extra_dejson.get.side_effect = connection_dict.get
+
+        test_hook = VaultHook(vault_conn_id="vault_conn_id", session=None)
+        mock_get_connection.assert_called_with("vault_conn_id")
+        test_client = test_hook.get_conn()
+        mock_hvac.Client.assert_called_with(url="http://localhost:8180";, 
session=None)
+        test_client.auth.jwt.jwt_login.assert_called_with(
+            role="my-role", jwt="eyJhbGciOiJSUzI1NiJ9.dejson-test"
+        )
+        test_client.is_authenticated.assert_called_with()
+        assert test_hook.vault_client.kv_engine_version == 2
+
+    
@mock.patch("airflow.providers.hashicorp.hooks.vault.VaultHook.get_connection")
+    
@mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
+    def test_jwt_with_token_path(self, mock_hvac, mock_get_connection):
+        mock_client = mock.MagicMock()
+        mock_hvac.Client.return_value = mock_client
+        mock_connection = self.get_mock_connection()
+        mock_get_connection.return_value = mock_connection
+
+        connection_dict = {
+            "auth_type": "jwt",
+            "jwt_role": "my-role",
+            "jwt_token_path": "/path/to/jwt",
+        }
+
+        mock_connection.extra_dejson.get.side_effect = connection_dict.get
+
+        with patch("builtins.open", 
mock_open(read_data="eyJhbGciOiJSUzI1NiJ9.from-file")) as mock_file:
+            test_hook = VaultHook(vault_conn_id="vault_conn_id", session=None)
+            test_client = test_hook.get_conn()
+        mock_get_connection.assert_called_with("vault_conn_id")
+        mock_file.assert_called_with("/path/to/jwt")
+        mock_hvac.Client.assert_called_with(url="http://localhost:8180";, 
session=None)
+        test_client.auth.jwt.jwt_login.assert_called_with(
+            role="my-role", jwt="eyJhbGciOiJSUzI1NiJ9.from-file"
+        )
+        test_client.is_authenticated.assert_called_with()
+        assert test_hook.vault_client.kv_engine_version == 2
+
     
@mock.patch("airflow.providers.hashicorp.hooks.vault.VaultHook.get_connection")
     
@mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
     def test_client_kwargs(self, mock_hvac, mock_get_connection):
diff --git a/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py 
b/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py
index 667e7859450..274e07119c9 100644
--- a/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py
+++ b/providers/hashicorp/tests/unit/hashicorp/secrets/test_vault.py
@@ -350,6 +350,57 @@ class TestVaultSecrets:
         with pytest.raises(FileNotFoundError, match=path):
             VaultBackend(**kwargs).get_connection(conn_id="test")
 
+    def test_auth_type_jwt_with_unreadable_jwt_raises_error(self):
+        path = "/var/tmp/this_does_not_exist/jwt_token_file"
+        kwargs = {
+            "auth_type": "jwt",
+            "jwt_role": "default",
+            "jwt_token_path": path,
+            "url": "http://127.0.0.1:8200";,
+        }
+
+        with pytest.raises(FileNotFoundError, match=path):
+            VaultBackend(**kwargs).get_connection(conn_id="test")
+
+    
@mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
+    def test_jwt_auth_type(self, mock_hvac):
+        mock_client = mock.MagicMock()
+        mock_hvac.Client.return_value = mock_client
+        mock_client.secrets.kv.v2.read_secret_version.return_value = {
+            "request_id": "94011e25-f8dc-ec29-221b-1f9c1d9ad2ae",
+            "lease_id": "",
+            "renewable": False,
+            "lease_duration": 0,
+            "data": {
+                "data": {"conn_uri": 
"postgresql://airflow:airflow@host:5432/airflow"},
+                "metadata": {
+                    "created_time": "2020-03-16T21:01:43.331126Z",
+                    "deletion_time": "",
+                    "destroyed": False,
+                    "version": 1,
+                },
+            },
+            "wrap_info": None,
+            "warnings": None,
+            "auth": None,
+        }
+
+        kwargs = {
+            "connections_path": "connections",
+            "mount_point": "airflow",
+            "auth_type": "jwt",
+            "jwt_role": "airflow-role",
+            "jwt_token": "eyJhbGciOiJSUzI1NiJ9.test",
+            "url": "http://127.0.0.1:8200";,
+        }
+
+        test_client = VaultBackend(**kwargs)
+        connection = test_client.get_connection(conn_id="test_postgres")
+        assert connection.get_uri() == 
"postgres://airflow:airflow@host:5432/airflow"
+        mock_client.auth.jwt.jwt_login.assert_called_with(
+            role="airflow-role", jwt="eyJhbGciOiJSUzI1NiJ9.test"
+        )
+
     
@mock.patch("airflow.providers.hashicorp._internal_client.vault_client.hvac")
     def test_get_config_value(self, mock_hvac):
         mock_client = mock.MagicMock()

Reply via email to