Slight auth revamp.
This commit is contained in:
parent
adb24dae9f
commit
bda95c747c
5 changed files with 306 additions and 70 deletions
|
@ -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() => '<!doctype html>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);
|
||||
}
|
||||
}
|
||||
|
|
267
src/AuthzContext.php
Normal file
267
src/AuthzContext.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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':
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<?php
|
||||
namespace Aleister\V1;
|
||||
|
||||
use Aleister\AleisterContext;
|
||||
use Aleister\RpcModels\Hanyuu\OAuth2AuthInfo;
|
||||
use Aleister\{AleisterContext,AuthzContext};
|
||||
use Aiwass\Client\IRpcClient;
|
||||
|
||||
class V1Context {
|
||||
|
@ -14,7 +13,7 @@ class V1Context {
|
|||
return $this->ctx->getRpcClient();
|
||||
}
|
||||
|
||||
public function getAuthInfo(): OAuth2AuthInfo {
|
||||
return $this->ctx->getAuthInfo();
|
||||
public function getAuthzContext(): AuthzContext {
|
||||
return $this->ctx->getAuthzContext();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue