1346 lines
40 KiB
PHP
1346 lines
40 KiB
PHP
<?php
|
|
|
|
/*
|
|
* Copyright 2015 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
namespace WPMailSMTP\Vendor\Google\Auth;
|
|
|
|
use WPMailSMTP\Vendor\Firebase\JWT\JWT;
|
|
use WPMailSMTP\Vendor\Firebase\JWT\Key;
|
|
use WPMailSMTP\Vendor\Google\Auth\HttpHandler\HttpClientCache;
|
|
use WPMailSMTP\Vendor\Google\Auth\HttpHandler\HttpHandlerFactory;
|
|
use WPMailSMTP\Vendor\GuzzleHttp\Psr7\Query;
|
|
use WPMailSMTP\Vendor\GuzzleHttp\Psr7\Request;
|
|
use WPMailSMTP\Vendor\GuzzleHttp\Psr7\Utils;
|
|
use InvalidArgumentException;
|
|
use WPMailSMTP\Vendor\Psr\Http\Message\RequestInterface;
|
|
use WPMailSMTP\Vendor\Psr\Http\Message\ResponseInterface;
|
|
use WPMailSMTP\Vendor\Psr\Http\Message\UriInterface;
|
|
/**
|
|
* OAuth2 supports authentication by OAuth2 2-legged flows.
|
|
*
|
|
* It primary supports
|
|
* - service account authorization
|
|
* - authorization where a user already has an access token
|
|
*/
|
|
class OAuth2 implements \WPMailSMTP\Vendor\Google\Auth\FetchAuthTokenInterface
|
|
{
|
|
const DEFAULT_EXPIRY_SECONDS = 3600;
|
|
// 1 hour
|
|
const DEFAULT_SKEW_SECONDS = 60;
|
|
// 1 minute
|
|
const JWT_URN = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
|
|
/**
|
|
* TODO: determine known methods from the keys of JWT::methods.
|
|
*
|
|
* @var array<string>
|
|
*/
|
|
public static $knownSigningAlgorithms = ['HS256', 'HS512', 'HS384', 'RS256'];
|
|
/**
|
|
* The well known grant types.
|
|
*
|
|
* @var array<string>
|
|
*/
|
|
public static $knownGrantTypes = ['authorization_code', 'refresh_token', 'password', 'client_credentials'];
|
|
/**
|
|
* - authorizationUri
|
|
* The authorization server's HTTP endpoint capable of
|
|
* authenticating the end-user and obtaining authorization.
|
|
*
|
|
* @var ?UriInterface
|
|
*/
|
|
private $authorizationUri;
|
|
/**
|
|
* - tokenCredentialUri
|
|
* The authorization server's HTTP endpoint capable of issuing
|
|
* tokens and refreshing expired tokens.
|
|
*
|
|
* @var UriInterface
|
|
*/
|
|
private $tokenCredentialUri;
|
|
/**
|
|
* The redirection URI used in the initial request.
|
|
*
|
|
* @var ?string
|
|
*/
|
|
private $redirectUri;
|
|
/**
|
|
* A unique identifier issued to the client to identify itself to the
|
|
* authorization server.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $clientId;
|
|
/**
|
|
* A shared symmetric secret issued by the authorization server, which is
|
|
* used to authenticate the client.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $clientSecret;
|
|
/**
|
|
* The resource owner's username.
|
|
*
|
|
* @var ?string
|
|
*/
|
|
private $username;
|
|
/**
|
|
* The resource owner's password.
|
|
*
|
|
* @var ?string
|
|
*/
|
|
private $password;
|
|
/**
|
|
* The scope of the access request, expressed either as an Array or as a
|
|
* space-delimited string.
|
|
*
|
|
* @var ?array<string>
|
|
*/
|
|
private $scope;
|
|
/**
|
|
* An arbitrary string designed to allow the client to maintain state.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $state;
|
|
/**
|
|
* The authorization code issued to this client.
|
|
*
|
|
* Only used by the authorization code access grant type.
|
|
*
|
|
* @var ?string
|
|
*/
|
|
private $code;
|
|
/**
|
|
* The issuer ID when using assertion profile.
|
|
*
|
|
* @var ?string
|
|
*/
|
|
private $issuer;
|
|
/**
|
|
* The target audience for assertions.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $audience;
|
|
/**
|
|
* The target sub when issuing assertions.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $sub;
|
|
/**
|
|
* The number of seconds assertions are valid for.
|
|
*
|
|
* @var int
|
|
*/
|
|
private $expiry;
|
|
/**
|
|
* The signing key when using assertion profile.
|
|
*
|
|
* @var ?string
|
|
*/
|
|
private $signingKey;
|
|
/**
|
|
* The signing key id when using assertion profile. Param kid in jwt header
|
|
*
|
|
* @var string
|
|
*/
|
|
private $signingKeyId;
|
|
/**
|
|
* The signing algorithm when using an assertion profile.
|
|
*
|
|
* @var ?string
|
|
*/
|
|
private $signingAlgorithm;
|
|
/**
|
|
* The refresh token associated with the access token to be refreshed.
|
|
*
|
|
* @var ?string
|
|
*/
|
|
private $refreshToken;
|
|
/**
|
|
* The current access token.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $accessToken;
|
|
/**
|
|
* The current ID token.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $idToken;
|
|
/**
|
|
* The scopes granted to the current access token
|
|
*
|
|
* @var string
|
|
*/
|
|
private $grantedScope;
|
|
/**
|
|
* The lifetime in seconds of the current access token.
|
|
*
|
|
* @var ?int
|
|
*/
|
|
private $expiresIn;
|
|
/**
|
|
* The expiration time of the access token as a number of seconds since the
|
|
* unix epoch.
|
|
*
|
|
* @var ?int
|
|
*/
|
|
private $expiresAt;
|
|
/**
|
|
* The issue time of the access token as a number of seconds since the unix
|
|
* epoch.
|
|
*
|
|
* @var ?int
|
|
*/
|
|
private $issuedAt;
|
|
/**
|
|
* The current grant type.
|
|
*
|
|
* @var ?string
|
|
*/
|
|
private $grantType;
|
|
/**
|
|
* When using an extension grant type, this is the set of parameters used by
|
|
* that extension.
|
|
*
|
|
* @var array<mixed>
|
|
*/
|
|
private $extensionParams;
|
|
/**
|
|
* When using the toJwt function, these claims will be added to the JWT
|
|
* payload.
|
|
*
|
|
* @var array<mixed>
|
|
*/
|
|
private $additionalClaims;
|
|
/**
|
|
* Create a new OAuthCredentials.
|
|
*
|
|
* The configuration array accepts various options
|
|
*
|
|
* - authorizationUri
|
|
* The authorization server's HTTP endpoint capable of
|
|
* authenticating the end-user and obtaining authorization.
|
|
*
|
|
* - tokenCredentialUri
|
|
* The authorization server's HTTP endpoint capable of issuing
|
|
* tokens and refreshing expired tokens.
|
|
*
|
|
* - clientId
|
|
* A unique identifier issued to the client to identify itself to the
|
|
* authorization server.
|
|
*
|
|
* - clientSecret
|
|
* A shared symmetric secret issued by the authorization server,
|
|
* which is used to authenticate the client.
|
|
*
|
|
* - scope
|
|
* The scope of the access request, expressed either as an Array
|
|
* or as a space-delimited String.
|
|
*
|
|
* - state
|
|
* An arbitrary string designed to allow the client to maintain state.
|
|
*
|
|
* - redirectUri
|
|
* The redirection URI used in the initial request.
|
|
*
|
|
* - username
|
|
* The resource owner's username.
|
|
*
|
|
* - password
|
|
* The resource owner's password.
|
|
*
|
|
* - issuer
|
|
* Issuer ID when using assertion profile
|
|
*
|
|
* - audience
|
|
* Target audience for assertions
|
|
*
|
|
* - expiry
|
|
* Number of seconds assertions are valid for
|
|
*
|
|
* - signingKey
|
|
* Signing key when using assertion profile
|
|
*
|
|
* - signingKeyId
|
|
* Signing key id when using assertion profile
|
|
*
|
|
* - refreshToken
|
|
* The refresh token associated with the access token
|
|
* to be refreshed.
|
|
*
|
|
* - accessToken
|
|
* The current access token for this client.
|
|
*
|
|
* - idToken
|
|
* The current ID token for this client.
|
|
*
|
|
* - extensionParams
|
|
* When using an extension grant type, this is the set of parameters used
|
|
* by that extension.
|
|
*
|
|
* @param array<mixed> $config Configuration array
|
|
*/
|
|
public function __construct(array $config)
|
|
{
|
|
$opts = \array_merge(['expiry' => self::DEFAULT_EXPIRY_SECONDS, 'extensionParams' => [], 'authorizationUri' => null, 'redirectUri' => null, 'tokenCredentialUri' => null, 'state' => null, 'username' => null, 'password' => null, 'clientId' => null, 'clientSecret' => null, 'issuer' => null, 'sub' => null, 'audience' => null, 'signingKey' => null, 'signingKeyId' => null, 'signingAlgorithm' => null, 'scope' => null, 'additionalClaims' => []], $config);
|
|
$this->setAuthorizationUri($opts['authorizationUri']);
|
|
$this->setRedirectUri($opts['redirectUri']);
|
|
$this->setTokenCredentialUri($opts['tokenCredentialUri']);
|
|
$this->setState($opts['state']);
|
|
$this->setUsername($opts['username']);
|
|
$this->setPassword($opts['password']);
|
|
$this->setClientId($opts['clientId']);
|
|
$this->setClientSecret($opts['clientSecret']);
|
|
$this->setIssuer($opts['issuer']);
|
|
$this->setSub($opts['sub']);
|
|
$this->setExpiry($opts['expiry']);
|
|
$this->setAudience($opts['audience']);
|
|
$this->setSigningKey($opts['signingKey']);
|
|
$this->setSigningKeyId($opts['signingKeyId']);
|
|
$this->setSigningAlgorithm($opts['signingAlgorithm']);
|
|
$this->setScope($opts['scope']);
|
|
$this->setExtensionParams($opts['extensionParams']);
|
|
$this->setAdditionalClaims($opts['additionalClaims']);
|
|
$this->updateToken($opts);
|
|
}
|
|
/**
|
|
* Verifies the idToken if present.
|
|
*
|
|
* - if none is present, return null
|
|
* - if present, but invalid, raises DomainException.
|
|
* - otherwise returns the payload in the idtoken as a PHP object.
|
|
*
|
|
* The behavior of this method varies depending on the version of
|
|
* `firebase/php-jwt` you are using. In versions 6.0 and above, you cannot
|
|
* provide multiple $allowed_algs, and instead must provide an array of Key
|
|
* objects as the $publicKey.
|
|
*
|
|
* @param string|Key|Key[] $publicKey The public key to use to authenticate the token
|
|
* @param string|array<string> $allowed_algs algorithm or array of supported verification algorithms.
|
|
* Providing more than one algorithm will throw an exception.
|
|
* @throws \DomainException if the token is missing an audience.
|
|
* @throws \DomainException if the audience does not match the one set in
|
|
* the OAuth2 class instance.
|
|
* @throws \UnexpectedValueException If the token is invalid
|
|
* @throws \InvalidArgumentException If more than one value for allowed_algs is supplied
|
|
* @throws \Firebase\JWT\SignatureInvalidException If the signature is invalid.
|
|
* @throws \Firebase\JWT\BeforeValidException If the token is not yet valid.
|
|
* @throws \Firebase\JWT\ExpiredException If the token has expired.
|
|
* @return null|object
|
|
*/
|
|
public function verifyIdToken($publicKey = null, $allowed_algs = [])
|
|
{
|
|
$idToken = $this->getIdToken();
|
|
if (\is_null($idToken)) {
|
|
return null;
|
|
}
|
|
$resp = $this->jwtDecode($idToken, $publicKey, $allowed_algs);
|
|
if (!\property_exists($resp, 'aud')) {
|
|
throw new \DomainException('No audience found the id token');
|
|
}
|
|
if ($resp->aud != $this->getAudience()) {
|
|
throw new \DomainException('Wrong audience present in the id token');
|
|
}
|
|
return $resp;
|
|
}
|
|
/**
|
|
* Obtains the encoded jwt from the instance data.
|
|
*
|
|
* @param array<mixed> $config array optional configuration parameters
|
|
* @return string
|
|
*/
|
|
public function toJwt(array $config = [])
|
|
{
|
|
if (\is_null($this->getSigningKey())) {
|
|
throw new \DomainException('No signing key available');
|
|
}
|
|
if (\is_null($this->getSigningAlgorithm())) {
|
|
throw new \DomainException('No signing algorithm specified');
|
|
}
|
|
$now = \time();
|
|
$opts = \array_merge(['skew' => self::DEFAULT_SKEW_SECONDS], $config);
|
|
$assertion = ['iss' => $this->getIssuer(), 'exp' => $now + $this->getExpiry(), 'iat' => $now - $opts['skew']];
|
|
foreach ($assertion as $k => $v) {
|
|
if (\is_null($v)) {
|
|
throw new \DomainException($k . ' should not be null');
|
|
}
|
|
}
|
|
if (!\is_null($this->getAudience())) {
|
|
$assertion['aud'] = $this->getAudience();
|
|
}
|
|
if (!\is_null($this->getScope())) {
|
|
$assertion['scope'] = $this->getScope();
|
|
}
|
|
if (empty($assertion['scope']) && empty($assertion['aud'])) {
|
|
throw new \DomainException('one of scope or aud should not be null');
|
|
}
|
|
if (!\is_null($this->getSub())) {
|
|
$assertion['sub'] = $this->getSub();
|
|
}
|
|
$assertion += $this->getAdditionalClaims();
|
|
return \WPMailSMTP\Vendor\Firebase\JWT\JWT::encode($assertion, $this->getSigningKey(), $this->getSigningAlgorithm(), $this->getSigningKeyId());
|
|
}
|
|
/**
|
|
* Generates a request for token credentials.
|
|
*
|
|
* @return RequestInterface the authorization Url.
|
|
*/
|
|
public function generateCredentialsRequest()
|
|
{
|
|
$uri = $this->getTokenCredentialUri();
|
|
if (\is_null($uri)) {
|
|
throw new \DomainException('No token credential URI was set.');
|
|
}
|
|
$grantType = $this->getGrantType();
|
|
$params = ['grant_type' => $grantType];
|
|
switch ($grantType) {
|
|
case 'authorization_code':
|
|
$params['code'] = $this->getCode();
|
|
$params['redirect_uri'] = $this->getRedirectUri();
|
|
$this->addClientCredentials($params);
|
|
break;
|
|
case 'password':
|
|
$params['username'] = $this->getUsername();
|
|
$params['password'] = $this->getPassword();
|
|
$this->addClientCredentials($params);
|
|
break;
|
|
case 'refresh_token':
|
|
$params['refresh_token'] = $this->getRefreshToken();
|
|
$this->addClientCredentials($params);
|
|
break;
|
|
case self::JWT_URN:
|
|
$params['assertion'] = $this->toJwt();
|
|
break;
|
|
default:
|
|
if (!\is_null($this->getRedirectUri())) {
|
|
# Grant type was supposed to be 'authorization_code', as there
|
|
# is a redirect URI.
|
|
throw new \DomainException('Missing authorization code');
|
|
}
|
|
unset($params['grant_type']);
|
|
if (!\is_null($grantType)) {
|
|
$params['grant_type'] = $grantType;
|
|
}
|
|
$params = \array_merge($params, $this->getExtensionParams());
|
|
}
|
|
$headers = ['Cache-Control' => 'no-store', 'Content-Type' => 'application/x-www-form-urlencoded'];
|
|
return new \WPMailSMTP\Vendor\GuzzleHttp\Psr7\Request('POST', $uri, $headers, \WPMailSMTP\Vendor\GuzzleHttp\Psr7\Query::build($params));
|
|
}
|
|
/**
|
|
* Fetches the auth tokens based on the current state.
|
|
*
|
|
* @param callable $httpHandler callback which delivers psr7 request
|
|
* @return array<mixed> the response
|
|
*/
|
|
public function fetchAuthToken(callable $httpHandler = null)
|
|
{
|
|
if (\is_null($httpHandler)) {
|
|
$httpHandler = \WPMailSMTP\Vendor\Google\Auth\HttpHandler\HttpHandlerFactory::build(\WPMailSMTP\Vendor\Google\Auth\HttpHandler\HttpClientCache::getHttpClient());
|
|
}
|
|
$response = $httpHandler($this->generateCredentialsRequest());
|
|
$credentials = $this->parseTokenResponse($response);
|
|
$this->updateToken($credentials);
|
|
if (isset($credentials['scope'])) {
|
|
$this->setGrantedScope($credentials['scope']);
|
|
}
|
|
return $credentials;
|
|
}
|
|
/**
|
|
* Obtains a key that can used to cache the results of #fetchAuthToken.
|
|
*
|
|
* The key is derived from the scopes.
|
|
*
|
|
* @return ?string a key that may be used to cache the auth token.
|
|
*/
|
|
public function getCacheKey()
|
|
{
|
|
if (\is_array($this->scope)) {
|
|
return \implode(':', $this->scope);
|
|
}
|
|
if ($this->audience) {
|
|
return $this->audience;
|
|
}
|
|
// If scope has not set, return null to indicate no caching.
|
|
return null;
|
|
}
|
|
/**
|
|
* Parses the fetched tokens.
|
|
*
|
|
* @param ResponseInterface $resp the response.
|
|
* @return array<mixed> the tokens parsed from the response body.
|
|
* @throws \Exception
|
|
*/
|
|
public function parseTokenResponse(\WPMailSMTP\Vendor\Psr\Http\Message\ResponseInterface $resp)
|
|
{
|
|
$body = (string) $resp->getBody();
|
|
if ($resp->hasHeader('Content-Type') && $resp->getHeaderLine('Content-Type') == 'application/x-www-form-urlencoded') {
|
|
$res = [];
|
|
\parse_str($body, $res);
|
|
return $res;
|
|
}
|
|
// Assume it's JSON; if it's not throw an exception
|
|
if (null === ($res = \json_decode($body, \true))) {
|
|
throw new \Exception('Invalid JSON response');
|
|
}
|
|
return $res;
|
|
}
|
|
/**
|
|
* Updates an OAuth 2.0 client.
|
|
*
|
|
* Example:
|
|
* ```
|
|
* $oauth->updateToken([
|
|
* 'refresh_token' => 'n4E9O119d',
|
|
* 'access_token' => 'FJQbwq9',
|
|
* 'expires_in' => 3600
|
|
* ]);
|
|
* ```
|
|
*
|
|
* @param array<mixed> $config
|
|
* The configuration parameters related to the token.
|
|
*
|
|
* - refresh_token
|
|
* The refresh token associated with the access token
|
|
* to be refreshed.
|
|
*
|
|
* - access_token
|
|
* The current access token for this client.
|
|
*
|
|
* - id_token
|
|
* The current ID token for this client.
|
|
*
|
|
* - expires_in
|
|
* The time in seconds until access token expiration.
|
|
*
|
|
* - expires_at
|
|
* The time as an integer number of seconds since the Epoch
|
|
*
|
|
* - issued_at
|
|
* The timestamp that the token was issued at.
|
|
* @return void
|
|
*/
|
|
public function updateToken(array $config)
|
|
{
|
|
$opts = \array_merge(['extensionParams' => [], 'access_token' => null, 'id_token' => null, 'expires_in' => null, 'expires_at' => null, 'issued_at' => null, 'scope' => null], $config);
|
|
$this->setExpiresAt($opts['expires_at']);
|
|
$this->setExpiresIn($opts['expires_in']);
|
|
// By default, the token is issued at `Time.now` when `expiresIn` is set,
|
|
// but this can be used to supply a more precise time.
|
|
if (!\is_null($opts['issued_at'])) {
|
|
$this->setIssuedAt($opts['issued_at']);
|
|
}
|
|
$this->setAccessToken($opts['access_token']);
|
|
$this->setIdToken($opts['id_token']);
|
|
// The refresh token should only be updated if a value is explicitly
|
|
// passed in, as some access token responses do not include a refresh
|
|
// token.
|
|
if (\array_key_exists('refresh_token', $opts)) {
|
|
$this->setRefreshToken($opts['refresh_token']);
|
|
}
|
|
}
|
|
/**
|
|
* Builds the authorization Uri that the user should be redirected to.
|
|
*
|
|
* @param array<mixed> $config configuration options that customize the return url
|
|
* @return UriInterface the authorization Url.
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public function buildFullAuthorizationUri(array $config = [])
|
|
{
|
|
if (\is_null($this->getAuthorizationUri())) {
|
|
throw new \InvalidArgumentException('requires an authorizationUri to have been set');
|
|
}
|
|
$params = \array_merge(['response_type' => 'code', 'access_type' => 'offline', 'client_id' => $this->clientId, 'redirect_uri' => $this->redirectUri, 'state' => $this->state, 'scope' => $this->getScope()], $config);
|
|
// Validate the auth_params
|
|
if (\is_null($params['client_id'])) {
|
|
throw new \InvalidArgumentException('missing the required client identifier');
|
|
}
|
|
if (\is_null($params['redirect_uri'])) {
|
|
throw new \InvalidArgumentException('missing the required redirect URI');
|
|
}
|
|
if (!empty($params['prompt']) && !empty($params['approval_prompt'])) {
|
|
throw new \InvalidArgumentException('prompt and approval_prompt are mutually exclusive');
|
|
}
|
|
// Construct the uri object; return it if it is valid.
|
|
$result = clone $this->authorizationUri;
|
|
$existingParams = \WPMailSMTP\Vendor\GuzzleHttp\Psr7\Query::parse($result->getQuery());
|
|
$result = $result->withQuery(\WPMailSMTP\Vendor\GuzzleHttp\Psr7\Query::build(\array_merge($existingParams, $params)));
|
|
if ($result->getScheme() != 'https') {
|
|
throw new \InvalidArgumentException('Authorization endpoint must be protected by TLS');
|
|
}
|
|
return $result;
|
|
}
|
|
/**
|
|
* Sets the authorization server's HTTP endpoint capable of authenticating
|
|
* the end-user and obtaining authorization.
|
|
*
|
|
* @param string $uri
|
|
* @return void
|
|
*/
|
|
public function setAuthorizationUri($uri)
|
|
{
|
|
$this->authorizationUri = $this->coerceUri($uri);
|
|
}
|
|
/**
|
|
* Gets the authorization server's HTTP endpoint capable of authenticating
|
|
* the end-user and obtaining authorization.
|
|
*
|
|
* @return ?UriInterface
|
|
*/
|
|
public function getAuthorizationUri()
|
|
{
|
|
return $this->authorizationUri;
|
|
}
|
|
/**
|
|
* Gets the authorization server's HTTP endpoint capable of issuing tokens
|
|
* and refreshing expired tokens.
|
|
*
|
|
* @return ?UriInterface
|
|
*/
|
|
public function getTokenCredentialUri()
|
|
{
|
|
return $this->tokenCredentialUri;
|
|
}
|
|
/**
|
|
* Sets the authorization server's HTTP endpoint capable of issuing tokens
|
|
* and refreshing expired tokens.
|
|
*
|
|
* @param string $uri
|
|
* @return void
|
|
*/
|
|
public function setTokenCredentialUri($uri)
|
|
{
|
|
$this->tokenCredentialUri = $this->coerceUri($uri);
|
|
}
|
|
/**
|
|
* Gets the redirection URI used in the initial request.
|
|
*
|
|
* @return ?string
|
|
*/
|
|
public function getRedirectUri()
|
|
{
|
|
return $this->redirectUri;
|
|
}
|
|
/**
|
|
* Sets the redirection URI used in the initial request.
|
|
*
|
|
* @param ?string $uri
|
|
* @return void
|
|
*/
|
|
public function setRedirectUri($uri)
|
|
{
|
|
if (\is_null($uri)) {
|
|
$this->redirectUri = null;
|
|
return;
|
|
}
|
|
// redirect URI must be absolute
|
|
if (!$this->isAbsoluteUri($uri)) {
|
|
// "postmessage" is a reserved URI string in Google-land
|
|
// @see https://developers.google.com/identity/sign-in/web/server-side-flow
|
|
if ('postmessage' !== (string) $uri) {
|
|
throw new \InvalidArgumentException('Redirect URI must be absolute');
|
|
}
|
|
}
|
|
$this->redirectUri = (string) $uri;
|
|
}
|
|
/**
|
|
* Gets the scope of the access requests as a space-delimited String.
|
|
*
|
|
* @return ?string
|
|
*/
|
|
public function getScope()
|
|
{
|
|
if (\is_null($this->scope)) {
|
|
return $this->scope;
|
|
}
|
|
return \implode(' ', $this->scope);
|
|
}
|
|
/**
|
|
* Sets the scope of the access request, expressed either as an Array or as
|
|
* a space-delimited String.
|
|
*
|
|
* @param string|array<string>|null $scope
|
|
* @return void
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public function setScope($scope)
|
|
{
|
|
if (\is_null($scope)) {
|
|
$this->scope = null;
|
|
} elseif (\is_string($scope)) {
|
|
$this->scope = \explode(' ', $scope);
|
|
} elseif (\is_array($scope)) {
|
|
foreach ($scope as $s) {
|
|
$pos = \strpos($s, ' ');
|
|
if ($pos !== \false) {
|
|
throw new \InvalidArgumentException('array scope values should not contain spaces');
|
|
}
|
|
}
|
|
$this->scope = $scope;
|
|
} else {
|
|
throw new \InvalidArgumentException('scopes should be a string or array of strings');
|
|
}
|
|
}
|
|
/**
|
|
* Gets the current grant type.
|
|
*
|
|
* @return ?string
|
|
*/
|
|
public function getGrantType()
|
|
{
|
|
if (!\is_null($this->grantType)) {
|
|
return $this->grantType;
|
|
}
|
|
// Returns the inferred grant type, based on the current object instance
|
|
// state.
|
|
if (!\is_null($this->code)) {
|
|
return 'authorization_code';
|
|
}
|
|
if (!\is_null($this->refreshToken)) {
|
|
return 'refresh_token';
|
|
}
|
|
if (!\is_null($this->username) && !\is_null($this->password)) {
|
|
return 'password';
|
|
}
|
|
if (!\is_null($this->issuer) && !\is_null($this->signingKey)) {
|
|
return self::JWT_URN;
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* Sets the current grant type.
|
|
*
|
|
* @param string $grantType
|
|
* @return void
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public function setGrantType($grantType)
|
|
{
|
|
if (\in_array($grantType, self::$knownGrantTypes)) {
|
|
$this->grantType = $grantType;
|
|
} else {
|
|
// validate URI
|
|
if (!$this->isAbsoluteUri($grantType)) {
|
|
throw new \InvalidArgumentException('invalid grant type');
|
|
}
|
|
$this->grantType = (string) $grantType;
|
|
}
|
|
}
|
|
/**
|
|
* Gets an arbitrary string designed to allow the client to maintain state.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getState()
|
|
{
|
|
return $this->state;
|
|
}
|
|
/**
|
|
* Sets an arbitrary string designed to allow the client to maintain state.
|
|
*
|
|
* @param string $state
|
|
* @return void
|
|
*/
|
|
public function setState($state)
|
|
{
|
|
$this->state = $state;
|
|
}
|
|
/**
|
|
* Gets the authorization code issued to this client.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getCode()
|
|
{
|
|
return $this->code;
|
|
}
|
|
/**
|
|
* Sets the authorization code issued to this client.
|
|
*
|
|
* @param string $code
|
|
* @return void
|
|
*/
|
|
public function setCode($code)
|
|
{
|
|
$this->code = $code;
|
|
}
|
|
/**
|
|
* Gets the resource owner's username.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getUsername()
|
|
{
|
|
return $this->username;
|
|
}
|
|
/**
|
|
* Sets the resource owner's username.
|
|
*
|
|
* @param string $username
|
|
* @return void
|
|
*/
|
|
public function setUsername($username)
|
|
{
|
|
$this->username = $username;
|
|
}
|
|
/**
|
|
* Gets the resource owner's password.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getPassword()
|
|
{
|
|
return $this->password;
|
|
}
|
|
/**
|
|
* Sets the resource owner's password.
|
|
*
|
|
* @param string $password
|
|
* @return void
|
|
*/
|
|
public function setPassword($password)
|
|
{
|
|
$this->password = $password;
|
|
}
|
|
/**
|
|
* Sets a unique identifier issued to the client to identify itself to the
|
|
* authorization server.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getClientId()
|
|
{
|
|
return $this->clientId;
|
|
}
|
|
/**
|
|
* Sets a unique identifier issued to the client to identify itself to the
|
|
* authorization server.
|
|
*
|
|
* @param string $clientId
|
|
* @return void
|
|
*/
|
|
public function setClientId($clientId)
|
|
{
|
|
$this->clientId = $clientId;
|
|
}
|
|
/**
|
|
* Gets a shared symmetric secret issued by the authorization server, which
|
|
* is used to authenticate the client.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getClientSecret()
|
|
{
|
|
return $this->clientSecret;
|
|
}
|
|
/**
|
|
* Sets a shared symmetric secret issued by the authorization server, which
|
|
* is used to authenticate the client.
|
|
*
|
|
* @param string $clientSecret
|
|
* @return void
|
|
*/
|
|
public function setClientSecret($clientSecret)
|
|
{
|
|
$this->clientSecret = $clientSecret;
|
|
}
|
|
/**
|
|
* Gets the Issuer ID when using assertion profile.
|
|
*
|
|
* @return ?string
|
|
*/
|
|
public function getIssuer()
|
|
{
|
|
return $this->issuer;
|
|
}
|
|
/**
|
|
* Sets the Issuer ID when using assertion profile.
|
|
*
|
|
* @param string $issuer
|
|
* @return void
|
|
*/
|
|
public function setIssuer($issuer)
|
|
{
|
|
$this->issuer = $issuer;
|
|
}
|
|
/**
|
|
* Gets the target sub when issuing assertions.
|
|
*
|
|
* @return ?string
|
|
*/
|
|
public function getSub()
|
|
{
|
|
return $this->sub;
|
|
}
|
|
/**
|
|
* Sets the target sub when issuing assertions.
|
|
*
|
|
* @param string $sub
|
|
* @return void
|
|
*/
|
|
public function setSub($sub)
|
|
{
|
|
$this->sub = $sub;
|
|
}
|
|
/**
|
|
* Gets the target audience when issuing assertions.
|
|
*
|
|
* @return ?string
|
|
*/
|
|
public function getAudience()
|
|
{
|
|
return $this->audience;
|
|
}
|
|
/**
|
|
* Sets the target audience when issuing assertions.
|
|
*
|
|
* @param string $audience
|
|
* @return void
|
|
*/
|
|
public function setAudience($audience)
|
|
{
|
|
$this->audience = $audience;
|
|
}
|
|
/**
|
|
* Gets the signing key when using an assertion profile.
|
|
*
|
|
* @return ?string
|
|
*/
|
|
public function getSigningKey()
|
|
{
|
|
return $this->signingKey;
|
|
}
|
|
/**
|
|
* Sets the signing key when using an assertion profile.
|
|
*
|
|
* @param string $signingKey
|
|
* @return void
|
|
*/
|
|
public function setSigningKey($signingKey)
|
|
{
|
|
$this->signingKey = $signingKey;
|
|
}
|
|
/**
|
|
* Gets the signing key id when using an assertion profile.
|
|
*
|
|
* @return ?string
|
|
*/
|
|
public function getSigningKeyId()
|
|
{
|
|
return $this->signingKeyId;
|
|
}
|
|
/**
|
|
* Sets the signing key id when using an assertion profile.
|
|
*
|
|
* @param string $signingKeyId
|
|
* @return void
|
|
*/
|
|
public function setSigningKeyId($signingKeyId)
|
|
{
|
|
$this->signingKeyId = $signingKeyId;
|
|
}
|
|
/**
|
|
* Gets the signing algorithm when using an assertion profile.
|
|
*
|
|
* @return ?string
|
|
*/
|
|
public function getSigningAlgorithm()
|
|
{
|
|
return $this->signingAlgorithm;
|
|
}
|
|
/**
|
|
* Sets the signing algorithm when using an assertion profile.
|
|
*
|
|
* @param ?string $signingAlgorithm
|
|
* @return void
|
|
*/
|
|
public function setSigningAlgorithm($signingAlgorithm)
|
|
{
|
|
if (\is_null($signingAlgorithm)) {
|
|
$this->signingAlgorithm = null;
|
|
} elseif (!\in_array($signingAlgorithm, self::$knownSigningAlgorithms)) {
|
|
throw new \InvalidArgumentException('unknown signing algorithm');
|
|
} else {
|
|
$this->signingAlgorithm = $signingAlgorithm;
|
|
}
|
|
}
|
|
/**
|
|
* Gets the set of parameters used by extension when using an extension
|
|
* grant type.
|
|
*
|
|
* @return array<mixed>
|
|
*/
|
|
public function getExtensionParams()
|
|
{
|
|
return $this->extensionParams;
|
|
}
|
|
/**
|
|
* Sets the set of parameters used by extension when using an extension
|
|
* grant type.
|
|
*
|
|
* @param array<mixed> $extensionParams
|
|
* @return void
|
|
*/
|
|
public function setExtensionParams($extensionParams)
|
|
{
|
|
$this->extensionParams = $extensionParams;
|
|
}
|
|
/**
|
|
* Gets the number of seconds assertions are valid for.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getExpiry()
|
|
{
|
|
return $this->expiry;
|
|
}
|
|
/**
|
|
* Sets the number of seconds assertions are valid for.
|
|
*
|
|
* @param int $expiry
|
|
* @return void
|
|
*/
|
|
public function setExpiry($expiry)
|
|
{
|
|
$this->expiry = $expiry;
|
|
}
|
|
/**
|
|
* Gets the lifetime of the access token in seconds.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getExpiresIn()
|
|
{
|
|
return $this->expiresIn;
|
|
}
|
|
/**
|
|
* Sets the lifetime of the access token in seconds.
|
|
*
|
|
* @param ?int $expiresIn
|
|
* @return void
|
|
*/
|
|
public function setExpiresIn($expiresIn)
|
|
{
|
|
if (\is_null($expiresIn)) {
|
|
$this->expiresIn = null;
|
|
$this->issuedAt = null;
|
|
} else {
|
|
$this->issuedAt = \time();
|
|
$this->expiresIn = (int) $expiresIn;
|
|
}
|
|
}
|
|
/**
|
|
* Gets the time the current access token expires at.
|
|
*
|
|
* @return ?int
|
|
*/
|
|
public function getExpiresAt()
|
|
{
|
|
if (!\is_null($this->expiresAt)) {
|
|
return $this->expiresAt;
|
|
}
|
|
if (!\is_null($this->issuedAt) && !\is_null($this->expiresIn)) {
|
|
return $this->issuedAt + $this->expiresIn;
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* Returns true if the acccess token has expired.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isExpired()
|
|
{
|
|
$expiration = $this->getExpiresAt();
|
|
$now = \time();
|
|
return !\is_null($expiration) && $now >= $expiration;
|
|
}
|
|
/**
|
|
* Sets the time the current access token expires at.
|
|
*
|
|
* @param int $expiresAt
|
|
* @return void
|
|
*/
|
|
public function setExpiresAt($expiresAt)
|
|
{
|
|
$this->expiresAt = $expiresAt;
|
|
}
|
|
/**
|
|
* Gets the time the current access token was issued at.
|
|
*
|
|
* @return ?int
|
|
*/
|
|
public function getIssuedAt()
|
|
{
|
|
return $this->issuedAt;
|
|
}
|
|
/**
|
|
* Sets the time the current access token was issued at.
|
|
*
|
|
* @param int $issuedAt
|
|
* @return void
|
|
*/
|
|
public function setIssuedAt($issuedAt)
|
|
{
|
|
$this->issuedAt = $issuedAt;
|
|
}
|
|
/**
|
|
* Gets the current access token.
|
|
*
|
|
* @return ?string
|
|
*/
|
|
public function getAccessToken()
|
|
{
|
|
return $this->accessToken;
|
|
}
|
|
/**
|
|
* Sets the current access token.
|
|
*
|
|
* @param string $accessToken
|
|
* @return void
|
|
*/
|
|
public function setAccessToken($accessToken)
|
|
{
|
|
$this->accessToken = $accessToken;
|
|
}
|
|
/**
|
|
* Gets the current ID token.
|
|
*
|
|
* @return ?string
|
|
*/
|
|
public function getIdToken()
|
|
{
|
|
return $this->idToken;
|
|
}
|
|
/**
|
|
* Sets the current ID token.
|
|
*
|
|
* @param string $idToken
|
|
* @return void
|
|
*/
|
|
public function setIdToken($idToken)
|
|
{
|
|
$this->idToken = $idToken;
|
|
}
|
|
/**
|
|
* Get the granted scopes (if they exist) for the last fetched token.
|
|
*
|
|
* @return string|null
|
|
*/
|
|
public function getGrantedScope()
|
|
{
|
|
return $this->grantedScope;
|
|
}
|
|
/**
|
|
* Sets the current ID token.
|
|
*
|
|
* @param string $grantedScope
|
|
* @return void
|
|
*/
|
|
public function setGrantedScope($grantedScope)
|
|
{
|
|
$this->grantedScope = $grantedScope;
|
|
}
|
|
/**
|
|
* Gets the refresh token associated with the current access token.
|
|
*
|
|
* @return ?string
|
|
*/
|
|
public function getRefreshToken()
|
|
{
|
|
return $this->refreshToken;
|
|
}
|
|
/**
|
|
* Sets the refresh token associated with the current access token.
|
|
*
|
|
* @param string $refreshToken
|
|
* @return void
|
|
*/
|
|
public function setRefreshToken($refreshToken)
|
|
{
|
|
$this->refreshToken = $refreshToken;
|
|
}
|
|
/**
|
|
* Sets additional claims to be included in the JWT token
|
|
*
|
|
* @param array<mixed> $additionalClaims
|
|
* @return void
|
|
*/
|
|
public function setAdditionalClaims(array $additionalClaims)
|
|
{
|
|
$this->additionalClaims = $additionalClaims;
|
|
}
|
|
/**
|
|
* Gets the additional claims to be included in the JWT token.
|
|
*
|
|
* @return array<mixed>
|
|
*/
|
|
public function getAdditionalClaims()
|
|
{
|
|
return $this->additionalClaims;
|
|
}
|
|
/**
|
|
* The expiration of the last received token.
|
|
*
|
|
* @return array<mixed>|null
|
|
*/
|
|
public function getLastReceivedToken()
|
|
{
|
|
if ($token = $this->getAccessToken()) {
|
|
// the bare necessity of an auth token
|
|
$authToken = ['access_token' => $token, 'expires_at' => $this->getExpiresAt()];
|
|
} elseif ($idToken = $this->getIdToken()) {
|
|
$authToken = ['id_token' => $idToken, 'expires_at' => $this->getExpiresAt()];
|
|
} else {
|
|
return null;
|
|
}
|
|
if ($expiresIn = $this->getExpiresIn()) {
|
|
$authToken['expires_in'] = $expiresIn;
|
|
}
|
|
if ($issuedAt = $this->getIssuedAt()) {
|
|
$authToken['issued_at'] = $issuedAt;
|
|
}
|
|
if ($refreshToken = $this->getRefreshToken()) {
|
|
$authToken['refresh_token'] = $refreshToken;
|
|
}
|
|
return $authToken;
|
|
}
|
|
/**
|
|
* Get the client ID.
|
|
*
|
|
* Alias of {@see Google\Auth\OAuth2::getClientId()}.
|
|
*
|
|
* @param callable $httpHandler
|
|
* @return string
|
|
* @access private
|
|
*/
|
|
public function getClientName(callable $httpHandler = null)
|
|
{
|
|
return $this->getClientId();
|
|
}
|
|
/**
|
|
* @todo handle uri as array
|
|
*
|
|
* @param ?string $uri
|
|
* @return null|UriInterface
|
|
*/
|
|
private function coerceUri($uri)
|
|
{
|
|
if (\is_null($uri)) {
|
|
return null;
|
|
}
|
|
return \WPMailSMTP\Vendor\GuzzleHttp\Psr7\Utils::uriFor($uri);
|
|
}
|
|
/**
|
|
* @param string $idToken
|
|
* @param Key|Key[]|string|string[] $publicKey
|
|
* @param string|string[] $allowedAlgs
|
|
* @return object
|
|
*/
|
|
private function jwtDecode($idToken, $publicKey, $allowedAlgs)
|
|
{
|
|
$keys = $this->getFirebaseJwtKeys($publicKey, $allowedAlgs);
|
|
// Default exception if none are caught. We are using the same exception
|
|
// class and message from firebase/php-jwt to preserve backwards
|
|
// compatibility.
|
|
$e = new \InvalidArgumentException('Key may not be empty');
|
|
foreach ($keys as $key) {
|
|
try {
|
|
return \WPMailSMTP\Vendor\Firebase\JWT\JWT::decode($idToken, $key);
|
|
} catch (\Exception $e) {
|
|
// try next alg
|
|
}
|
|
}
|
|
throw $e;
|
|
}
|
|
/**
|
|
* @param Key|Key[]|string|string[] $publicKey
|
|
* @param string|string[] $allowedAlgs
|
|
* @return Key[]
|
|
*/
|
|
private function getFirebaseJwtKeys($publicKey, $allowedAlgs)
|
|
{
|
|
// If $publicKey is instance of Key, return it
|
|
if ($publicKey instanceof \WPMailSMTP\Vendor\Firebase\JWT\Key) {
|
|
return [$publicKey];
|
|
}
|
|
// If $allowedAlgs is empty, $publicKey must be Key or Key[].
|
|
if (empty($allowedAlgs)) {
|
|
$keys = [];
|
|
foreach ((array) $publicKey as $kid => $pubKey) {
|
|
if (!$pubKey instanceof \WPMailSMTP\Vendor\Firebase\JWT\Key) {
|
|
throw new \InvalidArgumentException(\sprintf('When allowed algorithms is empty, the public key must' . 'be an instance of %s or an array of %s objects', \WPMailSMTP\Vendor\Firebase\JWT\Key::class, \WPMailSMTP\Vendor\Firebase\JWT\Key::class));
|
|
}
|
|
$keys[$kid] = $pubKey;
|
|
}
|
|
return $keys;
|
|
}
|
|
$allowedAlg = null;
|
|
if (\is_string($allowedAlgs)) {
|
|
$allowedAlg = $allowedAlg;
|
|
} elseif (\is_array($allowedAlgs)) {
|
|
if (\count($allowedAlgs) > 1) {
|
|
throw new \InvalidArgumentException('To have multiple allowed algorithms, You must provide an' . ' array of Firebase\\JWT\\Key objects.' . ' See https://github.com/firebase/php-jwt for more information.');
|
|
}
|
|
$allowedAlg = \array_pop($allowedAlgs);
|
|
} else {
|
|
throw new \InvalidArgumentException('allowed algorithms must be a string or array.');
|
|
}
|
|
if (\is_array($publicKey)) {
|
|
// When publicKey is greater than 1, create keys with the single alg.
|
|
$keys = [];
|
|
foreach ($publicKey as $kid => $pubKey) {
|
|
if ($pubKey instanceof \WPMailSMTP\Vendor\Firebase\JWT\Key) {
|
|
$keys[$kid] = $pubKey;
|
|
} else {
|
|
$keys[$kid] = new \WPMailSMTP\Vendor\Firebase\JWT\Key($pubKey, $allowedAlg);
|
|
}
|
|
}
|
|
return $keys;
|
|
}
|
|
return [new \WPMailSMTP\Vendor\Firebase\JWT\Key($publicKey, $allowedAlg)];
|
|
}
|
|
/**
|
|
* Determines if the URI is absolute based on its scheme and host or path
|
|
* (RFC 3986).
|
|
*
|
|
* @param string $uri
|
|
* @return bool
|
|
*/
|
|
private function isAbsoluteUri($uri)
|
|
{
|
|
$uri = $this->coerceUri($uri);
|
|
return $uri->getScheme() && ($uri->getHost() || $uri->getPath());
|
|
}
|
|
/**
|
|
* @param array<mixed> $params
|
|
* @return array<mixed>
|
|
*/
|
|
private function addClientCredentials(&$params)
|
|
{
|
|
$clientId = $this->getClientId();
|
|
$clientSecret = $this->getClientSecret();
|
|
if ($clientId && $clientSecret) {
|
|
$params['client_id'] = $clientId;
|
|
$params['client_secret'] = $clientSecret;
|
|
}
|
|
return $params;
|
|
}
|
|
}
|