oauth2_server-2.0.x-dev/src/Controller/OAuth2Controller.php
src/Controller/OAuth2Controller.php
<?php
namespace Drupal\oauth2_server\Controller;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\PrivateKey;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\State\State;
use Drupal\Core\Url;
use Drupal\oauth2_server\OAuth2StorageInterface;
use Drupal\oauth2_server\ScopeUtility;
use Drupal\oauth2_server\Utility;
use OAuth2\HttpFoundationBridge\Request as BridgeRequest;
use OAuth2\HttpFoundationBridge\Response as BridgeResponse;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class OAuth2 Controller.
*
* @package Drupal\oauth2_server\Controller
*/
class OAuth2Controller extends ControllerBase {
/**
* The OAuth2Storage service.
*
* @var \Drupal\oauth2_server\OAuth2StorageInterface
*/
protected $storage;
/**
* The state service.
*
* @var \Drupal\Core\State\State
*/
protected $state;
/**
* The private key service.
*
* @var \Drupal\Core\PrivateKey
*/
protected $privateKey;
/**
* The logger channel.
*
* @var \Drupal\Core\Logger\LoggerChannel|\Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* The class constructor.
*
* @param \Drupal\oauth2_server\OAuth2StorageInterface $oauth2_storage
* The oauth2 storage service.
* @param \Drupal\Core\State\State $state
* The state service.
* @param \Drupal\Core\PrivateKey $private_key
* The private key service.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger factory service.
*/
public function __construct(
OAuth2StorageInterface $oauth2_storage,
State $state,
PrivateKey $private_key,
LoggerChannelFactoryInterface $logger_factory,
) {
$this->storage = $oauth2_storage;
$this->state = $state;
$this->privateKey = $private_key;
$this->logger = $logger_factory->get('oauth2_server');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('oauth2_server.storage'),
$container->get('state'),
$container->get('private_key'),
$container->get('logger.factory')
);
}
/**
* Authorize.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return array|\OAuth2\HttpFoundationBridge\Response|\Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
* A form array or a response object.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function authorize(RouteMatchInterface $route_match, Request $request) {
$this->moduleHandler()->invokeAll('oauth2_server_pre_authorize');
// Workaround https://www.drupal.org/project/oauth2_server/issues/3049250
// Create a duplicate request with the parameters removed, so that the
// object can survive being serialized.
$duplicated_request = $request->duplicate(NULL, NULL, []);
$bridgeRequest = BridgeRequest::createFromRequest($duplicated_request);
if ($this->currentUser()->isAnonymous()) {
// A user may be redirected to the authorize request a second time
// but without parameters. For example when a user has to register
// rather than login. In such a case we have already stored a valid
// request and don't want to overwrite it with an invalid request.
// In case a user visits the authorize link directly this changes nothing.
if ($bridgeRequest->get('client_id')) {
$_SESSION['oauth2_server_authorize'] = $bridgeRequest;
}
$query = http_build_query($request->query->all());
$url = new Url('user.login', [], ['query' => ['destination' => 'oauth2/authorize?' . $query]]);
$url->setAbsolute(TRUE);
return new RedirectResponse($url->toString());
}
// A login happened: Create the request with parameters from the session.
if (!empty($_SESSION['oauth2_server_authorize'])) {
$bridgeRequest = $_SESSION['oauth2_server_authorize'];
}
$client = FALSE;
if ($bridgeRequest->get('client_id')) {
/** @var \Drupal\oauth2_server\ClientInterface[] $clients */
$clients = $this->entityTypeManager()->getStorage('oauth2_server_client')
->loadByProperties([
'client_id' => $bridgeRequest->get('client_id'),
]);
if ($clients) {
$client = reset($clients);
}
}
if (!$client) {
return new JsonResponse(
['error' => 'Client could not be found.'],
JsonResponse::HTTP_NOT_FOUND
);
}
// Load the server.
$server = $client->getServer();
// If the oauth2_server is not enabled, this does not exist.
if (!$server->status()) {
$this->logger->warning('Attempt to login using disabled oauth2_server %server_id', ['%server_id' => $server->id()]);
throw new NotFoundHttpException();
}
// Initialize the server.
$oauth2_server = Utility::startServer($server, $this->storage);
// Automatic authorization is enabled for this client. Finish authorization.
// handleAuthorizeRequest() will call validateAuthorizeRequest().
$response = new BridgeResponse();
if ($client->automatic_authorization) {
unset($_SESSION['oauth2_server_authorize']);
$oauth2_server
->handleAuthorizeRequest($bridgeRequest, $response, TRUE, $this->currentUser()->id());
return $response;
}
else {
// Validate the request.
if (!$oauth2_server->validateAuthorizeRequest($bridgeRequest, $response)) {
// Clear the parameters saved in the session to avoid reusing them when
// doing another request while logged in.
unset($_SESSION['oauth2_server_authorize']);
return $response;
}
// Determine the scope for this request.
$scope_util = new ScopeUtility($client->getServer());
if (!$scope = $scope_util->getScopeFromRequest($bridgeRequest)) {
$scope = $scope_util->getDefaultScope();
}
// Convert the scope string to a set of entities.
$scope_names = explode(' ', $scope);
$scopes = $this->entityTypeManager()->getStorage('oauth2_server_scope')
->loadByProperties([
'server_id' => $client->getServer()->id(),
'scope_id' => $scope_names,
]);
// Show the authorize form.
return $this->formBuilder()->getForm(
'Drupal\oauth2_server\Form\AuthorizeForm',
[
'client' => $client,
'scopes' => $scopes,
]
);
}
}
/**
* Token.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return \OAuth2\HttpFoundationBridge\Response|\Symfony\Component\HttpFoundation\JsonResponse
* A response object.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function token(RouteMatchInterface $route_match, Request $request) {
$bridgeRequest = BridgeRequest::createFromRequest($request);
$client_credentials = Utility::getClientCredentials($bridgeRequest);
// Get the client and use it to load the server and initialize the server.
$client = FALSE;
if ($client_credentials) {
/** @var \Drupal\oauth2_server\ClientInterface[] $clients */
$clients = $this->entityTypeManager()->getStorage('oauth2_server_client')
->loadByProperties(['client_id' => $client_credentials['client_id']]);
if ($clients) {
$client = reset($clients);
}
}
if (!$client) {
return new JsonResponse(
['error' => 'Client could not be found.'],
JsonResponse::HTTP_NOT_FOUND
);
}
// Load the server.
$server = $client->getServer();
// If the oauth2_server is not enabled, this does not exist.
if (!$server->status()) {
$this->logger->warning('Attempt to login using disabled oauth2_server %server_id', ['%server_id' => $server->id()]);
throw new NotFoundHttpException();
}
$response = new BridgeResponse();
$oauth2_server = Utility::startServer($server, $this->storage);
$oauth2_server->handleTokenRequest($bridgeRequest, $response);
return $response;
}
/**
* Tokens.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return \OAuth2\HttpFoundationBridge\Response|\Symfony\Component\HttpFoundation\JsonResponse
* The response object.
*/
public function tokens(RouteMatchInterface $route_match, Request $request) {
$token = $route_match->getRawParameter('oauth2_server_token');
$token = $this->storage->getAccessToken($token);
// No token found. Stop here.
if (!$token || $token['expires'] <= time()) {
return new BridgeResponse([], 404);
}
// Return the token, without the server and client_id keys.
unset($token['server']);
return new JsonResponse($token);
}
/**
* User info.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return \OAuth2\HttpFoundationBridge\Response
* The response object.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function userInfo(RouteMatchInterface $route_match, Request $request) {
$bridgeRequest = BridgeRequest::createFromRequest($request);
$client_credentials = Utility::getClientCredentials($bridgeRequest);
// Get the client and use it to load the server and initialize the server.
$client = FALSE;
if ($client_credentials) {
/** @var \Drupal\oauth2_server\ClientInterface[] $clients */
$clients = $this->entityTypeManager()->getStorage('oauth2_server_client')
->loadByProperties(['client_id' => $client_credentials['client_id']]);
if ($clients) {
$client = reset($clients);
}
}
$server = NULL;
if ($client) {
$server = $client->getServer();
// If the oauth2_server is not enabled, this does not exist.
if (!$server->status()) {
$this->logger->warning('Attempt to login using disabled oauth2_server %server_id', ['%server_id' => $server->id()]);
throw new NotFoundHttpException();
}
}
$response = new BridgeResponse();
$oauth2_server = Utility::startServer($server, $this->storage);
$oauth2_server->handleUserInfoRequest($bridgeRequest, $response);
return $response;
}
/**
* Revoke.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match service.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
*
* @return \OAuth2\HttpFoundationBridge\Response
* A response object.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function revoke(RouteMatchInterface $route_match, Request $request) {
$bridgeRequest = BridgeRequest::createFromRequest($request);
$client_credentials = Utility::getClientCredentials($bridgeRequest);
// Get the client and use it to load the server and initialize the server.
$client = FALSE;
if ($client_credentials) {
/** @var \Drupal\oauth2_server\ClientInterface[] $clients */
$clients = $this->entityTypeManager()->getStorage('oauth2_server_client')
->loadByProperties(['client_id' => $client_credentials['client_id']]);
if ($clients) {
$client = reset($clients);
}
}
$server = NULL;
if ($client) {
$server = $client->getServer();
// If the oauth2_server is not enabled, this does not exist.
if (!$server->status()) {
$this->logger->warning('Attempt to login using disabled oauth2_server %server_id', ['%server_id' => $server->id()]);
throw new NotFoundHttpException();
}
}
$response = new BridgeResponse();
$oauth2_server = Utility::startServer($server, $this->storage);
$oauth2_server->handleRevokeRequest($bridgeRequest, $response);
return $response;
}
/**
* Certificates.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The response object.
*/
public function certificates(RouteMatchInterface $route_match, Request $request) {
$keys = Utility::getKeys();
$certificates = [];
$certificates[] = $keys['public_key'];
return new JsonResponse($certificates);
}
/**
* Json Web Token (JWK).
*
* Output the public key as a JSON blob in JWK format, for ease of
* consumption by clients that support it.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The response object.
*
* @see https://tools.ietf.org/html/rfc7517
*/
public function jwk(RouteMatchInterface $route_match, Request $request) {
$keys = Utility::getKeys();
$cert = openssl_x509_read($keys['public_key']);
$publicKey = openssl_get_publickey($cert);
$keyDetails = openssl_pkey_get_details($publicKey);
// We still support PHP 7.4 for previous core, so avoid the deprecation
// warning by wrapping them.
if (PHP_MAJOR_VERSION < 8) {
// @codingStandardsIgnoreLine
openssl_x509_free($cert); // @phpstan-ignore-line
// @codingStandardsIgnoreLine
openssl_pkey_free($publicKey); // @phpstan-ignore-line
}
$jwk['e'] = self::base64urlEncode($keyDetails['rsa']['e']);
$jwk['n'] = self::base64urlEncode($keyDetails['rsa']['n']);
$jwk['mod'] = self::base64urlEncode($keyDetails['rsa']['n']);
// @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.7
$jwk['x5c'][] = base64_encode(self::pem2der($keys['public_key']));
$jwk['kty'] = 'RSA';
$jwk['use'] = "sig";
$jwk['alg'] = "RS256";
$jwk['kid'] = Crypt::hmacbase64(
$this->state()->get('oauth2_server.next_certificate_id', 0),
Settings::getHashSalt()
);
$response = ["keys" => [$jwk]];
// If (openssl_error_string()) {
// $this->logger->error("Error: @message", [
// "@code" => openssl_error_string(),
// ]);
// throw new HttpException(522, "SSL subsytem failure detected.");
// }
// .
return new JsonResponse($response, 200);
}
/**
* Generates a token based on $value, the token seed, and the private key.
*
* @param string $seed
* The per-session token seed.
* @param string $value
* (optional) An additional value to base the token on.
*
* @return string
* A 43-character URL-safe token for validation, based on the token seed,
* the hash salt provided by Settings::getHashSalt(), and the
* 'drupal_private_key' configuration variable.
*
* @see \Drupal\Core\Site\Settings::getHashSalt()
*/
protected function computeToken($seed, $value = '') {
return Crypt::hmacBase64($value, $seed . $this->privateKey->get() . Settings::getHashSalt());
}
/**
* Convert a PEM encoded to DER encoded certificate.
*
* @param string $pem
* The PEM encoded certificate.
*
* @return string|bool
* The DER encoded certificate or false.
*
* @see http://php.net/manual/en/ref.openssl.php#74188
*/
public static function pem2der($pem) {
$begin = "CERTIFICATE-----";
$end = "-----END";
$pem = substr($pem, strpos($pem, $begin) + strlen($begin));
$pem = substr($pem, 0, strpos($pem, $end));
$der = base64_decode($pem);
return $der;
}
/**
* Base64 URL encoding.
*
* @param string $data
* The data to be encoded.
*
* @return string
* The Base64 URL encoded data.
*
* @see http://php.net/manual/en/function.base64-encode.php#103849
*/
public static function base64urlEncode($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
}
