Skip to content
This repository was archived by the owner on Nov 26, 2023. It is now read-only.

Commit eba027b

Browse files
committed
Merge branch 'hotfix/v1.2.4'
2 parents eeeb261 + 1f69bd3 commit eba027b

File tree

4 files changed

+356
-36
lines changed

4 files changed

+356
-36
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## [1.2.4] - 2018-09-05
9+
10+
### Fixed
11+
- Completely refactored QSH generation logic (fixes #10)
12+
813
## [1.2.3] - 2018-08-15
914

1015
### Fixed
@@ -56,7 +61,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
5661
### Fixed
5762
- Package keywords at composer.json
5863

59-
[Unreleased]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.3...HEAD
64+
[Unreleased]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.4...HEAD
65+
[1.2.4]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.3...v1.2.4
6066
[1.2.3]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.2...v1.2.3
6167
[1.2.2]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.1...v1.2.2
6268
[1.2.1]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.0...v1.2.1

src/Helpers/JWTHelper.php

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace AtlassianConnectCore\Helpers;
44

5+
use AtlassianConnectCore\Http\Auth\QSH;
6+
57
/**
68
* Class JWTHelper
79
*
@@ -64,42 +66,9 @@ public static function create(string $url, string $method, string $issuer, strin
6466
*
6567
* @return string
6668
*/
67-
public static function qsh($url, $method)
69+
public static function qsh($url, $method): string
6870
{
69-
$method = strtoupper($method);
70-
71-
$parts = parse_url($url);
72-
$path = $parts['path'];
73-
74-
// The list of prefixes which must be removed from the path
75-
$prefixes = ['/wiki'];
76-
77-
foreach ($prefixes as $prefix) {
78-
$path = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $path);
79-
}
80-
81-
// Parse a query into the map of parameters
82-
parse_str($parts['query'], $params);
83-
84-
// Parameters should be sorted alphabetically
85-
ksort($params);
86-
87-
$canonicalQuery = http_build_query(
88-
$params,
89-
null,
90-
'&',
91-
PHP_QUERY_RFC3986
92-
);
93-
94-
$parts = [
95-
strtoupper($method),
96-
$path,
97-
$canonicalQuery
98-
];
99-
100-
$qsh = hash('sha256', implode('&', $parts));
101-
102-
return $qsh;
71+
return new QSH($url, $method);
10372
}
10473

10574
/**

src/Http/Auth/QSH.php

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
<?php
2+
3+
namespace AtlassianConnectCore\Http\Auth;
4+
5+
/**
6+
* Class QSH creates a Query String Hash
7+
*
8+
* Documentation:
9+
* https://docs.atlassian.com/DAC/bitbucket/concepts/qsh.html
10+
*
11+
* @package App\Http\Auth
12+
*
13+
* @author Artem Brezhnev <brezzhnev@gmail.com>
14+
*/
15+
class QSH
16+
{
17+
/**
18+
* The request URL.
19+
*
20+
* @var string
21+
*/
22+
protected $url;
23+
24+
/**
25+
* The request HTTP method.
26+
*
27+
* @var string
28+
*/
29+
protected $method;
30+
31+
/**
32+
* The URL parts (host, port, path...)
33+
*
34+
* @var array
35+
*/
36+
protected $parts = [];
37+
38+
/**
39+
* The list of prefixes which should be removed.
40+
*
41+
* @var array
42+
*/
43+
protected $prefixes = [
44+
'/wiki'
45+
];
46+
47+
/**
48+
* QSH constructor.
49+
*
50+
* @param string $url
51+
* @param string $method
52+
*/
53+
public function __construct(string $url, string $method)
54+
{
55+
$this->url = $url;
56+
$this->parts = parse_url($url);
57+
58+
$this->method = strtoupper($method);
59+
}
60+
61+
/**
62+
* Create a QSH string.
63+
*
64+
* More details:
65+
* https://docs.atlassian.com/DAC/bitbucket/concepts/qsh.html
66+
*
67+
* @return string
68+
*/
69+
public function create(): string
70+
{
71+
$parts = [
72+
$this->method,
73+
$this->canonicalUri(),
74+
$this->canonicalQuery()
75+
];
76+
77+
return hash('sha256', implode('&', $parts));
78+
}
79+
80+
/**
81+
* Make a canonical URI.
82+
*
83+
* @return string|null
84+
*/
85+
public function canonicalUri()
86+
{
87+
if(!$path = array_get($this->parts, 'path')) {
88+
return '/';
89+
}
90+
91+
// Remove a prefix of instance from the path
92+
// Eg. remove `/wiki` part which means Confluence instance.
93+
$uri = $this->removePrefix($path);
94+
95+
// The canonical URI should not contain & characters.
96+
// Therefore, any & characters should be URL-encoded to %26.
97+
$uri = str_replace('&', '%26', $uri);
98+
99+
// The canonical URI only ends with a / character if it is the only character.
100+
$uri = $uri === '/'
101+
? $uri
102+
: rtrim($uri, '/');
103+
104+
return $uri;
105+
}
106+
107+
/**
108+
* Make a canonical query string.
109+
*
110+
* @return string|null
111+
*/
112+
public function canonicalQuery()
113+
{
114+
if(!$query = array_get($this->parts, 'query')) {
115+
return null;
116+
}
117+
118+
$params = $this->parseQuery($query);
119+
120+
// We should ignore the "JWT" parameter.
121+
$params = array_filter($params, function(string $key) {
122+
return strtolower($key) !== 'jwt';
123+
}, ARRAY_FILTER_USE_KEY);
124+
125+
ksort($params);
126+
127+
$query = $this->buildQuery($params);
128+
129+
// Encode underscores.
130+
$query = str_replace('_', '%20', $query);
131+
132+
return $query;
133+
}
134+
135+
/**
136+
* Remove a prefix from the URL path.
137+
*
138+
* @param string $path
139+
*
140+
* @return string
141+
*/
142+
protected function removePrefix(string $path): string
143+
{
144+
foreach ($this->prefixes as $prefix) {
145+
$pattern = '/^' . preg_quote($prefix, '/') . '/';
146+
147+
if(preg_match($pattern, $path)) {
148+
$path = preg_replace($pattern, '', $path);
149+
150+
break;
151+
}
152+
}
153+
154+
return $path;
155+
}
156+
157+
/**
158+
* Parse a query to array of parameters.
159+
*
160+
* @param string $query
161+
*
162+
* @return array
163+
*/
164+
protected function parseQuery(string $query): array
165+
{
166+
$output = [];
167+
168+
$query = ltrim($query, '?');
169+
170+
$parameters = explode('&', $query);
171+
172+
foreach ($parameters as $parameter) {
173+
list($key, $value) = array_pad(explode('=', $parameter), 2, null);
174+
175+
$output = array_merge_recursive($output, [$key => $value]);
176+
}
177+
178+
return $output;
179+
}
180+
181+
/**
182+
* Build a query accordingly to RFC3986
183+
*
184+
* @param array $params
185+
*
186+
* @return string
187+
*/
188+
protected function buildQuery(array $params): string
189+
{
190+
$pieces = [];
191+
192+
foreach ($this->encodeQueryParams($params) as $param => $values) {
193+
$value = implode(',', $values);
194+
195+
$pieces[] = implode('=', !$value
196+
? [$param]
197+
: [$param, $value]
198+
);
199+
}
200+
201+
return implode('&', array_filter($pieces));
202+
}
203+
204+
/**
205+
* Encode query parameters.
206+
*
207+
* @param array $params
208+
*
209+
* @return array
210+
*/
211+
protected function encodeQueryParams(array $params): array
212+
{
213+
$encoded = [];
214+
215+
array_walk($params, function($value, string $param) use (&$encoded) {
216+
$key = str_replace('+', ' ', $param);
217+
$key = rawurlencode(rawurldecode($key));
218+
219+
$values = array_wrap($value);
220+
$values = array_map(function($value) {
221+
$value = str_replace('+', ' ', $value);
222+
return rawurlencode(rawurldecode($value));
223+
}, $values);
224+
225+
$encoded[$key] = $values;
226+
});
227+
228+
return $encoded;
229+
}
230+
231+
/**
232+
* Convert an object to a string representation.
233+
*
234+
* @return string
235+
*/
236+
public function __toString()
237+
{
238+
return $this->create();
239+
}
240+
}

0 commit comments

Comments
 (0)