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, '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->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. * * if $publicKey is null, the key is decoded without being verified. * * @param string $publicKey The public key to use to authenticate the token * @param array $allowed_algs List of supported verification algorithms * * @return null|object */ public function verifyIdToken($publicKey = null, $allowed_algs = array()) { $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 $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(), 'aud' => $this->getAudience(), '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->getScope()))) { $assertion['scope'] = $this->getScope(); } if (!(is_null($this->getSub()))) { $assertion['sub'] = $this->getSub(); } $assertion += $this->getAdditionalClaims(); return $this->jwtEncode($assertion, $this->getSigningKey(), $this->getSigningAlgorithm()); } /** * 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 = array('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 Request( 'POST', $uri, $headers, Psr7\build_query($params) ); } /** * Fetches the auth tokens based on the current state. * * @param callable $httpHandler callback which delivers psr7 request * * @return array the response */ public function fetchAuthToken(callable $httpHandler = null) { if (is_null($httpHandler)) { $httpHandler = HttpHandlerFactory::build(HttpClientCache::getHttpClient()); } $response = $httpHandler($this->generateCredentialsRequest()); $credentials = $this->parseTokenResponse($response); $this->updateToken($credentials); 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_string($this->scope)) { return $this->scope; } if (is_array($this->scope)) { return implode(':', $this->scope); } // If scope has not set, return null to indicate no caching. return null; } /** * Parses the fetched tokens. * * @param ResponseInterface $resp the response. * * @return array the tokens parsed from the response body. * * @throws \Exception */ public function parseTokenResponse(ResponseInterface $resp) { $body = (string)$resp->getBody(); if ($resp->hasHeader('Content-Type') && $resp->getHeaderLine('Content-Type') == 'application/x-www-form-urlencoded' ) { $res = array(); 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 * client.updateToken([ * 'refresh_token' => 'n4E9O119d', * 'access_token' => 'FJQbwq9', * 'expires_in' => 3600 * ]) * * @param array $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. */ public function updateToken(array $config) { $opts = array_merge([ 'extensionParams' => [], 'access_token' => null, 'id_token' => null, 'expires_in' => null, 'expires_at' => null, 'issued_at' => 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 $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 = Psr7\parse_query($result->getQuery()); $result = $result->withQuery( Psr7\build_query(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 */ 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 string */ public function getTokenCredentialUri() { return $this->tokenCredentialUri; } /** * Sets the authorization server's HTTP endpoint capable of issuing tokens * and refreshing expired tokens. * * @param string $uri */ 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 */ 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 $scope * * @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 $grantType * * @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 */ public function setState($state) { $this->state = $state; } /** * Gets the authorization code issued to this client. */ public function getCode() { return $this->code; } /** * Sets the authorization code issued to this client. * * @param string $code */ public function setCode($code) { $this->code = $code; } /** * Gets the resource owner's username. */ public function getUsername() { return $this->username; } /** * Sets the resource owner's username. * * @param string $username */ public function setUsername($username) { $this->username = $username; } /** * Gets the resource owner's password. */ public function getPassword() { return $this->password; } /** * Sets the resource owner's password. * * @param $password */ public function setPassword($password) { $this->password = $password; } /** * Sets a unique identifier issued to the client to identify itself to the * authorization server. */ public function getClientId() { return $this->clientId; } /** * Sets a unique identifier issued to the client to identify itself to the * authorization server. * * @param $clientId */ public function setClientId($clientId) { $this->clientId = $clientId; } /** * Gets a shared symmetric secret issued by the authorization server, which * is used to authenticate the client. */ public function getClientSecret() { return $this->clientSecret; } /** * Sets a shared symmetric secret issued by the authorization server, which * is used to authenticate the client. * * @param $clientSecret */ public function setClientSecret($clientSecret) { $this->clientSecret = $clientSecret; } /** * Gets the Issuer ID when using assertion profile. */ public function getIssuer() { return $this->issuer; } /** * Sets the Issuer ID when using assertion profile. * * @param string $issuer */ public function setIssuer($issuer) { $this->issuer = $issuer; } /** * Gets the target sub when issuing assertions. */ public function getSub() { return $this->sub; } /** * Sets the target sub when issuing assertions. * * @param string $sub */ public function setSub($sub) { $this->sub = $sub; } /** * Gets the target audience when issuing assertions. */ public function getAudience() { return $this->audience; } /** * Sets the target audience when issuing assertions. * * @param string $audience */ public function setAudience($audience) { $this->audience = $audience; } /** * Gets the signing key when using an assertion profile. */ public function getSigningKey() { return $this->signingKey; } /** * Sets the signing key when using an assertion profile. * * @param string $signingKey */ public function setSigningKey($signingKey) { $this->signingKey = $signingKey; } /** * 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 */ 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. */ public function getExtensionParams() { return $this->extensionParams; } /** * Sets the set of parameters used by extension when using an extension * grant type. * * @param $extensionParams */ public function setExtensionParams($extensionParams) { $this->extensionParams = $extensionParams; } /** * Gets the number of seconds assertions are valid for. */ public function getExpiry() { return $this->expiry; } /** * Sets the number of seconds assertions are valid for. * * @param int $expiry */ public function setExpiry($expiry) { $this->expiry = $expiry; } /** * Gets the lifetime of the access token in seconds. */ public function getExpiresIn() { return $this->expiresIn; } /** * Sets the lifetime of the access token in seconds. * * @param int $expiresIn */ 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 */ public function setExpiresAt($expiresAt) { $this->expiresAt = $expiresAt; } /** * Gets the time the current access token was issued at. */ public function getIssuedAt() { return $this->issuedAt; } /** * Sets the time the current access token was issued at. * * @param int $issuedAt */ public function setIssuedAt($issuedAt) { $this->issuedAt = $issuedAt; } /** * Gets the current access token. */ public function getAccessToken() { return $this->accessToken; } /** * Sets the current access token. * * @param string $accessToken */ public function setAccessToken($accessToken) { $this->accessToken = $accessToken; } /** * Gets the current ID token. */ public function getIdToken() { return $this->idToken; } /** * Sets the current ID token. * * @param $idToken */ public function setIdToken($idToken) { $this->idToken = $idToken; } /** * Gets the refresh token associated with the current access token. */ public function getRefreshToken() { return $this->refreshToken; } /** * Sets the refresh token associated with the current access token. * * @param $refreshToken */ public function setRefreshToken($refreshToken) { $this->refreshToken = $refreshToken; } /** * Sets additional claims to be included in the JWT token * * @param array $additionalClaims */ public function setAdditionalClaims(array $additionalClaims) { $this->additionalClaims = $additionalClaims; } /** * Gets the additional claims to be included in the JWT token. * * @return array */ public function getAdditionalClaims() { return $this->additionalClaims; } /** * The expiration of the last received token. * * @return array */ public function getLastReceivedToken() { if ($token = $this->getAccessToken()) { return [ 'access_token' => $token, 'expires_at' => $this->getExpiresAt(), ]; } return null; } /** * 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; } return Psr7\uri_for($uri); } /** * @param string $idToken * @param string|array|null $publicKey * @param array $allowedAlgs * * @return object */ private function jwtDecode($idToken, $publicKey, $allowedAlgs) { if (class_exists('Firebase\JWT\JWT')) { return \Firebase\JWT\JWT::decode($idToken, $publicKey, $allowedAlgs); } return \JWT::decode($idToken, $publicKey, $allowedAlgs); } private function jwtEncode($assertion, $signingKey, $signingAlgorithm) { if (class_exists('Firebase\JWT\JWT')) { return \Firebase\JWT\JWT::encode($assertion, $signingKey, $signingAlgorithm); } return \JWT::encode($assertion, $signingKey, $signingAlgorithm); } /** * 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 $params * * @return array */ private function addClientCredentials(&$params) { $clientId = $this->getClientId(); $clientSecret = $this->getClientSecret(); if ($clientId && $clientSecret) { $params['client_id'] = $clientId; $params['client_secret'] = $clientSecret; } return $params; } }