class AuthController in Auth0 Single Sign On 8
Same name and namespace in other branches
- 8.2 src/Controller/AuthController.php \Drupal\auth0\Controller\AuthController
Controller routines for auth0 authentication.
Hierarchy
- class \Drupal\Core\Controller\ControllerBase implements ContainerInjectionInterface uses LoggerChannelTrait, MessengerTrait, LinkGeneratorTrait, RedirectDestinationTrait, UrlGeneratorTrait, StringTranslationTrait
- class \Drupal\auth0\Controller\AuthController
Expanded class hierarchy of AuthController
File
- src/
Controller/ AuthController.php, line 33
Namespace
Drupal\auth0\ControllerView source
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;
/**
* Inicialize the controller.
*/
public function __construct(PrivateTempStoreFactory $tempStoreFactory, SessionManagerInterface $sessionManager) {
// Ensure the pages this controller servers never gets cached
\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'));
}
/**
* Handles the login page override.
*/
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 supporting SSO, redirect to the hosted login page for authorization
*/
if ($this->redirect_for_sso == TRUE) {
$prompt = 'none';
return new TrustedRedirectResponse($this
->buildAuthorizeUrl($prompt));
}
/* Not doing SSO, so show login page */
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",
);
}
/**
* Handles the login page override.
*/
public function logout() {
global $base_root;
$auth0Api = new Authentication($this->domain, $this->client_id);
user_logout();
// if we are using SSO, we need to logout completely from Auth0, otherwise they will just logout of their client
return new TrustedRedirectResponse($auth0Api
->get_logout_link($base_root, $this->redirect_for_sso ? null : $this->client_id));
}
/**
* Create a new nonce in session and return it
*/
protected function getNonce() {
// Have to start the session after putting something into the session, or we don't actually start it!
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;
}
/**
* Do our one-time check against the nonce stored in session
*/
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;
}
/**
* Build the Authorize url
* @param $prompt none|login if prompt=none should be passed, false if not
*/
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);
}
/**
* Handles the callback for the oauth transaction.
*/
public function callback(Request $request) {
global $base_root;
$config = \Drupal::service('config.factory')
->get('auth0.settings');
/* Check for errors */
// Check in query
if ($request->query
->has('error') && $request->query
->get('error') == 'login_required') {
return new TrustedRedirectResponse($this
->buildAuthorizeUrl(FALSE));
}
// Check in post
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,
// Set to null so that the store is set to SessionStore.
'persist_id_token' => FALSE,
'persist_user' => FALSE,
'persist_access_token' => FALSE,
'persist_refresh_token' => FALSE,
));
$userInfo = NULL;
/**
* Exchange the code for the tokens (happens behind the scenes in the SDK)
*/
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());
}
/**
* Validate the ID Token
*/
$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());
}
/**
* Validate the state if we redirected for login
*/
$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})");
}
/**
* Check the sub if it exists (this will exist if you have enabled OIDC Conformant)
*/
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');
}
}
/**
* Checks if the email is valid.
*/
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();
}
}
}
/**
* Process the auth0 user profile and signin or signup the user.
*/
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);
}
// See if there is a user in the auth0_user table with the user info client id.
$user = $this
->findAuth0User($userInfo['user_id']);
if ($user) {
// User exists!
// update the auth0_user with the new userInfo object.
$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('/');
}
/**
* Create or link a new user based on the auth0 profile.
*/
protected function signupUser($userInfo, $idToken) {
// If the user doesn't exist we need to either create a new one, or assign him to an existing one.
$isDatabaseUser = FALSE;
/* Make sure we have the identities array, if not, fetch it from the user endpoint */
$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 the user has a verified email or is a database user try to see if there is
// a user to join with. The isDatabase is because we don't want to allow database
// user creation if there is an existing one with no verified email.
if ($userInfo['email_verified'] || $isDatabaseUser) {
$joinUser = user_load_by_mail($userInfo['email']);
}
if ($joinUser) {
// If we are here, we have a potential join user.
// Don't allow creation or assignation of user if the email is not verified,
// that would be hijacking.
if (!$userInfo['email_verified']) {
throw new EmailNotVerifiedException();
}
$user = $joinUser;
}
else {
// If we are here, we need to create the user.
$user = $this
->createDrupalUser($userInfo);
}
return $user;
}
/**
* Email not verified error message.
*/
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');
}
/**
* Get the auth0 user profile.
*/
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']);
}
/**
* Update the auth0 user profile.
*/
protected function updateAuth0User($userInfo) {
db_update('auth0_user')
->fields(array(
'auth0_object' => serialize($userInfo),
))
->condition('auth0_id', $userInfo['user_id'], '=')
->execute();
}
/**
* Insert the auth0 user.
*/
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);
}
/**
* Create the Drupal user based on the Auth0 user profile.
*/
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");
}
// If the username already exists, create a new random one.
$username = $userInfo['nickname'];
if (user_load_by_name($username)) {
$username .= time();
}
$user
->setUsername($username);
$user
->activate();
$user
->save();
return $user;
}
/**
* Send the verification email.
*/
public function verify_email(Request $request) {
$idToken = $request
->get('idToken');
/**
* Validate the ID Token
*/
$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('/');
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
AuthController:: |
protected | property | ||
AuthController:: |
protected | function | Email not verified error message. | |
AuthController:: |
constant | |||
AuthController:: |
constant | |||
AuthController:: |
constant | |||
AuthController:: |
constant | |||
AuthController:: |
constant | |||
AuthController:: |
constant | |||
AuthController:: |
constant | |||
AuthController:: |
protected | function | Build the Authorize url | |
AuthController:: |
public | function | Handles the callback for the oauth transaction. | |
AuthController:: |
protected | function | Do our one-time check against the nonce stored in session | |
AuthController:: |
public static | function |
Instantiates a new instance of this class. Overrides ControllerBase:: |
|
AuthController:: |
protected | function | Create the Drupal user based on the Auth0 user profile. | |
AuthController:: |
protected | function | ||
AuthController:: |
protected | function | Get the auth0 user profile. | |
AuthController:: |
private | function | ||
AuthController:: |
protected | function | Create a new nonce in session and return it | |
AuthController:: |
private | function | ||
AuthController:: |
protected | function | Insert the auth0 user. | |
AuthController:: |
public | function | Handles the login page override. | |
AuthController:: |
public | function | Handles the login page override. | |
AuthController:: |
constant | |||
AuthController:: |
protected | function | Process the auth0 user profile and signin or signup the user. | |
AuthController:: |
constant | |||
AuthController:: |
protected | function | Create or link a new user based on the auth0 profile. | |
AuthController:: |
protected | function | Update the auth0 user profile. | |
AuthController:: |
protected | function | Checks if the email is valid. | |
AuthController:: |
public | function | Send the verification email. | |
AuthController:: |
public | function | Inicialize the controller. | |
ControllerBase:: |
protected | property | The configuration factory. | |
ControllerBase:: |
protected | property | The current user service. | 1 |
ControllerBase:: |
protected | property | The entity form builder. | |
ControllerBase:: |
protected | property | The entity manager. | |
ControllerBase:: |
protected | property | The entity type manager. | |
ControllerBase:: |
protected | property | The form builder. | 2 |
ControllerBase:: |
protected | property | The key-value storage. | 1 |
ControllerBase:: |
protected | property | The language manager. | 1 |
ControllerBase:: |
protected | property | The module handler. | 2 |
ControllerBase:: |
protected | property | The state service. | |
ControllerBase:: |
protected | function | Returns the requested cache bin. | |
ControllerBase:: |
protected | function | Retrieves a configuration object. | |
ControllerBase:: |
private | function | Returns the service container. | |
ControllerBase:: |
protected | function | Returns the current user. | 1 |
ControllerBase:: |
protected | function | Retrieves the entity form builder. | |
ControllerBase:: |
protected | function | Retrieves the entity manager service. | |
ControllerBase:: |
protected | function | Retrieves the entity type manager. | |
ControllerBase:: |
protected | function | Returns the form builder service. | 2 |
ControllerBase:: |
protected | function | Returns a key/value storage collection. | 1 |
ControllerBase:: |
protected | function | Returns the language manager service. | 1 |
ControllerBase:: |
protected | function | Returns the module handler. | 2 |
ControllerBase:: |
protected | function |
Returns a redirect response object for the specified route. Overrides UrlGeneratorTrait:: |
|
ControllerBase:: |
protected | function | Returns the state storage service. | |
LinkGeneratorTrait:: |
protected | property | The link generator. | 1 |
LinkGeneratorTrait:: |
protected | function | Returns the link generator. | |
LinkGeneratorTrait:: |
protected | function | Renders a link to a route given a route name and its parameters. | |
LinkGeneratorTrait:: |
public | function | Sets the link generator service. | |
LoggerChannelTrait:: |
protected | property | The logger channel factory service. | |
LoggerChannelTrait:: |
protected | function | Gets the logger for a specific channel. | |
LoggerChannelTrait:: |
public | function | Injects the logger channel factory. | |
MessengerTrait:: |
protected | property | The messenger. | 29 |
MessengerTrait:: |
public | function | Gets the messenger. | 29 |
MessengerTrait:: |
public | function | Sets the messenger. | |
RedirectDestinationTrait:: |
protected | property | The redirect destination service. | 1 |
RedirectDestinationTrait:: |
protected | function | Prepares a 'destination' URL query parameter for use with \Drupal\Core\Url. | |
RedirectDestinationTrait:: |
protected | function | Returns the redirect destination service. | |
RedirectDestinationTrait:: |
public | function | Sets the redirect destination service. | |
StringTranslationTrait:: |
protected | property | The string translation service. | 1 |
StringTranslationTrait:: |
protected | function | Formats a string containing a count of items. | |
StringTranslationTrait:: |
protected | function | Returns the number of plurals supported by a given language. | |
StringTranslationTrait:: |
protected | function | Gets the string translation service. | |
StringTranslationTrait:: |
public | function | Sets the string translation service to use. | 2 |
StringTranslationTrait:: |
protected | function | Translates a string to the current language or to a given language. | |
UrlGeneratorTrait:: |
protected | property | The url generator. | |
UrlGeneratorTrait:: |
protected | function | Returns the URL generator service. | |
UrlGeneratorTrait:: |
public | function | Sets the URL generator service. | |
UrlGeneratorTrait:: |
protected | function | Generates a URL or path for a specific route based on the given parameters. |