Skip to content

Commit 17624b0

Browse files
committed
feat: add auth header to push notifications
1 parent 12fd75c commit 17624b0

2 files changed

Lines changed: 67 additions & 4 deletions

File tree

src/a2a/server/tasks/base_push_notification_sender.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,7 @@ async def _dispatch_notification(
5252
) -> bool:
5353
url = push_info.url
5454
try:
55-
headers = None
56-
if push_info.token:
57-
headers = {'X-A2A-Notification-Token': push_info.token}
55+
headers = self._build_headers(push_info)
5856
response = await self._client.post(
5957
url,
6058
json=task.model_dump(mode='json', exclude_none=True),
@@ -72,3 +70,30 @@ async def _dispatch_notification(
7270
)
7371
return False
7472
return True
73+
74+
@staticmethod
75+
def _authorization_header(
76+
push_info: PushNotificationConfig,
77+
) -> str | None:
78+
auth = push_info.authentication
79+
if not auth or not auth.credentials:
80+
return None
81+
schemes = [scheme for scheme in auth.schemes if scheme]
82+
if not schemes:
83+
return None
84+
scheme = next(
85+
(scheme for scheme in schemes if scheme.lower() == 'bearer'),
86+
schemes[0],
87+
)
88+
return f'{scheme} {auth.credentials}'
89+
90+
def _build_headers(
91+
self, push_info: PushNotificationConfig
92+
) -> dict[str, str] | None:
93+
headers: dict[str, str] = {}
94+
if push_info.token:
95+
headers['X-A2A-Notification-Token'] = push_info.token
96+
authorization = self._authorization_header(push_info)
97+
if authorization:
98+
headers['Authorization'] = authorization
99+
return headers or None

tests/server/tasks/test_push_notification_sender.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
BasePushNotificationSender,
99
)
1010
from a2a.types import (
11+
PushNotificationAuthenticationInfo,
1112
PushNotificationConfig,
1213
Task,
1314
TaskState,
@@ -29,8 +30,14 @@ def create_sample_push_config(
2930
url: str = 'http://example.com/callback',
3031
config_id: str = 'cfg1',
3132
token: str | None = None,
33+
authentication: PushNotificationAuthenticationInfo | None = None,
3234
) -> PushNotificationConfig:
33-
return PushNotificationConfig(id=config_id, url=url, token=token)
35+
return PushNotificationConfig(
36+
id=config_id,
37+
url=url,
38+
token=token,
39+
authentication=authentication,
40+
)
3441

3542

3643
class TestBasePushNotificationSender(unittest.IsolatedAsyncioTestCase):
@@ -92,6 +99,37 @@ async def test_send_notification_with_token_success(self) -> None:
9299
)
93100
mock_response.raise_for_status.assert_called_once()
94101

102+
async def test_send_notification_with_auth_header(self) -> None:
103+
task_id = 'task_send_auth'
104+
task_data = create_sample_task(task_id=task_id)
105+
auth = PushNotificationAuthenticationInfo(
106+
schemes=['Basic', 'Bearer'], credentials='token_or_jwt'
107+
)
108+
config = create_sample_push_config(
109+
url='http://notify.me/here',
110+
token='unique_token',
111+
authentication=auth,
112+
)
113+
self.mock_config_store.get_info.return_value = [config]
114+
115+
mock_response = AsyncMock(spec=httpx.Response)
116+
mock_response.status_code = 200
117+
self.mock_httpx_client.post.return_value = mock_response
118+
119+
await self.sender.send_notification(task_data)
120+
121+
self.mock_config_store.get_info.assert_awaited_once_with
122+
123+
self.mock_httpx_client.post.assert_awaited_once_with(
124+
config.url,
125+
json=task_data.model_dump(mode='json', exclude_none=True),
126+
headers={
127+
'X-A2A-Notification-Token': 'unique_token',
128+
'Authorization': 'Bearer token_or_jwt',
129+
},
130+
)
131+
mock_response.raise_for_status.assert_called_once()
132+
95133
async def test_send_notification_no_config(self) -> None:
96134
task_id = 'task_send_no_config'
97135
task_data = create_sample_task(task_id=task_id)

0 commit comments

Comments
 (0)