Skip to content

Commit 5129b25

Browse files
robinhughessvermeulen
authored andcommitted
Fix JWT QSH generation for urls with repeated parameters (pycontribs#1157)
* Fix QshGenerator to combine repeated query string parameters into csv * add test for QshGenerator.generate_qsh
1 parent b6029bd commit 5129b25

File tree

2 files changed

+65
-5
lines changed

2 files changed

+65
-5
lines changed

jira/client.py

+18-5
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
cast,
3838
no_type_check,
3939
)
40-
from urllib.parse import urlparse
40+
from urllib.parse import parse_qs, quote, urlparse
4141

4242
import requests
4343
from pkg_resources import parse_version
@@ -177,19 +177,32 @@ def __init__(self, context_path):
177177
self.context_path = context_path
178178

179179
def __call__(self, req):
180+
qsh = self._generate_qsh(req)
181+
return hashlib.sha256(qsh.encode("utf-8")).hexdigest()
182+
183+
def _generate_qsh(self, req):
180184
parse_result = urlparse(req.url)
181185

182186
path = (
183187
parse_result.path[len(self.context_path) :]
184188
if len(self.context_path) > 1
185189
else parse_result.path
186190
)
187-
# Per Atlassian docs, use %20 for whitespace when generating qsh for URL
188-
# https://developer.atlassian.com/cloud/jira/platform/understanding-jwt/#qsh
189-
query = "&".join(sorted(parse_result.query.split("&"))).replace("+", "%20")
191+
192+
# create canonical query string according to docs at:
193+
# https://developer.atlassian.com/cloud/jira/platform/understanding-jwt-for-connect-apps/#qsh
194+
params = parse_qs(parse_result.query, keep_blank_values=True)
195+
joined = {
196+
key: ",".join(self._sort_and_quote_values(params[key])) for key in params
197+
}
198+
query = "&".join(f"{key}={joined[key]}" for key in sorted(joined.keys()))
199+
190200
qsh = f"{req.method.upper()}&{path}&{query}"
201+
return qsh
191202

192-
return hashlib.sha256(qsh.encode("utf-8")).hexdigest()
203+
def _sort_and_quote_values(self, values):
204+
ordered_values = sorted(values)
205+
return [quote(value, safe="~") for value in ordered_values]
193206

194207

195208
class JiraCookieAuth(AuthBase):

tests/test_qsh.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import pytest
2+
3+
from jira.client import QshGenerator
4+
5+
6+
class MockRequest:
7+
def __init__(self, method, url):
8+
self.method = method
9+
self.url = url
10+
11+
12+
@pytest.mark.parametrize(
13+
"method,url,expected",
14+
[
15+
("GET", "http://example.com", "GET&&"),
16+
# empty parameter
17+
("GET", "http://example.com?key=&key2=A", "GET&&key=&key2=A"),
18+
# whitespace
19+
("GET", "http://example.com?key=A+B", "GET&&key=A%20B"),
20+
# tilde
21+
("GET", "http://example.com?key=A~B", "GET&&key=A~B"),
22+
# repeated parameters
23+
(
24+
"GET",
25+
"http://example.com?key2=Z&key1=X&key3=Y&key1=A",
26+
"GET&&key1=A,X&key2=Z&key3=Y",
27+
),
28+
# repeated parameters with whitespace
29+
(
30+
"GET",
31+
"http://example.com?key2=Z+A&key1=X+B&key3=Y&key1=A+B",
32+
"GET&&key1=A%20B,X%20B&key2=Z%20A&key3=Y",
33+
),
34+
],
35+
ids=[
36+
"no parameters",
37+
"empty parameter",
38+
"whitespace",
39+
"tilde",
40+
"repeated parameters",
41+
"repeated parameters with whitespace",
42+
],
43+
)
44+
def test_qsh(method, url, expected):
45+
gen = QshGenerator("http://example.com")
46+
req = MockRequest(method, url)
47+
assert gen._generate_qsh(req) == expected

0 commit comments

Comments
 (0)