Ok, I've finally managed to do it, so I'll post a few things to hopefully help others.
A Quick overview:
- I've setup my app in the Azure Portal, making sure to give it access to the Office APIs Graph APIs and Active Directory APIs under the Keys section and making sure to click the 'Grant Permissions' button.
- Using the Auth endpoint https://login.windows.net/#tenant#/oauth2/authorize I could authorise the app correctly, making sure to add 'prompt=admin_consent' to the end of the Auth URL in order to get an admin consent, rather than a user consent - At this point I was authing with the client_id and client_secret
- I could access the Active Directory end point for getting a list of users in the Active Directory fine.
The main problem I had was I was trying to access a user's email's over the Outlook API, I could read my emails fine, but trying to read anyone else's would result in a 401 Error.
This it turns out, is as expected. If you get the auth token with a client_secret value (i.e. a password), you can only access your own details within the Outlook API. The moment you try and access anyone else's, you get an access denied error.
The way around this is to create a X.509 key and use that to authenticate with, not the client_secret.
But information on how to do this in PHP is far and few between, this is how I did it:
Ok, firstly creating the X.509 cert. I followed this guide here: https://github.com/Azure/azure-iot-sdk-c/blob/master/tools/CACertificates/CACertificateOverview.md.
Then I needed to work out how to create the JWT code for the client_assertion parameter that you pass when you get the auth token.
There's an excellent library for PHP called Firebase, which contains a JWT encoder - https://github.com/firebase/php-jwt its installable via composer, so really easy to install.
I then needed to hack a class away from an Azure SDK for managing the certificate, but first of all I had to convert the pem certificate into a pfx, which I did with the following command (from the same directory as the cert_gen.sh file)
openssl pkcs12 -export -out certs/azure-iot-test-only.chain.pfx -inkey private/azure-iot-test-only.intermediate.key.pem -in certs/azure-iot-test-only.chain.ca.cert.pem -certfile certs/azure-iot-test-only.chain.ca.cert.pem
https://github.com/Azure/azure-sdk-for-php is the SDK you'll need and the file is AzureAdClientAsymmetricKey.php
So, to put that all together into some code - this isn't designed to be runable, its chopped out of my system, but it should hopefully point you in the right direction.
In my app I create two auth tokens, one for the outlook API and one for the graph API, so hence why you'll see two different scopes in use.
$result = [
'uri' => str_replace('#tenant#',$this->tenantId,'https://login.windows.net/#tenant#/oauth2/authorize'),
'params' => [
'response_type' => 'code',
'client_id' => $this->clientId, // the app client id
'grant_type' => 'client_credentials',
'scope' => $this->getScopeParam($scope),
],
];
$result['params']['tenant'] = $this->tenantId;
$result['params']['code'] = $this->azureAuthCode; // THe code returned from the admin authorisation
$pfxFileName = '/path/to/certs/azure-iot-test-only.chain.pfx';
$pfxPassword = '1234';
if ((!$cert_store = file_get_contents($pfxFileName)) ||
(!openssl_pkcs12_read($cert_store, $cert_info, $pfxPassword))) {
$this->logger->addError("Unable to read the cert file");
return $result;
}
$result['params']['resource'] = $scope == 'outlook' ? 'https://outlook.office.com' : 'https://graph.microsoft.com';
$credentials = new AdClientAsymmetricKey($this->clientId,$cert_info);
// We need to create the JWT for the authentication
$head = [];
$head['x5t'] = $credentials->getFingerprint();
$head['x5c'] = [ $credentials->getCertificate() ];
$token = [];
$token['aud'] = $result['uri'];
$token['sub'] = $credentials->getClientId();
$token['iss'] = $credentials->getClientId();
$token['nbf'] = (string)((new \DateTime("now", new \DateTimeZone('UTC')))->getTimestamp() - 60);
$token['exp'] = (string)((new \DateTime("now", new \DateTimeZone('UTC')))->getTimestamp() + 520);
$result['params']['client_assertion_type'] = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
$result['params']['client_assertion'] = JWT::encode($token, $credentials->getPrivateKey(), 'RS256', null, $head);
return $result;
One last issue I encountered was when I tried to access the mail after successfully getting tokens was an invalid resource error. It turns out this was stupidly simply and rather helpfully, not documented on microsoft's documents. You see in the code above, there's a line...
$result['params']['resource'] = $scope == 'outlook' ? 'https://outlook.office.com' : 'https://graph.microsoft.com';
This is the key parameter as it sets the resource the token can access, $scope is passed into the function above and is either outlook or graph, to setup the request for the relevant API endpoint.
Anyway, I hope this helps someone out, it took me about 8 hours to get to the bottom of that!