OAuth2Client.php in OAuth2 Client 8
Namespace
Drupal\oauth2_client\ServiceFile
src/Service/OAuth2Client.phpView source
<?php
namespace Drupal\oauth2_client\Service;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use GuzzleHttp\ClientInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
/**
* OAuth2Client service.
*
* The class OAuth2Client is used to get authorization from
* an OAuth2 server.
*
* It can use authorization flows: server-side, client-credentials
* and user-password. The details for each case are passed
* to the constructor. All the three cases need a client_id,
* a client_secret, and a token_endpoint. There can be an optional
* scope as well.
*/
class OAuth2Client implements OAuth2ClientInterface {
use StringTranslationTrait;
/**
* Unique identifier of an OAuth2Client object.
*
* @var string|null
*/
protected $id = NULL;
/**
* Parameters.
*
* Associative array of the parameters that are needed
* by the different types of authorization flows.
* - auth_flow :: server-side | client-credentials | user-password
* - client_id :: Client ID, as registered on the oauth2 server
* - client_secret :: Client secret, as registered on the oauth2 server
* - token_endpoint :: something like:
* https://oauth2_server.example.org/oauth2/token
* - authorization_endpoint :: somethig like:
* https://oauth2_server.example.org/oauth2/authorize
* - redirect_uri :: something like:
* url('oauth2/authorized', array('absolute' => TRUE)) or
* https://oauth2_client.example.org/oauth2/authorized
* - scope :: requested scopes, separated by a space
* - username :: username of the resource owner
* - password :: password of the resource owner
* - skip-ssl-verification :: Skip verification of the SSL connection
* (needed for testing).
*
* @var array
*/
protected $params = [
'auth_flow' => NULL,
'client_id' => NULL,
'client_secret' => NULL,
'token_endpoint' => NULL,
'authorization_endpoint' => NULL,
'redirect_uri' => NULL,
'scope' => NULL,
'username' => NULL,
'password' => NULL,
'skip-ssl-verification' => FALSE,
];
/**
* Associated array that keeps data about the access token.
*
* @var array
*/
protected $token = [
'access_token' => NULL,
'expires_in' => NULL,
'token_type' => NULL,
'scope' => NULL,
'refresh_token' => NULL,
'expiration_time' => NULL,
];
/**
* The HTTP Request client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* The Request Stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The oauth2 client tempstore - acts like $_SESSION.
*
* @var \Drupal\user\PrivateTempStore
*/
protected $tempstore;
/**
* Construct an OAuth2Client object.
*
* @param \GuzzleHttp\ClientInterface $httpClient
* The HTTP Request client.
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
* The Request Stack.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $tempstore
* The user private tempstore - acts like $_SESSION.
*/
public function __construct(ClientInterface $httpClient, RequestStack $requestStack, PrivateTempStoreFactory $tempstore) {
$this->httpClient = $httpClient;
$this->requestStack = $requestStack;
$this->tempstore = $tempstore
->get('oauth2_client');
}
/**
* {@inheritdoc}
*/
public function init($params = NULL, $id = NULL) {
if ($params) {
$this->params = $params + $this->params;
}
if (!$id) {
$id = md5($this->params['token_endpoint'] . $this->params['client_id'] . $this->params['auth_flow']);
}
$this->id = $id;
// Get the token data from the tempstore, if it is stored there.
$tokens = $this->tempstore
->get('token');
if (isset($tokens[$this->id])) {
$this->token = $tokens[$this->id] + $this->token;
}
}
/**
* {@inheritdoc}
*/
public function clearToken() {
$tokens = $this->tempstore
->get('token');
if (isset($tokens[$this->id])) {
unset($tokens[$this->id]);
$this->tempstore
->set('token', $tokens);
}
$this->token = [
'access_token' => NULL,
'expires_in' => NULL,
'token_type' => NULL,
'scope' => NULL,
'refresh_token' => NULL,
'expiration_time' => NULL,
];
}
/**
* {@inheritdoc}
*
* @throws \Exception
*/
public function getAccessToken($redirect = TRUE) {
// Check wheather the existing token has expired.
// We take the expiration time to be shorter by 10 sec
// in order to account for any delays during the request.
// Usually a token is valid for 1 hour, so making
// the expiration time shorter by 10 sec is insignificant.
// However it should be kept in mind during the tests,
// where the expiration time is much shorter.
$expiration_time = $this->token['expiration_time'];
if ($expiration_time > time() + 10) {
// The existing token can still be used.
return $this->token['access_token'];
}
try {
// Try to use refresh_token.
$token = $this
->getTokenRefreshToken();
} catch (\Exception $e) {
// Get a token.
switch ($this->params['auth_flow']) {
case 'client-credentials':
$token = $this
->getToken([
'grant_type' => 'client_credentials',
'scope' => $this->params['scope'],
]);
break;
case 'user-password':
$token = $this
->getToken([
'grant_type' => 'password',
'username' => $this->params['username'],
'password' => $this->params['password'],
'scope' => $this->params['scope'],
]);
break;
case 'server-side':
if ($redirect) {
$token = $this
->getTokenServerSide();
}
else {
$this
->clearToken();
return NULL;
}
break;
default:
throw new \Exception(t('Unknown authorization flow "@auth_flow". Supported values for auth_flow are: client-credentials, user-password, server-side.', [
'@auth_flow' => $this->params['auth_flow'],
]));
}
}
// Store the token (on session as well).
$this->token = $token;
$tokens = $this->tempstore
->get('token');
$tokens[$this->id] = $token;
$this->tempstore
->set('token', $tokens);
// Redirect to the original path (if this is a redirection
// from the server-side flow).
self::redirect();
// Return the token.
return $token['access_token'];
}
/**
* {@inheritdoc}
*/
public static function setRedirect($state, $redirect = NULL) {
if ($redirect == NULL) {
$redirect = [
'uri' => \Drupal::request()
->getRequestUri(),
'client' => 'oauth2_client',
];
}
if (!isset($redirect['client'])) {
$redirect['client'] = 'external';
}
/** @var \Drupal\Core\TempStore\PrivateTempStore $tempstore */
$tempstore = \Drupal::service('tempstore.private')
->get('oauth2_client');
$redirects = $tempstore
->get('redirect');
$redirects[$state] = $redirect;
$tempstore
->set('redirect', $redirects);
}
/**
* {@inheritdoc}
*/
public static function redirect($clean = TRUE) {
if (!\Drupal::service('request_stack')
->getCurrentRequest()
->get('state')) {
return;
}
$state = \Drupal::service('request_stack')
->getCurrentRequest()
->get('state');
/** @var \Drupal\Core\TempStore\PrivateTempStore $tempstore */
$tempstore = \Drupal::service('tempstore.private')
->get('oauth2_client');
$redirects = $tempstore
->get('redirect');
if (!isset($redirects[$state])) {
return;
}
$redirect = $redirects[$state];
if ($redirect['client'] != 'oauth2_client') {
unset($redirects[$state]);
$tempstore
->set('redirect', $redirects);
$params = isset($redirect['params']) ? $redirect['params'] : [];
$params = $params + \Drupal::request()->query
->all();
$url = Url::fromUri($redirect['uri'], [
'query' => $params,
]);
$redirect = new RedirectResponse($url);
$redirect
->send();
exit;
}
else {
$params = \Drupal::request()->query
->all();
if ($clean) {
unset($redirects[$state]);
$tempstore
->set('redirect', $redirects);
unset($params['code']);
unset($params['state']);
}
if (isset($redirect['params'])) {
$params = $redirect['params'] + $params;
}
$url = Url::fromUri('internal:' . $redirect['uri'], [
'query' => $params,
]);
$redirect = new RedirectResponse($url
->toString());
$redirect
->send();
exit;
}
}
/**
* Get a new access_token using the refresh_token.
*
* This is used for the server-side and user-password
* flows (not for client-credentials, there is no
* refresh_token in it).
*
* @throws \Exception
*/
protected function getTokenRefreshToken() {
if (!$this->token['refresh_token']) {
throw new \Exception(t('There is no refresh_token.'));
}
return $this
->getToken([
'grant_type' => 'refresh_token',
'refresh_token' => $this->token['refresh_token'],
]);
}
/**
* Get an access_token using the server-side (authorization code) flow.
*
* This is done in two steps:
* - First, a redirection is done to the authentication
* endpoint, in order to request an authorization code.
* - Second, using this code, an access_token is requested.
*
* There are lots of redirects in this case and this part is the most
* tricky and difficult to understand of the oauth2_client, so let
* me try to explain how it is done.
*
* Suppose that in the controller of the path 'test/xyz'
* we try to get an access_token:
* $client = oauth2_client_load('server-side-test');
* $access_token = $client->getAccessToken();
* or:
* $client = new OAuth2\Client(array(
* 'token_endpoint' => 'https://oauth2_server/oauth2/token',
* 'client_id' => 'client1',
* 'client_secret' => 'secret1',
* 'auth_flow' => 'server-side',
* 'authorization_endpoint' =>
* 'https://oauth2_server/oauth2/authorize',
* 'redirect_uri' => 'https://oauth2_client/oauth2/authorized',
* ));
* $access_token = $client->getAccessToken();
*
* From getAccessToken() we come to this function, getTokenServerSide(),
* and since there is no $_GET['code'], we redirect to the authentication
* url, but first we save the current path in the session:
* $_SESSION['oauth2_client']['redirect'][$state]['uri'] = 'test/xyz';
*
* Once the authentication and authorization is done on the server, we are
* redirected by the server to the redirect uri: 'oauth2/authorized'. In
* the controller of this path we redirect to the saved path 'test/xyz'
* (since $_SESSION['oauth2_client']['redirect'][$state] exists), passing
* along the query parameters sent by the server (which include 'code',
* 'state', and maybe other parameters as well.)
*
* Now the code: $access_token = $client->getAccessToken(); is
* called again and we come back for a second time to the function
* getTokenServerSide(). However this time we do have a
* $_GET['code'], so we get a token from the server and return it.
*
* Inside the function getAccessToken() we save the returned token in
* session and then, since $_SESSION['oauth2_client']['redirect'][$state]
* exists, we delete it and make another redirect to 'test/xyz'. This third
* redirect is in order to have in browser the original url, because from
* the last redirect we have something like this:
* 'test/xyz?code=8557&state=3d7dh3&....'
*
* We come again for a third time to the code
* $access_token = $client->getAccessToken();
* But this time we have a valid token already saved in session,
* so the $client can find and return it without having to redirect etc.
*
* @throws \Exception
*/
protected function getTokenServerSide() {
if (!$this->requestStack
->getCurrentRequest()
->get('code')) {
$url = $this
->getAuthenticationUrl();
$url = Url::fromUri($url);
$redirect = new RedirectResponse($url
->toString());
$redirect
->send();
exit;
}
// Check the query parameter 'state'.
$state = $this->requestStack
->getCurrentRequest()
->get('state');
$redirects = $this->tempstore
->get('redirect');
if (!$state || !isset($redirects[$state])) {
throw new \Exception(t("'State query parameter does not match"));
}
// Get and return a token.
return $this
->getToken([
'grant_type' => 'authorization_code',
'code' => $this->requestStack
->getCurrentRequest()
->get('code'),
'redirect_uri' => $this->params['redirect_uri'],
]);
}
/**
* Return the authentication url (used in case of the server-side flow).
*/
protected function getAuthenticationUrl() {
$state = md5(uniqid(rand(), TRUE));
$query_params = [
'response_type' => 'code',
'client_id' => $this->params['client_id'],
'redirect_uri' => $this->params['redirect_uri'],
'state' => $state,
];
if ($this->params['scope']) {
$query_params['scope'] = $this->params['scope'];
}
$endpoint = $this->params['authorization_endpoint'];
self::setRedirect($state);
return $endpoint . '?' . http_build_query($query_params);
}
/**
* Get and return an access token for the grant_type given in $params.
*/
protected function getToken($data) {
if (array_key_exists('scope', $data) && $data['scope'] === NULL) {
unset($data['scope']);
}
$client_id = $this->params['client_id'];
$client_secret = $this->params['client_secret'];
$token_endpoint = $this->params['token_endpoint'];
$data['client_id'] = $client_id;
$data['client_secret'] = $client_secret;
$options = [
'form_params' => $data,
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
'Authorization' => 'Basic ' . base64_encode("{$client_id}:{$client_secret}"),
],
];
if ($this->params['skip-ssl-verification']) {
$options['context'] = stream_context_create([
'ssl' => [
'verify_peer' => FALSE,
'verify_peer_name' => FALSE,
],
]);
}
$response = $this->httpClient
->request('POST', $token_endpoint, $options);
$response_data = (string) $response
->getBody();
if (empty($response_data)) {
throw new \Exception($this
->t("Failed to get an access token of grant_type @grant_type.\nError: @result_error", [
'@grant_type' => $data['grant_type'],
]));
}
$serializer = new Serializer([
new GetSetMethodNormalizer(),
], [
'json' => new JsonEncoder(),
]);
$token = $serializer
->decode($response_data, 'json');
if (!isset($token['expiration_time'])) {
// Some providers do not return an 'expires_in' value, so we
// set a default of an hour. If the token expires dies within that time,
// the system will request a new token automatically.
$token['expiration_time'] = \Drupal::time()
->getRequestTime() + 3600;
}
return $token;
}
}
Classes
Name | Description |
---|---|
OAuth2Client | OAuth2Client service. |