|
37 | 37 | cast,
|
38 | 38 | no_type_check,
|
39 | 39 | )
|
40 |
| -from urllib.parse import urlparse |
| 40 | +from urllib.parse import parse_qs, quote, urlparse |
41 | 41 |
|
42 | 42 | import requests
|
43 | 43 | from pkg_resources import parse_version
|
@@ -177,19 +177,32 @@ def __init__(self, context_path):
|
177 | 177 | self.context_path = context_path
|
178 | 178 |
|
179 | 179 | 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): |
180 | 184 | parse_result = urlparse(req.url)
|
181 | 185 |
|
182 | 186 | path = (
|
183 | 187 | parse_result.path[len(self.context_path) :]
|
184 | 188 | if len(self.context_path) > 1
|
185 | 189 | else parse_result.path
|
186 | 190 | )
|
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 | + |
190 | 200 | qsh = f"{req.method.upper()}&{path}&{query}"
|
| 201 | + return qsh |
191 | 202 |
|
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] |
193 | 206 |
|
194 | 207 |
|
195 | 208 | class JiraCookieAuth(AuthBase):
|
|
0 commit comments