vendor\thenetworg\oauth2-azure\src\Provider\Azure.php line 47

Open in your IDE?
  1. <?php
  2. namespace TheNetworg\OAuth2\Client\Provider;
  3. use Firebase\JWT\JWT;
  4. use Firebase\JWT\JWK;
  5. use Firebase\JWT\Key;
  6. use League\OAuth2\Client\Grant\AbstractGrant;
  7. use League\OAuth2\Client\Provider\AbstractProvider;
  8. use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
  9. use League\OAuth2\Client\Provider\ResourceOwnerInterface;
  10. use League\OAuth2\Client\Token\AccessTokenInterface;
  11. use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
  12. use Psr\Http\Message\ResponseInterface;
  13. use TheNetworg\OAuth2\Client\Grant\JwtBearer;
  14. use TheNetworg\OAuth2\Client\Token\AccessToken;
  15. class Azure extends AbstractProvider
  16. {
  17.     const ENDPOINT_VERSION_1_0 '1.0';
  18.     const ENDPOINT_VERSION_2_0 '2.0';
  19.     const ENDPOINT_VERSIONS = [self::ENDPOINT_VERSION_1_0self::ENDPOINT_VERSION_2_0];
  20.     use BearerAuthorizationTrait;
  21.     public $urlLogin 'https://login.microsoftonline.com/';
  22.     /** @var array|null */
  23.     protected $openIdConfiguration;
  24.     public $scope = [];
  25.     public $scopeSeparator ' ';
  26.     public $tenant 'common';
  27.     public $defaultEndPointVersion self::ENDPOINT_VERSION_1_0;
  28.     public $urlAPI 'https://graph.windows.net/';
  29.     public $resource '';
  30.     public $API_VERSION '1.6';
  31.     public $authWithResource true;
  32.     public function __construct(array $options = [], array $collaborators = [])
  33.     {
  34.         parent::__construct($options$collaborators);
  35.         if (isset($options['scopes'])) {
  36.             $this->scope array_merge($options['scopes'], $this->scope);
  37.         }
  38.         if (isset($options['defaultEndPointVersion']) &&
  39.             in_array($options['defaultEndPointVersion'], self::ENDPOINT_VERSIONStrue)) {
  40.             $this->defaultEndPointVersion $options['defaultEndPointVersion'];
  41.         }
  42.         $this->grantFactory->setGrant('jwt_bearer', new JwtBearer());
  43.     }
  44.     /**
  45.      * @param string $tenant
  46.      * @param string $version
  47.      */
  48.     protected function getOpenIdConfiguration($tenant$version) {
  49.         if (!is_array($this->openIdConfiguration)) {
  50.             $this->openIdConfiguration = [];
  51.         }
  52.         if (!array_key_exists($tenant$this->openIdConfiguration)) {
  53.             $this->openIdConfiguration[$tenant] = [];
  54.         }
  55.         if (!array_key_exists($version$this->openIdConfiguration[$tenant])) {
  56.             $versionInfix $this->getVersionUriInfix($version);
  57.               $openIdConfigurationUri $this->urlLogin $tenant $versionInfix '/.well-known/openid-configuration?appid=' $this->clientId;
  58.             
  59.             $factory $this->getRequestFactory();
  60.             $request $factory->getRequestWithOptions(
  61.                 'get',
  62.                 $openIdConfigurationUri,
  63.                 []
  64.             );
  65.             $response $this->getParsedResponse($request);
  66.             $this->openIdConfiguration[$tenant][$version] = $response;
  67.         }
  68.         return $this->openIdConfiguration[$tenant][$version];
  69.     }
  70.     /**
  71.      * @inheritdoc
  72.      */
  73.     public function getBaseAuthorizationUrl(): string
  74.     {
  75.         $openIdConfiguration $this->getOpenIdConfiguration($this->tenant$this->defaultEndPointVersion);
  76.         return $openIdConfiguration['authorization_endpoint'];
  77.     }
  78.     /**
  79.      * @inheritdoc
  80.      */
  81.     public function getBaseAccessTokenUrl(array $params): string
  82.     {
  83.         $openIdConfiguration $this->getOpenIdConfiguration($this->tenant$this->defaultEndPointVersion);
  84.         return $openIdConfiguration['token_endpoint'];
  85.     }
  86.     /**
  87.      * @inheritdoc
  88.      */
  89.     public function getAccessToken($grant, array $options = []): AccessTokenInterface
  90.     {
  91.         if ($this->defaultEndPointVersion != self::ENDPOINT_VERSION_2_0) {
  92.             // Version 2.0 does not support the resources parameter
  93.             // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
  94.             // while version 1.0 does recommend it
  95.             // https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code
  96.             if ($this->authWithResource) {
  97.                 $options['resource'] = $this->resource $this->resource $this->urlAPI;
  98.             }
  99.         }
  100.         return parent::getAccessToken($grant$options);
  101.     }
  102.     /**
  103.      * @inheritdoc
  104.      */
  105.     public function getResourceOwner(\League\OAuth2\Client\Token\AccessToken $token): ResourceOwnerInterface
  106.     {
  107.         $data $token->getIdTokenClaims();
  108.         return $this->createResourceOwner($data$token);
  109.     }
  110.     /**
  111.      * @inheritdoc
  112.      */
  113.     public function getResourceOwnerDetailsUrl(\League\OAuth2\Client\Token\AccessToken $token): string
  114.     {
  115.         return ''// shouldn't that return such a URL?
  116.     }
  117.     public function getObjects($tenant$ref, &$accessToken$headers = [])
  118.     {
  119.         $objects = [];
  120.         $response null;
  121.         do {
  122.             if (false === filter_var($refFILTER_VALIDATE_URL)) {
  123.                 $ref $tenant '/' $ref;
  124.             }
  125.             $response $this->request('get'$ref$accessToken, ['headers' => $headers]);
  126.             $values   $response;
  127.             if (isset($response['value'])) {
  128.                 $values $response['value'];
  129.             }
  130.             foreach ($values as $value) {
  131.                 $objects[] = $value;
  132.             }
  133.             if (isset($response['odata.nextLink'])) {
  134.                 $ref $response['odata.nextLink'];
  135.             } elseif (isset($response['@odata.nextLink'])) {
  136.                 $ref $response['@odata.nextLink'];
  137.             } else {
  138.                 $ref null;
  139.             }
  140.         } while (null != $ref);
  141.         return $objects;
  142.     }
  143.     /**
  144.      * @param $accessToken AccessToken|null
  145.      * @return string
  146.      */
  147.     public function getRootMicrosoftGraphUri($accessToken)
  148.     {
  149.         if (is_null($accessToken)) {
  150.             $tenant $this->tenant;
  151.             $version $this->defaultEndPointVersion;
  152.         } else {
  153.             $idTokenClaims $accessToken->getIdTokenClaims();
  154.             $tenant array_key_exists('tid'$idTokenClaims) ? $idTokenClaims['tid'] : $this->tenant;
  155.             $version array_key_exists('ver'$idTokenClaims) ? $idTokenClaims['ver'] : $this->defaultEndPointVersion;
  156.         }
  157.         $openIdConfiguration $this->getOpenIdConfiguration($tenant$version);
  158.         return 'https://' $openIdConfiguration['msgraph_host'];
  159.     }
  160.     public function get($ref, &$accessToken$headers = [], $doNotWrap false)
  161.     {
  162.         $response $this->request('get'$ref$accessToken, ['headers' => $headers]);
  163.         return $doNotWrap $response $this->wrapResponse($response);
  164.     }
  165.     public function post($ref$body, &$accessToken$headers = [])
  166.     {
  167.         $response $this->request('post'$ref$accessToken, ['body' => $body'headers' => $headers]);
  168.         return $this->wrapResponse($response);
  169.     }
  170.     public function put($ref$body, &$accessToken$headers = [])
  171.     {
  172.         $response $this->request('put'$ref$accessToken, ['body' => $body'headers' => $headers]);
  173.         return $this->wrapResponse($response);
  174.     }
  175.     public function delete($ref, &$accessToken$headers = [])
  176.     {
  177.         $response $this->request('delete'$ref$accessToken, ['headers' => $headers]);
  178.         return $this->wrapResponse($response);
  179.     }
  180.     public function patch($ref$body, &$accessToken$headers = [])
  181.     {
  182.         $response $this->request('patch'$ref$accessToken, ['body' => $body'headers' => $headers]);
  183.         return $this->wrapResponse($response);
  184.     }
  185.     public function request($method$ref, &$accessToken$options = [])
  186.     {
  187.         if ($accessToken->hasExpired()) {
  188.             $accessToken $this->getAccessToken('refresh_token', [
  189.                 'refresh_token' => $accessToken->getRefreshToken(),
  190.             ]);
  191.         }
  192.         $url null;
  193.         if (false !== filter_var($refFILTER_VALIDATE_URL)) {
  194.             $url $ref;
  195.         } else {
  196.             if (false !== strpos($this->urlAPI'graph.windows.net')) {
  197.                 $tenant 'common';
  198.                 if (property_exists($this'tenant')) {
  199.                     $tenant $this->tenant;
  200.                 }
  201.                 $ref "$tenant/$ref";
  202.                 $url $this->urlAPI $ref;
  203.                 $url .= (false === strrpos($url'?')) ? '?' '&';
  204.                 $url .= 'api-version=' $this->API_VERSION;
  205.             } else {
  206.                 $url $this->urlAPI $ref;
  207.             }
  208.         }
  209.         if (isset($options['body']) && ('array' == gettype($options['body']) || 'object' == gettype($options['body']))) {
  210.             $options['body'] = json_encode($options['body']);
  211.         }
  212.         if (!isset($options['headers']['Content-Type']) && isset($options['body'])) {
  213.             $options['headers']['Content-Type'] = 'application/json';
  214.         }
  215.         $request  $this->getAuthenticatedRequest($method$url$accessToken$options);
  216.         $response $this->getParsedResponse($request);
  217.         return $response;
  218.     }
  219.     public function getClientId()
  220.     {
  221.         return $this->clientId;
  222.     }
  223.     /**
  224.      * Obtain URL for logging out the user.
  225.      *
  226.      * @param $post_logout_redirect_uri string The URL which the user should be redirected to after logout
  227.      *
  228.      * @return string
  229.      */
  230.     public function getLogoutUrl($post_logout_redirect_uri "")
  231.     {
  232.         $openIdConfiguration $this->getOpenIdConfiguration($this->tenant$this->defaultEndPointVersion);
  233.         $logoutUri $openIdConfiguration['end_session_endpoint'];
  234.         if (!empty($post_logout_redirect_uri)) {
  235.             $logoutUri .= '?post_logout_redirect_uri=' rawurlencode($post_logout_redirect_uri);
  236.         }
  237.         return $logoutUri;
  238.     }
  239.     /**
  240.      * Validate the access token you received in your application.
  241.      *
  242.      * @param $accessToken string The access token you received in the authorization header.
  243.      *
  244.      * @return array
  245.      */
  246.     public function validateAccessToken($accessToken)
  247.     {
  248.         $keys        $this->getJwtVerificationKeys();
  249.         $tokenClaims = (array)JWT::decode($accessToken$keys, ['RS256']);
  250.         $this->validateTokenClaims($tokenClaims);
  251.         return $tokenClaims;
  252.     }
  253.     /**
  254.      * Validate the access token claims from an access token you received in your application.
  255.      *
  256.      * @param $tokenClaims array The token claims from an access token you received in the authorization header.
  257.      *
  258.      * @return void
  259.      */
  260.     public function validateTokenClaims($tokenClaims) {
  261.         if ($this->getClientId() != $tokenClaims['aud']) {
  262.             throw new \RuntimeException('The client_id / audience is invalid!');
  263.         }
  264.         if ($tokenClaims['nbf'] > time() || $tokenClaims['exp'] < time()) {
  265.             // Additional validation is being performed in firebase/JWT itself
  266.             throw new \RuntimeException('The id_token is invalid!');
  267.         }
  268.         if ('common' == $this->tenant) {
  269.             $this->tenant $tokenClaims['tid'];
  270.         }
  271.         $version array_key_exists('ver'$tokenClaims) ? $tokenClaims['ver'] : $this->defaultEndPointVersion;
  272.         $tenant $this->getTenantDetails($this->tenant$version);
  273.         if ($tokenClaims['iss'] != $tenant['issuer']) {
  274.             throw new \RuntimeException('Invalid token issuer (tokenClaims[iss]' $tokenClaims['iss'] . ', tenant[issuer] ' $tenant['issuer'] . ')!');
  275.         }
  276.     }
  277.     /**
  278.      * Get JWT verification keys from Azure Active Directory.
  279.      *
  280.      * @return array
  281.      */
  282.     public function getJwtVerificationKeys()
  283.     {
  284.         $openIdConfiguration $this->getOpenIdConfiguration($this->tenant$this->defaultEndPointVersion);
  285.         $keysUri $openIdConfiguration['jwks_uri'];
  286.         $factory $this->getRequestFactory();
  287.         $request $factory->getRequestWithOptions('get'$keysUri, []);
  288.         $response $this->getParsedResponse($request);
  289.         $keys = [];
  290.         foreach ($response['keys'] as $i => $keyinfo) {
  291.             if (isset($keyinfo['x5c']) && is_array($keyinfo['x5c'])) {
  292.                 foreach ($keyinfo['x5c'] as $encodedkey) {
  293.                     $cert =
  294.                         '-----BEGIN CERTIFICATE-----' PHP_EOL
  295.                         chunk_split($encodedkey64,  PHP_EOL)
  296.                         . '-----END CERTIFICATE-----' PHP_EOL;
  297.                     $cert_object openssl_x509_read($cert);
  298.                     if ($cert_object === false) {
  299.                         throw new \RuntimeException('An attempt to read ' $encodedkey ' as a certificate failed.');
  300.                     }
  301.                     $pkey_object openssl_pkey_get_public($cert_object);
  302.                     if ($pkey_object === false) {
  303.                         throw new \RuntimeException('An attempt to read a public key from a ' $encodedkey ' certificate failed.');
  304.                     }
  305.                     $pkey_array openssl_pkey_get_details($pkey_object);
  306.                     if ($pkey_array === false) {
  307.                         throw new \RuntimeException('An attempt to get a public key as an array from a ' $encodedkey ' certificate failed.');
  308.                     }
  309.                     $publicKey $pkey_array ['key'];
  310.                     $keys[$keyinfo['kid']] = new Key($publicKey'RS256');
  311.                 }
  312.             } else if (isset($keyinfo['n']) && isset($keyinfo['e'])) {
  313.                 $pkey_object JWK::parseKey($keyinfo);
  314.                 if ($pkey_object === false) {
  315.                     throw new \RuntimeException('An attempt to read a public key from a ' $keyinfo['n'] . ' certificate failed.');
  316.                 }
  317.                 $pkey_array openssl_pkey_get_details($pkey_object);
  318.                 if ($pkey_array === false) {
  319.                     throw new \RuntimeException('An attempt to get a public key as an array from a ' $keyinfo['n'] . ' certificate failed.');
  320.                 }
  321.                 $publicKey $pkey_array ['key'];
  322.                $keys[$keyinfo['kid']] = new Key($publicKey'RS256');;
  323.             }
  324.         }
  325.         return $keys;
  326.     }
  327.     protected function getVersionUriInfix($version)
  328.     {
  329.         return
  330.             ($version == self::ENDPOINT_VERSION_2_0)
  331.                 ? '/v' self::ENDPOINT_VERSION_2_0
  332.                 '';
  333.     }
  334.     /**
  335.      * Get the specified tenant's details.
  336.      *
  337.      * @param string $tenant
  338.      * @param string|null $version
  339.      *
  340.      * @return array
  341.      * @throws IdentityProviderException
  342.      */
  343.     public function getTenantDetails($tenant$version)
  344.     {
  345.         return $this->getOpenIdConfiguration($this->tenant$this->defaultEndPointVersion);
  346.     }
  347.     /**
  348.      * @inheritdoc
  349.      */
  350.     protected function checkResponse(ResponseInterface $response$data): void
  351.     {
  352.         if (isset($data['odata.error']) || isset($data['error'])) {
  353.             if (isset($data['odata.error']['message']['value'])) {
  354.                 $message $data['odata.error']['message']['value'];
  355.             } elseif (isset($data['error']['message'])) {
  356.                 $message $data['error']['message'];
  357.             } elseif (isset($data['error']) && !is_array($data['error'])) {
  358.                 $message $data['error'];
  359.             } else {
  360.                 $message $response->getReasonPhrase();
  361.             }
  362.             if (isset($data['error_description']) && !is_array($data['error_description'])) {
  363.                 $message .= PHP_EOL $data['error_description'];
  364.             }
  365.             throw new IdentityProviderException(
  366.                 $message,
  367.                 $response->getStatusCode(),
  368.                 $response->getBody()
  369.             );
  370.         }
  371.     }
  372.     /**
  373.      * @inheritdoc
  374.      */
  375.     protected function getDefaultScopes(): array
  376.     {
  377.         return $this->scope;
  378.     }
  379.     /**
  380.      * @inheritdoc
  381.      */
  382.     protected function getScopeSeparator(): string
  383.     {
  384.         return $this->scopeSeparator;
  385.     }
  386.     /**
  387.      * @inheritdoc
  388.      */
  389.     protected function createAccessToken(array $responseAbstractGrant $grant): AccessToken
  390.     {
  391.         return new AccessToken($response$this);
  392.     }
  393.     /**
  394.      * @inheritdoc
  395.      */
  396.     protected function createResourceOwner(array $response\League\OAuth2\Client\Token\AccessToken $token): AzureResourceOwner
  397.     {
  398.         return new AzureResourceOwner($response);
  399.     }
  400.     private function wrapResponse($response)
  401.     {
  402.         if (empty($response)) {
  403.             return;
  404.         } elseif (isset($response['value'])) {
  405.             return $response['value'];
  406.         }
  407.         return $response;
  408.     }
  409. }