* @license MIT; see LICENSE.txt **/ namespace Webmasterskaya\Component\OauthServer\Site\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Application\SiteApplication; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Log\Log; use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\CMS\Router\Route; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\UserFactoryInterface; use Joomla\Input\Input; use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequestFactory; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\AuthCodeGrant; use League\OAuth2\Server\Grant\ClientCredentialsGrant; use League\OAuth2\Server\Grant\ImplicitGrant; use League\OAuth2\Server\Grant\RefreshTokenGrant; use League\OAuth2\Server\RequestEvent as LeagueRequestEvent; use League\OAuth2\Server\ResourceServer; use Webmasterskaya\Component\OauthServer\Administrator\Event\RequestAccessTokenEvent; use Webmasterskaya\Component\OauthServer\Administrator\Event\RequestEvent; use Webmasterskaya\Component\OauthServer\Administrator\Event\RequestRefreshTokenEvent; use Webmasterskaya\Component\OauthServer\Administrator\Event\ResolveTokenRequestEvent; use Webmasterskaya\Component\OauthServer\Administrator\Model\AccessTokenModel; use Webmasterskaya\Component\OauthServer\Administrator\Model\AuthCodeModel; use Webmasterskaya\Component\OauthServer\Administrator\Model\ClientModel; use Webmasterskaya\Component\OauthServer\Administrator\Model\RefreshTokenModel; use Webmasterskaya\Component\OauthServer\Site\Entity\User; use Webmasterskaya\Component\OauthServer\Site\Repository\AccessTokenRepository; use Webmasterskaya\Component\OauthServer\Site\Repository\AuthCodeRepository; use Webmasterskaya\Component\OauthServer\Site\Repository\ClientRepository; use Webmasterskaya\Component\OauthServer\Site\Repository\RefreshTokenRepository; use Webmasterskaya\Component\OauthServer\Site\Repository\ScopeRepository; \defined('_JEXEC') or die; class LoginController extends BaseController { private AuthorizationServer $authorizationServer; /** * @throws \Exception * @since version */ public function __construct($config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null) { parent::__construct($config, $factory, $app, $input); $this->setupAuthorizationServer(); } /** * @return void * @throws \Exception * @since version */ private function setupAuthorizationServer() { if (isset($authorizationServer)) { return; } // Init our repositories /** * @var ClientModel $clientModel * @var AccessTokenModel $accessTokenModel * @var AuthCodeModel $authCodeModel * @var RefreshTokenModel $refreshTokenModel */ $clientModel = $this->factory->createModel('Client', 'Administrator', ['request_ignore' => true]); $clientRepository = new ClientRepository($clientModel); $accessTokenModel = $this->factory->createModel('AccessToken', 'Administrator', ['request_ignore' => true]); $accessTokenRepository = new AccessTokenRepository($accessTokenModel, $clientModel); $scopeRepository = new ScopeRepository($clientModel); $scopeRepository->setDispatcher($this->getDispatcher()); $authCodeModel = $this->factory->createModel('AuthCode', 'Administrator', ['request_ignore' => true]); $authCodeRepository = new AuthCodeRepository($authCodeModel, $clientModel); $refreshTokenModel = $this->factory->createModel('RefreshToken', 'Administrator', ['request_ignore' => true]); $refreshTokenRepository = new RefreshTokenRepository($refreshTokenModel, $accessTokenModel); $params = ComponentHelper::getParams('com_oauthserver'); if ($params->get('key_method_paste')) { $private_key = $params->get('private_key_raw'); } else { $private_key = $params->get('private_key_path'); } if (!!($private_key_passphrase = $params->get('private_key_passphrase'))) { $private_key = new CryptKey($private_key, $private_key_passphrase); } $encryption_key = $this->app->get('secret'); $server = new AuthorizationServer( $clientRepository, $accessTokenRepository, $scopeRepository, $private_key, $encryption_key ); $access_token_ttl = $params->get('access_token_ttl', 'PT1H'); if (!!$params->get('enable_auth_code_grant', true)) { $grant = new AuthCodeGrant( $authCodeRepository, $refreshTokenRepository, new \DateInterval($params->get('auth_code_ttl', 'PT10M')) // authorization codes will expire after 10 minutes ); $grant->setRefreshTokenTTL(new \DateInterval($params->get('refresh_token_ttl', 'P1M'))); $server->enableGrantType( $grant, new \DateInterval($access_token_ttl) ); } if (!!$params->get('enable_refresh_token_grant', false)) { $grant = new RefreshTokenGrant($refreshTokenRepository); $grant->setRefreshTokenTTL(new \DateInterval($params->get('refresh_token_ttl', 'P1M'))); $server->enableGrantType( $grant, new \DateInterval($access_token_ttl) ); } if (!!$params->get('enable_client_credentials_grant', false)) { $server->enableGrantType( new ClientCredentialsGrant(), new \DateInterval($access_token_ttl) ); } if (!!$params->get('enable_implicit_grant', false)) { $server->enableGrantType( new ImplicitGrant(new \DateInterval($access_token_ttl)), new \DateInterval($access_token_ttl) ); } $server->getEmitter() ->addListener( LeagueRequestEvent::USER_AUTHENTICATION_FAILED, function (LeagueRequestEvent $event) { $name = 'onOauthUserAuthenticationFailed'; $this->getDispatcher()->dispatch($name, new RequestEvent( $name, ['request' => $event->getRequest()] )); } )->addListener( LeagueRequestEvent::CLIENT_AUTHENTICATION_FAILED, function (LeagueRequestEvent $event) { $name = 'onOauthClientAuthenticationFailed'; $this->getDispatcher()->dispatch($name, new RequestEvent( $name, ['request' => $event->getRequest()] )); } )->addListener( LeagueRequestEvent::REFRESH_TOKEN_CLIENT_FAILED, function (LeagueRequestEvent $event) { $name = 'onOauthRefreshTokenClientFailed'; $this->getDispatcher()->dispatch($name, new RequestEvent( $name, ['request' => $event->getRequest()] )); } )->addListener( LeagueRequestEvent::REFRESH_TOKEN_ISSUED, function (LeagueRequestEvent $event) { /** @var \League\OAuth2\Server\RequestRefreshTokenEvent $event */ $name = 'onOauthRefreshTokenIssued'; $this->getDispatcher()->dispatch($name, new RequestRefreshTokenEvent( $name, [ 'request' => $event->getRequest(), 'refreshToken' => $event->getRefreshToken() ] )); } )->addListener( LeagueRequestEvent::ACCESS_TOKEN_ISSUED, function (LeagueRequestEvent $event) { /** @var \League\OAuth2\Server\RequestAccessTokenEvent $event */ $name = 'onOauthAccessTokenIssued'; $this->getDispatcher()->dispatch($name, new RequestAccessTokenEvent( $name, [ 'request' => $event->getRequest(), 'accessToken' => $event->getAccessToken() ] )); } ); $this->authorizationServer = $server; } /** * @return LoginController * @throws OAuthServerException * @since version */ public function authorize(): static { $app = $this->app; $input = $app->getInput(); $user = $app->getIdentity(); $uri = Uri::getInstance(); $state_prefix = 'oauthserver.login.authorize.request'; // Create PSR-7 Request object and store all query params in user state, to use it after user login is it required. $serverRequest = (new ServerRequest([], [], $app->getUserState("$state_prefix.uri", (string) $uri))) ->withQueryParams([ 'response_type' => $app->getUserStateFromRequest("$state_prefix.response_type", 'response_type'), 'client_id' => $app->getUserStateFromRequest("$state_prefix.client_id", 'client_id', $input->server->get('PHP_AUTH_USER')), 'redirect_uri' => $app->getUserStateFromRequest("$state_prefix.redirect_uri", 'redirect_uri'), 'scope' => $app->getUserStateFromRequest("$state_prefix.scope", 'scope'), 'code_challenge' => $app->getUserStateFromRequest("$state_prefix.code_challenge", 'code_challenge'), 'code_challenge_method' => $app->getUserStateFromRequest("$state_prefix.code_challenge_method", 'code_challenge_method', 'plain'), 'state' => $app->getUserStateFromRequest("$state_prefix.state", 'state', ''), ]); if (!$user->id) { if ($app->getUserState("$state_prefix.uri") === null) { $app->setUserState("$state_prefix.uri", (string) $uri); } // Build the cleared current uri and encode to pass it to the login form as a callback uri. $return = http_build_query(['return' => base64_encode($uri->toString(['scheme', 'user', 'pass', 'host', 'port', 'path']))]); $redirect = Route::_('index.php?option=com_users&view=login&' . $return); // The current page is not tied to any menu item, so the main page item id will be added to the route. It needs to be removed. $redirect = preg_replace('/((&|&)itemid=\d+)/i', '', $redirect); $app->enqueueMessage('Необходимо авторизоваться!'); $app->redirect($redirect); return $this; } // Clean user state after login checks $app->setUserState($state_prefix, null); try { $server = $this->authorizationServer; // Validate the HTTP request and return an AuthorizationRequest object. $authRequest = $server->validateAuthorizationRequest($serverRequest); // The auth request object can be serialized and saved into a user's session. // You will probably want to redirect the user at this point to a login endpoint. // Once the user has logged in set the user on the AuthorizationRequest $authRequest->setUser(new User($user)); // an instance of UserEntityInterface // At this point you should redirect the user to an authorization page. // This form will ask the user to approve the client and the scopes requested. // Once the user has approved or denied the client update the status // (true = approved, false = denied) $authRequest->setAuthorizationApproved(true); $app->setResponse($server->completeAuthorizationRequest($authRequest, $app->getResponse())); } catch (OAuthServerException $e) { $this->handleOAuthServerException($e); } return $this; } /** * @return LoginController * @throws \Exception * @since version */ public function token(): static { $server = $this->authorizationServer; $serverRequest = ServerRequestFactory::fromGlobals(); try { $response = $this->app->getResponse(); $response = $server->respondToAccessTokenRequest($serverRequest, $response); $event = new ResolveTokenRequestEvent('onResolveTokenRequest', ['response' => $response]); $this->getDispatcher()->dispatch($event->getName(), $event); $this->app->setResponse($event->getArgument('response')); echo $response->getBody(); } catch (OAuthServerException $e) { $this->handleOAuthServerException($e); } return $this; } /** * @throws OAuthServerException * @throws \Exception * @since version */ public function profile(): LoginController { /** * @var ClientModel $clientModel * @var AccessTokenModel $accessTokenModel */ $clientModel = $this->factory->createModel('Client', 'Administrator', ['request_ignore' => true]); $accessTokenModel = $this->factory->createModel('AccessToken', 'Administrator', ['request_ignore' => true]); $accessTokenRepository = new AccessTokenRepository($accessTokenModel, $clientModel); $params = ComponentHelper::getParams('com_oauthserver'); if ($params->get('key_method_paste')) { $public_key = $params->get('public_key_raw'); } else { $public_key = $params->get('public_key_path'); } $server = new ResourceServer($accessTokenRepository, $public_key); $request = ServerRequestFactory::fromGlobals(); $request = $server->validateAuthenticatedRequest($request); /** @var \Joomla\CMS\User\User $user */ $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($request->getAttribute('oauth_user_id')); $this->app->loadIdentity($user); $data = [ 'full_name' => $user->name, 'email' => $user->email, 'login' => $user->username, 'id' => $user->id, ]; echo json_encode($data); return $this; } protected function handleOAuthServerException(OAuthServerException $exception) { /** @var SiteApplication $app */ $app = $this->app; $app->setResponse($exception->generateHttpResponse($app->getResponse())); $message = $exception->getMessage(); if (($hint = $exception->getHint()) !== null) { $message .= ' ' . $hint; } Log::add($message, Log::ERROR, 'com_oauthserver'); throw new \RuntimeException($message, $exception->getHttpStatusCode(), $exception); } }