You are here

class WindowsAad in OpenID Connect Microsoft Azure Active Directory client 2.0.x

Same name and namespace in other branches
  1. 8 src/Plugin/OpenIDConnectClient/WindowsAad.php \Drupal\openid_connect_windows_aad\Plugin\OpenIDConnectClient\WindowsAad

Generic OpenID Connect client.

Used primarily to login to Drupal sites powered by oauth2_server or PHP sites powered by oauth2-server-php.

Plugin annotation


@OpenIDConnectClient(
  id = "windows_aad",
  label = @Translation("Windows Azure AD")
)

Hierarchy

Expanded class hierarchy of WindowsAad

File

src/Plugin/OpenIDConnectClient/WindowsAad.php, line 27

Namespace

Drupal\openid_connect_windows_aad\Plugin\OpenIDConnectClient
View source
class WindowsAad extends OpenIDConnectClientBase {

  /**
   * The key repository interface.
   *
   * @var \Drupal\key\KeyRepositoryInterface
   */
  protected $keyRepository;

  /**
   * The constructor.
   *
   * @param array $configuration
   *   The plugin configuration.
   * @param string $plugin_id
   *   The plugin identifier.
   * @param mixed $plugin_definition
   *   The plugin definition.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
   * @param \GuzzleHttp\ClientInterface $http_client
   *   The http client.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger factory.
   * @param \Drupal\key\KeyRepositoryInterface $key_repository
   *   The Key Repository interface.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, RequestStack $request_stack, ClientInterface $http_client, LoggerChannelFactoryInterface $logger_factory, KeyRepositoryInterface $key_repository) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $request_stack, $http_client, $logger_factory);
    $this->keyRepository = $key_repository;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static($configuration, $plugin_id, $plugin_definition, $container
      ->get('request_stack'), $container
      ->get('http_client'), $container
      ->get('logger.factory'), $container
      ->get('key.repository'));
  }

  /**
   * Overrides OpenIDConnectClientBase::settingsForm().
   *
   * @param array $form
   *   Windows AAD form array containing form elements.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Submitted form values.
   *
   * @return array
   *   Renderable form array with form elements.
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildConfigurationForm($form, $form_state);
    $form['enable_single_sign_out'] = [
      '#title' => $this
        ->t('Enable Single Sign Out'),
      '#type' => 'checkbox',
      '#default_value' => !empty($this->configuration['enable_single_sign_out']) ? $this->configuration['enable_single_sign_out'] : FALSE,
      '#description' => $this
        ->t('Checking this option will enable Single Sign Out to occur so long as the logout url has been set to (http(s)://yoursite.com/openid-connect/windows_aad/signout) in your Azure AD registered app settings. If a user logs out of the Drupal app then they will be logged out of their SSO session elsewhere as well. Conversely if a user signs out of their SSO account elsewhere, such as Office 365, they will also be logged out of this app.'),
    ];
    $form['authorization_endpoint_wa'] = [
      '#title' => $this
        ->t('Authorization endpoint'),
      '#type' => 'textfield',
      '#default_value' => $this->configuration['authorization_endpoint_wa'],
    ];
    $form['token_endpoint_wa'] = [
      '#title' => $this
        ->t('Token endpoint'),
      '#type' => 'textfield',
      '#default_value' => $this->configuration['token_endpoint_wa'],
    ];
    $form['map_ad_groups_to_roles'] = [
      '#title' => $this
        ->t('Map user\'s AD groups to Drupal roles'),
      '#type' => 'checkbox',
      '#default_value' => !empty($this->configuration['map_ad_groups_to_roles']) ? $this->configuration['map_ad_groups_to_roles'] : '',
      '#description' => $this
        ->t('Enable this to configure Drupal user role assignment based on AD group membership.'),
    ];

    // AD group mapping configuration field set.
    $form['group_mapping'] = [
      '#type' => 'fieldset',
      '#title' => $this
        ->t('AD group mapping options'),
      '#states' => [
        'invisible' => [
          ':input[name="clients[windows_aad][settings][map_ad_groups_to_roles]"]' => [
            'checked' => FALSE,
          ],
        ],
      ],
    ];
    $form['group_mapping']['method'] = [
      '#type' => 'radios',
      '#title' => $this
        ->t('Method for mapping AD groups to roles'),
      '#options' => [
        0 => $this
          ->t('Automatic (AD group names or ids identically match Drupal role names)'),
        1 => $this
          ->t('Manual (Specify which AD groups map to which Drupal roles)'),
      ],
      '#default_value' => !empty($this->configuration['group_mapping']['method']) ? $this->configuration['group_mapping']['method'] : 0,
      '#description' => $this
        ->t('Note: For name mapping to function the Azure AD Graph or Windows Graph APIs must be selected as a User endpoint. Otherwise only mapping based on Group Object IDs can be used.'),
    ];
    $form['group_mapping']['mappings'] = [
      '#title' => $this
        ->t('Manual mappings'),
      '#type' => 'textarea',
      '#default_value' => isset($this->configuration['group_mapping']) && isset($this->configuration['group_mapping']['mappings']) ? $this->configuration['group_mapping']['mappings'] : '',
      '#description' => $this
        ->t('Add one role|group(s) mapping per line. Role and Group should be separated by "|". Multiple groups can be mapped to a single role on the same line using ";" to separate the groups. Ideally you should use the group id since it is immutable, but the title (displayName) may also be used.'),
      '#states' => [
        'invisible' => [
          ':input[name="clients[windows_aad][settings][group_mapping][method]"]' => [
            'value' => 0,
          ],
        ],
      ],
    ];
    $form['group_mapping']['strict'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->t('Only allow users to have roles that map to an AD group they are a member of.'),
      '#default_value' => !empty($this->configuration['group_mapping']['strict']) ? $this->configuration['group_mapping']['strict'] : '',
      '#description' => $this
        ->t('Removes roles from a Drupal user account that do not map to AD groups the user is a member of. Also, with this enabled you can not grant unmapped roles to a user through the usual Drupal user/role interface such as editing a user account. Note: Only affects users with connected AD accounts.'),
    ];
    $form['userinfo_graph_api_wa'] = [
      '#title' => $this
        ->t('User info endpoint configuration'),
      '#type' => 'radios',
      '#default_value' => !empty($this->configuration['userinfo_graph_api_wa']) ? $this->configuration['userinfo_graph_api_wa'] : 0,
      '#options' => [
        0 => $this
          ->t('Alternate or no user endpoint'),
        1 => $this
          ->t('Azure AD Graph API (v1.6)'),
        2 => $this
          ->t('Windows Graph API (v1.0)'),
      ],
      '#description' => $this
        ->t('Most user/group info can be returned in the access token response through proper claims/permissions configuration for your app registration within Azure AD. If this is the case for your setup then you can choose "Alternate or no user endpoint" and leave blank the dependent "Alternate userinfo endpoint" text box. Otherwise you can choose to use the Azure AD graph API or the Windows Graph API (recommended) to retrieve user and/or graph info.'),
    ];
    $form['userinfo_endpoint_wa'] = [
      '#title' => $this
        ->t('Alternate UserInfo endpoint'),
      '#type' => 'textfield',
      '#default_value' => $this->configuration['userinfo_endpoint_wa'],
      '#states' => [
        'visible' => [
          ':input[name="clients[windows_aad][settings][userinfo_graph_api_wa]"]' => [
            'value' => 0,
          ],
        ],
      ],
    ];
    $form['userinfo_graph_api_use_other_mails'] = [
      '#title' => $this
        ->t('Use Graph API otherMails property for email address'),
      '#type' => 'checkbox',
      '#default_value' => !empty($this->configuration['userinfo_graph_api_use_other_mails']) ? $this->configuration['userinfo_graph_api_use_other_mails'] : '',
      '#description' => $this
        ->t('Find the first occurrence of an email address in the Graph otherMails property and use this as email address.'),
      '#states' => [
        'visible' => [
          ':input[name="clients[windows_aad][settings][userinfo_graph_api_wa]"]' => [
            'value' => 1,
          ],
        ],
      ],
    ];
    $form['userinfo_update_email'] = [
      '#title' => $this
        ->t('Update email address in user profile'),
      '#type' => 'checkbox',
      '#default_value' => !empty($this->configuration['userinfo_update_email']) ? $this->configuration['userinfo_update_email'] : '',
      '#description' => $this
        ->t('If email address has been changed for existing user, save the new value to the user profile.'),
    ];
    $form['hide_email_address_warning'] = [
      '#title' => $this
        ->t('Hide missing email address warning'),
      '#type' => 'checkbox',
      '#default_value' => !empty($this->configuration['hide_email_address_warning']) ? $this->configuration['hide_email_address_warning'] : '',
      '#description' => $this
        ->t('By default, when email address is not found, a message will appear on the screen. This option hides that message (as it might be confusing for end users).'),
    ];
    $form['client_secret'] = [
      '#title' => $this
        ->t('Client secret'),
      '#type' => 'key_select',
      '#default_value' => $this->configuration['client_secret'],
    ];
    return $form;
  }

  /**
   * Overrides OpenIDConnectClientBase::getEndpoints().
   *
   * @return array
   *   Endpoint details with authorization endpoints, user access token and
   *   userinfo object.
   */
  public function getEndpoints() {
    return [
      'authorization' => $this->configuration['authorization_endpoint_wa'],
      'token' => $this->configuration['token_endpoint_wa'],
      'userinfo' => $this->configuration['userinfo_endpoint_wa'],
    ];
  }

  /**
   * Implements OpenIDConnectClientInterface::retrieveIDToken().
   *
   * @param string $authorization_code
   *   A authorization code string.
   *
   * @return array|bool
   *   A result array or false.
   */
  public function retrieveTokens($authorization_code) {

    // Exchange `code` for access token and ID token.
    $language_none = \Drupal::languageManager()
      ->getLanguage(LanguageInterface::LANGCODE_NOT_APPLICABLE);
    $redirect_uri = Url::fromRoute('openid_connect.redirect_controller_redirect', [
      'client_name' => $this->pluginId,
    ], [
      'absolute' => TRUE,
      'language' => $language_none,
    ])
      ->toString();
    $endpoints = $this
      ->getEndpoints();
    $secret = $this->keyRepository
      ->getKey($this->configuration['client_secret'])
      ->getKeyValue();
    $request_options = [
      'form_params' => [
        'code' => $authorization_code,
        'client_id' => $this->configuration['client_id'],
        'client_secret' => $secret,
        'redirect_uri' => $redirect_uri,
        'grant_type' => 'authorization_code',
      ],
    ];

    // Add a Graph API as resource if an option is selected.
    switch ($this->configuration['userinfo_graph_api_wa']) {
      case 1:
        $request_options['form_params']['resource'] = 'https://graph.windows.net';
        break;
      case 2:
        $request_options['form_params']['resource'] = 'https://graph.microsoft.com';
        break;
    }
    $client = $this->httpClient;
    try {
      $response = $client
        ->post($endpoints['token'], $request_options);
      $response_data = json_decode((string) $response
        ->getBody(), TRUE);

      // Expected result.
      $tokens = [
        'id_token' => $response_data['id_token'],
        'access_token' => $response_data['access_token'],
        'refresh_token' => isset($response_data['refresh_token']) ? $response_data['refresh_token'] : FALSE,
      ];
      if (array_key_exists('expires_in', $response_data)) {
        $tokens['expire'] = \Drupal::time()
          ->getRequestTime() + $response_data['expires_in'];
      }
      return $tokens;
    } catch (RequestException $e) {
      $variables = [
        '@message' => 'Could not retrieve tokens',
        '@error_message' => $e
          ->getMessage(),
      ];
      $this->loggerFactory
        ->get('openid_connect_windows_aad')
        ->error('@message. Details: @error_message', $variables);
      return FALSE;
    }
  }

  /**
   * Implements OpenIDConnectClientInterface::retrieveUserInfo().
   *
   * @param string $access_token
   *   An access token string.
   *
   * @return array|bool
   *   A result array or false.
   */
  public function retrieveUserInfo($access_token) {

    // Determine if we use Graph API or default O365 Userinfo as this will
    // affect the data we collect and use in the Userinfo array.
    switch ($this->configuration['userinfo_graph_api_wa']) {
      case 1:
        $userinfo = $this
          ->buildUserinfo($access_token, 'https://graph.windows.net/me?api-version=1.6', 'userPrincipalName', 'displayName');
        break;
      case 2:
        $userinfo = $this
          ->buildUserinfo($access_token, 'https://graph.microsoft.com/v1.0/me', 'userPrincipalName', 'displayName');
        break;
      default:
        $endpoints = $this
          ->getEndpoints();
        if ($endpoints['userinfo']) {
          $userinfo = $this
            ->buildUserinfo($access_token, $endpoints['userinfo'], 'upn', 'name');
        }
        else {
          $userinfo = [];
        }
        break;
    }

    // If AD group to Drupal role mapping has been enabled then attach group
    // data from a graph API if configured to do so.
    if (!empty($this->configuration['map_ad_groups_to_roles'])) {
      $userinfo['groups'] = $this
        ->retrieveGroupInfo($access_token);
    }

    // Check to see if we have changed email data, O365_connect doesn't
    // give us the possibility to add a mapping for it, so we do the change
    // now, first checking if this is wanted by checking the setting for it.
    if ($userinfo && $this->configuration['userinfo_update_email'] === 1) {

      /** @var \Drupal\user\UserInterface $user */
      $user = user_load_by_name($userinfo['name']);
      if ($user && $user
        ->getEmail() !== $userinfo['email']) {
        $user
          ->setEmail($userinfo['email']);
        $user
          ->save();
      }
    }
    return $userinfo;
  }

  /**
   * Helper function to do the call to the endpoint and build userinfo array.
   *
   * @param string $access_token
   *   The access token.
   * @param string $url
   *   The endpoint we want to send the request to.
   * @param string $upn
   *   The name of the property that holds the Azure username.
   * @param string $name
   *   The name of the property we want to map to Drupal username.
   *
   * @return array
   *   The userinfo array. Empty array if unsuccessful.
   */
  private function buildUserinfo($access_token, $url, $upn, $name) {
    $profile_data = [];

    // Perform the request.
    $options = [
      'method' => 'GET',
      'headers' => [
        'Content-Type' => 'application/json',
        'Authorization' => 'Bearer ' . $access_token,
      ],
    ];
    $client = $this->httpClient;
    try {
      $response = $client
        ->get($url, $options);
      $response_data = (string) $response
        ->getBody();

      // Profile Information.
      $profile_data = json_decode($response_data, TRUE);
      $profile_data['name'] = $profile_data[$name];

      // Azure provides 'mail' for userinfo vs email.
      if (!isset($profile_data['mail'])) {

        // See if we have the Graph otherMails property and use it if available,
        // if not, add the principal name as email instead, so Drupal still will
        // create the user anyway.
        if ($this->configuration['userinfo_graph_api_use_other_mails'] === 1) {
          if (!empty($profile_data['otherMails'])) {

            // Use first occurrence of otherMails attribute.
            $profile_data['email'] = current($profile_data['otherMails']);
          }
        }
        else {

          // Show message to user.
          if ($this->configuration['hide_email_address_warning'] !== 1) {
            \Drupal::messenger()
              ->addWarning(t('Email address not found in UserInfo. Used username instead, please check this in your profile.'));
          }

          // Write watchdog warning.
          $variables = [
            '@user' => $profile_data[$upn],
          ];
          $this->loggerFactory
            ->get('openid_connect_windows_aad')
            ->warning('Email address of user @user not found in UserInfo. Used username instead, please check.', $variables);
          $profile_data['email'] = $profile_data[$upn];
        }
      }
      else {

        // OpenID Connect module expects the 'email' token for userinfo.
        $profile_data['email'] = $profile_data['mail'];
      }
    } catch (RequestException $e) {
      $variables = [
        '@error_message' => $e
          ->getMessage(),
      ];
      $this->loggerFactory
        ->get('openid_connect_windows_aad')
        ->error('Could not retrieve user profile information. Details: @error_message', $variables);
    }
    return $profile_data;
  }

  /**
   * Calls a graph api to retrieve teh user's group membership information.
   *
   * @param string $access_token
   *   An access token string.
   *
   * @return array
   *   An array of group informaion.
   */
  protected function retrieveGroupInfo($access_token) {

    // By default or if an error occurs return empty group information.
    $group_data = [];
    switch ($this->configuration['userinfo_graph_api_wa']) {
      case 1:
        $uri = 'https://graph.windows.net/me/memberOf?api-version=1.6';
        break;
      case 2:
        $uri = 'https://graph.microsoft.com/v1.0/me/memberOf';
        break;
      default:
        $uri = FALSE;
        break;
    }
    if ($uri) {

      // Perform the request.
      $options = [
        'method' => 'GET',
        'headers' => [
          'Content-Type' => 'application/json',
          'Authorization' => 'Bearer ' . $access_token,
        ],
      ];
      $client = $this->httpClient;
      try {
        $response = $client
          ->get($uri, $options);
        $response_data = (string) $response
          ->getBody();

        // Group Information.
        $group_data = json_decode($response_data, TRUE);
      } catch (RequestException $e) {
        $variables = [
          '@api' => $uri,
          '@error_message' => $e
            ->getMessage(),
        ];
        $this->loggerFactory
          ->get('openid_connect_windows_aad')
          ->error('Failed to retrieve AD group information from graph api (@api). Details: @error_message', $variables);
      }
    }

    // Return group information or an empty array.
    return $group_data;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DependencySerializationTrait::$_entityStorages protected property
DependencySerializationTrait::$_serviceIds protected property
DependencySerializationTrait::__sleep public function 2
DependencySerializationTrait::__wakeup public function 2
MessengerTrait::$messenger protected property The messenger. 27
MessengerTrait::messenger public function Gets the messenger. 27
MessengerTrait::setMessenger public function Sets the messenger.
OpenIDConnectClientBase::$autoDiscover protected property The OpenID well-known discovery service.
OpenIDConnectClientBase::$dateTime protected property The datetime.time service.
OpenIDConnectClientBase::$httpClient protected property The HTTP client to fetch the feed data with.
OpenIDConnectClientBase::$languageManager protected property The language manager.
OpenIDConnectClientBase::$loggerFactory protected property The logger factory used for logging.
OpenIDConnectClientBase::$pageCacheKillSwitch protected property Page cache kill switch.
OpenIDConnectClientBase::$parentEntityId protected property The parent entity identifier.
OpenIDConnectClientBase::$requestStack protected property The request stack used to access request globals.
OpenIDConnectClientBase::$stateToken protected property The OpenID state token service.
OpenIDConnectClientBase::authorize public function Redirects the user to the authorization endpoint. Overrides OpenIDConnectClientInterface::authorize 3
OpenIDConnectClientBase::calculateDependencies public function Calculates dependencies for the configured plugin. Overrides DependentPluginInterface::calculateDependencies
OpenIDConnectClientBase::defaultConfiguration public function Gets default configuration for this plugin. Overrides ConfigurableInterface::defaultConfiguration 3
OpenIDConnectClientBase::getClientScopes public function Gets an array of of scopes. Overrides OpenIDConnectClientInterface::getClientScopes 2
OpenIDConnectClientBase::getConfiguration public function Gets this plugin's configuration. Overrides ConfigurableInterface::getConfiguration
OpenIDConnectClientBase::getLabel public function Return the plugin label as defined in the annotation. Overrides OpenIDConnectClientInterface::getLabel
OpenIDConnectClientBase::getParentEntityId public function Returns the parent entity ID. Overrides OpenIDConnectClientInterface::getParentEntityId
OpenIDConnectClientBase::getRedirectUrl protected function Returns the redirect URL.
OpenIDConnectClientBase::getRequestOptions protected function Helper function for request options.
OpenIDConnectClientBase::getUrlOptions protected function Helper function for URL options.
OpenIDConnectClientBase::setConfiguration public function Sets the configuration for this plugin instance. Overrides ConfigurableInterface::setConfiguration
OpenIDConnectClientBase::setParentEntityId public function Sets the parent entity ID. Overrides OpenIDConnectClientInterface::setParentEntityId
OpenIDConnectClientBase::submitConfigurationForm public function Form submission handler. Overrides PluginFormInterface::submitConfigurationForm 2
OpenIDConnectClientBase::unsetConfigurationKeys protected function Unsets some elements of the configuration.
OpenIDConnectClientBase::usesUserInfo public function Check if the client uses the userinfo endpoint. Overrides OpenIDConnectClientInterface::usesUserInfo
OpenIDConnectClientBase::validateConfigurationForm public function Form validation handler. Overrides PluginFormInterface::validateConfigurationForm 1
PluginBase::$configuration protected property Configuration information passed into the plugin. 1
PluginBase::$pluginDefinition protected property The plugin implementation definition. 1
PluginBase::$pluginId protected property The plugin_id.
PluginBase::DERIVATIVE_SEPARATOR constant A string which is used to separate base plugin IDs from the derivative ID.
PluginBase::getBaseId public function Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface::getBaseId
PluginBase::getDerivativeId public function Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface::getDerivativeId
PluginBase::getPluginDefinition public function Gets the definition of the plugin implementation. Overrides PluginInspectionInterface::getPluginDefinition 2
PluginBase::getPluginId public function Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface::getPluginId
PluginBase::isConfigurable public function Determines if the plugin is configurable.
PluginWithFormsTrait::getFormClass public function Implements \Drupal\Core\Plugin\PluginWithFormsInterface::getFormClass().
PluginWithFormsTrait::hasFormClass public function Implements \Drupal\Core\Plugin\PluginWithFormsInterface::hasFormClass().
StringTranslationTrait::$stringTranslation protected property The string translation service. 4
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.
WindowsAad::$keyRepository protected property The key repository interface.
WindowsAad::buildConfigurationForm public function Overrides OpenIDConnectClientBase::settingsForm(). Overrides OpenIDConnectClientBase::buildConfigurationForm
WindowsAad::buildUserinfo private function Helper function to do the call to the endpoint and build userinfo array.
WindowsAad::create public static function Creates an instance of the plugin. Overrides OpenIDConnectClientBase::create
WindowsAad::getEndpoints public function Overrides OpenIDConnectClientBase::getEndpoints(). Overrides OpenIDConnectClientInterface::getEndpoints
WindowsAad::retrieveGroupInfo protected function Calls a graph api to retrieve teh user's group membership information.
WindowsAad::retrieveTokens public function Implements OpenIDConnectClientInterface::retrieveIDToken(). Overrides OpenIDConnectClientBase::retrieveTokens
WindowsAad::retrieveUserInfo public function Implements OpenIDConnectClientInterface::retrieveUserInfo(). Overrides OpenIDConnectClientBase::retrieveUserInfo
WindowsAad::__construct public function The constructor. Overrides OpenIDConnectClientBase::__construct