View source
<?php
namespace Drupal\auth0\Controller;
define('AUTH0_PATH', drupal_get_path('module', 'auth0'));
if (file_exists(AUTH0_PATH . '/vendor/autoload.php')) {
require_once AUTH0_PATH . '/vendor/autoload.php';
}
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Url;
use Drupal\user\Entity\User;
use Drupal\user\PrivateTempStoreFactory;
use Drupal\Core\Session\SessionManagerInterface;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Render\Markup;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\auth0\Event\Auth0UserSigninEvent;
use Drupal\auth0\Event\Auth0UserSignupEvent;
use Drupal\auth0\Event\Auth0UserPreLoginEvent;
use Drupal\auth0\Exception\EmailNotSetException;
use Drupal\auth0\Exception\EmailNotVerifiedException;
use Drupal\Core\PageCache\ResponsePolicyInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\auth0\Util\AuthHelper;
use Auth0\SDK\JWTVerifier;
use Auth0\SDK\Auth0;
use Auth0\SDK\API\Authentication;
use Auth0\SDK\API\Helpers\State\SessionStateHandler;
use Auth0\SDK\Store\SessionStore;
use GuzzleHttp\Client;
class AuthController extends ControllerBase {
use StringTranslationTrait;
const SESSION = 'auth0';
const STATE = 'state';
const AUTH0_LOGGER = 'auth0_controller';
const AUTH0_DOMAIN = 'auth0_domain';
const AUTH0_CLIENT_ID = 'auth0_client_id';
const AUTH0_CLIENT_SECRET = 'auth0_client_secret';
const AUTH0_REDIRECT_FOR_SSO = 'auth0_redirect_for_sso';
const AUTH0_JWT_SIGNING_ALGORITHM = 'auth0_jwt_signature_alg';
const AUTH0_SECRET_ENCODED = 'auth0_secret_base64_encoded';
const AUTH0_OFFLINE_ACCESS = 'auth0_allow_offline_access';
protected $eventDispatcher;
protected $tempStore;
protected $sessionManager;
protected $logger;
protected $config;
protected $domain;
protected $customDomain;
protected $clientId;
protected $clientSecret;
protected $redirectForSso;
protected $auth0JwtSignatureAlg;
protected $secretBase64Encoded;
protected $offlineAccess;
protected $helper;
protected $auth0;
protected $auth0Logger;
protected $httpClient;
public function __construct(PrivateTempStoreFactory $temp_store_factory, SessionManagerInterface $session_manager, ResponsePolicyInterface $page_cache, LoggerChannelFactoryInterface $logger_factory, EventDispatcherInterface $event_dispatcher, ConfigFactoryInterface $config_factory, AuthHelper $auth0_helper, Client $http_client) {
$page_cache
->trigger();
$this->helper = $auth0_helper;
$this->eventDispatcher = $event_dispatcher;
$this->tempStore = $temp_store_factory
->get(AuthController::SESSION);
$this->sessionManager = $session_manager;
$this->logger = $logger_factory
->get(AuthController::AUTH0_LOGGER);
$this->auth0Logger = $logger_factory
->get('auth0');
$this->config = $config_factory
->get('auth0.settings');
$this->domain = $this->config
->get(AuthController::AUTH0_DOMAIN);
$this->customDomain = $this->config
->get(AuthHelper::AUTH0_CUSTOM_DOMAIN);
$this->clientId = $this->config
->get(AuthController::AUTH0_CLIENT_ID);
$this->clientSecret = $this->config
->get(AuthController::AUTH0_CLIENT_SECRET);
$this->redirectForSso = $this->config
->get(AuthController::AUTH0_REDIRECT_FOR_SSO);
$this->auth0JwtSignatureAlg = $this->config
->get(AuthController::AUTH0_JWT_SIGNING_ALGORITHM);
$this->secretBase64Encoded = FALSE || $this->config
->get(AuthController::AUTH0_SECRET_ENCODED);
$this->offlineAccess = FALSE || $this->config
->get(AuthController::AUTH0_OFFLINE_ACCESS);
$this->httpClient = $http_client;
$this->auth0 = FALSE;
}
public static function create(ContainerInterface $container) {
return new static($container
->get('user.private_tempstore'), $container
->get('session_manager'), $container
->get('page_cache_kill_switch'), $container
->get('logger.factory'), $container
->get('event_dispatcher'), $container
->get('config.factory'), $container
->get('auth0.helper'), $container
->get('http_client'));
}
public function login(Request $request) {
global $base_root;
$lockExtraSettings = $this->config
->get('auth0_lock_extra_settings');
if (trim($lockExtraSettings) == "") {
$lockExtraSettings = "{}";
}
$returnTo = $request->request
->get('returnTo', $request->query
->get('returnTo', NULL));
if ($this->redirectForSso) {
return new TrustedRedirectResponse($this
->buildAuthorizeUrl(NULL, $returnTo));
}
return [
'#theme' => 'auth0_login',
'#loginCSS' => $this->config
->get('auth0_login_css'),
'#attached' => [
'library' => [
'auth0/auth0.lock',
],
'drupalSettings' => [
'auth0' => [
'clientId' => $this->config
->get('auth0_client_id'),
'domain' => $this->helper
->getAuthDomain(),
'lockExtraSettings' => $lockExtraSettings,
'configurationBaseUrl' => $this->helper
->getTenantCdn($this->config
->get('auth0_domain')),
'showSignup' => $this->config
->get('auth0_allow_signup'),
'callbackURL' => "{$base_root}/auth0/callback",
'state' => $this
->getNonce($returnTo),
'scopes' => AUTH0_DEFAULT_SCOPES,
'offlineAccess' => $this->offlineAccess,
'formTitle' => $this->config
->get('auth0_form_title'),
'jsonErrorMsg' => $this
->t('There was an error parsing the "Lock extra settings" field.'),
],
],
],
];
}
public function logout() {
$auth0Api = new Authentication($this->helper
->getAuthDomain(), $this->clientId);
user_logout();
return new TrustedRedirectResponse($auth0Api
->get_logout_link(\Drupal::request()
->getSchemeAndHttpHost(), $this->redirectForSso ? NULL : $this->clientId));
}
protected function getNonce($returnTo) {
if (!$this->sessionManager
->isStarted() && !isset($_SESSION['auth0_is_session_started'])) {
$_SESSION['auth0_is_session_started'] = 'yes';
$this->sessionManager
->regenerate();
}
$sessionStateHandler = new SessionStateHandler(new SessionStore());
$states = $this->tempStore
->get(AuthController::STATE);
if (!is_array($states)) {
$states = [];
}
$nonce = $sessionStateHandler
->issue();
$states[$nonce] = $returnTo === NULL ? '' : $returnTo;
$this->tempStore
->set(AuthController::STATE, $states);
return $nonce;
}
protected function buildAuthorizeUrl($prompt, $returnTo = NULL) {
global $base_root;
$auth0Api = new Authentication($this->helper
->getAuthDomain(), $this->clientId);
$response_type = 'code';
$redirect_uri = "{$base_root}/auth0/callback";
$connection = NULL;
$state = $this
->getNonce($returnTo);
$additional_params = [];
$additional_params['scope'] = AUTH0_DEFAULT_SCOPES;
if ($this->offlineAccess) {
$additional_params['scope'] .= ' offline_access';
}
if ($prompt) {
$additional_params['prompt'] = $prompt;
}
return $auth0Api
->get_authorize_link($response_type, $redirect_uri, $connection, $state, $additional_params);
}
private function checkForError(Request $request, $returnTo) {
$error_msg = $this
->t('There was a problem logging you in');
$error_code = $request->query
->get('error', $request->request
->get('error'));
$redirect_errors = [
'login_required',
'interaction_required',
'consent_required',
];
if ($error_code && in_array($error_code, $redirect_errors)) {
return new TrustedRedirectResponse($this
->buildAuthorizeUrl(FALSE, $returnTo));
}
elseif ($error_code) {
$error_desc = $request->query
->get('error_description', $request->request
->get('error_description', $error_code));
return $this
->failLogin($error_msg . ': ' . $error_desc, $error_desc);
}
return NULL;
}
public function callback(Request $request) {
global $base_root;
$problem_logging_in_msg = $this
->t('There was a problem logging you in, sorry for the inconvenience.');
$response = $this
->checkForError($request, NULL);
if ($response !== NULL) {
return $response;
}
$this->auth0 = new Auth0([
'domain' => $this->helper
->getAuthDomain(),
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'redirect_uri' => "{$base_root}/auth0/callback",
'persist_user' => FALSE,
]);
$userInfo = NULL;
$refreshToken = NULL;
try {
$userInfo = $this->auth0
->getUser();
$idToken = $this->auth0
->getIdToken();
} catch (\Exception $e) {
return $this
->failLogin($problem_logging_in_msg, $this
->t('Failed to exchange code for tokens: @exception', [
'@exception' => $e
->getMessage(),
]));
}
if ($this->offlineAccess) {
try {
$refreshToken = $this->auth0
->getRefreshToken();
} catch (\Exception $e) {
$this->auth0Logger
->warning($this
->t('Failed getting refresh token: @exception', [
'@exception' => $e
->getMessage(),
]));
}
}
try {
$user = $this->helper
->validateIdToken($idToken);
} catch (\Exception $e) {
return $this
->failLogin($problem_logging_in_msg, $this
->t('Failed to validate JWT: @exception', [
'@exception' => $e
->getMessage(),
]));
}
$returnTo = NULL;
$validatedState = $request->query
->get('state');
$currentSession = $this->tempStore
->get(AuthController::STATE);
if (!empty($currentSession[$validatedState])) {
$returnTo = $currentSession[$validatedState];
unset($currentSession[$validatedState]);
}
if ($userInfo) {
if (empty($userInfo['sub']) && !empty($userInfo['user_id'])) {
$userInfo['sub'] = $userInfo['user_id'];
}
elseif (empty($userInfo['user_id']) && !empty($userInfo['sub'])) {
$userInfo['user_id'] = $userInfo['sub'];
}
if ($userInfo['sub'] != $user->sub) {
return $this
->failLogin($problem_logging_in_msg, $this
->t('Failed to verify JWT sub'));
}
$this->auth0Logger
->notice('Good Login');
return $this
->processUserLogin($request, $userInfo, $idToken, $refreshToken, $user->exp, $returnTo);
}
else {
return $this
->failLogin($problem_logging_in_msg, 'No userinfo found');
}
}
protected function validateUserEmail(array $userInfo) {
$requires_email = $this->config
->get('auth0_requires_verified_email');
if ($requires_email) {
if (!isset($userInfo['email']) || empty($userInfo['email'])) {
throw new EmailNotSetException();
}
if (!$userInfo['email_verified']) {
throw new EmailNotVerifiedException();
}
}
}
protected function processUserLogin(Request $request, array $userInfo, $idToken, $refreshToken, $expiresAt, $returnTo) {
$this->auth0Logger
->notice('process user login');
$event = new Auth0UserPreLoginEvent($userInfo);
$this->eventDispatcher
->dispatch(Auth0UserPreLoginEvent::NAME, $event);
try {
$this
->validateUserEmail($userInfo);
$this->auth0Logger
->notice($userInfo['user_id'] . ' looking up Drupal user by Auth0 user_id');
$user = $this
->findAuth0User($userInfo['user_id']);
if ($user) {
$this->auth0Logger
->notice('uid of existing Drupal user found');
$this
->updateAuth0User($userInfo);
$this
->auth0UpdateFieldsAndRoles($userInfo, $user);
$event = new Auth0UserSigninEvent($user, $userInfo, $refreshToken, $expiresAt);
$this->eventDispatcher
->dispatch(Auth0UserSigninEvent::NAME, $event);
}
else {
$this->auth0Logger
->notice('existing Drupal user NOT found');
$user = $this
->signupUser($userInfo);
$this
->insertAuth0User($userInfo, $user
->id());
$event = new Auth0UserSignupEvent($user, $userInfo);
$this->eventDispatcher
->dispatch(Auth0UserSignupEvent::NAME, $event);
}
} catch (EmailNotSetException $e) {
return $this
->failLogin($this
->t('This account does not have an email associated. Please login with a different provider.'), 'No Email Found');
} catch (EmailNotVerifiedException $e) {
return $this
->auth0FailWithVerifyEmail($idToken);
}
user_login_finalize($user);
if ($returnTo) {
return new RedirectResponse($returnTo);
}
elseif ($request->request
->has('destination')) {
return new RedirectResponse($request->request
->get('destination'));
}
return $this
->redirect('entity.user.canonical', [
'user' => $user
->id(),
]);
}
protected function failLogin($message, $logMessage) {
\Drupal::messenger()
->addError($message);
$this->logger
->error($logMessage);
if ($this->auth0) {
$this->auth0
->logout();
}
return new RedirectResponse('/');
}
protected function signupUser(array $userInfo, $idToken = '') {
$isDatabaseUser = FALSE;
$user_sub_arr = explode('|', $userInfo['user_id']);
$provider = $user_sub_arr[0];
if ('auth0' === $provider) {
$isDatabaseUser = TRUE;
}
$joinUser = FALSE;
$user_name_claim = $this->config
->get('auth0_username_claim') ?: AUTH0_DEFAULT_USERNAME_CLAIM;
$user_name_used = !empty($userInfo[$user_name_claim]) ? $userInfo[$user_name_claim] : str_replace('|', '_', $userInfo['user_id']);
if ($this->config
->get('auth0_join_user_by_mail_enabled') && !empty($userInfo['email'])) {
$this->auth0Logger
->notice($userInfo['email'] . ' join user by mail is enabled, looking up user by email');
if ($userInfo['email_verified'] || $isDatabaseUser) {
$joinUser = user_load_by_mail($userInfo['email']);
}
}
else {
$this->auth0Logger
->notice($user_name_used . ' join user by username');
if (!empty($userInfo['email_verified']) || $isDatabaseUser) {
$joinUser = user_load_by_name($user_name_used);
}
}
if ($joinUser) {
$this->auth0Logger
->notice($joinUser
->id() . ' Drupal user found by email with uid');
if (!$userInfo['email_verified']) {
throw new EmailNotVerifiedException();
}
$user = $joinUser;
}
else {
$this->auth0Logger
->notice($user_name_used . ' creating new Drupal user from Auth0 user');
$user = $this
->createDrupalUser($userInfo);
$this
->auth0UpdateFieldsAndRoles($userInfo, $user);
}
return $user;
}
protected function auth0FailWithVerifyEmail($idToken) {
$messageHtml = sprintf('<p>%s.</p>', $this
->t('Please verify your email and log in again'));
return $this
->failLogin(Markup::create($messageHtml), 'Email not verified');
}
protected function findAuth0User($id) {
$auth0_user = db_select('auth0_user', 'a')
->fields('a', [
'drupal_id',
])
->condition('auth0_id', $id, '=')
->execute()
->fetchAssoc();
return empty($auth0_user) ? FALSE : User::load($auth0_user['drupal_id']);
}
protected function updateAuth0User(array $userInfo) {
db_update('auth0_user')
->fields([
'auth0_object' => serialize($userInfo),
])
->condition('auth0_id', $userInfo['user_id'], '=')
->execute();
}
protected function auth0UpdateFieldsAndRoles(array $userInfo, User $user) {
$edit = [];
$this
->auth0UpdateFields($userInfo, $user, $edit);
$this
->auth0UpdateRoles($userInfo, $user, $edit);
$user
->save();
}
protected function auth0UpdateFields(array $userInfo, User $user, array &$edit) {
$auth0_claim_mapping = $this->config
->get('auth0_claim_mapping');
if (isset($auth0_claim_mapping) && !empty($auth0_claim_mapping)) {
$mappings = $this
->auth0PipeListToArray($auth0_claim_mapping);
$skip_mappings = [
'uid',
'name',
'mail',
'init',
'is_new',
'status',
'pass',
];
foreach ($mappings as $mapping) {
$this->auth0Logger
->notice('mapping ' . $mapping);
$key = $mapping[1];
if (in_array($key, $skip_mappings)) {
$this->auth0Logger
->notice('skipping mapping handled already by Auth0 module ' . $mapping);
}
else {
$value = isset($userInfo[$mapping[0]]) ? $userInfo[$mapping[0]] : '';
$current_value = $user
->get($key)->value;
if ($current_value === $value) {
$this->auth0Logger
->notice('value is unchanged ' . $key);
}
else {
$this->auth0Logger
->notice('value changed ' . $key . ' from [' . $current_value . '] to [' . $value . ']');
$edit[$key] = $value;
$user
->set($key, $value);
}
}
}
}
}
protected function auth0UpdateRoles(array $userInfo, User $user, array &$edit) {
$this->auth0Logger
->notice("Mapping Roles");
$auth0_claim_to_use_for_role = $this->config
->get('auth0_claim_to_use_for_role');
if (isset($auth0_claim_to_use_for_role) && !empty($auth0_claim_to_use_for_role)) {
$claim_value = isset($userInfo[$auth0_claim_to_use_for_role]) ? $userInfo[$auth0_claim_to_use_for_role] : '';
$this->auth0Logger
->notice('claim_value ' . print_r($claim_value, TRUE));
$claim_values = [];
if (is_array($claim_value)) {
$claim_values = $claim_value;
}
else {
$claim_values[] = $claim_value;
}
$auth0_role_mapping = $this->config
->get('auth0_role_mapping');
$mappings = $this
->auth0PipeListToArray($auth0_role_mapping);
$roles_granted = [];
$roles_managed_by_mapping = [];
foreach ($mappings as $mapping) {
$this->auth0Logger
->notice('mapping ' . print_r($mapping, TRUE));
$roles_managed_by_mapping[] = $mapping[1];
if (in_array($mapping[0], $claim_values)) {
$roles_granted[] = $mapping[1];
}
}
$roles_granted = array_unique($roles_granted);
$roles_managed_by_mapping = array_unique($roles_managed_by_mapping);
$not_granted = array_diff($roles_managed_by_mapping, $roles_granted);
$user_roles = $user
->getRoles();
$new_user_roles = array_merge(array_diff($user_roles, $not_granted), $roles_granted);
$roles_to_add = array_diff($new_user_roles, $user_roles);
$roles_to_remove = array_diff($user_roles, $new_user_roles);
if (empty($roles_to_add) && empty($roles_to_remove)) {
$this->auth0Logger
->notice('no changes to roles detected');
return;
}
$this->auth0Logger
->notice('changes to roles detected');
$edit['roles'] = $new_user_roles;
foreach ($roles_to_add as $new_role) {
$user
->addRole($new_role);
}
foreach ($roles_to_remove as $remove_role) {
$user
->removeRole($remove_role);
}
}
}
protected function auth0MappingsToPipeList(array $mappings) {
$result_text = "";
foreach ($mappings as $map) {
$result_text .= $map['from'] . '|' . $map['user_entered'] . "\n";
}
return $result_text;
}
protected function auth0PipeListToArray($mappingListTxt) {
$return = [];
$mappings = explode(PHP_EOL, $mappingListTxt);
foreach ($mappings as $line) {
if (empty($line) || FALSE === strpos($line, '|')) {
continue;
}
$line_parts = explode('|', $line);
$return[] = [
trim($line_parts[0]),
trim($line_parts[1]),
];
}
return $return;
}
protected function insertAuth0User(array $userInfo, $uid) {
db_insert('auth0_user')
->fields([
'auth0_id' => $userInfo['user_id'],
'drupal_id' => $uid,
'auth0_object' => json_encode($userInfo),
])
->execute();
}
private function getRandomBytes($nbBytes = 32) {
$bytes = openssl_random_pseudo_bytes($nbBytes, $strong);
if (FALSE !== $bytes && TRUE === $strong) {
return $bytes;
}
else {
throw new \Exception("Unable to generate secure token from OpenSSL.");
}
}
private function generatePassword($length) {
return substr(preg_replace("/[^a-zA-Z0-9]\\+\\//", "", base64_encode($this
->getRandomBytes($length + 1))), 0, $length);
}
protected function createDrupalUser(array $userInfo) {
$user_name_claim = $this->config
->get('auth0_username_claim');
if ($user_name_claim == '') {
$user_name_claim = 'nickname';
}
$user = User::create();
$user
->setPassword($this
->generatePassword(16));
$user
->enforceIsNew();
if (!empty($userInfo['email'])) {
$user
->setEmail($userInfo['email']);
}
else {
$user
->setEmail("change_this_email@" . uniqid() . ".com");
}
$username = !empty($userInfo[$user_name_claim]) ? $userInfo[$user_name_claim] : $userInfo['user_id'];
if (user_load_by_name($username)) {
$username .= time();
}
$user
->setUsername($username);
$user
->activate();
$user
->save();
return $user;
}
public function verify_email(Request $request) {
$idToken = $request
->get('idToken');
$auth0_domain = 'https://' . $this->domain . '/';
$auth0_settings = [];
$auth0_settings['authorized_iss'] = [
$auth0_domain,
];
$auth0_settings['supported_algs'] = [
$this->auth0JwtSignatureAlg,
];
$auth0_settings['valid_audiences'] = [
$this->clientId,
];
$auth0_settings['client_secret'] = $this->clientSecret;
$auth0_settings['secret_base64_encoded'] = $this->secretBase64Encoded;
$jwt_verifier = new JWTVerifier($auth0_settings);
try {
$user = $jwt_verifier
->verifyAndDecode($idToken);
} catch (\Exception $e) {
return $this
->failLogin($this
->t('There was a problem resending the verification email, sorry for the inconvenience.'), "Failed to verify and decode the JWT ({$idToken}) for the verify email page: " . $e
->getMessage());
}
try {
$userId = $user->sub;
$url = "https://{$this->domain}/api/users/{$userId}/send_verification_email";
$client = $this->httpClient;
$client
->request('POST', $url, [
"headers" => [
"Authorization" => "Bearer {$idToken}",
],
]);
\Drupal::messenger()
->addStatus($this
->t('An Authorization email was sent to your account.'));
} catch (\UnexpectedValueException $e) {
\Drupal::messenger()
->addError($this
->t('Your session has expired.'));
} catch (\Exception $e) {
\Drupal::messenger()
->addError($this
->t('Sorry, we could not send the email.'));
}
return new RedirectResponse('/');
}
}