View source
<?php
namespace Drupal\auth0\Controller;
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 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\Exception\EmailNotSetException;
use Drupal\auth0\Exception\EmailNotVerifiedException;
use Auth0\SDK\JWTVerifier;
use Auth0\SDK\Auth0;
use Auth0\SDK\API\Authentication;
use Auth0\SDK\API\Management;
use RandomLib\Factory;
class AuthController extends ControllerBase {
const SESSION = 'auth0';
const NONCE = 'nonce';
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';
protected $eventDispatcher;
public function __construct(PrivateTempStoreFactory $tempStoreFactory, SessionManagerInterface $sessionManager) {
\Drupal::service('page_cache_kill_switch')
->trigger();
$this->eventDispatcher = \Drupal::service('event_dispatcher');
$this->tempStore = $tempStoreFactory
->get(AuthController::SESSION);
$this->sessionManager = $sessionManager;
$this->logger = \Drupal::logger(AuthController::AUTH0_LOGGER);
$this->config = \Drupal::service('config.factory')
->get('auth0.settings');
$this->domain = $this->config
->get(AuthController::AUTH0_DOMAIN);
$this->client_id = $this->config
->get(AuthController::AUTH0_CLIENT_ID);
$this->client_secret = $this->config
->get(AuthController::AUTH0_CLIENT_SECRET);
$this->redirect_for_sso = $this->config
->get(AuthController::AUTH0_REDIRECT_FOR_SSO);
$this->auth0_jwt_signature_alg = $this->config
->get(AuthController::AUTH0_JWT_SIGNING_ALGORITHM);
$this->secret_base64_encoded = FALSE || $this->config
->get(AuthController::AUTH0_SECRET_ENCODED);
$this->auth0 = FALSE;
}
public static function create(ContainerInterface $container) {
return new static($container
->get('user.private_tempstore'), $container
->get('session_manager'));
}
public function login() {
global $base_root;
$config = \Drupal::service('config.factory')
->get('auth0.settings');
$lockExtraSettings = $config
->get('auth0_lock_extra_settings');
if (trim($lockExtraSettings) == "") {
$lockExtraSettings = "{}";
}
if ($this->redirect_for_sso == TRUE) {
$prompt = 'none';
return new TrustedRedirectResponse($this
->buildAuthorizeUrl($prompt));
}
return array(
'#theme' => 'auth0_login',
'#domain' => $config
->get('auth0_domain'),
'#clientID' => $config
->get('auth0_client_id'),
'#state' => $this
->getNonce(),
'#showSignup' => $config
->get('auth0_allow_signup'),
'#widgetCdn' => $config
->get('auth0_widget_cdn'),
'#loginCSS' => $config
->get('auth0_login_css'),
'#lockExtraSettings' => $lockExtraSettings,
'#callbackURL' => "{$base_root}/auth0/callback",
);
}
public function logout() {
global $base_root;
$auth0Api = new Authentication($this->domain, $this->client_id);
user_logout();
return new TrustedRedirectResponse($auth0Api
->get_logout_link($base_root, $this->redirect_for_sso ? null : $this->client_id));
}
protected function getNonce() {
if (!$this->sessionManager
->isStarted() && !isset($_SESSION['auth0_is_session_started'])) {
$_SESSION['auth0_is_session_started'] = 'yes';
$this->sessionManager
->start();
}
$factory = new Factory();
$generator = $factory
->getMediumStrengthGenerator();
$nonces = $this->tempStore
->get(AuthController::NONCE);
if (!is_array($nonces)) {
$nonces = array();
}
$nonce = base64_encode($generator
->generate(32));
$newNonceArray = array_merge($nonces, [
$nonce,
]);
$this->tempStore
->set(AuthController::NONCE, $newNonceArray);
return $nonce;
}
protected function compareNonce($nonce) {
$nonces = $this->tempStore
->get(AuthController::NONCE);
if (!is_array($nonces)) {
$this->logger
->error("Couldn't verify state because there was no nonce in storage");
return FALSE;
}
$index = array_search($nonce, $nonces);
if ($index !== FALSE) {
unset($nonces[$index]);
$this->tempStore
->set(AuthController::NONCE, $nonces);
return TRUE;
}
$this->logger
->error("{$nonce} not found in: " . implode(',', $nonces));
return FALSE;
}
protected function buildAuthorizeUrl($prompt) {
global $base_root;
$auth0Api = new Authentication($this->domain, $this->client_id);
$response_type = 'code';
$redirect_uri = "{$base_root}/auth0/callback";
$connection = null;
$state = $this
->getNonce();
$additional_params = [];
$additional_params['scope'] = 'openid profile email';
if ($prompt) {
$additional_params['prompt'] = $prompt;
}
return $auth0Api
->get_authorize_link($response_type, $redirect_uri, $connection, $state, $additional_params);
}
public function callback(Request $request) {
global $base_root;
$config = \Drupal::service('config.factory')
->get('auth0.settings');
if ($request->query
->has('error') && $request->query
->get('error') == 'login_required') {
return new TrustedRedirectResponse($this
->buildAuthorizeUrl(FALSE));
}
if ($request->request
->has('error') && $request->request
->get('error') == 'login_required') {
return new TrustedRedirectResponse($this
->buildAuthorizeUrl(FALSE));
}
$this->auth0 = new Auth0(array(
'domain' => $config
->get('auth0_domain'),
'client_id' => $config
->get('auth0_client_id'),
'client_secret' => $config
->get('auth0_client_secret'),
'redirect_uri' => "{$base_root}/auth0/callback",
'store' => NULL,
'persist_id_token' => FALSE,
'persist_user' => FALSE,
'persist_access_token' => FALSE,
'persist_refresh_token' => FALSE,
));
$userInfo = NULL;
try {
$userInfo = $this->auth0
->getUser();
$idToken = $this->auth0
->getIdToken();
} catch (\Exception $e) {
return $this
->failLogin(t('There was a problem logging you in, sorry for the inconvenience.'), 'Failed to exchange code for tokens: ' . $e
->getMessage());
}
$auth0_domain = 'https://' . $this->domain . '/';
$auth0_settings = array();
$auth0_settings['authorized_iss'] = [
$auth0_domain,
];
$auth0_settings['supported_algs'] = [
$this->auth0_jwt_signature_alg,
];
$auth0_settings['valid_audiences'] = [
$this->client_id,
];
$auth0_settings['client_secret'] = $this->client_secret;
$auth0_settings['secret_base64_encoded'] = $this->secret_base64_encoded;
$jwt_verifier = new JWTVerifier($auth0_settings);
try {
$user = $jwt_verifier
->verifyAndDecode($idToken);
} catch (\Exception $e) {
return $this
->failLogin(t('There was a problem logging you in, sorry for the inconvenience.'), 'Failed to verify and decode the JWT: ' . $e
->getMessage());
}
$state = 'invalid';
if ($request->query
->has('state')) {
$state = $request->query
->get('state');
}
elseif ($request->request
->has('state')) {
$state = $request->request
->get('state');
}
if (!$this
->compareNonce($state)) {
return $this
->failLogin(t('There was a problem logging you in, sorry for the inconvenience.'), "Failed to verify the state ({$state})");
}
if ($userInfo['sub'] != $user->sub) {
return $this
->failLogin(t('There was a problem logging you in, sorry for the inconvenience.'), 'Failed to verify the JWT sub.');
}
elseif (array_key_exists('sub', $userInfo)) {
$userInfo['user_id'] = $userInfo['sub'];
}
if ($userInfo) {
return $this
->processUserLogin($request, $userInfo, $idToken);
}
else {
return $this
->failLogin(t('There was a problem logging you in, sorry for the inconvenience.'), 'No userInfo found');
}
}
protected function validateUserEmail($userInfo) {
$config = \Drupal::service('config.factory')
->get('auth0.settings');
$requires_email = $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, $userInfo, $idToken) {
try {
$this
->validateUserEmail($userInfo);
} catch (EmailNotSetException $e) {
return $this
->failLogin(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 = $this
->findAuth0User($userInfo['user_id']);
if ($user) {
$this
->updateAuth0User($userInfo);
$event = new Auth0UserSigninEvent($user, $userInfo);
$this->eventDispatcher
->dispatch(Auth0UserSigninEvent::NAME, $event);
}
else {
try {
$user = $this
->signupUser($userInfo, $idToken);
} catch (EmailNotVerifiedException $e) {
return $this
->auth0FailWithVerifyEmail($idToken);
}
$this
->insertAuth0User($userInfo, $user
->id());
$event = new Auth0UserSignupEvent($user, $userInfo);
$this->eventDispatcher
->dispatch(Auth0UserSignupEvent::NAME, $event);
}
user_login_finalize($user);
if ($request->request
->has('destination')) {
return $this
->redirect($request->request
->get('destination'));
}
return $this
->redirect('entity.user.canonical', array(
'user' => $user
->id(),
));
}
protected function failLogin($message, $logMessage) {
$this->logger
->error($logMessage);
drupal_set_message($message, 'error');
if ($this->auth0) {
$this->auth0
->logout();
}
return new RedirectResponse('/');
}
protected function signupUser($userInfo, $idToken) {
$isDatabaseUser = FALSE;
$hasIdentities = is_object($userInfo) && $userInfo
->has('identities') || is_array($userInfo) && array_key_exists('identities', $userInfo);
if (!$hasIdentities) {
$mgmtClient = new Management($idToken, $this->domain);
$user = $mgmtClient->users
->get($userInfo['user_id']);
$userInfo['identities'] = $user['identities'];
}
foreach ($userInfo['identities'] as $identity) {
if ($identity['provider'] == "auth0") {
$isDatabaseUser = TRUE;
}
}
$joinUser = FALSE;
if ($userInfo['email_verified'] || $isDatabaseUser) {
$joinUser = user_load_by_mail($userInfo['email']);
}
if ($joinUser) {
if (!$userInfo['email_verified']) {
throw new EmailNotVerifiedException();
}
$user = $joinUser;
}
else {
$user = $this
->createDrupalUser($userInfo);
}
return $user;
}
protected function auth0FailWithVerifyEmail($idToken) {
$url = Url::fromRoute('auth0.verify_email', array(), array());
$formText = "<form style='display:none' name='auth0VerifyEmail' action=@url method='post'><input type='hidden' value=@token name='idToken'/></form>";
$linkText = "<a href='javascript:null' onClick='document.forms[\"auth0VerifyEmail\"].submit();'>here</a>";
return $this
->failLogin(t($formText . "Please verify your email and log in again. Click {$linkText} to Resend verification email.", array(
'@url' => $url
->toString(),
'@token' => $idToken,
)), 'Email not verified');
}
protected function findAuth0User($id) {
$auth0_user = db_select('auth0_user', 'a')
->fields('a', array(
'drupal_id',
))
->condition('auth0_id', $id, '=')
->execute()
->fetchAssoc();
return empty($auth0_user) ? FALSE : User::load($auth0_user['drupal_id']);
}
protected function updateAuth0User($userInfo) {
db_update('auth0_user')
->fields(array(
'auth0_object' => serialize($userInfo),
))
->condition('auth0_id', $userInfo['user_id'], '=')
->execute();
}
protected function insertAuth0User($userInfo, $uid) {
db_insert('auth0_user')
->fields(array(
'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($userInfo) {
$user = User::create();
$user
->setPassword($this
->generatePassword(16));
$user
->enforceIsNew();
if (isset($userInfo['email']) && !empty($userInfo['email'])) {
$user
->setEmail($userInfo['email']);
}
else {
$user
->setEmail("change_this_email@" . uniqid() . ".com");
}
$username = $userInfo['nickname'];
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 = array();
$auth0_settings['authorized_iss'] = [
$auth0_domain,
];
$auth0_settings['supported_algs'] = [
$this->auth0_jwt_signature_alg,
];
$auth0_settings['valid_audiences'] = [
$this->client_id,
];
$auth0_settings['client_secret'] = $this->client_secret;
$auth0_settings['secret_base64_encoded'] = $this->secret_base64_encoded;
$jwt_verifier = new JWTVerifier($auth0_settings);
try {
$user = $jwt_verifier
->verifyAndDecode($idToken);
} catch (\Exception $e) {
return $this
->failLogin(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 = \Drupal::httpClient();
$client
->request('POST', $url, array(
"headers" => array(
"Authorization" => "Bearer {$idToken}",
),
));
drupal_set_message(t('An Authorization email was sent to your account'));
} catch (\UnexpectedValueException $e) {
drupal_set_message(t('Your session has expired.'), 'error');
} catch (\Exception $e) {
drupal_set_message(t('Sorry, we couldnt send the email'), 'error');
}
return new RedirectResponse('/');
}
}