Updated OAuth2 error handling.

This commit is contained in:
flash 2024-07-31 16:25:36 +00:00
parent 53822c5fd9
commit c259493303

View file

@ -23,12 +23,28 @@ final class OAuth2Routes extends RouteHandler {
throw new InvalidArgumentException('$getAuthInfo must be callable'); throw new InvalidArgumentException('$getAuthInfo must be callable');
} }
private static function error(string $code, string $message = '', string $url = ''): array { private static function error(
$response,
string $code,
string $message = '',
string $uri = '',
bool $authzHeader = false
) {
$info = ['error' => $code]; $info = ['error' => $code];
if($message !== '') if($message !== '')
$info['error_description'] = $message; $info['error_description'] = $message;
if($url !== '') if($uri !== '')
$info['error_uri'] = $url; $info['error_uri'] = $uri;
if($authzHeader) {
$wwwAuth = sprintf('Basic realm="%s"', $_SERVER['HTTP_HOST']);
foreach($info as $name => $value)
$wwwAuth .= sprintf(', %s="%s"', $name, rawurlencode($value));
$response->setStatusCode($authzHeader ? 401 : 400);
$response->setHeader('WWW-Authenticate', $wwwAuth);
} else
$response->setStatusCode(400);
return $info; return $info;
} }
@ -327,10 +343,8 @@ final class OAuth2Routes extends RouteHandler {
public function postRequestAuthorise($response, $request) { public function postRequestAuthorise($response, $request) {
$response->setHeader('Cache-Control', 'no-store'); $response->setHeader('Cache-Control', 'no-store');
if(!$request->isFormContent()) { if(!$request->isFormContent())
$response->setStatusCode(400); return self::error($response, 'invalid_request', 'Your request must use content type application/x-www-form-urlencoded.');
return self::error('invalid_request', 'Your request must use content type application/x-www-form-urlencoded.');
}
$content = $request->getContent(); $content = $request->getContent();
@ -340,9 +354,7 @@ final class OAuth2Routes extends RouteHandler {
$clientId = $authzHeader[0]; $clientId = $authzHeader[0];
$clientSecret = $authzHeader[1] ?? ''; $clientSecret = $authzHeader[1] ?? '';
} elseif($authzHeader[0] !== '') { } elseif($authzHeader[0] !== '') {
$response->setStatusCode(401); return self::error($response, 'invalid_client', 'You must use the Basic method for Authorization parameters.', authzHeader: true);
$response->setHeader('WWW-Authenticate', 'Basic');
return self::error('invalid_client', 'You must use the Basic method for Authorization parameters.');
} else { } else {
$clientId = (string)$content->getParam('client_id'); $clientId = (string)$content->getParam('client_id');
$clientSecret = ''; $clientSecret = '';
@ -352,31 +364,23 @@ final class OAuth2Routes extends RouteHandler {
try { try {
$appInfo = $appsData->getAppInfo(clientId: $clientId, deleted: false); $appInfo = $appsData->getAppInfo(clientId: $clientId, deleted: false);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
if($authzHeader[0] === '') { return self::error(
$response->setStatusCode(400); $response, 'invalid_client', 'No application has been registered with this client ID.',
} else { authzHeader: $authzHeader[0] !== ''
$response->setStatusCode(401); );
$response->setHeader('WWW-Authenticate', 'Basic');
}
return self::error('invalid_client', 'No application has been registered with this client ID.');
} }
$appAuthenticated = false; $appAuthenticated = false;
if($clientSecret !== '') { if($clientSecret !== '') {
// TODO: rate limiting // TODO: rate limiting
if(!$appInfo->verifyClientSecret($clientSecret)) { if(!$appInfo->verifyClientSecret($clientSecret))
$response->setStatusCode(401); return self::error($response, 'invalid_client', 'Provided client secret is not correct for this application.', authzHeader: true);
$response->setHeader('WWW-Authenticate', 'Basic');
return self::error('invalid_client', 'Provided client secret is not correct for this application.');
}
} }
$scopes = self::filterScopes((string)$content->getParam('scope')); $scopes = self::filterScopes((string)$content->getParam('scope'));
if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes)) { if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes))
$response->setStatusCode(400); return self::error($response, 'invalid_scope', 'An invalid scope was requested.');
return self::error('invalid_scope', 'An invalid scope was requested.');
}
$scope = implode(' ', $scopes); $scope = implode(' ', $scopes);
$deviceInfo = $this->oauth2Ctx->getDevicesData()->createDevice($appInfo, $scope); $deviceInfo = $this->oauth2Ctx->getDevicesData()->createDevice($appInfo, $scope);
@ -428,10 +432,8 @@ final class OAuth2Routes extends RouteHandler {
if($request->getMethod() === 'OPTIONS') if($request->getMethod() === 'OPTIONS')
return 204; return 204;
if(!$request->isFormContent()) { if(!$request->isFormContent())
$response->setStatusCode(400); return self::error($response, 'invalid_request', 'Your request must use content type application/x-www-form-urlencoded.');
return self::error('invalid_request', 'Your request must use content type application/x-www-form-urlencoded.');
}
$content = $request->getContent(); $content = $request->getContent();
@ -442,9 +444,7 @@ final class OAuth2Routes extends RouteHandler {
$clientId = $authzHeader[0]; $clientId = $authzHeader[0];
$clientSecret = $authzHeader[1] ?? ''; $clientSecret = $authzHeader[1] ?? '';
} elseif($authzHeader[0] !== '') { } elseif($authzHeader[0] !== '') {
$response->setStatusCode(401); return self::error($response, 'invalid_client', 'You must either use the Basic method for Authorization or use the client_id and client_secret parameters.', authzHeader: true);
$response->setHeader('WWW-Authenticate', 'Basic');
return self::error('invalid_client', 'You must either use the Basic method for Authorization or use the client_id and client_secret parameters.');
} else { } else {
$clientId = (string)$content->getParam('client_id'); $clientId = (string)$content->getParam('client_id');
$clientSecret = (string)$content->getParam('client_secret'); $clientSecret = (string)$content->getParam('client_secret');
@ -454,30 +454,15 @@ final class OAuth2Routes extends RouteHandler {
try { try {
$appInfo = $appsData->getAppInfo(clientId: $clientId, deleted: false); $appInfo = $appsData->getAppInfo(clientId: $clientId, deleted: false);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
if($authzHeader[0] === '') { return self::error($response, 'invalid_client', 'No application has been registered with this client id.', authzHeader: $authzHeader[0] !== '');
$response->setStatusCode(400);
} else {
$response->setStatusCode(401);
$response->setHeader('WWW-Authenticate', 'Basic');
}
return self::error('invalid_client', 'No application has been registered with this client id.');
} }
$appAuthenticated = false; $appAuthenticated = false;
if($clientSecret !== '') { if($clientSecret !== '') {
// TODO: rate limiting // TODO: rate limiting
$appAuthenticated = $appInfo->verifyClientSecret($clientSecret); $appAuthenticated = $appInfo->verifyClientSecret($clientSecret);
if(!$appAuthenticated) { if(!$appAuthenticated)
if($authzHeader[0] === '') { return self::error($response, 'invalid_client', 'Provided client secret is not correct for this application.', authzHeader: $authzHeader[0] !== '');
$response->setStatusCode(400);
} else {
$response->setStatusCode(401);
$response->setHeader('WWW-Authenticate', 'Basic');
}
return self::error('invalid_client', 'Provided client secret is not correct for this application.');
}
} }
$type = (string)$content->getParam('grant_type'); $type = (string)$content->getParam('grant_type');
@ -489,27 +474,20 @@ final class OAuth2Routes extends RouteHandler {
code: (string)$content->getParam('code'), code: (string)$content->getParam('code'),
); );
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$response->setStatusCode(400); return self::error($response, 'invalid_grant', 'No authorisation request with this code exists.');
return self::error('invalid_grant', 'No authorisation request with this code exists.');
} }
if($authsInfo->hasExpired()) { if($authsInfo->hasExpired())
$response->setStatusCode(400); return self::error($response, 'invalid_grant', 'Authorisation request has expired.');
return self::error('invalid_grant', 'Authorisation request has expired.'); if(!$authsInfo->verifyCodeChallenge((string)$content->getParam('code_verifier')))
} return self::error($response, 'invalid_request', 'Code challenge verification failed.');
if(!$authsInfo->verifyCodeChallenge((string)$content->getParam('code_verifier'))) {
$response->setStatusCode(400);
return self::error('invalid_request', 'Code challenge verification failed.');
}
$authsData->deleteAuthorisation($authsInfo); $authsData->deleteAuthorisation($authsInfo);
$scopes = $authsInfo->getScopes(); $scopes = $authsInfo->getScopes();
if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes)) { if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes))
$response->setStatusCode(400); return self::error($response, 'invalid_scope', 'One or more requested scopes are no longer valid for this application, please restart authorisation.');
return self::error('invalid_scope', 'One or more requested scopes are no longer valid for this application, please restart authorisation.');
}
$scope = implode(' ', $scopes); $scope = implode(' ', $scopes);
$tokensData = $this->oauth2Ctx->getTokensData(); $tokensData = $this->oauth2Ctx->getTokensData();
@ -530,19 +508,13 @@ final class OAuth2Routes extends RouteHandler {
try { try {
$refreshInfo = $tokensData->getRefreshInfo((string)$content->getParam('refresh_token'), OAuth2TokensData::REFRESH_BY_TOKEN); $refreshInfo = $tokensData->getRefreshInfo((string)$content->getParam('refresh_token'), OAuth2TokensData::REFRESH_BY_TOKEN);
} catch(RuntimeException $ex) { } catch(RuntimeException $ex) {
$response->setStatusCode(400); return self::error($response, 'invalid_grant', 'No such refresh token exists.');
return self::error('invalid_grant', 'No such refresh token exists.');
} }
if($refreshInfo->getAppId() !== $appInfo->getId()) { if($refreshInfo->getAppId() !== $appInfo->getId())
$response->setStatusCode(400); return self::error($response, 'invalid_grant', 'This refresh token is not associated with this application.');
return self::error('invalid_grant', 'This refresh token is not associated with this application.'); if($refreshInfo->hasExpired())
} return self::error($response, 'invalid_grant', 'This refresh token has expired.');
if($refreshInfo->hasExpired()) {
$response->setStatusCode(400);
return self::error('invalid_grant', 'This refresh token has expired.');
}
$tokensData->deleteRefresh($refreshInfo); $tokensData->deleteRefresh($refreshInfo);
@ -553,10 +525,8 @@ final class OAuth2Routes extends RouteHandler {
$newScopes = self::filterScopes((string)$content->getParam('scope')); $newScopes = self::filterScopes((string)$content->getParam('scope'));
$oldScopes = []; $oldScopes = [];
if(!$this->oauth2Ctx->validateScopes($appInfo, $newScopes)) { if(!$this->oauth2Ctx->validateScopes($appInfo, $newScopes))
$response->setStatusCode(400); return self::error($response, 'invalid_scope', 'One or more requested scopes are no longer valid for this application, please restart authorisation.');
return self::error('invalid_scope', 'One or more requested scopes are no longer valid for this application, please restart authorisation.');
}
$accessInfo = $tokensData->createAccess( $accessInfo = $tokensData->createAccess(
$appInfo, $appInfo,
@ -569,21 +539,15 @@ final class OAuth2Routes extends RouteHandler {
if($appInfo->shouldIssueRefreshToken()) if($appInfo->shouldIssueRefreshToken())
$refreshInfo = $this->oauth2Ctx->createRefresh($appInfo, $accessInfo); $refreshInfo = $this->oauth2Ctx->createRefresh($appInfo, $accessInfo);
} elseif($type === 'client_credentials') { } elseif($type === 'client_credentials') {
if(!$appInfo->isConfidential()) { if(!$appInfo->isConfidential())
$response->setStatusCode(400); return self::error($response, 'unauthorized_client', 'This application is not allowed to use this grant type.');
return self::error('unauthorized_client', 'This application is not allowed to use this grant type.'); if(!$appAuthenticated)
} return self::error($response, 'invalid_client', 'Application must authenticate with client secret in order to use this grant type.');
if(!$appAuthenticated) {
$response->setStatusCode(400);
return self::error('invalid_client', 'Application must authenticate with client secret in order to use this grant type.');
}
$scopes = self::filterScopes((string)$content->getParam('scope')); $scopes = self::filterScopes((string)$content->getParam('scope'));
if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes)) { if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes))
$response->setStatusCode(400); return self::error($response, 'invalid_scope', 'One or more requested scopes are no longer valid for this application, please restart authorisation.');
return self::error('invalid_scope', 'One or more requested scopes are no longer valid for this application, please restart authorisation.');
}
$scope = implode(' ', $scopes); $scope = implode(' ', $scopes);
$accessInfo = $this->oauth2Ctx->getTokensData()->createAccess($appInfo, scope: $scope); $accessInfo = $this->oauth2Ctx->getTokensData()->createAccess($appInfo, scope: $scope);
@ -598,44 +562,34 @@ final class OAuth2Routes extends RouteHandler {
code: (string)$content->getParam('device_code') code: (string)$content->getParam('device_code')
); );
} catch(RuntimeException) { } catch(RuntimeException) {
$response->setStatusCode(400); return self::error($response, 'invalid_grant', 'No such device code exists.');
return self::error('invalid_grant', 'No such device code exists.');
} }
if($deviceInfo->hasExpired()) { if($deviceInfo->hasExpired())
$response->setStatusCode(400); return self::error($response, 'expired_token', 'This device code has expired.');
return self::error('expired_token', 'This device code has expired.');
}
if($deviceInfo->isSpeedy()) { if($deviceInfo->isSpeedy()) {
$devicesData->incrementDevicePollInterval($deviceInfo); $devicesData->incrementDevicePollInterval($deviceInfo);
$response->setStatusCode(400); return self::error($response, 'slow_down', 'You are polling too fast, please increase your interval by 5 seconds.');
return self::error('slow_down', 'You are polling too fast, please increase your interval by 5 seconds.');
} }
if($deviceInfo->isPending()) { if($deviceInfo->isPending()) {
$devicesData->bumpDevicePollTime($deviceInfo); $devicesData->bumpDevicePollTime($deviceInfo);
$response->setStatusCode(400); return self::error($response, 'authorization_pending', 'User has not yet completed authorisation, check again in a bit.');
return self::error('authorization_pending', 'User has not yet completed authorisation, check again in a bit.');
} }
$devicesData->deleteDevice($deviceInfo); $devicesData->deleteDevice($deviceInfo);
if(!$deviceInfo->isApproved()) { if(!$deviceInfo->isApproved())
$response->setStatusCode(400); return self::error($response, 'access_denied', 'User has rejected authorisation attempt.');
return self::error('access_denied', 'User has rejected authorisation attempt.');
}
if(!$deviceInfo->hasUserId()) { if(!$deviceInfo->hasUserId())
$response->setStatusCode(400); return self::error($response, 'invalid_request', 'Device code was approved but has no associated user, please contact the system administrator because something is wrong.');
return self::error('invalid_request', 'Device code was approved but has no associated user, please contact the system administrator because something is wrong.');
}
$scopes = $deviceInfo->getScopes(); $scopes = $deviceInfo->getScopes();
if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes)) { if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes))
$response->setStatusCode(400); return self::error($response, 'invalid_scope', 'One or more requested scopes are no longer valid for this application, please restart authorisation.');
return self::error('invalid_scope', 'One or more requested scopes are no longer valid for this application, please restart authorisation.');
}
$scope = implode(' ', $scopes); $scope = implode(' ', $scopes);
$tokensData = $this->oauth2Ctx->getTokensData(); $tokensData = $this->oauth2Ctx->getTokensData();
@ -651,15 +605,11 @@ final class OAuth2Routes extends RouteHandler {
if($appInfo->shouldIssueRefreshToken()) if($appInfo->shouldIssueRefreshToken())
$refreshInfo = $this->oauth2Ctx->createRefresh($appInfo, $accessInfo); $refreshInfo = $this->oauth2Ctx->createRefresh($appInfo, $accessInfo);
} else { } else
$response->setStatusCode(400); return self::error($response, 'unsupported_grant_type', 'Requested grant type is not supported by this server.');
return self::error('unsupported_grant_type', 'Requested grant type is not supported by this server.');
}
if(empty($accessInfo)) { if(empty($accessInfo))
$response->setStatusCode(400); return self::error($response, 'invalid_grant', 'Failed to request access token.');
return self::error('invalid_grant', 'Failed to request access token.');
}
$result = [ $result = [
'access_token' => $accessInfo->getToken(), 'access_token' => $accessInfo->getToken(),