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;
}
}
