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