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

Commit

Permalink
Merge branch 'hotfix/v1.2.4'
Browse files Browse the repository at this point in the history
  • Loading branch information
breart committed Sep 5, 2018
2 parents eeeb261 + 1f69bd3 commit eba027b
Show file tree
Hide file tree
Showing 4 changed files with 356 additions and 36 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [1.2.4] - 2018-09-05

### Fixed
- Completely refactored QSH generation logic (fixes #10)

## [1.2.3] - 2018-08-15

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

[Unreleased]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.3...HEAD
[Unreleased]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.4...HEAD
[1.2.4]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.3...v1.2.4
[1.2.3]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.2...v1.2.3
[1.2.2]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.1...v1.2.2
[1.2.1]: https://github.com/brezzhnev/atlassian-connect-core/compare/v1.2.0...v1.2.1
Expand Down
39 changes: 4 additions & 35 deletions src/Helpers/JWTHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace AtlassianConnectCore\Helpers;

use AtlassianConnectCore\Http\Auth\QSH;

/**
* Class JWTHelper
*
Expand Down Expand Up @@ -64,42 +66,9 @@ public static function create(string $url, string $method, string $issuer, strin
*
* @return string
*/
public static function qsh($url, $method)
public static function qsh($url, $method): string
{
$method = strtoupper($method);

$parts = parse_url($url);
$path = $parts['path'];

// The list of prefixes which must be removed from the path
$prefixes = ['/wiki'];

foreach ($prefixes as $prefix) {
$path = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $path);
}

// Parse a query into the map of parameters
parse_str($parts['query'], $params);

// Parameters should be sorted alphabetically
ksort($params);

$canonicalQuery = http_build_query(
$params,
null,
'&',
PHP_QUERY_RFC3986
);

$parts = [
strtoupper($method),
$path,
$canonicalQuery
];

$qsh = hash('sha256', implode('&', $parts));

return $qsh;
return new QSH($url, $method);
}

/**
Expand Down
240 changes: 240 additions & 0 deletions src/Http/Auth/QSH.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
<?php

namespace AtlassianConnectCore\Http\Auth;

/**
* Class QSH creates a Query String Hash
*
* Documentation:
* https://docs.atlassian.com/DAC/bitbucket/concepts/qsh.html
*
* @package App\Http\Auth
*
* @author Artem Brezhnev <[email protected]>
*/
class QSH
{
/**
* The request URL.
*
* @var string
*/
protected $url;

/**
* The request HTTP method.
*
* @var string
*/
protected $method;

/**
* The URL parts (host, port, path...)
*
* @var array
*/
protected $parts = [];

/**
* The list of prefixes which should be removed.
*
* @var array
*/
protected $prefixes = [
'/wiki'
];

/**
* QSH constructor.
*
* @param string $url
* @param string $method
*/
public function __construct(string $url, string $method)
{
$this->url = $url;
$this->parts = parse_url($url);

$this->method = strtoupper($method);
}

/**
* Create a QSH string.
*
* More details:
* https://docs.atlassian.com/DAC/bitbucket/concepts/qsh.html
*
* @return string
*/
public function create(): string
{
$parts = [
$this->method,
$this->canonicalUri(),
$this->canonicalQuery()
];

return hash('sha256', implode('&', $parts));
}

/**
* Make a canonical URI.
*
* @return string|null
*/
public function canonicalUri()
{
if(!$path = array_get($this->parts, 'path')) {
return '/';
}

// Remove a prefix of instance from the path
// Eg. remove `/wiki` part which means Confluence instance.
$uri = $this->removePrefix($path);

// The canonical URI should not contain & characters.
// Therefore, any & characters should be URL-encoded to %26.
$uri = str_replace('&', '%26', $uri);

// The canonical URI only ends with a / character if it is the only character.
$uri = $uri === '/'
? $uri
: rtrim($uri, '/');

return $uri;
}

/**
* Make a canonical query string.
*
* @return string|null
*/
public function canonicalQuery()
{
if(!$query = array_get($this->parts, 'query')) {
return null;
}

$params = $this->parseQuery($query);

// We should ignore the "JWT" parameter.
$params = array_filter($params, function(string $key) {
return strtolower($key) !== 'jwt';
}, ARRAY_FILTER_USE_KEY);

ksort($params);

$query = $this->buildQuery($params);

// Encode underscores.
$query = str_replace('_', '%20', $query);

return $query;
}

/**
* Remove a prefix from the URL path.
*
* @param string $path
*
* @return string
*/
protected function removePrefix(string $path): string
{
foreach ($this->prefixes as $prefix) {
$pattern = '/^' . preg_quote($prefix, '/') . '/';

if(preg_match($pattern, $path)) {
$path = preg_replace($pattern, '', $path);

break;
}
}

return $path;
}

/**
* Parse a query to array of parameters.
*
* @param string $query
*
* @return array
*/
protected function parseQuery(string $query): array
{
$output = [];

$query = ltrim($query, '?');

$parameters = explode('&', $query);

foreach ($parameters as $parameter) {
list($key, $value) = array_pad(explode('=', $parameter), 2, null);

$output = array_merge_recursive($output, [$key => $value]);
}

return $output;
}

/**
* Build a query accordingly to RFC3986
*
* @param array $params
*
* @return string
*/
protected function buildQuery(array $params): string
{
$pieces = [];

foreach ($this->encodeQueryParams($params) as $param => $values) {
$value = implode(',', $values);

$pieces[] = implode('=', !$value
? [$param]
: [$param, $value]
);
}

return implode('&', array_filter($pieces));
}

/**
* Encode query parameters.
*
* @param array $params
*
* @return array
*/
protected function encodeQueryParams(array $params): array
{
$encoded = [];

array_walk($params, function($value, string $param) use (&$encoded) {
$key = str_replace('+', ' ', $param);
$key = rawurlencode(rawurldecode($key));

$values = array_wrap($value);
$values = array_map(function($value) {
$value = str_replace('+', ' ', $value);
return rawurlencode(rawurldecode($value));
}, $values);

$encoded[$key] = $values;
});

return $encoded;
}

/**
* Convert an object to a string representation.
*
* @return string
*/
public function __toString()
{
return $this->create();
}
}
Loading

0 comments on commit eba027b

Please sign in to comment.