bankid_oidc-1.x-dev/src/Controller/LoginController.php
src/Controller/LoginController.php
<?php
namespace Drupal\bankid_oidc\Controller;
use BankID\OAuth2\Client\Provider\BankIdProvider;
use Drupal\bankid_oidc\BankIdAuthInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Url;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Class LoginController.
*/
class LoginController extends ControllerBase {
protected const SESSION_OAUTH_STATE_NAME = 'bankid_oidc_state';
/**
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $moduleConfig;
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var \Drupal\externalauth\ExternalAuthInterface
*/
protected $userAuth;
/**
* @var \BankID\OAuth2\Client\Provider\BankIdProvider
*/
protected $oauthClient;
protected function userAuth(): BankIdAuthInterface {
if (!$this->userAuth) {
$this->userAuth = \Drupal::service('bankid_oidc.user_auth');
}
return $this->userAuth;
}
protected function logger(): LoggerInterface {
if (!$this->logger) {
$this->logger = $this->getLogger('bankid_oidc');
}
return $this->logger;
}
protected function oauthClient(): BankIdProvider {
if (!$this->oauthClient) {
$this->oauthClient = $this->userAuth()->oauthClient();
}
return $this->oauthClient;
}
/**
* Returns one configuration item or the whole config for this module.
*
* @param string|null $name
* Optional: Return only 1 config item.
*
* @return \Drupal\Core\Config\ImmutableConfig|mixed
*/
protected function getConfig(string $name = NULL) {
if (!$this->moduleConfig) {
$this->moduleConfig = $this->config('bankid_oidc.config');
}
return ($name) ? $this->moduleConfig->get($name) : $this->moduleConfig;
}
public function login(Request $request) {
if (\Drupal::currentUser()->isAuthenticated()) {
throw new AccessDeniedHttpException('User already authenticated.');
}
$authorizationUrl = $this->oauthClient()->getAuthorizationUrl();
$this->setSessionState($request, $this->oauthClient()->getState());
$destination = $request->query->get('destination');
if ($request->query->has('destination')) {
$request->getSession()->remove('destination');
$request->getSession()->set('destination', $destination);
$request->query->remove('destination');
}
$response = new TrustedRedirectResponse($authorizationUrl);
// We can't cache the response, since this will prevent the state to be
// added to the session. The kill switch will prevent the page getting
// cached for anonymous users when page cache is active.
\Drupal::service('page_cache_kill_switch')->trigger();
return $response
->setPrivate()
->setMaxAge(0);
}
public function authenticate(Request $request) {
if (!$this->isValidState($request)) {
throw new AccessDeniedHttpException('Invalid oauth state');
}
$query = $request->query;
$code = $query->get('code');
$session = $request->getSession();
if ($session && $session->has('destination')) {
$destination_uri = $session->get('destination');
$session->remove('destination');
$destination = Url::fromUserInput($destination_uri)
->toString(TRUE);
}
else {
$destination = Url::fromRoute('user.page')
->toString(TRUE);
}
$response = new TrustedRedirectResponse($destination->getGeneratedUrl());
$response->addCacheableDependency($destination);
try {
$grant_options = ['code' => $code];
/** @var \BankID\OAuth2\Client\Token\AccessToken $accessToken */
$accessToken = $this->oauthClient()
->getAccessToken('authorization_code', $grant_options);
}
catch (IdentityProviderException $e) {
watchdog_exception('bankid_oidc', $e);
$message = $this->getConfig('auth_error');
if (!$message) {
$url = Url::fromRoute('bankid_oidc.login')->toString(TRUE);
$response->addCacheableDependency($url);
$message = $this->t(
'There was an error during the authentication, please wait a few seconds and <a href="@url">click here to try again</a>.',
['@url' => $url->getGeneratedUrl()]
);
}
$this->messenger()->addError($message);
return $response;
}
$auth = $this->userAuth();
\Drupal::service('renderer')
->executeInRenderContext(
new RenderContext(),
static function() use ($auth, $accessToken) {
return $auth->authenticate($accessToken);
}
);
// We can't cache the response, since this will prevent the state to be
// added to the session. The kill switch will prevent the page getting
// cached for anonymous users when page cache is active.
\Drupal::service('page_cache_kill_switch')->trigger();
return $response;
}
/**
* Ensures that the 'state' matches in both session and request query.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request to validate.
*
* @return bool
*/
protected function isValidState(Request $request): bool {
// We need a valid session!
if (!$request->hasSession()) {
return FALSE;
}
/** @noinspection NullPointerExceptionInspection */
$session__state = $request->getSession()->get(self::SESSION_OAUTH_STATE_NAME);
$request__state = $request->query->get('state');
// Both 'states' need to have a value.
if ($session__state === NULL || $request__state === NULL) {
return FALSE;
}
return $session__state === $request__state;
}
/**
* Sets the 'state' code for a given session (will start a new one if required).
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request for the session.
* @param string $state_code
* The 'state' to save in the session.
*/
protected function setSessionState(Request $request, string $state_code): void {
$session = $request->getSession();
if (!$session) {
/** @var \Symfony\Component\HttpFoundation\Session\SessionInterface $session */
$session = \Drupal::service('session');
$request->setSession($session);
$session->start();
}
$session->set(self::SESSION_OAUTH_STATE_NAME, $state_code);
}
}
