o365-2.0.3/src/AuthenticationService.php
src/AuthenticationService.php
<?php
namespace Drupal\o365;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Messenger\Messenger;
use Drupal\Core\Routing\RedirectDestinationTrait;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\Core\Url;
use Drupal\externalauth\AuthmapInterface;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\GenericProvider;
use League\OAuth2\Client\Token\AccessTokenInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Service used to authenticate users between Microsoft 365 and Drupal.
*/
class AuthenticationService implements AuthenticationServiceInterface {
use RedirectDestinationTrait;
use StringTranslationTrait;
/**
* The config factory interface.
*/
protected ConfigFactoryInterface $configFactory;
/**
* The modules base config.
*/
protected ImmutableConfig $moduleConfig;
/**
* The o365_sso config.
*/
protected ImmutableConfig $ssoConfig;
/**
* The private temp store.
*/
protected PrivateTempStoreFactory $tempStore;
/**
* An oauth provider.
*/
protected GenericProvider $oauthClient;
/**
* The ConstantsService implementation.
*/
protected ConstantsService $constants;
/**
* The logger service.
*/
protected O365LoggerServiceInterface $loggerService;
/**
* The current request.
*/
protected Request $request;
/**
* If we want to add debug messages.
*/
private bool $debug;
/**
* The auth data.
*/
private array $authValues = [];
/**
* The current user account.
*/
protected AccountProxyInterface $currentUser;
/**
* The auth map.
*/
protected AuthmapInterface $authmap;
/**
* The o365 helper service.
*/
protected HelperService $helperService;
/**
* The messenger.
*/
protected Messenger $messenger;
/**
* The entity type manager.
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* Constructs a new AuthenticationService object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory interface.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $tempStoreFactory
* The private store factory.
* @param \Drupal\o365\ConstantsService $constantsService
* The constants service from the o365 module.
* @param \Drupal\o365\O365LoggerServiceInterface $loggerService
* The logger service from the o365 module.
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
* The request stack.
* @param \Drupal\o365\HelperService $helperService
* The helper service used to get the api settings.
* @param \Drupal\Core\Session\AccountProxyInterface $accountProxy
* The account proxy for the current user.
* @param \Drupal\externalauth\AuthmapInterface $authmap
* The auth map.
* @param \Drupal\Core\Messenger\Messenger $messenger
* The messenger class.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
*/
public function __construct(ConfigFactoryInterface $configFactory, PrivateTempStoreFactory $tempStoreFactory, ConstantsService $constantsService, O365LoggerServiceInterface $loggerService, RequestStack $requestStack, HelperService $helperService, AccountProxyInterface $accountProxy, AuthmapInterface $authmap, Messenger $messenger, EntityTypeManagerInterface $entityTypeManager) {
$this->configFactory = $configFactory;
$this->helperService = $helperService;
$this->moduleConfig = $this->configFactory->get('o365.settings');
$this->ssoConfig = $this->configFactory->get('o365_sso.settings');
$this->tempStore = $tempStoreFactory;
$this->constants = $constantsService;
$this->loggerService = $loggerService;
$this->request = $requestStack->getCurrentRequest();
$this->currentUser = $accountProxy;
$this->authmap = $authmap;
$this->messenger = $messenger;
$this->entityTypeManager = $entityTypeManager;
$this->debug = !empty($this->moduleConfig->get('verbose_logging'));
}
/**
* {@inheritdoc}
*/
public function redirectToAuthorizationUrl(O365ConnectorInterface $o365_connector) {
if ($this->currentUser->isAnonymous() && !$this->helperService->strStartsWith($this->request->getPathInfo(), '/o365/login')) {
$message = $this->t('An anonymous user tried to log in using o365, this is not allowed.');
$this->loggerService->debug($message);
$url = Url::fromRoute('user.login')->toString(TRUE);
$response = new TrustedRedirectResponse($url->getGeneratedUrl());
$response->addCacheableDependency($url);
return $response;
}
if ($this->debug) {
$message = $this->t('-- redirectToAuthorizationUrl()');
$this->loggerService->debug($message);
}
// Save the destination.
$this->saveDestination();
$clientId = $o365_connector->getClientId();
if ($this->debug) {
$message = $this->t('clientId: @client', ['@client' => $clientId]);
$this->loggerService->debug($message);
}
$this->generateProvider($o365_connector);
$authUrl = $this->oauthClient->getAuthorizationUrl() . '&client_id=' . $clientId;
$prompt = $this->ssoConfig->get('prompt') ?? FALSE;
if ($prompt && $prompt !== '<noprompt>') {
$authUrl .= '&prompt=' . $prompt;
}
$authMapUser = $this->authmap->get($this->currentUser->getAccount()->id(), 'o365_sso');
if ($this->currentUser->isAnonymous() || $authMapUser) {
return new TrustedRedirectResponse($authUrl);
}
throw new AccessDeniedHttpException('User is not authorized to access this page.');
}
/**
* {@inheritdoc}
*/
public function setAccessToken(string $code, O365ConnectorInterface $o365_connector, $redirect = '') {
try {
// Make the token request.
$this->generateProvider($o365_connector);
$accessToken = $this->oauthClient->getAccessToken('authorization_code', [
'code' => $code,
'client_id' => $o365_connector->getClientId(),
'client_secret' => $o365_connector->getClientSecret(),
]);
$this->saveAuthData($accessToken, $o365_connector);
if ($redirect) {
$redirect .= '?access_token=' . $accessToken->getToken();
$redirect .= '&refresh_token=' . $accessToken->getRefreshToken();
$redirect .= '&expires_on=' . $accessToken->getExpires();
$redirect .= '&connector_id=' . $o365_connector->id();
return new TrustedRedirectResponse($redirect);
}
}
catch (IdentityProviderException $e) {
$error = $e->getResponseBody();
$message = $this->t('Error description: @error', ['@error' => $error['error_description']]);
$this->loggerService->log($message, 'error');
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getAccessToken($login = TRUE) {
$authData = $this->getAuthData();
if (!empty($authData)) {
$now = time();
if ($authData['expires_on'] > $now) {
return $authData['access_token'];
}
if ($this->debug) {
$message = $this->t('accessToken is expired, refresh the token');
$this->loggerService->debug($message);
}
return $this->refreshToken($authData['refresh_token'], $authData['connector_id']);
}
// We don't have any auth data, so return a redirect response to log in
// and back.
if ($login) {
$connector = $this->request->get('o365_connector');
return $this->redirectToAuthorizationUrl($connector);
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function saveAuthDataFromUrl(): void {
$this->authValues = [
'access_token' => $this->request->get('access_token'),
'refresh_token' => $this->request->get('refresh_token'),
'expires_on' => $this->request->get('expires_on'),
'connector_id' => $this->request->get('connector_id'),
];
$this->saveDataToTempStore($this->constants->getUserTempStoreDataName(), $this->authValues);
}
/**
* {@inheritdoc}
*/
public function checkForOfficeLogin() {
if (!empty($this->getAuthData())) {
return $this->authmap->get($this->currentUser->id(), 'o365_sso');
}
return FALSE;
}
/**
* Generate a new access token based on the refresh token.
*
* @param string $refreshToken
* The refresh token.
* @param string $connector_id
* The connector entity ID.
*
* @return string
* The new access token.
*/
private function refreshToken(string $refreshToken, string $connector_id): string {
/** @var \Drupal\o365\O365ConnectorInterface $o365_connector */
$o365_connector = $this->entityTypeManager->getStorage('o365_connector')
->load($connector_id);
$this->generateProvider($o365_connector);
$accessToken = $this->oauthClient->getAccessToken('refresh_token', [
'refresh_token' => $refreshToken,
'client_id' => $o365_connector->getClientId(),
'client_secret' => $o365_connector->getClientSecret(),
]);
$this->saveAuthData($accessToken, $o365_connector);
return $accessToken->getToken();
}
/**
* Generate a basic oAuth2 provider.
*
* @param \Drupal\o365\O365ConnectorInterface $o365_connector
* The o365_connector entity type.
*/
private function generateProvider(O365ConnectorInterface $o365_connector): void {
$scopes = $this->helperService->getAuthScopes($o365_connector);
$redirect_uri = Url::fromRoute('o365_sso.login_callback_controller_callback', ['o365_connector' => $o365_connector->id()], ['absolute' => TRUE])
->toString();
$providerData = [
'client_id' => $o365_connector->getClientId(),
'client_secret' => $o365_connector->getClientSecret(),
'redirectUri' => $redirect_uri,
'urlAuthorize' => $this->constants->getAuthorizeUrl($o365_connector),
'urlAccessToken' => $this->constants->getTokenUrl($o365_connector),
'urlResourceOwnerDetails' => '',
'scopes' => $scopes,
];
if ($this->debug) {
$message = $this->t('providerData: @data', ['@data' => print_r($providerData, TRUE)]);
$this->loggerService->debug($message);
}
$this->oauthClient = new GenericProvider($providerData);
}
/**
* Get the auth data from temp store or cookie.
*
* @return mixed
* The saved auth data.
*/
private function getAuthData(): mixed {
// Check if there is some auth data on the url, if so, save it.
if (!empty($this->request->get('access_token'))) {
$this->saveAuthDataFromUrl();
}
$tempstore = $this->tempStore->get($this->constants->getUserTempStoreName());
return $tempstore->get($this->constants->getUserTempStoreDataName());
}
/**
* Save the auth data to the temp store.
*
* @param \League\OAuth2\Client\Token\AccessTokenInterface $accessToken
* The access token object.
* @param \Drupal\o365\O365ConnectorInterface $o365_connector
* The o365_connector entity type.
*
* @throws \Drupal\Core\TempStore\TempStoreException
*/
private function saveAuthData(AccessTokenInterface $accessToken, O365ConnectorInterface $o365_connector): void {
$this->authValues = [
'access_token' => $accessToken->getToken(),
'refresh_token' => $accessToken->getRefreshToken(),
'expires_on' => $accessToken->getExpires(),
'connector_id' => $o365_connector->id(),
];
if ($this->debug) {
$message = $this->t('Saving authData: @data', ['@data' => print_r($this->authValues, TRUE)]);
$this->loggerService->debug($message);
}
$this->saveDataToTempStore($this->constants->getUserTempStoreDataName(), $this->authValues);
}
/**
* Save data to the tempstore.
*
* @param string $name
* The name of the store.
* @param mixed $value
* The value.
*
* @throws \Drupal\Core\TempStore\TempStoreException
*/
private function saveDataToTempStore(string $name, mixed $value): void {
$tempstore = $this->tempStore->get($this->constants->getUserTempStoreName());
$tempstore->set($name, $value);
}
/**
* Get data from the temp store.
*
* @param string $name
* The name of the temp store value.
*
* @return mixed
* The value.
*/
public function getDataFromTempStore(string $name): mixed {
$tempstore = $this->tempStore->get($this->constants->getUserTempStoreName());
return $tempstore->get($name);
}
/**
* Save the destination that is part of the url.
*
* @throws \Drupal\Core\TempStore\TempStoreException
*/
private function saveDestination() {
$destination = NULL;
if ($this->request->query->has('destination')) {
$destination = $this->getRedirectDestination()->get();
// Remove the destination to stop the RedirectResponseSubscriber from
// picking it up.
$this->request->query->remove('destination');
}
$this->saveDataToTempStore('destination', $destination);
}
}
