Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support endpoint v2.0 #76

Merged
merged 5 commits into from
May 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 83 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ This package provides [Azure Active Directory](https://azure.microsoft.com/en-us
- [Authorization Code Flow](#authorization-code-flow)
- [Advanced flow](#advanced-flow)
- [Using custom parameters](#using-custom-parameters)
- [**NEW** - Call on behalf of a token provided by another app](#call-on-behalf-of-a-token-provided-by-another-app)
- [**NEW** - Logging out](#logging-out)
- [Making API Requests](#making-api-requests)
- [Variables](#variables)
- [Resource Owner](#resource-owner)
- [Microsoft Graph](#microsoft-graph)
- [**UPDATED** - Microsoft Graph](#microsoft-graph)
- [**NEW** - Protecting your API - *experimental*](#protecting-your-api---experimental)
- [Azure Active Directory B2C - *experimental*](#azure-active-directory-b2c---experimental)
- [Multipurpose refresh tokens - *experimental*](#multipurpose-refresh-tokens---experimental)
Expand Down Expand Up @@ -46,45 +47,66 @@ $provider = new TheNetworg\OAuth2\Client\Provider\Azure([
'redirectUri' => 'https://example.com/callback-url'
]);

if (!isset($_GET['code'])) {
// Set to use v2 API, skip the line or set the value to Azure::ENDPOINT_VERSION_1_0 if willing to use v1 API
$provider->defaultEndPointVersion = Azure::ENDPOINT_VERSION_2_0;

// If we don't have an authorization code then get one
$authUrl = $provider->getAuthorizationUrl();
$_SESSION['oauth2state'] = $provider->getState();
header('Location: '.$authUrl);
exit;
$baseGraphUri = $provider->getRootMicrosoftGraphUri(null);
$provider->scope = 'openid profile email offline_access ' . $baseGraphUri . '/User.Read';

// Check given state against previously stored one to mitigate CSRF attack
} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {
if (isset($_GET['code']) && isset($_SESSION['OAuth2.state']) && isset($_GET['state'])) {
if ($_GET['state'] == $_SESSION['OAuth2.state']) {
unset($_SESSION['OAuth2.state']);

unset($_SESSION['oauth2state']);
exit('Invalid state');
// Try to get an access token (using the authorization code grant)
/** @var AccessToken $token */
$token = $provider->getAccessToken('authorization_code', [
'scope' => $provider->scope,
'code' => $_GET['code'],
]);

} else {

// Try to get an access token (using the authorization code grant)
$token = $provider->getAccessToken('authorization_code', [
'code' => $_GET['code'],
'resource' => 'https://graph.windows.net',
]);

// Optional: Now you have a token you can look up a users profile data
try {

// We got an access token, let's now get the user's details
$me = $provider->get("me", $token);

// Use these details to create a new profile
printf('Hello %s!', $me['givenName']);
// Verify token
// Save it to local server session data

return $token->getToken();
} else {
echo 'Invalid state';

} catch (Exception $e) {

// Failed to get user details
exit('Oh dear...');
return null;
}

// Use this to interact with an API on the users behalf
echo $token->getToken();
} else {
// // Check local server's session data for a token
// // and verify if still valid
// /** @var ?AccessToken $token */
// $token = // token cached in session data, null if not found;
//
// if (isset($token)) {
// $me = $provider->get($provider->getRootMicrosoftGraphUri($token) . '/v1.0/me', $token);
// $userEmail = $me['mail'];
//
// if ($token->hasExpired()) {
// if (!is_null($token->getRefreshToken())) {
// $token = $provider->getAccessToken('refresh_token', [
// 'scope' => $provider->scope,
// 'refresh_token' => $token->getRefreshToken()
// ]);
// } else {
// $token = null;
// }
// }
//}
//
// If the token is not found in
// if (!isset($token)) {
$authorizationUrl = $provider->getAuthorizationUrl(['scope' => $provider->scope]);

$_SESSION['OAuth2.state'] = $provider->getState();

header('Location: ' . $authorizationUrl);

exit;
// }

return $token->getToken();
}
```

Expand All @@ -111,6 +133,26 @@ $logoutUrl = $provider->getLogoutUrl($post_logout_redirect_uri);
header('Location: '.$logoutUrl); // Redirect the user to the generated URL
```

#### Call on behalf of a token provided by another app

```php
// Use token provided by the other app
// Make sure the other app mentioned this app in the scope when requesting the token
$suppliedToken = '';

$provider = xxxxx;// Initialize provider

// Call this to get claims
// $claims = $provider->validateAccessToken($suppliedToken);

/** @var AccessToken $token */
$token = $provider->getAccessToken('jwt_bearer', [
'scope' => $provider->scope,
'assertion' => $suppliedToken,
'requested_token_use' => 'on_behalf_of',
]);
```

## Making API Requests

This library also provides easy interface to make it easier to interact with [Azure Graph API](https://msdn.microsoft.com/en-us/library/azure/hh974476.aspx) and [Microsoft Graph](http://graph.microsoft.io), the following methods are available on `provider` object (it also handles automatic token refresh flow should it be needed during making the request):
Expand Down Expand Up @@ -151,8 +193,12 @@ The exposed attributes and function are:
## Microsoft Graph
Calling [Microsoft Graph](http://graph.microsoft.io/) is very simple with this library. After provider initialization simply change the API URL followingly (replace `v1.0` with your desired version):
```php
$provider->urlAPI = "https://graph.microsoft.com/v1.0/";
$provider->resource = "https://graph.microsoft.com/";
// Mention Microsoft Graph scope when initializing the provider
$baseGraphUri = $provider->getRootMicrosoftGraphUri(null);
$provider->scope = 'your scope ' . $baseGraphUri . '/User.Read';

// Call a query
$provider->get($provider->getRootMicrosoftGraphUri($token) . '/v1.0/me', $token);
```
After that, when requesting access token, refresh token or so, provide the `resource` with value `https://graph.microsoft.com/` in order to be able to make calls to the Graph (see more about `resource` [here](#advanced-flow)).

Expand Down Expand Up @@ -223,6 +269,7 @@ We accept contributions via [Pull Requests on Github](https://github.com/thenetw
- [Jan Hajek](https://github.com/hajekj) ([TheNetw.org](https://thenetw.org))
- [Vittorio Bertocci](https://github.com/vibronet) (Microsoft)
- Thanks for the splendid support while implementing #16
- [Martin Cetkovský](https://github.com/mcetkovsky) ([cetkovsky.eu](https://www.cetkovsky.eu)]
- [All Contributors](https://github.com/thenetworg/oauth2-azure/contributors)

## Support
Expand Down
136 changes: 100 additions & 36 deletions src/Provider/Azure.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@ class Azure extends AbstractProvider
{
const ENDPOINT_VERSION_1_0 = '1.0';
const ENDPOINT_VERSION_2_0 = '2.0';

use BearerAuthorizationTrait;

public $urlLogin = 'https://login.microsoftonline.com/';

public $pathAuthorize = '/oauth2/authorize';

public $pathToken = '/oauth2/token';
/** @var array|null */
protected $openIdConfiguration;

public $scope = [];

Expand All @@ -46,20 +45,56 @@ public function __construct(array $options = [], array $collaborators = [])
$this->grantFactory->setGrant('jwt_bearer', new JwtBearer());
}

/**
* @param string $tenant
* @param string $version
*/
protected function
Configuration($tenant, $version) {
if (!is_array($this->openIdConfiguration)) {
$this->openIdConfiguration = [];
}
if (!array_key_exists($tenant, $this->openIdConfiguration)) {
$this->openIdConfiguration[$tenant] = [];
}
if (!array_key_exists($version, $this->openIdConfiguration[$tenant])) {
$versionInfix = $this->getVersionUriInfix($version);
$openIdConfigurationUri = 'https://login.microsoftonline.com/' . $tenant . $versionInfix . '/.well-known/openid-configuration';
$factory = $this->getRequestFactory();
$request = $factory->getRequestWithOptions(
'get',
$openIdConfigurationUri,
[]
);
$response = $this->getParsedResponse($request);
$this->openIdConfiguration[$tenant][$version] = $response;
}

return $this->openIdConfiguration[$tenant][$version];
}

public function getBaseAuthorizationUrl()
{
return $this->urlLogin . $this->tenant . $this->pathAuthorize;
$openIdConfiguration = $this->getOpenIdConfiguration($this->tenant, $this->defaultEndPointVersion);
return $openIdConfiguration['authorization_endpoint'];
}

public function getBaseAccessTokenUrl(array $params)
{
return $this->urlLogin . $this->tenant . $this->pathToken;
$openIdConfiguration = $this->getOpenIdConfiguration($this->tenant, $this->defaultEndPointVersion);
return $openIdConfiguration['token_endpoint'];
}

public function getAccessToken($grant, array $options = [])
{
if ($this->authWithResource) {
$options['resource'] = $this->resource ? $this->resource : $this->urlAPI;
if ($this->defaultEndPointVersion != self::ENDPOINT_VERSION_2_0) {
// Version 2.0 does not support the resources parameter
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
// while version 1.0 does recommend it
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code
if ($this->authWithResource) {
$options['resource'] = $this->resource ? $this->resource : $this->urlAPI;
}
}
return parent::getAccessToken($grant, $options);
}
Expand Down Expand Up @@ -104,6 +139,24 @@ public function getObjects($tenant, $ref, &$accessToken, $headers = [])
return $objects;
}

/**
* @param $accessToken AccessToken|null
* @return string
*/
public function getRootMicrosoftGraphUri($accessToken)
{
if (is_null($accessToken)) {
$tenant = $this->tenant;
$version = $this->defaultEndPointVersion;
} else {
$idTokenClaims = $accessToken->getIdTokenClaims();
$tenant = array_key_exists('tid', $idTokenClaims) ? $idTokenClaims['tid'] : $this->tenant;
$version = array_key_exists('ver', $idTokenClaims) ? $idTokenClaims['ver'] : $this->defaultEndPointVersion;
}
$openIdConfiguration = $this->getOpenIdConfiguration($tenant, $version);
return 'https://' . $openIdConfiguration['msgraph_host'];
}

public function get($ref, &$accessToken, $headers = [])
{
$response = $this->request('get', $ref, $accessToken, ['headers' => $headers]);
Expand Down Expand Up @@ -194,12 +247,15 @@ public function getClientId()
*/
public function getLogoutUrl($post_logout_redirect_uri = "")
{
$logoutUrl = 'https://login.microsoftonline.com/' . $this->tenant . '/oauth2/logout';
$openIdConfiguration = $this->
Configuration($this->tenant, $this->defaultEndPointVersion);
$logoutUri = $openIdConfiguration['end_session_endpoint'];

if (!empty($post_logout_redirect_uri)) {
$logoutUrl .= '?post_logout_redirect_uri=' . rawurlencode($post_logout_redirect_uri);
$logoutUri .= '?post_logout_redirect_uri=' . rawurlencode($post_logout_redirect_uri);
}

return $logoutUrl;
return $logoutUri;
}

/**
Expand All @@ -214,6 +270,19 @@ public function validateAccessToken($accessToken)
$keys = $this->getJwtVerificationKeys();
$tokenClaims = (array)JWT::decode($accessToken, $keys, ['RS256']);

$this->validateTokenClaims($tokenClaims);

return $tokenClaims;
}

/**
* Validate the access token claims from an access token you received in your application.
*
* @param $tokenClaims array The token claims from an access token you received in the authorization header.
*
* @return void
*/
public function validateTokenClaims($tokenClaims) {
if ($this->getClientId() != $tokenClaims['aud'] && $this->getClientId() != $tokenClaims['appid']) {
throw new \RuntimeException('The client_id / audience is invalid!');
}
Expand All @@ -224,19 +293,13 @@ public function validateAccessToken($accessToken)

if ('common' == $this->tenant) {
$this->tenant = $tokenClaims['tid'];

$tenant = $this->getTenantDetails($this->tenant);
if ($tokenClaims['iss'] != $tenant['issuer']) {
throw new \RuntimeException('Invalid token issuer!');
}
} else {
$tenant = $this->getTenantDetails($this->tenant);
if ($tokenClaims['iss'] != $tenant['issuer']) {
throw new \RuntimeException('Invalid token issuer!');
}
}

return $tokenClaims;
$version = array_key_exists('ver', $tokenClaims) ? $tokenClaims['ver'] : $this->defaultEndPointVersion;
$tenant = $this->getTenantDetails($this->tenant, $version);
if ($tokenClaims['iss'] != $tenant['issuer']) {
throw new \RuntimeException('Invalid token issuer (tokenClaims[iss]' . $tokenClaims['iss'] . ', tenant[issuer] ' . $tenant['issuer'] . ')!');
}
}

/**
Expand All @@ -246,8 +309,11 @@ public function validateAccessToken($accessToken)
*/
public function getJwtVerificationKeys()
{
$openIdConfiguration = $this->getOpenIdConfiguration($this->tenant, $this->defaultEndPointVersion);
$keysUri = $openIdConfiguration['jwks_uri'];

$factory = $this->getRequestFactory();
$request = $factory->getRequestWithOptions('get', 'https://login.windows.net/common/discovery/keys', []);
$request = $factory->getRequestWithOptions('get', $keysUri, []);

$response = $this->getParsedResponse($request);

Expand Down Expand Up @@ -288,28 +354,26 @@ public function getJwtVerificationKeys()
return $keys;
}

protected function getVersionUriInfix($version)
{
return
($version == self::ENDPOINT_VERSION_2_0)
? '/v' . self::ENDPOINT_VERSION_2_0
: '';
}

/**
* Get the specified tenant's details.
*
* @param string $tenant
* @param string|null $version
*
* @return array
* @throws IdentityProviderException
*/
public function getTenantDetails($tenant)
public function getTenantDetails($tenant, $version)
{
$versionPath = $this->defaultEndPointVersion === '2.0' ? '/v2.0' : '';

$factory = $this->getRequestFactory();
$request = $factory->getRequestWithOptions(
'get',
$this->urlLogin . '/' . $tenant . $versionPath . '/.well-known/openid-configuration',
[]
);

$response = $this->getParsedResponse($request);

return $response;
return $this->getOpenIdConfiguration($this->tenant, $this->defaultEndPointVersion);
}

protected function checkResponse(ResponseInterface $response, $data)
Expand Down
Loading