From 89a68902694c0302251ae750e1f0b3484ef853c1 Mon Sep 17 00:00:00 2001 From: "John-Mason P. Shackelford" Date: Thu, 19 Feb 2026 16:40:29 -0500 Subject: [PATCH] Fix URL encoding in Jira OAuth authorization URLs (#12399) Co-authored-by: openhands Co-authored-by: Rohit Malhotra --- enterprise/server/routes/integration/jira.py | 10 +--- .../server/routes/integration/jira_dc.py | 6 +- .../routes/test_jira_dc_integration_routes.py | 57 +++++++++++++++++++ .../routes/test_jira_integration_routes.py | 55 ++++++++++++++++++ 4 files changed, 118 insertions(+), 10 deletions(-) diff --git a/enterprise/server/routes/integration/jira.py b/enterprise/server/routes/integration/jira.py index 9ce281c100..060baa7224 100644 --- a/enterprise/server/routes/integration/jira.py +++ b/enterprise/server/routes/integration/jira.py @@ -4,7 +4,7 @@ import json import os import re import uuid -from urllib.parse import urlparse +from urllib.parse import urlencode, urlparse import requests from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Request, status @@ -371,9 +371,7 @@ async def create_jira_workspace(request: Request, workspace_data: JiraWorkspaceC 'prompt': 'consent', } - auth_url = ( - f"{JIRA_AUTH_URL}?{'&'.join([f'{k}={v}' for k, v in auth_params.items()])}" - ) + auth_url = f'{JIRA_AUTH_URL}?{urlencode(auth_params)}' return JSONResponse( content={ @@ -432,9 +430,7 @@ async def create_workspace_link(request: Request, link_data: JiraLinkCreate): 'response_type': 'code', 'prompt': 'consent', } - auth_url = ( - f"{JIRA_AUTH_URL}?{'&'.join([f'{k}={v}' for k, v in auth_params.items()])}" - ) + auth_url = f'{JIRA_AUTH_URL}?{urlencode(auth_params)}' return JSONResponse( content={ diff --git a/enterprise/server/routes/integration/jira_dc.py b/enterprise/server/routes/integration/jira_dc.py index 4d9f344951..c842da5465 100644 --- a/enterprise/server/routes/integration/jira_dc.py +++ b/enterprise/server/routes/integration/jira_dc.py @@ -2,7 +2,7 @@ import json import os import re import uuid -from urllib.parse import urlparse +from urllib.parse import urlencode, urlparse import requests from fastapi import ( @@ -316,7 +316,7 @@ async def create_jira_dc_workspace( 'response_type': 'code', } - auth_url = f"{JIRA_DC_AUTH_URL}?{'&'.join([f'{k}={v}' for k, v in auth_params.items()])}" + auth_url = f'{JIRA_DC_AUTH_URL}?{urlencode(auth_params)}' return JSONResponse( content={ @@ -436,7 +436,7 @@ async def create_workspace_link(request: Request, link_data: JiraDcLinkCreate): 'state': state, 'response_type': 'code', } - auth_url = f"{JIRA_DC_AUTH_URL}?{'&'.join([f'{k}={v}' for k, v in auth_params.items()])}" + auth_url = f'{JIRA_DC_AUTH_URL}?{urlencode(auth_params)}' return JSONResponse( content={ diff --git a/enterprise/tests/unit/server/routes/test_jira_dc_integration_routes.py b/enterprise/tests/unit/server/routes/test_jira_dc_integration_routes.py index 7e8040f957..ff5388a35c 100644 --- a/enterprise/tests/unit/server/routes/test_jira_dc_integration_routes.py +++ b/enterprise/tests/unit/server/routes/test_jira_dc_integration_routes.py @@ -1220,3 +1220,60 @@ async def test_validate_workspace_update_permissions_no_current_link(mock_manage result = await _validate_workspace_update_permissions('user1', 'test-workspace') assert result == mock_workspace + + +# Tests for OAuth URL encoding +class TestJiraDcOAuthUrlEncoding: + """Tests to verify OAuth authorization URLs are properly URL-encoded.""" + + @pytest.mark.asyncio + @patch('server.routes.integration.jira_dc.get_user_auth') + @patch('server.routes.integration.jira_dc.redis_client') + @patch('server.routes.integration.jira_dc.JIRA_DC_ENABLE_OAUTH', True) + async def test_create_jira_dc_workspace_url_encoding( + self, mock_redis, mock_get_auth, mock_request, mock_user_auth + ): + """Test that create_jira_dc_workspace properly URL-encodes the authorization URL.""" + mock_get_auth.return_value = mock_user_auth + mock_redis.setex.return_value = True + workspace_data = JiraDcWorkspaceCreate( + workspace_name='test-workspace', + webhook_secret='secret', + svc_acc_email='svc@test.com', + svc_acc_api_key='key', + is_active=True, + ) + + response = await create_jira_dc_workspace(mock_request, workspace_data) + content = json.loads(response.body) + + auth_url = content['authorizationUrl'] + # Verify no raw spaces in the URL (spaces should be encoded as + or %20) + assert ' ' not in auth_url + # Verify scope parameter contains encoded scopes (+ is valid URL encoding for space) + assert 'scope=read%3Ame+read%3Ajira-user+read%3Ajira-work' in auth_url + # Verify redirect_uri is properly encoded + assert 'redirect_uri=https%3A%2F%2F' in auth_url + + @pytest.mark.asyncio + @patch('server.routes.integration.jira_dc.get_user_auth') + @patch('server.routes.integration.jira_dc.redis_client') + @patch('server.routes.integration.jira_dc.JIRA_DC_ENABLE_OAUTH', True) + async def test_create_workspace_link_url_encoding( + self, mock_redis, mock_get_auth, mock_request, mock_user_auth + ): + """Test that create_workspace_link properly URL-encodes the authorization URL.""" + mock_get_auth.return_value = mock_user_auth + mock_redis.setex.return_value = True + link_data = JiraDcLinkCreate(workspace_name='test-workspace') + + response = await create_workspace_link(mock_request, link_data) + content = json.loads(response.body) + + auth_url = content['authorizationUrl'] + # Verify no raw spaces in the URL (spaces should be encoded as + or %20) + assert ' ' not in auth_url + # Verify scope parameter contains encoded scopes (+ is valid URL encoding for space) + assert 'scope=read%3Ame+read%3Ajira-user+read%3Ajira-work' in auth_url + # Verify redirect_uri is properly encoded + assert 'redirect_uri=https%3A%2F%2F' in auth_url diff --git a/enterprise/tests/unit/server/routes/test_jira_integration_routes.py b/enterprise/tests/unit/server/routes/test_jira_integration_routes.py index 211957cc36..4a95ef27c1 100644 --- a/enterprise/tests/unit/server/routes/test_jira_integration_routes.py +++ b/enterprise/tests/unit/server/routes/test_jira_integration_routes.py @@ -1323,3 +1323,58 @@ async def test_validate_workspace_update_permissions_no_current_link(mock_manage result = await _validate_workspace_update_permissions('user1', 'test-workspace') assert result == mock_workspace + + +# Tests for OAuth URL encoding +class TestJiraOAuthUrlEncoding: + """Tests to verify OAuth authorization URLs are properly URL-encoded.""" + + @pytest.mark.asyncio + @patch('server.routes.integration.jira.get_user_auth') + @patch('server.routes.integration.jira.redis_client') + async def test_create_jira_workspace_url_encoding( + self, mock_redis, mock_get_auth, mock_request, mock_user_auth + ): + """Test that create_jira_workspace properly URL-encodes the authorization URL.""" + mock_get_auth.return_value = mock_user_auth + mock_redis.setex.return_value = True + workspace_data = JiraWorkspaceCreate( + workspace_name='test-workspace', + webhook_secret='secret', + svc_acc_email='svc@test.com', + svc_acc_api_key='key', + is_active=True, + ) + + response = await create_jira_workspace(mock_request, workspace_data) + content = json.loads(response.body) + + auth_url = content['authorizationUrl'] + # Verify no raw spaces in the URL (spaces should be encoded as + or %20) + assert ' ' not in auth_url + # Verify scope parameter contains encoded scopes (+ is valid URL encoding for space) + assert 'scope=read%3Ame+read%3Ajira-user+read%3Ajira-work' in auth_url + # Verify redirect_uri is properly encoded + assert 'redirect_uri=https%3A%2F%2F' in auth_url + + @pytest.mark.asyncio + @patch('server.routes.integration.jira.get_user_auth') + @patch('server.routes.integration.jira.redis_client') + async def test_create_workspace_link_url_encoding( + self, mock_redis, mock_get_auth, mock_request, mock_user_auth + ): + """Test that create_workspace_link properly URL-encodes the authorization URL.""" + mock_get_auth.return_value = mock_user_auth + mock_redis.setex.return_value = True + link_data = JiraLinkCreate(workspace_name='test-workspace') + + response = await create_workspace_link(mock_request, link_data) + content = json.loads(response.body) + + auth_url = content['authorizationUrl'] + # Verify no raw spaces in the URL (spaces should be encoded as + or %20) + assert ' ' not in auth_url + # Verify scope parameter contains encoded scopes (+ is valid URL encoding for space) + assert 'scope=read%3Ame+read%3Ajira-user+read%3Ajira-work' in auth_url + # Verify redirect_uri is properly encoded + assert 'redirect_uri=https%3A%2F%2F' in auth_url