oidc-1.0.0-alpha2/src/Controller/OpenidConnectController.php
src/Controller/OpenidConnectController.php
<?php
namespace Drupal\oidc\Controller;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\Session\SessionManagerInterface;
use Drupal\Core\Url;
use Drupal\externalauth\ExternalAuthInterface;
use Drupal\oidc\ExistingAccountValidator;
use Drupal\oidc\Event\LinkExistingAccountEvent;
use Drupal\oidc\OpenidConnectLoginException;
use Drupal\oidc\OpenidConnectSessionInterface;
use Drupal\oidc\Plugin\OpenidConnectRealm\GenericOpenidConnectRealm;
use Drupal\oidc\Routing\ImmutableTrustedRedirectResponse;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Controller for the OpenID Connect requests.
*/
class OpenidConnectController extends ControllerBase {
/**
* The session manager.
*
* @var \Drupal\Core\Session\SessionManagerInterface
*/
protected $sessionManager;
/**
* The OpenID Connect session service.
*
* @var \Drupal\oidc\OpenidConnectSessionInterface
*/
protected $session;
/**
* The external authentication service.
*
* @var \Drupal\externalauth\ExternalAuthInterface
*/
protected $externalauth;
/**
* The existing account validator.
*
* @var \Drupal\oidc\ExistingAccountValidator
*/
protected $existingAccountValidator;
/**
* The UUID service.
*
* @var \Drupal\Component\Uuid\UuidInterface
*/
protected $uuid;
/**
* The user storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $userStorage;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Class constructor.
*
* @param \Drupal\Core\Session\SessionManagerInterface $session_manager
* The session manager.
* @param \Drupal\oidc\OpenidConnectSessionInterface $session
* The OpenID Connect session service.
* @param \Drupal\externalauth\ExternalAuthInterface $externalauth
* The external authentication service.
* @param \Drupal\oidc\ExistingAccountValidator $existing_account_validator
* The existing account validator service.
* @param \Drupal\Component\Uuid\UuidInterface $uuid
* The UUID service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function __construct(SessionManagerInterface $session_manager, OpenidConnectSessionInterface $session, ExternalAuthInterface $externalauth, ExistingAccountValidator $existing_account_validator, UuidInterface $uuid, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $event_dispatcher) {
$this->sessionManager = $session_manager;
$this->session = $session;
$this->externalauth = $externalauth;
$this->existingAccountValidator = $existing_account_validator;
$this->uuid = $uuid;
$this->userStorage = $entity_type_manager->getStorage('user');
$this->eventDispatcher = $event_dispatcher;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('session_manager'),
$container->get('oidc.openid_connect_session'),
$container->get('externalauth.externalauth'),
$container->get('oidc.existing_account_validator'),
$container->get('uuid'),
$container->get('entity_type.manager'),
$container->get('event_dispatcher')
);
}
/**
* Start the OpenID Connect authentication.
*
* @param string $realm
* The realm plugin ID.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Drupal\oidc\Routing\ImmutableTrustedRedirectResponse
* Redirect to the login URL.
*/
public function login($realm, Request $request) {
// Initialize the realm.
try {
$this->session->initRealm($realm);
$plugin = $this->session->getRealmPlugin();
}
catch (PluginNotFoundException $ex) {
$this->session->destroy();
throw new NotFoundHttpException();
}
// Ensure it's enabled.
if (!$plugin->isEnabled()) {
$this->session->destroy();
throw new NotFoundHttpException();
}
// Initialize the state.
$destination = $request->query->get('destination');
$state = $this->session->initState($destination);
// Get and redirect to the login URL.
$redirect_url = Url::fromRoute('oidc.openid_connect.login_redirect');
$url = $plugin->getLoginUrl($state, $redirect_url)
->setAbsolute();
return new ImmutableTrustedRedirectResponse($url);
}
/**
* Redirect callback triggered after an OpenID Connect login.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Redirect to the front page.
*/
public function loginRedirect(Request $request) {
$query = $request->query;
$plugin_id = $this->session->getRealmPluginId();
if ($query->has('error') && $query->get('error_description')) {
// An error occurred.
$this->getLogger('oidc')->critical('@error error occurred on @realm realm: %description', [
'@error' => $query->get('error'),
'@realm' => $plugin_id,
'%description' => $query->get('error_description'),
]);
}
elseif (!$query->has('code')) {
// No code specified.
$this->getLogger('oidc')->error('Code missing in redirect from @realm realm.', [
'@realm' => $plugin_id,
]);
}
else {
// Get the realm plugin.
$plugin = $this->session->getRealmPlugin();
try {
// Get the tokens.
$tokens = $plugin->getJsonWebTokensForLogin($this->session->getState(), $query->get('code'));
$this->session->setJsonWebTokens($tokens);
// Check if there's a mapped user.
$authname = $tokens->getId();
$provider = 'oidc:' . $plugin_id;
$account = $this->externalauth->load($authname, $provider);
// No user found, try to link to an existing user.
if (!$account) {
$event = new LinkExistingAccountEvent($provider, $tokens);
$this->eventDispatcher->dispatch($event);
$account = $event->getAccount();
if ($account && $this->existingAccountValidator->isValid($account)) {
$this->externalauth->linkExistingAccount($authname, $provider, $account);
$account->setPassword(NULL)->save();
}
}
// Stil no user, create a new one.
if (!$account) {
do {
$name = $this->uuid->generate();
$count = $this->userStorage->getQuery()
->count()
->condition('name', $name)
->accessCheck(FALSE)
->execute();
} while ($count);
$account = $this->externalauth->register($authname, $provider, [
'name' => $name,
]);
}
// Login.
$this->externalauth->userLoginFinalize($account, $authname, $provider);
// Clear the state and restore the destination.
if (($destination = $this->session->clearState()) !== NULL) {
$request->query->set('destination', $destination);
}
// Destroy the session if the realm is only to be used for authentication.
if ($plugin instanceof GenericOpenidConnectRealm && $plugin->isOnlyForAuthentication()) {
$this->session->destroy();
}
}
catch (OpenidConnectLoginException $ex) {
if ($this->currentUser()->isAnonymous()) {
// Redirect to the destination.
$redirect = new ImmutableTrustedRedirectResponse($ex->getDestination());
$this->session->destroy();
}
else {
// Logout before redirecting to the destination.
$request->query->set('destination', $ex->getDestination()->getInternalPath());
$redirect = $this->logout($request);
}
// Set the exception message as error.
if (($message = $ex->getMessage()) !== '') {
$this->messenger()->addError($message);
}
return $redirect;
}
catch (\Exception $ex) {
$this->getLogger('oidc')->error('Error on authentication via @realm realm: %error', [
'@realm' => $plugin_id,
'%error' => $ex->getMessage(),
]);
}
}
// Destroy the session and set an error message if authentication failed.
if ($this->currentUser()->isAnonymous()) {
$this->session->destroy();
$this->messenger()->addError($this->t('Authentication failed, please try again later.'));
}
return $this->redirect('<front>');
}
/**
* Start the OpenID Connect logout.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Drupal\oidc\Routing\ImmutableTrustedRedirectResponse
* Redirect to the logout URL.
*/
public function logout(Request $request) {
// Get the realm plugin.
$plugin = $this->session->getRealmPlugin();
$plugin_id = $plugin->getPluginId();
// Get the tokens.
$tokens = $this->session->getJsonWebTokens();
// Log out of Drupal. This code is copied from user_logout() because that function
// also destroys the PHP session, which breaks the OpenID Connect session.
$user = $this->currentUser();
$this->getLogger('user')->notice('Session closed for %name.', ['%name' => $user->getAccountName()]);
$this->moduleHandler()->invokeAll('user_logout', [$user]);
$user->setAccount(new AnonymousUserSession());
// Clear the session and restore the realm.
$this->sessionManager->clear();
$this->session->initRealm($plugin_id);
// Initialize the state.
$destination = $request->query->get('destination');
$state = $this->session->initState($destination);
// Get and redirect to the logout URL.
$redirect_url = Url::fromRoute('oidc.openid_connect.logout_redirect');
$url = $plugin->getLogoutUrl($tokens->getIdToken(), $state, $redirect_url)
->setAbsolute();
return new ImmutableTrustedRedirectResponse($url);
}
/**
* Redirect callback triggered after an OpenID Connect logout.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Redirect to the front page.
*/
public function logoutRedirect(Request $request) {
// Set a message if there are no error messages.
if (!$this->messenger()->messagesByType(MessengerInterface::TYPE_ERROR)) {
$this->messenger()->addStatus($this->t('You have been logged out successfully.'));
}
// Clear the state and restore the destination.
if (($destination = $this->session->clearState()) !== NULL) {
$request->query->set('destination', $destination);
}
$this->session->destroy();
return $this->redirect('<front>');
}
}
