1
votes

I'm developing a PHP script to login to Microsoft's 365 API and scanning a user's emails for matches with entries within a CRM, so we can link the emails in Outlook with the People in the CRM.

I've got the normal client_secret login method to work via Azure, so I have setup a webapp entry in the Azure Portal and I can get the token & get a list of users using the AD users endpoint (https://graph.windows.net/<tenant>/users?api-version=1.6)

However, to get the user's emails, I need to authenticate using the X.509 certificate method as detailed here:

https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols-oauth-client-creds

However, despite searching Google for the last few hours, I cannot find an sample PHP for doing this, plus I'm in Linux as well, so just generating a compatible X509 Cert wasn't straightforward (nor was it on my Windows VM as it appears that the makecert program most of the examples use is no longer available!)

However, using a temporary 30 day cert I appear to have done that bit as its uploaded successfully into the Azure portal.

Anyway, does anyone have any or know any links to PHP code for submitting an access token request using the client assertion method?

In particular how to generate the JWT value (I've read the article https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials#code-sample, but it talks about submitting various claims values without saying where you actually get the data from or how to work it out.

I'm an experienced Dev, but a bit of a newbie when it comes to talking to Azure.

Many Thanks in advance

1
Why do you need to authenticate with an X.509 certificate? I was under the understanding that a regular client secret would work. Also, I would recommend using Microsoft Graph API instead of Azure AD Graph API, because a) It is not recommended to use AAD Graph anymore (with a couple exceptions) and b) you can get both user info and emails from MS Graph API. - juunas
Hi, sorry for the confusion, its probably down to a) the confusing mess that are microsoft's APIs and b) the fact I didn't get the terminology right. I am using the MS Graph API, but as the script is running in the background, it uses the Azure API to get the access token as that supports Admin level access to the entire organization via the Client Credentials flow, without this each user would have to login to the app in order to give it permission to access their emails. This is why it needs the X.509 cert; Microsoft don't allow access to mails from other users in the org without it. - Steve Childs
You can give the application permission on MS Graph API to read all users' emails. - juunas
I have, but to access the user email, you need to use the X.509 cert. I've managed to get the access token using it now, but the call to the outlook end point is now failing with a 401 unauthorised error :/ The audience claim value is invalid 'aud'.";error_category="invalid_resource" - Steve Childs
Which endpoint are you calling? - juunas

1 Answers

3
votes

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!