Slight auth revamp.

This commit is contained in:
flash 2024-08-25 23:04:11 +00:00
parent adb24dae9f
commit bda95c747c
5 changed files with 306 additions and 70 deletions

View file

@ -9,16 +9,16 @@ use Syokuhou\IConfig;
class AleisterContext { class AleisterContext {
private RpcClientWrapper $rpcWrapper; private RpcClientWrapper $rpcWrapper;
private HttpRouter $router; private HttpRouter $router;
private AuthzContext $authz;
private OAuth2AuthInfo $authInfo;
public function __construct( public function __construct(
private IConfig $config private IConfig $config
) { ) {
$this->authInfo = new OAuth2AuthInfo(['error' => 'none']);
$this->rpcWrapper = new RpcClientWrapper; $this->rpcWrapper = new RpcClientWrapper;
$this->rpcWrapper->createHmacConfig($config, 'hanyuu'); $this->rpcWrapper->createHmacConfig($config, 'hanyuu');
$this->rpcWrapper->createHmacConfig($config, 'misuzu');
$this->authz = new AuthzContext($this->rpcWrapper);
$this->router = new HttpRouter( $this->router = new HttpRouter(
errorHandler: 'plain', errorHandler: 'plain',
@ -26,7 +26,6 @@ class AleisterContext {
); );
$this->router->use('/', fn($resp) => $resp->setPoweredBy('Flashii')); $this->router->use('/', fn($resp) => $resp->setPoweredBy('Flashii'));
$this->router->use('/', $this->handleAcceptHeader(...)); $this->router->use('/', $this->handleAcceptHeader(...));
$this->router->use('/', $this->handleAuthzHeader(...));
$this->router->get('/', fn() => '<!doctype html>Hello! Someday this page will probably redirect to documentation, but none exists yet.'); $this->router->get('/', fn() => '<!doctype html>Hello! Someday this page will probably redirect to documentation, but none exists yet.');
$this->router->scopeTo('/oauth2')->register(new OAuth2\OAuth2Routes($this)); $this->router->scopeTo('/oauth2')->register(new OAuth2\OAuth2Routes($this));
@ -41,8 +40,8 @@ class AleisterContext {
return $this->router; return $this->router;
} }
public function getAuthInfo(): OAuth2AuthInfo { public function getAuthzContext(): AuthzContext {
return $this->authInfo; return $this->authz;
} }
public function handleAcceptHeader($response, $request) { public function handleAcceptHeader($response, $request) {
@ -55,38 +54,4 @@ class AleisterContext {
$this->router->setErrorHandler(new AleisterErrorHandler($contentHandler)); $this->router->setErrorHandler(new AleisterErrorHandler($contentHandler));
$this->router->registerContentHandler($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);
}
} }

267
src/AuthzContext.php Normal file
View file

@ -0,0 +1,267 @@
<?php
namespace Aleister;
use Aiwass\Client\IRpcClient;
class AuthzContext {
private bool $authed = false;
private string $error = '';
private string $method = '';
private string $type = '';
private string $userId = '';
private string $appId = '';
private ?array $scope = null;
private ?int $expires = null;
public function __construct(
private IRpcClient $rpcClient
) {}
public function hasAuthed(): bool {
return $this->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
);
}
}

View file

@ -4,7 +4,7 @@ namespace Aleister\OAuth2;
use RuntimeException; use RuntimeException;
use Aleister\AleisterContext; use Aleister\AleisterContext;
use Aleister\RpcModels\Hanyuu\{OAuth2AuthInfo,OAuth2RfcModel}; 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 { class OAuth2Routes extends RouteHandler {
public const REQ_AUTH_ERRORS = [ public const REQ_AUTH_ERRORS = [
@ -16,6 +16,11 @@ class OAuth2Routes extends RouteHandler {
private AleisterContext $ctx 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 { private static function filter($response, array $result, bool $authzHeader = false): array {
if(array_key_exists('error', $result)) { if(array_key_exists('error', $result)) {
if($authzHeader) { if($authzHeader) {
@ -36,8 +41,8 @@ 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');
$authInfo = $this->ctx->getAuthInfo(); $authz = $this->ctx->getAuthzContext();
if($authInfo->getError() === 'secret') if($authz->getError() === 'secret')
return self::filter($response, [ return self::filter($response, [
'error' => 'invalid_client', 'error' => 'invalid_client',
'error_description' => 'Provided client secret is not correct for this application.', 'error_description' => 'Provided client secret is not correct for this application.',
@ -52,14 +57,10 @@ class OAuth2Routes extends RouteHandler {
$rpc = $this->ctx->getRpcClient(); $rpc = $this->ctx->getRpcClient();
$content = $request->getContent(); $content = $request->getContent();
if($authInfo->getMethod() === '') if(!$authz->hasAuthed())
try { $authz->attemptBasicAppAuth((string)$content->getParam('client_id'));
$authInfo = new OAuth2AuthInfo($rpc->procedure('hanyuu:oauth2:attemptAppAuth', [
'clientId' => (string)$content->getParam('client_id'),
]));
} catch(RuntimeException $ex) {}
if(!$authInfo->isAppUser()) if(!$authz->isApp())
return self::filter($response, [ return self::filter($response, [
'error' => 'invalid_client', 'error' => 'invalid_client',
'error_description' => 'App authentication failed.', 'error_description' => 'App authentication failed.',
@ -67,7 +68,7 @@ class OAuth2Routes extends RouteHandler {
try { try {
$reqInfo = new OAuth2RfcModel($rpc->procedure('hanyuu:oauth2:createAuthoriseRequest', [ $reqInfo = new OAuth2RfcModel($rpc->procedure('hanyuu:oauth2:createAuthoriseRequest', [
'appId' => $authInfo->getAppId(), 'appId' => $authz->getAppId(),
'scope' => (string)$content->getParam('scope'), 'scope' => (string)$content->getParam('scope'),
])); ]));
} catch(RuntimeException $ex) {} } catch(RuntimeException $ex) {}
@ -118,31 +119,31 @@ class OAuth2Routes extends RouteHandler {
$rpc = $this->ctx->getRpcClient(); $rpc = $this->ctx->getRpcClient();
$content = $request->getContent(); $content = $request->getContent();
$authInfo = $this->ctx->getAuthInfo(); $authz = $this->ctx->getAuthzContext();
if($authInfo->getMethod() === '') $authzHeader = true;
try { if(!$authz->hasAuthed()) {
$authInfo = new OAuth2AuthInfo($rpc->procedure('hanyuu:oauth2:attemptAppAuth', [ $authzHeader = false;
'clientId' => (string)$content->getParam('client_id'), $authz->attemptBasicAppAuth(
'clientSecret' => (string)$content->getParam('client_secret') (string)$content->getParam('client_id'),
])); (string)$content->getParam('client_secret')
} catch(RuntimeException $ex) { );
$authInfo = null;
} }
if($authInfo === null || !$authInfo->isAppUser()) if(!$authz->isApp())
return self::filter($response, [ return self::filter($response, [
'error' => 'invalid_client', 'error' => 'invalid_client',
'error_description' => 'App authentication failed.', 'error_description' => 'App authentication failed.',
]); ]);
if($authInfo->getError() === 'secret')
if($authz->getError() === 'secret')
return self::filter($response, [ return self::filter($response, [
'error' => 'invalid_client', 'error' => 'invalid_client',
'error_description' => 'Provided client secret is not correct for this application.' 'error_description' => 'Provided client secret is not correct for this application.'
], authzHeader: true); ], authzHeader: $authzHeader);
try { try {
$name = ''; $name = '';
$args = ['appId' => $authInfo->getAppId(), 'isAuthed' => $authInfo->isAuthed()]; $args = ['appId' => $authz->getAppId(), 'isAuthed' => $authz->isConfidentialApp()];
switch($content->getParam('grant_type')) { switch($content->getParam('grant_type')) {
case 'authorization_code': case 'authorization_code':

View file

@ -1,8 +1,7 @@
<?php <?php
namespace Aleister\V1; namespace Aleister\V1;
use Aleister\AleisterContext; use Aleister\{AleisterContext,AuthzContext};
use Aleister\RpcModels\Hanyuu\OAuth2AuthInfo;
use Aiwass\Client\IRpcClient; use Aiwass\Client\IRpcClient;
class V1Context { class V1Context {
@ -14,7 +13,7 @@ class V1Context {
return $this->ctx->getRpcClient(); return $this->ctx->getRpcClient();
} }
public function getAuthInfo(): OAuth2AuthInfo { public function getAuthzContext(): AuthzContext {
return $this->ctx->getAuthInfo(); return $this->ctx->getAuthzContext();
} }
} }

View file

@ -11,6 +11,10 @@ class V1Routes implements IRouteHandler {
) {} ) {}
public function registerRoutes(IRouter $router): void { public function registerRoutes(IRouter $router): void {
$authz = $this->ctx->getAuthzContext();
$router->use('/', $authz->bearerTokenAuthMiddleware(...));
$router->use('/', $authz->misuzuTokenAuthMiddleware(...));
$router->get('/', fn() => ['status' => 'operational']); $router->get('/', fn() => ['status' => 'operational']);
} }
} }