oidc-1.0.0-alpha2/src/OpenidConnectRealm/OpenidConnectRealmBase.php

src/OpenidConnectRealm/OpenidConnectRealmBase.php
<?php

namespace Drupal\oidc\OpenidConnectRealm;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Url;
use Drupal\oidc\JsonHttp\JsonHttpClientInterface;
use Drupal\oidc\JsonHttp\JsonHttpGetRequest;
use Drupal\oidc\JsonHttp\JsonHttpPostRequest;
use Drupal\oidc\JsonHttp\JsonHttpPostRequestInterface;
use Drupal\oidc\JsonWebTokens;
use Drupal\oidc\Token;
use Psr\Log\LoggerInterface;
use Sop\JWX\JWK\JWK;
use Sop\JWX\JWT\JWT;
use Sop\JWX\JWT\ValidationContext;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Base class for an OpenID Connect realm.
 */
abstract class OpenidConnectRealmBase extends PluginBase implements OpenidConnectRealmInterface, ContainerFactoryPluginInterface {

  /**
   * The JSON web keys set storage.
   *
   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
   */
  protected $jwksStorage;

  /**
   * The JSON HTTP client.
   *
   * @var \Drupal\oidc\JsonHttp\JsonHttpClientInterface
   */
  protected $jsonHttpClient;

  /**
   * The time service.
   *
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected $time;

  /**
   * The logger.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

  /**
   * Class constructor.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
   *   The key-value storage factory.
   * @param \Drupal\oidc\JsonHttp\JsonHttpClientInterface $json_http_client
   *   The JSON HTTP client.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   The time service.
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, KeyValueFactoryInterface $key_value_factory, JsonHttpClientInterface $json_http_client, TimeInterface $time, LoggerInterface $logger) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);

    $this->jwksStorage = $key_value_factory->get('oidc.jwks.' . $plugin_id);
    $this->jsonHttpClient = $json_http_client;
    $this->time = $time;
    $this->logger = $logger;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('keyvalue'),
      $container->get('oidc.json_http_client'),
      $container->get('datetime.time'),
      $container->get('logger.channel.oidc')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function isEnabled() {
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function getLoginUrl($state, Url $redirect_url) {
    return Url::fromUri($this->getAuthorizationEndpoint(), [
      'query' => [
        'response_type' => 'code',
        'scope' => $this->getScopeParameter(),
        'client_id' => $this->getClientId(),
        'state' => $state,
        'redirect_uri' => $redirect_url->setAbsolute()->toString(TRUE)->getGeneratedUrl(),
      ],
    ]);
  }

  /**
   * {@inheritdoc}
   */
  public function getJsonWebTokensForLogin($state, $code) {
    $endpoint = $this->getTokenEndpoint();
    $redirect_uri = Url::fromRoute('<current>')
      ->setAbsolute()
      ->toString(TRUE)
      ->getGeneratedUrl();

    try {
      return $this->getJsonWebTokens(
        JsonHttpPostRequest::create($this->getTokenEndpoint())
          ->setBasicAuth($this->getClientId(), $this->getClientSecret())
          ->addFormParameter('code', $code)
          ->addFormParameter('grant_type', 'authorization_code')
          ->addFormParameter('redirect_uri', $redirect_uri)
      );
    }
    catch (\Exception $ex) {
      $this->logger->error('Failed to retrieve tokens for login from %endpoint: @error.', [
        '%endpoint' => $endpoint,
        '@error' => $ex->getMessage(),
      ]);

      throw new \RuntimeException('Failed to retrieve tokens for login', 0, $ex);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getJsonWebTokensForRefresh(Token $refresh_token) {
    $endpoint = $this->getTokenEndpoint();

    try {
      return $this->getJsonWebTokens(
        JsonHttpPostRequest::create($endpoint)
          ->setBasicAuth($this->getClientId(), $this->getClientSecret())
          ->addFormParameter('grant_type', 'refresh_token')
          ->addFormParameter('refresh_token', $refresh_token->getValue())
          ->addFormParameter('scope', $this->getScopeParameter())
      );
    }
    catch (\Exception $ex) {
      $this->logger->error('Failed to retrieve tokens for refresh from %endpoint: @error.', [
        '%endpoint' => $endpoint,
        '@error' => $ex->getMessage(),
      ]);

      throw new \RuntimeException('Failed to retrieve tokens for refresh', 0, $ex);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getLogoutUrl(Token $id_token, $state, Url $redirect_url) {
    if ($endpoint = $this->getEndSessionEndpoint()) {
      return Url::fromUri($endpoint, [
        'query' => [
          'id_token_hint' => $id_token->getValue(),
          'state' => $state,
          'post_logout_redirect_uri' => $redirect_url->setAbsolute()->toString(TRUE)->getGeneratedUrl(),
        ],
      ]);
    }

    // Add the state query parameter for the redirect access check.
    if ($redirect_url->isRouted()) {
      $query = $redirect_url->getOption('query') ?? [];
      $query['state'] = $state;
      $redirect_url->setOption('query', $query);
    }

    return $redirect_url;
  }

  /**
   * {@inheritdoc}
   */
  public function updateJwks() {
    $url = $this->getJwksUrl();

    try {
      $response = $this->jsonHttpClient->get(new JsonHttpGetRequest($url));
    }
    catch (\Exception $ex) {
      $this->logger->error('Failed to update the JSON web keys at %url: @error.', [
        '%url' => $url,
        '@error' => $ex->getMessage(),
      ]);

      return FALSE;
    }

    if (empty($response['keys'])) {
      $this->logger->error('The JSON web keys set at %url has no keys.', [
        '%url' => $url,
      ]);

      return FALSE;
    }

    // Add the missing keys.
    $added = 0;
    foreach ($response['keys'] as $key) {
      if (!isset($key['kid'], $key['kty'])) {
        continue;
      }

      if ($this->jwksStorage->setIfNotExists($key['kid'], $key)) {
        $added++;
      }
    }

    // Log the result.
    if (!$added) {
      $this->logger->info('Updated the JSON web keys set at %url, no new keys were added.', [
        '%url' => $url,
      ]);
    }
    else {
      $this->logger->info('Updated the JSON web keys set at %url, @count key(s) were added.', [
        '%url' => $url,
        '@count' => $added,
      ]);
    }

    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function clearJwks() {
    $this->jwksStorage->deleteAll();
  }

  /**
   * {@inheritdoc}
   */
  public function getDisplayNameFormat() {
    return '[user:account-name]';
  }

  /**
   * Get the client ID.
   *
   * @return string
   *   The client ID.
   */
  abstract protected function getClientId();

  /**
   * Get the client secret.
   *
   * @return string
   *   The client secret.
   */
  abstract protected function getClientSecret();

  /**
   * Get the scopes.
   *
   * @return array
   *   A list of scopes to aquire.
   */
  abstract protected function getScopes();

  /**
   * Get the issuer.
   *
   * @return string
   *   The issuer.
   */
  abstract protected function getIssuer();

  /**
   * Get the authorization endpoint URL.
   *
   * @return string
   *   The authorization endpoint URL.
   */
  abstract protected function getAuthorizationEndpoint();

  /**
   * Get the token endpoint URL.
   *
   * @return string
   *   The token endpoint URL.
   */
  abstract protected function getTokenEndpoint();

  /**
   * Get the userinfo endpoint URL.
   *
   * @return string|null
   *   The userinfo endpoint URL or NULL if not applicable.
   */
  protected function getUserinfoEndpoint() {
    return NULL;
  }

  /**
   * Get the end session endpoint URL.
   *
   * @return string|null
   *   The end session endpoint URL or NULL if not specified.
   */
  protected function getEndSessionEndpoint() {
    return NULL;
  }

  /**
   * Get the JSON web keys set URL.
   *
   * @return string
   *   The JSON web keys set URL.
   */
  abstract protected function getJwksUrl();

  /**
   * Get the scope parameter.
   *
   * @return string
   *   The scope parameter.
   */
  protected function getScopeParameter() {
    $scopes = $this->getScopes();
    $scopes[] = 'openid';

    return implode(' ', $scopes);
  }

  /**
   * Get the JSON web tokens.
   *
   * @param \Drupal\oidc\JsonHttp\JsonHttpPostRequestInterface $json_http_post_request
   *   The JSON HTTP post request.
   *
   * @return \Drupal\oidc\JsonWebTokens
   *   The JSON web tokens.
   *
   * @throws \Exception
   */
  protected function getJsonWebTokens(JsonHttpPostRequestInterface $json_http_post_request) {
    $response = $this->jsonHttpClient->post($json_http_post_request);

    // Ensure we have all the data we need to continue.
    if (!isset($response['id_token'], $response['access_token'], $response['token_type'], $response['expires_in'])) {
      throw new \RuntimeException('Some data is missing in the token response');
    }

    // Parse the ID token.
    $jwt = new JWT($response['id_token']);

    // Get the key.
    $kid = $jwt->header()->keyID()->value();
    $key = JWK::fromArray($this->getJwk($kid));

    // Create the validation context.
    $context = ValidationContext::fromJWK($key)
      ->withIssuer($this->getIssuer())
      ->withAudience($this->getClientId());

    // Validate and get the claims.
    $claims = $jwt->claims($context);

    // Create the tokens object.
    $expires = $this->time->getRequestTime() + $response['expires_in'];
    $id_token = new Token($response['id_token'], $expires);
    $access_token = new Token($response['access_token'], $expires);

    $tokens = new JsonWebTokens($response['token_type'], $id_token, $access_token);

    if (isset($response['refresh_token'], $response['refresh_expires_in'])) {
      $expires = $this->time->getRequestTime() + $response['refresh_expires_in'];
      $tokens->setRefreshToken(new Token($response['refresh_token'], $expires));
    }

    foreach ($claims->all() as $claim) {
      $tokens->setClaim($claim->name(), $claim->value());
    }

    // Add the user info as claims.
    if ($endpoint = $this->getUserinfoEndpoint()) {
      $jhgr = JsonHttpGetRequest::create($endpoint)
        ->addHeader('Authorization', $tokens->getType() . ' ' . $tokens->getAccessToken()->getValue());

      try {
        $response = $this->jsonHttpClient->get($jhgr);
      }
      catch (\Exception $ex) {
        $this->logger->error('Failed to retrieve user info from %endpoint: @error.', [
          '%endpoint' => $endpoint,
          '@error' => $ex->getMessage(),
        ]);

        throw new \RuntimeException('Failed to retrieve the user info', 0, $ex);
      }

      foreach ($response as $name => $value) {
        $tokens->setClaim($name, $value);
      }
    }

    return $tokens;
  }

  /**
   * Get a single JSON web key.
   *
   * @param string $id
   *   The key ID.
   * @param bool $update_if_missing
   *   Set to FALSE to prevent a set update if the ID doesn't exist.
   *
   * @return array
   *   The JSON web key.
   *
   * @throws \InvalidArgumentException
   */
  protected function getJwk($id, $update_if_missing = TRUE) {
    $key = $this->jwksStorage->get($id);

    if ($key === NULL && $update_if_missing && $this->updateJwks()) {
      $key = $this->jwksStorage->get($id);
    }

    if ($key === NULL) {
      throw new \InvalidArgumentException('JSON web key ' . $id . ' does not exist');
    }

    return $key;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc