使用客户端凭据(X.509证书)访问PHP Azure Active Directory API

使用客户端凭据(X.509证书)访问PHP Azure Active Directory API,php,azure,oauth-2.0,azure-active-directory,Php,Azure,Oauth 2.0,Azure Active Directory,我正在开发一个PHP脚本,用于登录Microsoft的365 API,并扫描用户的电子邮件,以查找与CRM中的条目相匹配的内容,这样我们就可以将Outlook中的电子邮件与CRM中的人员链接起来 我已经通过Azure使用了正常的client_secret登录方法,因此我在Azure门户中设置了一个webapp条目,我可以获得令牌并获得使用AD users端点的用户列表() 但是,要获取用户的电子邮件,我需要使用X.509证书方法进行身份验证,如下所述: 然而,尽管在过去的几个小时里搜索了谷歌,

我正在开发一个PHP脚本,用于登录Microsoft的365 API,并扫描用户的电子邮件,以查找与CRM中的条目相匹配的内容,这样我们就可以将Outlook中的电子邮件与CRM中的人员链接起来

我已经通过Azure使用了正常的client_secret登录方法,因此我在Azure门户中设置了一个webapp条目,我可以获得令牌并获得使用AD users端点的用户列表()

但是,要获取用户的电子邮件,我需要使用X.509证书方法进行身份验证,如下所述:

然而,尽管在过去的几个小时里搜索了谷歌,我还是找不到一个PHP示例,加上我也在Linux中,所以仅仅生成一个兼容的X509证书并不简单(在我的Windows VM上也不简单,因为大多数示例使用的makecert程序似乎不再可用!)

然而,使用一个临时的30天证书,我似乎已经做到了这一点,因为它已成功上传到Azure门户

无论如何,是否有人拥有或知道使用客户端断言方法提交访问令牌请求的PHP代码的链接

特别是如何生成JWT值(我读过这篇文章,但它讨论了提交各种索赔值,但没有说明实际从何处获取数据或如何计算

我是一名经验丰富的开发人员,但在与Azure交谈时有点新手


事先非常感谢

好的,我终于做到了,所以我将发布一些东西,希望能帮助其他人

快速概述:

  • 我已经在Azure门户中设置了我的应用程序,确保它可以访问密钥部分下的Office API图形API和Active Directory API,并确保单击“授予权限”按钮
  • 使用Auth端点,我可以正确授权应用程序,确保在Auth URL的末尾添加“prompt=admin_approve”,以获得管理员同意,而不是用户同意-此时我正在使用客户端id和客户端机密进行身份验证
  • 我可以访问Active Directory端点以获取Active Directory中的用户列表
我遇到的主要问题是,我试图通过Outlook API访问用户的电子邮件,我可以很好地阅读我的电子邮件,但试图阅读其他人的电子邮件会导致401错误

事实证明,这是意料之中的。如果您获得具有客户端密钥值(即密码)的身份验证令牌,则只能在Outlook API中访问您自己的详细信息。当您尝试访问其他人的详细信息时,您会收到一个拒绝访问错误

解决这个问题的方法是创建一个X.509密钥,并使用该密钥进行身份验证,而不是使用客户机密钥

但是关于如何在PHP中实现这一点的信息非常少,我就是这样做的:

好的,首先创建X.509证书。我遵循以下指南:

然后,我需要解决如何为获取auth令牌时传递的client_断言参数创建JWT代码


有一个很好的PHP库Firebase,它包含一个JWT编码器——它可以通过composer安装,所以安装起来非常简单

然后,我需要从Azure SDK中破解一个类来管理证书,但首先我必须将pem证书转换为pfx,我使用以下命令(从与cert_gen.sh文件相同的目录中)完成了此操作

是您需要的SDK,文件为azureadclientasymetrickey.php

所以,把这些放在一起编写一些代码——这不是设计为可运行的,它是从我的系统中删掉的,但它应该有望为您指明正确的方向

在我的应用程序中,我创建了两个auth令牌,一个用于outlookapi,另一个用于graphapi,因此您将看到两个不同的作用域在使用

    $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;
我遇到的最后一个问题是,当我在成功获取令牌后尝试访问邮件时,出现了一个无效的资源错误。事实证明,这是一个愚蠢的简单且非常有用的错误,没有记录在microsoft的文档中。您可以在上面的代码中看到,有一行

$result['params']['resource'] = $scope == 'outlook' ? 'https://outlook.office.com' : 'https://graph.microsoft.com';
这是一个关键参数,因为它设置令牌可以访问的资源,$scope被传递到上述函数中,它是outlook或graph,用于设置对相关API端点的请求


无论如何,我希望这能帮助一些人,我花了大约8个小时才弄清这一点!

为什么您需要使用X.509证书进行身份验证?我知道常规的客户端机密可以工作。此外,我建议使用Microsoft Graph API而不是Azure AD Graph API,因为a)不建议再使用AAD Graph(除了几个例外)和b)您可以从MS Graph API获取用户信息和电子邮件。您好,很抱歉造成混淆,这可能是由于a)microsoft API的混乱和b)我没有正确理解术语。我使用的是MS Graph API,但由于脚本在后台运行,它使用Azure API获取访问令牌,因为该令牌支持通过客户端凭据流对整个组织进行管理员级别的访问,否则每个用户都必须登录到应用程序,以授予其访问电子邮件的权限。这就是为什么它需要X.509证书;Microsoft不允许在没有它的情况下访问组织中其他用户的邮件。您可以在MS Graph API上授予应用程序读取所有用户电子邮件的权限。我有,但要访问用户电子邮件,您需要使用X.509证书。我现在已设法使用它获取访问令牌,但是对outlook端点的调用现在失败了,出现了一个401未经授权的错误:/访问群体声明值“aud”无效。“error\u category=“invalid\u resource”您正在调用哪个端点?”可通过composer安装,因此安装非常简单" ... 除了在生产环境中,composer是一个巨大的安全漏洞,并且是整个系统的一个障碍
$result['params']['resource'] = $scope == 'outlook' ? 'https://outlook.office.com' : 'https://graph.microsoft.com';