diff --git a/src/AleisterContext.php b/src/AleisterContext.php index 0f23ebe..1471653 100644 --- a/src/AleisterContext.php +++ b/src/AleisterContext.php @@ -9,16 +9,16 @@ use Syokuhou\IConfig; class AleisterContext { private RpcClientWrapper $rpcWrapper; private HttpRouter $router; - - private OAuth2AuthInfo $authInfo; + private AuthzContext $authz; public function __construct( private IConfig $config ) { - $this->authInfo = new OAuth2AuthInfo(['error' => 'none']); - $this->rpcWrapper = new RpcClientWrapper; $this->rpcWrapper->createHmacConfig($config, 'hanyuu'); + $this->rpcWrapper->createHmacConfig($config, 'misuzu'); + + $this->authz = new AuthzContext($this->rpcWrapper); $this->router = new HttpRouter( errorHandler: 'plain', @@ -26,7 +26,6 @@ class AleisterContext { ); $this->router->use('/', fn($resp) => $resp->setPoweredBy('Flashii')); $this->router->use('/', $this->handleAcceptHeader(...)); - $this->router->use('/', $this->handleAuthzHeader(...)); $this->router->get('/', fn() => 'Hello! Someday this page will probably redirect to documentation, but none exists yet.'); $this->router->scopeTo('/oauth2')->register(new OAuth2\OAuth2Routes($this)); @@ -41,8 +40,8 @@ class AleisterContext { return $this->router; } - public function getAuthInfo(): OAuth2AuthInfo { - return $this->authInfo; + public function getAuthzContext(): AuthzContext { + return $this->authz; } public function handleAcceptHeader($response, $request) { @@ -55,38 +54,4 @@ class AleisterContext { $this->router->setErrorHandler(new AleisterErrorHandler($contentHandler)); $this->router->registerContentHandler($contentHandler); } - - public function handleAuthzHeader($response, $request): void { - $authInfo = null; - - $authzHeader = explode(' ', (string)$request->getHeaderLine('Authorization'), 2); - if(count($authzHeader) > 1) { - $authzMethod = array_shift($authzHeader); - $authzInfo = array_shift($authzHeader); - - if(strcasecmp($authzMethod, 'basic') === 0) { - $authzInfo = base64_decode($authzInfo); - if($authzInfo !== false) { - $authzInfo = explode(':', $authzInfo, 2); - if(count($authzInfo) > 0) - try { - $authInfo = $this->rpcWrapper->procedure( - 'hanyuu:oauth2:attemptAppAuth', - ['clientId' => array_shift($authzInfo), 'clientSecret' => array_shift($authzInfo) ?? ''] - ); - } catch(RuntimeException $ex) {} - } - } elseif(strcasecmp($authzMethod, 'bearer') === 0) { - try { - $authInfo = $this->rpcWrapper->procedure( - 'hanyuu:oauth2:getTokenInfo', - ['type' => 'Bearer', 'token' => $authzInfo] - ); - } catch(RuntimeException $ex) {} - } - } - - if($authInfo !== null) - $this->authInfo = new OAuth2AuthInfo($authInfo); - } } diff --git a/src/AuthzContext.php b/src/AuthzContext.php new file mode 100644 index 0000000..3459cf4 --- /dev/null +++ b/src/AuthzContext.php @@ -0,0 +1,267 @@ +authed; + } + + public function hasError(): bool { + return $this->error !== ''; + } + + public function getError(): string { + return $this->error; + } + + public function getMethod(): string { + return $this->method; + } + + public function isBasicAuth(): bool { + return strcasecmp('basic', $this->method) === 0; + } + + public function isBearerAuth(): bool { + return strcasecmp('bearer', $this->method) === 0; + } + + public function isMisuzuAuth(): bool { + return strcasecmp('misuzu', $this->method) === 0; + } + + public function getType(): string { + return $this->type; + } + + public function isRealUser(): bool { + return strcasecmp('user', $this->type) === 0; + } + + public function isAppUser(): bool { + return strcasecmp('app', $this->type) === 0; + } + + public function isUser(): bool { + return $this->isRealUser() + || $this->isAppUser(); + } + + public function isPublicApp(): bool { + return strcasecmp('pubapp', $this->type) === 0; + } + + public function isConfidentialApp(): bool { + return strcasecmp('confapp', $this->type) === 0; + } + + public function isApp(): bool { + return $this->isPublicApp() + || $this->isConfidentialApp(); + } + + public function getAppId(): string { + return $this->appId; + } + + public function getUserId(): string { + return $this->userId; + } + + public function hasWildcardScope(): bool { + return $this->scope === null; + } + + public function getScopeString(): string { + return $this->scope === null ? '' : implode(' ', $this->scope); + } + + public function getScopeArray(): array { + return $this->scope ?? []; + } + + public function hasScope(string $scope): bool { + return $this->scope === null + || in_array($scope, $this->scope); + } + + public function getExpiresAt(): int { + return $this->expires ?? PHP_INT_MAX; + } + + public function hasExpired(): bool { + return $this->expires !== null + && $this->expires <= time(); + } + + private function handleRpcResponse(mixed $info): void { + if(!is_array($info)) { + $this->error = json_encode($info); + return; + } + + if(array_key_exists('method', $info) && is_string($info['method'])) + $this->method = $info['method']; + + if(array_key_exists('error', $info) && is_string($info['error'])) + $this->error = $info['error']; + + if(array_key_exists('type', $info) && is_string($info['type'])) + $this->type = $info['type']; + + if(array_key_exists('user', $info) && is_string($info['user'])) + $this->userId = $info['user']; + + if(array_key_exists('app', $info) && is_string($info['app'])) + $this->appId = $info['app']; + + if(array_key_exists('scope', $info) && is_array($info['scope'])) + $this->scope = $info['scope']; + + if(array_key_exists('expires', $info) && is_array($info['expires'])) + $this->expires = $info['expires']; + } + + private function attemptBasicAppAuthInternal(string $remoteAddr, string $clientId, string $clientSecret = ''): void { + try { + $this->handleRpcResponse($this->rpcClient->procedure('hanyuu:oauth2:attemptAppAuth', [ + 'remoteAddr' => $remoteAddr, + 'clientId' => $clientId, + 'clientSecret' => $clientSecret, + ])); + } catch(RuntimeException $ex) { + $this->error = 'rpc'; + } + } + + public function attemptBasicAppAuth(string $remoteAddr, string $clientId, string $clientSecret = ''): void { + if($this->authed) + return; + $this->authed = true; + $this->attemptBasicAppAuthInternal($remoteAddr, $clientId, $clientSecret); + } + + public function basicAppAuthMiddleware($response, $request): void { + if($this->authed) + return; + + $header = explode(' ', (string)$request->getHeaderLine('Authorization'), 2); + if(strcasecmp('basic', $header[0]) !== 0) + return; + + $this->authed = true; + $this->method = $header[0]; + + $parts = base64_decode($header[1] ?? ''); + if($parts === false) { + $this->error = 'format'; + return; + } + + $parts = explode(':', $parts, 2); + $this->attemptBasicAppAuthInternal( + (string)filter_input(INPUT_SERVER, 'REMOTE_ADDR'), + $parts[0], + $parts[1] ?? '' + ); + } + + private function attemptBearerTokenAuthInternal(string $remoteAddr, string $bearerToken): void { + try { + $this->handleRpcResponse($this->rpcClient->procedure( + 'hanyuu:oauth2:attemptBearerAuth', + ['remoteAddr' => $remoteAddr, 'token' => $bearerToken] + )); + } catch(RuntimeException $ex) { + $this->error = 'rpc'; + } + } + + public function attemptBearerTokenAuth(string $remoteAddr, string $bearerToken): void { + if($this->authed) + return; + $this->authed = true; + + $this->attemptBearerTokenAuthInternal($remoteAddr, $bearerToken); + } + + public function bearerTokenAuthMiddleware($response, $request): void { + if($this->authed) + return; + + $header = explode(' ', (string)$request->getHeaderLine('Authorization'), 2); + if(strcasecmp('bearer', $header[0]) !== 0) + return; + + $this->authed = true; + $this->method = $header[0]; + + $bearerToken = trim($header[1] ?? ''); + if($bearerToken === '') { + $this->error = 'format'; + return; + } + + $this->attemptBearerTokenAuthInternal( + (string)filter_input(INPUT_SERVER, 'REMOTE_ADDR'), + $bearerToken + ); + } + + private function attemptMisuzuTokenAuthInternal(string $remoteAddr, string $misuzuToken): void { + try { + $this->handleRpcResponse($this->rpcClient->procedure( + 'misuzu:auth:attemptMisuzuAuth', + ['remoteAddr' => $remoteAddr, 'token' => $misuzuToken] + )); + } catch(RuntimeException $ex) { + $this->error = 'rpc'; + } + } + + public function attemptMisuzuTokenAuth(string $remoteAddr, string $misuzuToken): void { + if($this->authed) + return; + $this->authed = true; + + $this->attemptMisuzuTokenAuthInternal($remoteAddr, $misuzuToken); + } + + public function misuzuTokenAuthMiddleware($response, $request) { + if($this->authed) + return; + + $header = explode(' ', (string)$request->getHeaderLine('Authorization'), 2); + if(strcasecmp('misuzu', $header[0]) !== 0) + return; + + $this->authed = true; + $this->method = $header[0]; + + $misuzuToken = trim($header[1] ?? ''); + if($misuzuToken === '') { + $this->error = 'format'; + return; + } + + $this->attemptMisuzuTokenAuthInternal( + (string)filter_input(INPUT_SERVER, 'REMOTE_ADDR'), + $misuzuToken + ); + } +} diff --git a/src/OAuth2/OAuth2Routes.php b/src/OAuth2/OAuth2Routes.php index 2df027b..d61c42e 100644 --- a/src/OAuth2/OAuth2Routes.php +++ b/src/OAuth2/OAuth2Routes.php @@ -4,7 +4,7 @@ namespace Aleister\OAuth2; use RuntimeException; use Aleister\AleisterContext; use Aleister\RpcModels\Hanyuu\{OAuth2AuthInfo,OAuth2RfcModel}; -use Index\Http\Routing\{HttpGet,HttpOptions,HttpPost,RouteHandler}; +use Index\Http\Routing\{HttpGet,HttpOptions,HttpPost,IRouter,RouteHandler}; class OAuth2Routes extends RouteHandler { public const REQ_AUTH_ERRORS = [ @@ -16,6 +16,11 @@ class OAuth2Routes extends RouteHandler { private AleisterContext $ctx ) {} + public function registerRoutes(IRouter $router): void { + $router->use('/', $this->ctx->getAuthzContext()->basicAppAuthMiddleware(...)); + parent::registerRoutes($router); + } + private static function filter($response, array $result, bool $authzHeader = false): array { if(array_key_exists('error', $result)) { if($authzHeader) { @@ -36,8 +41,8 @@ class OAuth2Routes extends RouteHandler { public function postRequestAuthorise($response, $request) { $response->setHeader('Cache-Control', 'no-store'); - $authInfo = $this->ctx->getAuthInfo(); - if($authInfo->getError() === 'secret') + $authz = $this->ctx->getAuthzContext(); + if($authz->getError() === 'secret') return self::filter($response, [ 'error' => 'invalid_client', 'error_description' => 'Provided client secret is not correct for this application.', @@ -52,14 +57,10 @@ class OAuth2Routes extends RouteHandler { $rpc = $this->ctx->getRpcClient(); $content = $request->getContent(); - if($authInfo->getMethod() === '') - try { - $authInfo = new OAuth2AuthInfo($rpc->procedure('hanyuu:oauth2:attemptAppAuth', [ - 'clientId' => (string)$content->getParam('client_id'), - ])); - } catch(RuntimeException $ex) {} + if(!$authz->hasAuthed()) + $authz->attemptBasicAppAuth((string)$content->getParam('client_id')); - if(!$authInfo->isAppUser()) + if(!$authz->isApp()) return self::filter($response, [ 'error' => 'invalid_client', 'error_description' => 'App authentication failed.', @@ -67,7 +68,7 @@ class OAuth2Routes extends RouteHandler { try { $reqInfo = new OAuth2RfcModel($rpc->procedure('hanyuu:oauth2:createAuthoriseRequest', [ - 'appId' => $authInfo->getAppId(), + 'appId' => $authz->getAppId(), 'scope' => (string)$content->getParam('scope'), ])); } catch(RuntimeException $ex) {} @@ -118,31 +119,31 @@ class OAuth2Routes extends RouteHandler { $rpc = $this->ctx->getRpcClient(); $content = $request->getContent(); - $authInfo = $this->ctx->getAuthInfo(); - if($authInfo->getMethod() === '') - try { - $authInfo = new OAuth2AuthInfo($rpc->procedure('hanyuu:oauth2:attemptAppAuth', [ - 'clientId' => (string)$content->getParam('client_id'), - 'clientSecret' => (string)$content->getParam('client_secret') - ])); - } catch(RuntimeException $ex) { - $authInfo = null; - } + $authz = $this->ctx->getAuthzContext(); + $authzHeader = true; + if(!$authz->hasAuthed()) { + $authzHeader = false; + $authz->attemptBasicAppAuth( + (string)$content->getParam('client_id'), + (string)$content->getParam('client_secret') + ); + } - if($authInfo === null || !$authInfo->isAppUser()) + if(!$authz->isApp()) return self::filter($response, [ 'error' => 'invalid_client', 'error_description' => 'App authentication failed.', ]); - if($authInfo->getError() === 'secret') + + if($authz->getError() === 'secret') return self::filter($response, [ 'error' => 'invalid_client', 'error_description' => 'Provided client secret is not correct for this application.' - ], authzHeader: true); + ], authzHeader: $authzHeader); try { $name = ''; - $args = ['appId' => $authInfo->getAppId(), 'isAuthed' => $authInfo->isAuthed()]; + $args = ['appId' => $authz->getAppId(), 'isAuthed' => $authz->isConfidentialApp()]; switch($content->getParam('grant_type')) { case 'authorization_code': diff --git a/src/V1/V1Context.php b/src/V1/V1Context.php index faa4caf..92051e2 100644 --- a/src/V1/V1Context.php +++ b/src/V1/V1Context.php @@ -1,8 +1,7 @@ ctx->getRpcClient(); } - public function getAuthInfo(): OAuth2AuthInfo { - return $this->ctx->getAuthInfo(); + public function getAuthzContext(): AuthzContext { + return $this->ctx->getAuthzContext(); } } diff --git a/src/V1/V1Routes.php b/src/V1/V1Routes.php index ec8db88..9207459 100644 --- a/src/V1/V1Routes.php +++ b/src/V1/V1Routes.php @@ -11,6 +11,10 @@ class V1Routes implements IRouteHandler { ) {} public function registerRoutes(IRouter $router): void { + $authz = $this->ctx->getAuthzContext(); + $router->use('/', $authz->bearerTokenAuthMiddleware(...)); + $router->use('/', $authz->misuzuTokenAuthMiddleware(...)); + $router->get('/', fn() => ['status' => 'operational']); } }