wiki-grav/plugins/login-oauth2/login-oauth2.php

491 lines
17 KiB
PHP

<?php
namespace Grav\Plugin;
use Composer\Autoload\ClassLoader;
use Exception;
use Grav\Common\Data\Data;
use Grav\Common\Debugger;
use Grav\Common\Language\Language;
use Grav\Common\Plugin;
use Grav\Common\Session;
use Grav\Common\Uri;
use Grav\Common\User\Interfaces\UserCollectionInterface;
use Grav\Plugin\Login\Events\UserLoginEvent;
use Grav\Plugin\Login\Login;
use Grav\Plugin\Login\OAuth2\OAuth2;
use Grav\Plugin\Login\OAuth2\ProviderFactory;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\Session\Message;
use RuntimeException;
/**
* Class GravPluginLoginOauth2Plugin
* @package Grav\Plugin
*/
class LoginOauth2Plugin extends Plugin
{
/** @var bool */
protected $admin = false;
protected $debug = false;
/**
* @return array
*
* The getSubscribedEvents() gives the core a list of events
* that the plugin wants to listen to. The key of each
* array section is the event that the plugin listens to
* and the value (in the form of an array) contains the
* callable (or function) as well as the priority. The
* higher the number the higher the priority.
*/
public static function getSubscribedEvents(): array
{
return [
'onPluginsInitialized' => [
['onPluginsInitialized', 0]
],
];
}
/**
* [onPluginsInitialized:100000] Composer autoload.
*
* @return ClassLoader
*/
public function autoload(): ClassLoader
{
return require __DIR__ . '/vendor/autoload.php';
}
public function onTwigLoader(): void
{
$media_paths = $this->grav['locator']->findResources('plugins://login-oauth2/media');
foreach(array_reverse($media_paths) as $images_path) {
$this->grav['twig']->addPath($images_path, 'oauth2-media');
}
}
/**
* [onTwigTemplatePaths] Add twig paths to plugin templates.
*/
public function onTwigTemplatePaths(): void
{
$twig = $this->grav['twig'];
$twig->twig_paths[] = __DIR__ . '/templates';
}
public function onTwigSiteVariables(): void
{
// add CSS for frontend if required
if ((!$this->isAdmin() && $this->config->get('plugins.login-oauth2.built_in_css')) ||
($this->admin && $this->config->get('plugins.login-oauth2.admin.built_in_css'))) {
$this->grav['assets']->add('plugin://login-oauth2/css/login-oauth2.css');
}
}
/**
* Initialize the plugin
*/
public function onPluginsInitialized(): void
{
if ($this->isAdmin()) {
if (!$this->grav['config']->get('plugins.login-oauth2.admin.enabled')) {
// Don't proceed if we are in the admin plugin
return;
}
$this->admin = true;
}
$this->enable([
'onTask.login.oauth2' => ['loginRedirect', 0],
'onTask.callback.oauth2' => ['loginCallback', 0],
'onTask.delete.oauth2' => ['loginDataDelete', 0],
'onTwigLoader' => ['onTwigLoader', 0],
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
'onTwigSiteVariables' => ['onTwigSiteVariables', 0],
'onLoginPage' => ['onLoginPage', 10],
'onUserLoginAuthenticate' => ['userLoginAuthenticate', 1000],
'onUserLoginFailure' => ['userLoginFailure', 0],
'onUserLogin' => ['userLogin', 0],
'onUserLogout' => ['userLogout', 0],
'onOAuth2Username' => ['onOAuth2Username', 0],
]
);
// Check to ensure login plugin is enabled.
if (!$this->config->get('plugins.login.enabled')) {
throw new RuntimeException('The Login plugin needs to be installed and enabled');
}
$this->debug = $this->config->get('plugins.login-oauth2.debug', false);
$isAdmin = $this->admin;
$this->grav['oauth2'] = static function () use ($isAdmin) {
// Add OAuth2 object to Grav
$oauth2 = new OAuth2($isAdmin);
$oauth2->addEnabledProviders();
return $oauth2;
};
}
/**
* Add navigation item to the admin plugin
*/
public function onLoginPage(): void
{
if ($this->grav['oauth2']->getProviders()) {
$this->grav['login']->addProviderLoginTemplate('login-oauth2/login-oauth2.html.twig');
}
}
/**
* Task: login.oauth2
*/
public function loginRedirect(): void
{
/** @var OAuth2 $oauth2 */
$oauth2 = $this->grav['oauth2'];
$user = $this->grav['user'] ?? null;
if ($user && $user->authorized) {
throw new RuntimeException('You have already been logged in', 403);
}
$provider_name = isset($_POST['oauth2']) ? htmlspecialchars(strip_tags($_POST['oauth2']), ENT_QUOTES, 'UTF-8') : null;
if (!isset($provider_name)) {
throw new RuntimeException('Bad Request', 400);
}
if ($oauth2->isValidProvider($provider_name)) {
$provider = ProviderFactory::create($provider_name, $oauth2->getProviderOptions($provider_name));
/** @var Session $session */
$session = $this->grav['session'];
$session->oauth2_state = $provider->getState();
$session->oauth2_provider = $provider_name;
if ($this->isAdmin()) {
$redirect = (string)$this->grav['admin']->request->getUri();
} else {
if ($this->config->get('plugins.login.redirect_after_login')) {
$redirect = (string) $this->config->get('plugins.login.route_after_login');
} else {
/** @var Uri $uri */
$request = $this->grav['request'];
$redirect = (string) $request->getUri();
}
}
$session->redirect_after_login = $redirect;
$authorizationUrl = $provider->getAuthorizationUrl();
$this->grav->redirect($authorizationUrl);
}
}
/**
* Task: callback.oauth2
*/
public function loginCallback(): void
{
/** @var Login $login */
$login = $this->grav['login'];
/** @var OAuth2 $oauth2 */
$oauth2 = $this->grav['oauth2'];
/** @var Session $session */
$session = $this->grav['session'];
$this->debug("session: " . json_encode($session->getAll()));
$provider_name = $session->oauth2_provider;
$login_redirect = $session->redirect_after_login;
/** @var Language $t */
$t = $this->grav['language'];
/** @var Message $messages */
$messages = $this->grav['messages'];
$is_valid = $oauth2->isValidProvider($provider_name);
$this->debug("provider: $provider_name - redirect: $login_redirect - is_valid: $is_valid");
if ($provider_name && $oauth2->isValidProvider($provider_name)) {
$state = isset($_GET['state']) ? htmlspecialchars(strip_tags($_GET['state']), ENT_QUOTES, 'UTF-8') : null;
if (empty($state)) {
$state = isset($_POST['state']) ? htmlspecialchars(strip_tags($_POST['state']), ENT_QUOTES, 'UTF-8') : null;
}
$this->debug("sent state: $state, stored state: $session->oauth2_state");
if (empty($state) || ($state !== $session->oauth2_state)) {
unset($session->oauth2_state);
$this->debug("Error: $session->oauth2_state != $state");
$messages->add($t->translate('PLUGIN_LOGIN.LOGIN_FAILED'), 'error');
} else {
// Fire Login process.
$event = $login->login(
['rememberme' => true],
['admin' => $this->isAdmin(), 'remember_me' => true, 'oauth2' => true, 'provider' => $provider_name],
['authorize' => $this->isAdmin() ? 'admin.login' : 'site.login', 'return_event' => true]);
// Note: session variables have been reset!
$user = $event->getUser();
if ($user->authorized) {
$event->defMessage('PLUGIN_LOGIN.LOGIN_SUCCESSFUL', 'info');
if ($this->isAdmin()) {
$event->defRedirect($login_redirect ?? '/');
} else {
$event->defRedirect(
$login_redirect
?: LoginPlugin::defaultRedirectAfterLogin()
?: $this->grav['uri']->referrer('/')
);
}
} elseif ($user->authenticated) {
$event->defMessage('PLUGIN_LOGIN.ACCESS_DENIED', 'error');
if ($this->isAdmin()) {
$event->defRedirect($login_redirect ?? '/');
} else {
$event->defRedirect($this->grav['config']->get('plugins.login.route_unauthorized', '/'));
}
} else {
$event->defMessage('PLUGIN_LOGIN.LOGIN_FAILED', 'error');
if ($this->isAdmin()) {
$event->defRedirect($login_redirect ?? '/');
} else {
$event->defRedirect($this->grav['config']->get('plugins.login.route', '/'));
}
}
$message = $event->getMessage();
if ($message) {
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addMessage($t->translate($message), 'debug');
$messages->add($t->translate($message), $event->getMessageType());
}
$redirect = $event->getRedirect();
if ($redirect) {
$this->grav->redirect($redirect, $event->getRedirectCode());
}
}
} else {
$this->grav->redirect($login_redirect ?? '/');
}
$uri = $this->grav['uri'];
$redirect = $uri->url(true);
$this->grav->redirect($redirect);
}
function loginDataDelete()
{
/** @var Login $login */
$login = $this->grav['login'];
/** @var OAuth2 $oauth2 */
$oauth2 = $this->grav['oauth2'];
/** @var Session $session */
$session = $this->grav['session'];
$this->debug("session: " . json_encode($session->getAll()));
$provider_name = $session->oauth2_provider;
$login_redirect = $session->redirect_after_login;
/** @var Language $t */
$t = $this->grav['language'];
/** @var Message $messages */
$messages = $this->grav['messages'];
$is_valid = $oauth2->isValidProvider($provider_name);
$this->debug("provider: $provider_name - redirect: $login_redirect - is_valid: $is_valid");
}
public function userLoginAuthenticate(UserLoginEvent $event): void
{
// Second parameter of Login::login() call.
$options = $event->getOptions();
if (isset($options['oauth2'])) {
$code = isset($_GET['code']) ? htmlspecialchars(strip_tags($_GET['code']), ENT_QUOTES, 'UTF-8') : null;
if (!$code) {
$code = isset($_POST['code']) ? htmlspecialchars(strip_tags($_POST['code']), ENT_QUOTES, 'UTF-8') : null;
}
$provider_name = $options['provider'];
$provider = ProviderFactory::create($provider_name, $options);
try {
// Try to get an access token (using the authorization code grant)
$token = $provider->getAccessToken('authorization_code', ['code' => $code]);
// We got an access token, let's now get the user's details
$user = $provider->getResourceOwner($token);
$userdata = $provider->getUserData($user);
$userdata_event = new Event(
[
'userdata' => $userdata,
'oauth2user' => $user,
'provider' => $provider,
'token' => $token
]
);
$this->grav->fireEvent('onOAuth2Userdata', $userdata_event);
// Set again with any event-based modifications
$userdata = $userdata_event['userdata'];
$username_event = new Event(
[
'userdata' => $userdata,
'oauth2user' => $user,
'provider' => $provider,
'token' => $token
]
);
// Get username from an event to allow you to modify oauth2 filename
$this->grav->fireEvent('onOAuth2Username', $username_event);
$username = $username_event['username'];
/** @var UserCollectionInterface $accounts */
$accounts = $this->grav['accounts'];
$grav_user = $accounts->load($username);
// If username cannot be found, fall back to email address.
$exists = $grav_user->exists();
if (!$exists) {
$found_user = $accounts->find($userdata['email'], ['email']);
if ($found_user->exists()) {
$grav_user = $found_user;
$exists = true;
}
}
// Make sure we're using the same provider, multiple providers are not supported.
if ($exists) {
$provider_test = $grav_user->get('provider');
if ($provider_test && $provider_test !== $provider_name) {
throw new RuntimeException($this->translate('PLUGIN_LOGIN_OAUTH2.ERROR_EXISTING_ACCOUNT', $provider_test));
}
}
if ($this->config->get('plugins.login-oauth2.require_grav_user', false) && !$exists) {
throw new RuntimeException($this->translate('PLUGIN_LOGIN_OAUTH2.ERROR_NO_ACCOUNT', $username));
}
// Add token to user
$grav_user->set('token', json_encode($token, JSON_THROW_ON_ERROR));
// Set provider
$grav_user->set('provider', $provider_name);
// Default Access levels
$current_access = $grav_user->get('access');
if (!$current_access) {
$access = $this->config->get('plugins.login-oauth2.default_access_levels.access', []);
if (count($access) > 0) {
$grav_user->set('access', $access);
}
}
// Default Groups
$current_groups = $grav_user->get('groups');
if (!$current_groups) {
$groups = $this->config->get('plugins.login-oauth2.default_groups', []);
if (count($groups) > 0) {
$grav_user->set('groups', $groups);
}
}
// Remove Provider Userdata if configured
if (!$this->config->get('plugins.login-oauth2.store_provider_data', false)) {
unset($userdata[$provider_name]);
}
$grav_user->merge($userdata);
$this->debug("userdata: " . json_encode($userdata));
// Save Grav user if so configured
if ($this->config->get('plugins.login-oauth2.save_grav_user', false)) {
$grav_user->save();
}
$event->setUser($grav_user);
// Do something...
$event->setStatus($event::AUTHENTICATION_SUCCESS);
$event->stopPropagation();
} catch (Exception $e) {
$event->setMessage($this->translate('PLUGIN_LOGIN_OAUTH2.OAUTH2_LOGIN_FAILED', ucfirst($provider_name), $e->getMessage()), 'error');
$event->setStatus($event::AUTHENTICATION_FAILURE);
}
}
}
public function onOAuth2Username(Event $event): void
{
$userdata = $event['userdata'];
$provider = $event['provider'];
$provider_name = strtolower($provider->getName());
$username_parts = [$provider_name, $userdata['id'], $userdata['login']];
$username = implode('.', $username_parts);
$event['username'] = $username;
$event->stopPropagation();
}
public function userLoginFailure(UserLoginEvent $event): void
{
// This gets fired if user fails to log in.
}
public function userLogin(UserLoginEvent $event): void
{
}
public function userLogout(UserLoginEvent $event): void
{
// This gets fired on user logout.
}
/**
* @param mixed ...$args
* @return string
*/
private function translate(...$args): string
{
/** @var Language $language */
$language = $this->grav['language'];
return $language->translate($args);
}
private function debug($message): void
{
if ($this->debug) {
$this->grav['log']->debug($message);
}
}
}