为 SharePoint Online O365 构建多租户应用程序

Posted

技术标签:

【中文标题】为 SharePoint Online O365 构建多租户应用程序【英文标题】:Building a multi-tenant app for SharePoint Online O365 【发布时间】:2015-02-02 21:31:57 【问题描述】:

我正在尝试为 Office 365 构建一个多租户应用程序,该应用程序专注于 SharePoint Online,并使用 OAuth2 通过 Azure 进行身份验证。该问题特定于通过 Azure 登录的 SharePoint 访问,但仅在使用此 API 使用 OAuth2 进行身份验证时才发现。

在 Azure 和 Office 中正确注册应用程序和设置用户的许多机制虽然有些复杂,但只要投入时间就可以克服。

即使是基本的 OAuth2 协议在 Azure 中的使用也相对顺利。但是,由于 SharePoint 的“资源”参数,我无法让我的应用程序真正实现多租户。这显然要求我的应用程序在最终用户完成登录序列之前知道他们的根 SharePoint 网站 URL。我看不出这怎么可能。有人请指出我正确的方向。

以下是实际登录顺序的示例:

GET /common/oauth2/authorize?client_id=5cb5e93b-57f5-4e09-97c5-e0d20661c59a
&redirect_uri=https://myappdomain.com/v1/oauth2_redirect/
&response_type=code&prompt=login&state=D79E5777 HTTP/1.1
Host: login.windows.net
Cache-Control: no-cache

当用户进行身份验证时,这会导致对提供的重定向的调用如下所示:

https://myappdomain.com/v1/oauth2_redirect/?code=AAABAAAAvPM1KaPlrEq...blah*3 

到目前为止很棒! 3-legged 身份验证的下一步是返回到 /token 端点的 POST,以获取要在所有后续 REST 调用中使用的实际 Bearer 令牌。这只是经典的 OAuth2...

POST /common/oauth2/token HTTP/1.1
Host: login.windows.net
Accept: text/json
Cache-Control: no-cache

----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="grant_type"

authorization_code
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="code"

AAABAAAAvPM1KaPlrEq...blah*3
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="client_id"

5cb5e93b-57f5-4e09-97c5-e0d20661c59a
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="client_secret"

02my little secretI=
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="redirect_uri"

https://myappdomain.com/v1/oauth2_redirect/
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="resource"

https://contoso.sharepoint.com/
----WebKitFormBoundaryE19zNvXGzXaLvS5C

这就是它变得粘稠的地方。 'resource' 参数是必需的,并且必须指向您要访问的特定于用户的端点。对于 Exchange 或 Azure,端点始终相同。 (https://graph.windows.nethttps://outlook.office365.com)但 SharePoint 对每个租户都有不同的端点。您实际上还没有让用户登录,但您已经需要关于您还没有的用户的信息..

如果我部署的应用程序版本假定“contoso”作为租户名称(如上所述),那么只有 contoso 租户中的用户才能成功使用我的应用程序读取 SharePoint 数据。只要 fabrikam 中的另一个用户尝试使用它,我的 POST/token 端点就会请求访问错误站点的权限...问题就在这里。

如何在用户实际登录之前检测到POST/token 端点的正确端点?是否有一些我可以使用的隐藏信息?是否有某种发现可以检测租户的根 SharePoint URL?或者更好的是,是否有一个端点可以作为租户家的代表资源传递(类似于https://office.microsoft.com/sharepoint)?然后可能从返回的 user_id JWT 令牌中收集到它。这将类似于其他服务,并且对于客户来说非常容易管理。但是,我没有看到这个。

如果没有对这些问题的明确答案或解决这些问题的方法,我不得不推测不可能编写一个在 SharePoint Online O365 中进行身份验证的多租户应用程序......而且这似乎并不正确.有人请帮忙!

【问题讨论】:

啊,终于。经过数小时的研究、实验和开发 - 我偶然发现了一个解决方案!这是 Azure / O365 API 的一个未记录的“功能”,因此我想确保对此主题感兴趣的每个人都能发现关键:显然使用资源“api.office.com/discovery”对 Azure 端点进行身份验证会产生一个代码,该代码可以在 POST to /token 步骤中多次使用 - 允许您对“discovery/v1.0/me/services”进行 REST 调用并遍历结果,为每个具有相同 CODE 值的令牌获取令牌。不客气:) 回顾:[yazezo.com/2013/10/… 设置 SaaS 云?多租户、CRM 2011、Outlook2010) 【参考方案1】:

我想为我在上面的评论中简要提到的解决方案添加详细信息 - 这对于在 Office 365 中开发多租户应用程序的任何人都非常重要,尤其是当应用程序将访问包括 OneDrive 在内的 SharePoint 网站时。

从 OAuth 2.0 的角度来看,这里的过程有点不标准,但在多租户世界中是有意义的。关键是重用从 Azure 返回的第一个 CODE。跟我来:

首先我们遵循标准的 OAuth 身份验证步骤:

GET /common/oauth2/authorize?client_id=5cb5e93b-57f5-4e09-97c5-e0d20661c59a
&redirect_uri=https://myappdomain.com/v1/oauth2_redirect/
&response_type=code&prompt=login&state=D79E5777 HTTP/1.1
Host: login.windows.net
Cache-Control: no-cache

这将重定向到用户登录的 Azure 登录页面。如果成功,Azure 然后使用代码回调您的端点:

https://myappdomain.com/v1/oauth2_redirect/?code=AAABAAAA...ONE-CODE-To-RULE-THEM-ALLxyz

现在我们 POST 回/token 端点以获取实际的 Bearer 令牌,以便在后续 REST 调用中使用。同样,这只是经典的 OAuth2 ......但请注意我们如何使用 /Discovery 端点作为资源 - 而不是我们实际用于收集数据的任何端点。另外,我们要求UserProfile.Read 范围。

POST /common/oauth2/token HTTP/1.1
Host: login.windows.net
Accept: text/json
Cache-Control: no-cache

----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="grant_type"

authorization_code
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="code"

AAABAAAA...ONE-CODE-To-RULE-THEM-ALLxyz
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="client_id"

5cb5e93b-57f5-4e09-97c5-e0d20661c59a
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="client_secret"

02my little secretI=
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="redirect_uri"

https://myappdomain.com/v1/oauth2_redirect/
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="scope"

UserProfile.Read
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="resource"

https://api.office.com/discovery/
----WebKitFormBoundaryE19zNvXGzXaLvS5C

对此 POST 的响应将包含一个 access-token,可用于对 /discovery 端点进行 REST 调用。


    "refresh-token":  "AAABsvRw-mAAWHr8XOY2lVOKZNLJBARxkSAA", 
    "resource": "https://api.office.com/discovery/", 
    "pwd_exp": "3062796", 
    "pwd_url": "https://portal.microsoftonline.com/ChangePassword.aspx", 
    "expires_in": "3599", 
    "access-token": "ey_0_J0eXAiOiJjsp6PpUhSjpXlm0F00-j0aLiFg", 
    "scope": "Contacts.Read", 
    "token-type": "Bearer", 
    "not_before": "1422385173", 
    "expires_on": "1422389073"

现在,使用此access-token,查询/Services 端点以了解Office 365 中针对该用户的其他可用内容。

GET /discovery/v1.0/me/services HTTP/1.1
Host: api.office.com
Cache-Control: no-cache

----WebKitFormBoundaryE19zNvXGzXaLvS5D
Content-Disposition: form-data; name="Authorization"

Bearer ey_0_J0eXAiOiJjsp6PpUhSjpXlm0F00-j0aLiFg
----WebKitFormBoundaryE19zNvXGzXaLvS5D

结果将包含一个服务结构数组,描述各种端点和每个端点的功能。


    "@odata.context": "https://api.office.com/discovery/v1.0/me/$metadata#allServices",
    "value": [
        
            "capability": "MyFiles",
            "entityKey": "MyFiles@O365_SHAREPOINT",
            "providerId": "72f988bf-86f1-41af-91ab-2d7cd011db47",
            "serviceEndpointUri": "https://contoso-my.sharepoint.com/_api/v1.0/me",
            "serviceId": "O365_SHAREPOINT",
            "serviceName": "Office 365 SharePoint",
            "serviceResourceId": "https://contoso-my.sharepoint.com/"
        ,
        
            "capability": "RootSite",
            "entityKey": "RootSite@O365_SHAREPOINT",
            "providerId": "72f988bf-86f1-41af-91ab-2d7cd011db47",
            "serviceEndpointUri": "https://contoso.sharepoint.com/_api",
            "serviceId": "O365_SHAREPOINT",
            "serviceName": "Office 365 SharePoint",
            "serviceResourceId": "https://contoso.sharepoint.com/"
        ,
        
            "capability": "Contacts",
            "entityKey": "Contacts@O365_EXCHANGE",
            "providerId": "72f988bf-86f1-41af-91ab-2d7cd011db47",
            "serviceEndpointUri": "https://outlook.office365.com/api/v1.0",
            "serviceId": "O365_EXCHANGE",
            "serviceName": "Office 365 Exchange",
            "serviceResourceId": "https://outlook.office365.com/"
        
    ]

现在到了棘手的部分......此时,我们知道了我们真正想要验证的端点——其中一些是特定于租户的。通常,您会认为我们需要使用这些端点中的每一个重新播放 OAuth2 舞蹈。但在这种情况下,我们可以稍微作弊 - 只需发布我们最初从 Azure 收到的相同代码 - 使用上面相同的 HTTP 请求,只使用 serviceResourceId 和 @ 更改 resourcescope 字段987654338@ 来自上面的服务结构。像这样:

POST /common/oauth2/token HTTP/1.1
Host: login.windows.net
Accept: text/json
Cache-Control: no-cache

----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="grant_type"

authorization_code
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="code"

AAABAAAA...ONE-CODE-To-RULE-THEM-ALLxyz
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="client_id"

5cb5e93b-57f5-4e09-97c5-e0d20661c59a
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="client_secret"

02my little secretI=
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="redirect_uri"

https://myappdomain.com/v1/oauth2_redirect/
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="scope"

MyFiles.Read
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="resource"

https://contoso-my.sharepoint.com/
----WebKitFormBoundaryE19zNvXGzXaLvS5C

然后对其他两个做同样的事情:

...
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="scope"

RootSite.Read
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="resource"

https://contoso.sharepoint.com/
----WebKitFormBoundaryE19zNvXGzXaLvS5C

...
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="scope"

Contacts.Read
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="resource"

https://outlook.office365.com/
----WebKitFormBoundaryE19zNvXGzXaLvS5C

所有这三个调用都会产生类似于上面第一个 POST 的响应,为您提供每个相应端点的刷新令牌和访问令牌。所有这一切都只需要一个用户身份验证的代价。 :)

维奥拉!谜团解开了——您可以为 O365 编写多租户应用程序。 :)

【讨论】:

天才。谢谢!这是我处理过的最可怕的身份验证过程。很高兴我找到了这个。 “这是我处理过的最可怕的身份验证过程” - 听到! +1 干得好! :) - 但要小心,此代码的工作时间可能会有时间限制。 Nils 是对的 - 这是增量:docs.microsoft.com/en-us/azure/active-directory/develop/…

以上是关于为 SharePoint Online O365 构建多租户应用程序的主要内容,如果未能解决你的问题,请参考以下文章

O365(世纪互联)SharePoint 之使用Designer报错

O365(世纪互联)SharePoint 之站点个性化

O365(世纪互联)SharePoint 之使用列表库发布新闻

SharePoint Online office 365缺少将站点另存为模板选项

SharePoint Online 为Modern Page添加脚本

SharePoint Online 为Modern Page添加脚本