diff --git a/src/OAuth2/OAuth2Routes.php b/src/OAuth2/OAuth2Routes.php index cd4ca06..5b6553b 100644 --- a/src/OAuth2/OAuth2Routes.php +++ b/src/OAuth2/OAuth2Routes.php @@ -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(),