Finished first draft of the authorization endpoint implementation

Made some minor tweaks and improvements to the utility classes
Required some new dependencies
Server::__construct now takes a single config array as the list of
parameters was getting rather long.
This commit is contained in:
Barnaby Walters 2021-06-06 14:47:05 +02:00
parent 8ab57bee25
commit 1b2bd1f513
6 changed files with 380 additions and 149 deletions

View File

@ -16,7 +16,10 @@
"indieauth/client": "^1.1",
"psr/http-client": "^1.0",
"psr/http-server-middleware": "^1.0",
"dflydev/fig-cookies": "^3.0"
"dflydev/fig-cookies": "^3.0",
"mf2/mf2": "^0.4.6",
"barnabywalters/mf-cleaner": "^0.1.4",
"guzzlehttp/psr7": "^1.8"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.3"

284
composer.lock generated
View File

@ -4,8 +4,52 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e7b261990b1458b0da10c6ff5fa2a6d2",
"content-hash": "a3c5834a1f1214cdf5ec23a831b9c2cf",
"packages": [
{
"name": "barnabywalters/mf-cleaner",
"version": "v0.1.4",
"source": {
"type": "git",
"url": "https://github.com/barnabywalters/php-mf-cleaner.git",
"reference": "ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barnabywalters/php-mf-cleaner/zipball/ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4",
"reference": "ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4",
"shasum": ""
},
"require-dev": {
"php": ">=5.3",
"phpunit/phpunit": "*"
},
"suggest": {
"mf2/mf2": "To parse microformats2 structures from (X)HTML"
},
"type": "library",
"autoload": {
"files": [
"src/BarnabyWalters/Mf2/Functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Barnaby Walters",
"email": "barnaby@waterpigs.co.uk"
}
],
"description": "Cleans up microformats2 array structures",
"support": {
"issues": "https://github.com/barnabywalters/php-mf-cleaner/issues",
"source": "https://github.com/barnabywalters/php-mf-cleaner/tree/v0.1.4"
},
"time": "2014-10-06T23:11:15+00:00"
},
{
"name": "dflydev/fig-cookies",
"version": "v3.0.0",
@ -68,6 +112,81 @@
},
"time": "2021-01-22T02:53:56+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "1.8.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "dc960a912984efb74d0a90222870c72c87f10c91"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91",
"reference": "dc960a912984efb74d0a90222870c72c87f10c91",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"psr/http-message": "~1.0",
"ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"ext-zlib": "*",
"phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.7-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Schultze",
"homepage": "https://github.com/Tobion"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/1.8.2"
},
"time": "2021-04-26T09:17:50+00:00"
},
{
"name": "indieauth/client",
"version": "1.1.5",
@ -775,6 +894,50 @@
"source": "https://github.com/php-fig/log/tree/1.1.4"
},
"time": "2021-05-03T11:20:27+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
"reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"support": {
"issues": "https://github.com/ralouphie/getallheaders/issues",
"source": "https://github.com/ralouphie/getallheaders/tree/develop"
},
"time": "2019-03-08T08:55:37+00:00"
}
],
"packages-dev": [
@ -935,125 +1098,6 @@
"source": "https://github.com/guzzle/promises/tree/1.4.1"
},
"time": "2021-03-07T09:25:29+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "1.8.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "dc960a912984efb74d0a90222870c72c87f10c91"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91",
"reference": "dc960a912984efb74d0a90222870c72c87f10c91",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"psr/http-message": "~1.0",
"ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"ext-zlib": "*",
"phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.7-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Schultze",
"homepage": "https://github.com/Tobion"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/1.8.2"
},
"time": "2021-04-26T09:17:50+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
"reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"support": {
"issues": "https://github.com/ralouphie/getallheaders/issues",
"source": "https://github.com/ralouphie/getallheaders/tree/develop"
},
"time": "2019-03-08T08:55:37+00:00"
}
],
"aliases": [],

View File

@ -8,6 +8,9 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Dflydev\FigCookies;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
// From https://github.com/indieweb/indieauth-client-php/blob/main/src/IndieAuth/Client.php, thanks aaronpk.
function generateRandomString($numBytes) {
@ -24,7 +27,7 @@ function generateRandomString($numBytes) {
return bin2hex($bytes);
}
class DoubleSubmitCookieCsrfMiddleware implements MiddlewareInterface {
class DoubleSubmitCookieCsrfMiddleware implements MiddlewareInterface, LoggerAwareInterface {
const READ_METHODS = ['HEAD', 'GET', 'OPTIONS'];
const TTL = 60 * 20;
const ATTRIBUTE = 'csrf';
@ -39,7 +42,9 @@ class DoubleSubmitCookieCsrfMiddleware implements MiddlewareInterface {
public int $tokenLength;
public function __construct(string $attribute=self::ATTRIBUTE, int $ttl=self::TTL, $errorResponse=self::DEFAULT_ERROR_RESPONSE_STRING, $tokenLength=self::CSRF_TOKEN_LENGTH) {
public LoggerInterface $logger;
public function __construct(string $attribute=self::ATTRIBUTE, int $ttl=self::TTL, $errorResponse=self::DEFAULT_ERROR_RESPONSE_STRING, $tokenLength=self::CSRF_TOKEN_LENGTH, $logger=null) {
$this->attribute = $attribute;
$this->ttl = $ttl;
$this->tokenLength = $tokenLength;
@ -54,6 +59,15 @@ class DoubleSubmitCookieCsrfMiddleware implements MiddlewareInterface {
$errorResponse = function (ServerRequestInterface $request) use ($errorResponse) { return $errorResponse; };
}
$this->errorResponse = $errorResponse;
if (!$logger instanceof LoggerInterface) {
$logger = new NullLogger();
}
$this->logger = $logger;
}
public function setLogger(LoggerInterface $logger) {
$this->logger = $logger;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {

View File

@ -23,9 +23,9 @@ class FilesystemJsonStorage implements TokenStorageInterface {
$deleted = 0;
// A TTL of 0 means the token should live until deleted, and negative TTLs are invalid.
if ($ttl >= 0) {
if ($ttl > 0) {
foreach (new DirectoryIterator($this->path) as $fileInfo) {
if ($fileInfo->isFile() && time() - max($fileInfo->getMTime(), $fileInfo->getCTime()) > $ttl) {
if ($fileInfo->isFile() && $fileInfo->getExtension() == 'json' && time() - max($fileInfo->getMTime(), $fileInfo->getCTime()) > $ttl) {
unlink($fileInfo->getPathname());
$deleted++;
}

View File

@ -7,8 +7,14 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* No-Op Middleware
*
* A PSR-15 Middleware which does nothing, simply passing `$request` onto `$handler` and returning
* the response.
*/
class NoOpMiddleware implements MiddlewareInterface {
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
return $handler->handle($request);
}
}
}

View File

@ -3,15 +3,27 @@
namespace Taproot\IndieAuth;
use Exception;
use GuzzleHttp\Exception\ServerException;
use IndieAuth\Client as IndieAuthClient;
use Mf2;
use BarnabyWalters\Mf2 as M;
use GuzzleHttp\Psr7\Header as HeaderParser;
use Nyholm\Psr7\Response;
use GuzzleHttp\Psr7\ServerRequest;
use Nyholm\Psr7\Request;
use Nyholm\Psr7\ServerRequest;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface as HttpClientInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use IndieAuth\Client as IndieAuthClient;
use Psr\Http\Server\MiddlewareInterface;
use function PHPSTORM_META\type;
/**
* Development Reference
@ -51,6 +63,26 @@ function buildQueryString(array $parameters) {
return join('&', $qs);
}
/**
* Append Query Parameters
*
* Converts `$queryParams` into a query string, then checks `$uri` for an
* existing query string. Then appends the newly generated query string
* with either ? or & as appropriate.
*/
function appendQueryParams(string $uri, array $queryParams) {
$queryString = buildQueryString($queryParams);
$separator = parse_url($uri, \PHP_URL_QUERY) ? '&' : '?';
return "{$uri}{$separator}{$queryString}";
}
function trySetLogger($target, LoggerInterface $logger) {
if ($target instanceof LoggerAwareInterface) {
$target->setLogger($logger);
}
return $target;
}
class Server {
const CUSTOMIZE_AUTHORIZATION_CODE = 'customise_authorization_code';
const SHOW_AUTHORIZATION_PAGE = 'show_authorization_page';
@ -68,20 +100,39 @@ class Server {
public LoggerInterface $logger;
public HttpClientInterface $httpClient;
public callable $httpGetWithEffectiveUrl;
public string $csrfKey;
public function __construct(array $callbacks, $authorizationCodeStorage, $accessTokenStorage, $csrfMiddleware=null, string $csrfKey=self::DEFAULT_CSRF_KEY, LoggerInterface $logger=null) {
$callbacks = array_merge([
self::CUSTOMIZE_AUTHORIZATION_CODE => function (array $code) { return $code; }, // Default to no-op.
self::SHOW_AUTHORIZATION_PAGE => function (ServerRequestInterface $request, array $authenticationResult, string $authenticationRedirect) { }, // TODO: Put the default implementation here.
self::HANDLE_NON_INDIEAUTH_REQUEST => function (ServerRequestInterface $request) { return null; }, // Default to no-op.
], $callbacks);
public function __construct(array $config) {
$config = array_merge_recursive([
'csrfMiddleware' => null,
'csrfKey' => self::DEFAULT_CSRF_KEY,
'logger' => null,
'callbacks' => [
self::CUSTOMIZE_AUTHORIZATION_CODE => function (array $code) { return $code; }, // Default to no-op.
self::SHOW_AUTHORIZATION_PAGE => function (ServerRequestInterface $request, array $authenticationResult, string $authenticationRedirect) { }, // TODO: Put the default implementation here.
self::HANDLE_NON_INDIEAUTH_REQUEST => function (ServerRequestInterface $request) { return null; }, // Default to no-op.
],
'authorizationCodeStorage' => null,
'accessTokenStorage' => null,
'httpGetWithEffectiveUrl' => null
], $config);
if (!$config['logger'] instanceof LoggerInterface) {
throw new Exception("\$config['logger'] must be an instance of \\Psr\\Log\\LoggerInterface or null.");
}
$this->logger = $config['logger'] ?? new NullLogger();
$callbacks = $config['callbacks'];
if (!(array_key_exists(self::HANDLE_AUTHENTICATION_REQUEST, $callbacks) and is_callable($callbacks[self::HANDLE_AUTHENTICATION_REQUEST]))) {
throw new Exception('$callbacks[\'' . self::HANDLE_AUTHENTICATION_REQUEST .'\'] must be present and callable.');
}
$this->callbacks = $callbacks;
$authorizationCodeStorage = $config['authorizationCodeStorage'];
if (!$authorizationCodeStorage instanceof TokenStorageInterface) {
if (is_string($authorizationCodeStorage)) {
$authorizationCodeStorage = new FilesystemJsonStorage($authorizationCodeStorage, 600, true);
@ -89,8 +140,10 @@ class Server {
throw new Exception('$authorizationCodeStorage parameter must be either a string (path) or an instance of Taproot\IndieAuth\TokenStorageInterface.');
}
}
trySetLogger($authorizationCodeStorage, $this->logger);
$this->authorizationCodeStorage = $authorizationCodeStorage;
$accessTokenStorage = $config['accessTokenStorage'];
if (!$accessTokenStorage instanceof TokenStorageInterface) {
if (is_string($accessTokenStorage)) {
// Create a default access token storage with a TTL of 7 days.
@ -99,22 +152,51 @@ class Server {
throw new Exception('$accessTokenStorage parameter must be either a string (path) or an instance of Taproot\IndieAuth\TokenStorageInterface.');
}
}
trySetLogger($accessTokenStorage, $this->logger);
$this->accessTokenStorage = $accessTokenStorage;
$this->csrfKey = $csrfKey;
$this->csrfKey = $config['csrfKey'];
$csrfMiddleware = $config['csrfMiddleware'];
if (!$csrfMiddleware instanceof MiddlewareInterface) {
// Default to the statless Double-Submit Cookie CSRF Middleware, with default settings.
$csrfMiddleware = new DoubleSubmitCookieCsrfMiddleware($this->csrfKey);
}
trySetLogger($csrfMiddleware, $this->logger);
$this->csrfMiddleware = $csrfMiddleware;
$this->logger = $logger ?? new NullLogger();
$httpGetWithEffectiveUrl = $config['httpGetWithEffectiveUrl'];
if (!is_callable($httpGetWithEffectiveUrl)) {
if (class_exists('\GuzzleHttp\Client')) {
$httpGetWithEffectiveUrl = function (string $uri) {
$resp = (new \GuzzleHttp\Client([
\GuzzleHttp\RequestOptions::ALLOW_REDIRECTS => [
'max' => 10,
'strict' => true,
'referer' => true,
'track_redirects' => true
]
]))->get($uri);
$rdh = $resp->getHeader('X-Guzzle-Redirect-History');
$effectiveUrl = empty($rdh) ? $uri : array_values($rdh)[count($rdh) - 1];
return [$resp, $effectiveUrl];
};
} else {
throw new Exception('No valid $httpGetWithEffectiveUrl was provided, and guzzlehttp/guzzle was not installed. Either require guzzlehttp/guzzle, or provide a valid callable.');
}
}
trySetLogger($httpGetWithEffectiveUrl, $this->logger);
$this->httpGetWithEffectiveUrl = $httpGetWithEffectiveUrl;
}
public function handleAuthorizationEndpointRequest(ServerRequestInterface $request): ResponseInterface {
$this->logger->info('Handling an IndieAuth Authorization Endpoint request.');
// If its a profile information request:
if (isIndieAuthAuthorizationCodeRedeemingRequest($request)) {
$this->logger->info('Handling a request to redeem an authorization code for profile information.');
// Verify that the authorization code is valid and has not yet been used.
$this->authorizationCodeStorage->get($request->getParsedBody()['code']);
@ -136,10 +218,30 @@ class Server {
// If this is an authorization or approval request (allowing POST requests as well to accommodate
// approval requests and custom auth form submission.
if (isIndieAuthAuthorizationRequest($request, ['get', 'post'])) {
$this->logger->info('Handling an authorization request', ['method' => $request->getMethod()]);
$queryParams = $request->getQueryParams();
// Return an error if were missing required parameters.
$requiredParameters = ['client_id', 'redirect_uri', 'state', 'code_challenge', 'code_challenge_method'];
$missingRequiredParameters = array_filter($requiredParameters, function ($p) use ($queryParams) {
return !array_key_exists($p, $queryParams) || empty($queryParams[$p]);
});
if (!empty($missingRequiredParameters)) {
$this->logger->warning('The authorization request was missing required parameters. Returning an error response.', ['missing' => $missingRequiredParameters]);
// TODO: return a better response, or at least allow the library consumer to configure a better response.
return new Response(400, ['content-type' => 'text/plain'], 'The IndieAuth request was missing the following parameters: ' . join("\n", $missingRequiredParameters));
}
// Normalise the me parameter, if it exists.
if (array_key_exists('me', $queryParams)) {
$queryParams['me'] = IndieAuthClient::normalizeMeURL($queryParams['me']);
}
// Build a URL for the authentication flow to redirect to, if it needs to.
// TODO: perhaps filter queryParams to only include the indieauth-relevant params?
$authenticationRedirect = $request->getUri() . '?' . buildQueryString($request->getQueryParams());
$authenticationRedirect = $request->getUri() . '?' . buildQueryString($queryParams);
$this->logger->info('Calling handle_authentication_request callback');
$authenticationResult = call_user_func($this->callbacks[self::HANDLE_AUTHENTICATION_REQUEST], $request, $authenticationRedirect);
// If the authentication handler returned a Response, return that as-is.
@ -147,15 +249,77 @@ class Server {
return $authenticationResult;
} elseif (is_array($authenticationResult)) {
// Check the resulting array for errors.
if (!array_key_exists('me', $authenticationResult)) {
$this->logger->error('The handle_authentication_request callback returned an array with no me key.', ['array' => $authenticationResult]);
return new Response(500, ['content-type' => 'text/plain'], 'An internal error occurred.');
}
// The user is logged in.
// Fetch the client_id URL to find information about the client to present to the user.
try {
/** @var ResponseInterface $clientIdResponse */
list($clientIdResponse, $clientIdEffectiveUrl) = call_user_func($this->httpGetWithEffectiveUrl, $queryParams['client_id']);
$clientIdMf2 = Mf2\parse($clientIdResponse->getBody()->getContents(), $clientIdEffectiveUrl);
} catch (ClientExceptionInterface | RequestExceptionInterface | NetworkExceptionInterface $e) {
$this->logger->error("Caught an HTTP exception while trying to fetch the client_id. Returning an error response.", [
'client_id' => $queryParams['client_id'],
'exception' => $e->__toString()
]);
return new Response(500, ['content-type' => 'text/plain'], 'An internal error occurred.');
} catch (Exception $e) {
$this->logger->error("Caught an unknown exception while trying to fetch the client_id. Returning an error response.", [
'exception' => $e->__toString()
]);
return new Response(500, ['content-type' => 'text/plain'], 'An internal error occurred.');
}
// Search for an h-app with u-url matching the client_id.
$clientHApps = M\findMicroformatsByProperty(M\findMicroformatsByType($clientIdMf2, 'h-app'), 'url', $queryParams['client-id']);
$clientHApp = empty($clientHApps) ? null : $clientHApps[0];
// Search for all link@rel=redirect_uri at the client_id.
$clientIdRedirectUris = [];
if (array_key_exists('redirect_uri', $clientIdMf2['rels'])) {
$clientIdRedirectUris = array_merge($clientIdRedirectUris, $clientIdMf2['rels']);
}
foreach (HeaderParser::parse($clientIdResponse->getHeader('Link')) as $link) {
if (array_key_exists('rel', $link) and str_contains(" {$link['rel']} ", " redirect_uri ")) {
// Strip off the < > which surround the link URL for some reason.
$clientIdRedirectUris[] = substr($link[0], 1, strlen($link[0]) - 2);
}
}
// If the authority of the redirect_uri does not match the client_id, or exactly match one of their redirect URLs, return an error.
$cidComponents = M\parseUrl($queryParams['client_id']);
$ruriComponents = M\parseUrl($queryParams['redirect_uri']);
$clientIdMatchesRedirectUri = $cidComponents['scheme'] == $ruriComponents['scheme']
&& $cidComponents['host'] == $ruriComponents['host']
&& $cidComponents['port'] == $ruriComponents['port'];
$redirectUriValid = $clientIdMatchesRedirectUri || in_array($queryParams['redirect_uri'], $clientIdRedirectUris);
if (!$redirectUriValid) {
$this->logger->warning("The provided redirect_uri did not match either the client_id, nor the discovered redirect URIs.", [
'provided_redirect_uri' => $queryParams['redirect_uri'],
'provided_client_id' => $queryParams['client_id'],
'discovered_redirect_uris' => $clientIdRedirectUris
]);
return new Response(500, ['content-type' => 'text/plain'], 'An internal error occurred.');
}
// If this is a POST request sent from the authorization (i.e. scope-choosing) form:
if (isAuthorizationApprovalRequest($request)) {
// Assemble the data for the authorization code, store it somewhere persistent.
$code = [
];
$code = array_merge($authenticationResult, [
'client_id' => $queryParams['client_id'],
'redirect_uri' => $queryParams['redirect_uri'],
'state' => $queryParams['state'],
'code_challenge' => $queryParams['code_challenge'],
'code_challenge_method' => $queryParams['code_challenge_method'],
'requested_scope' => $queryParams['scope'] ?? '',
'code' => generateRandomString(256)
]);
// Pass it to the auth code customisation callback, if any.
$code = call_user_func($this->callbacks[self::CUSTOMIZE_AUTHORIZATION_CODE], $code, $request);
@ -164,16 +328,16 @@ class Server {
$this->authorizationCodeStorage->put($code['code'], $code);
// Return a redirect to the client app.
return new Response(302, ['Location' => appendQueryParams($queryParams['redirect_uri'], [
'code' => $code['code'],
'state' => $code['state']
])]);
}
// Otherwise, the user is authenticated and needs to authorize the client app + choose scopes.
// Fetch the client_id URL to find information about the client to present to the user.
// If the authority of the redirect_uri does not match the client_id or one of their redirect URLs, return an error.
// Present the authorization UI.
return call_user_func($this->callbacks[self::SHOW_AUTHORIZATION_PAGE], $request, $authenticationResult, $authenticationRedirect);
return call_user_func($this->callbacks[self::SHOW_AUTHORIZATION_PAGE], $request, $authenticationResult, $authenticationRedirect, $clientHApp);
}
}