为 SharePoint Online O365 构建多租户应用程序 [英] Building a multi-tenant app for SharePoint Online O365

查看:25
本文介绍了为 SharePoint Online O365 构建多租户应用程序的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am attempting to build a multi-tenant application for Office 365 which focuses on SharePoint Online and authenticates through Azure using OAuth2. The problem is specific to SharePoint access via the Azure login, but is only found when using this API to authenticate using OAuth2.

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

Many of the mechanics of registering the application properly and setting up users in Azure and Office, while somewhat complex, are conquerable with the right about of time investment.

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

Even the basic OAuth2 protocol usage with Azure works relatively smoothly. However, I'm thwarted in making my application truly multi-tenant by virtue of SharePoint's 'resource' parameter. This apparently requires my application to know the end-user's root SharePoint site URL before they complete the login sequence. I can't see how that is possible. Someone please point me in the right direction.

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

Here is a sample of the actual login sequence:

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

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

When the user authenticates, this results in a call to the redirect provided that looks like this:

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

到目前为止很棒!三足身份验证的下一步是向/token 端点发送 POST 请求,以获取要在所有后续 REST 调用中使用的实际 Bearer 令牌.这只是经典的 OAuth2...

Great so far! The next step in the 3-legged authentication is a POST back to the /token endpoint to acquire the actual Bearer token to be used in all subsequent REST calls. This is just classic 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"

02{my little secret}I=
----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 对每个租户都有不同的端点.您实际上还没有登录用户,但是您已经需要有关您还没有的用户的信息..

and here's where it gets sticky. The 'resource' parameter is required, and must point to the user-specific endpoint that you want access to. For Exchange or Azure, the endpoint is always the same. (https://graph.windows.net or https://outlook.office365.com) But SharePoint has a different endpoint for every tenancy. You haven't actually logged the user in yet, but already you need information about the user that you don't yet have..

如果我部署的应用程序版本假定租户名称为contoso"(如上所述),则只有 contoso 租户中的用户才能成功使用我的应用程序读取 SharePoint 数据.一旦 fabrikam 中的另一个用户尝试使用它,我到 /token 端点的 POST 将请求对错误站点的许可......这就是问题所在.

If I deploy a version of my application that assumes 'contoso' as the tenant name (as above) then only users in the contoso tenancy will succeed using my app to read SharePoint data. As soon as another user in fabrikam tries to use it, my POST to the /token endpoint will ask for permission to the wrong site... and there's the rub.

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

How can I detect the correct endpoint to POST to the /token endpoint prior to the user actually logging in? Is there some hidden information that is given to me that I can use? Is there some kind of discovery possible to detect the tenant's root SharePoint URL? Or better yet, is there an endpoint that I can pass as the resource that can be representative of the tenant's home (something like https://office.microsoft.com/sharepoint)? Then it could be potentially gleaned from the user_id JWT token returned. This would be similar to the other services, and quite simple for a client to manage. I don't see this, however.

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

Without a definitive answer to these questions, or workaround to these issues, I have to surmise that it is not possible to write a multi-tenant application that Authenticates into SharePoint Online O365... and that just doesn't seem right. Somebody please help!

推荐答案

我想在上面的评论中简要提到的解决方案添加详细信息 - 这对于在 Office 365 中开发多租户应用程序的任何人来说都很重要,特别是如果应用程序将永远访问 SharePoint 网站,包括 OneDrive.

I want to add detail to the solution mentioned briefly in my comment above - This will be important to anyone developing multi-tenant applications in Office 365, especially if the application will ever access SharePoint sites including OneDrive.

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

The procedure here is a little non-standard from the OAuth 2.0 perspective, but makes some sense in the multi-tenant world. The key is re-using the first CODE returned from Azure. Follow me here:

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

First we follow the standard OAuth authentication steps:

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 会使用代码回调您的端点:

This redirects to the Azure login page where the user logs in. If successful, Azure then calls back to your endpoint with a code:

https://myappdomain.com/v1/oauth2_redirect/?code=AAABAAAA...{ONE-CODE-To-RULE-THEM-ALL}xyz

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

Now we POST back to the /token endpoint to acquire the actual Bearer token to be used in subsequent REST calls. Again, this is just classic OAuth2... but watch how we use the /Discovery endpoint as the resource - instead of any of the endpoints we will actually be using to gather data. Also, we ask for UserProfile.Read scope.

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-ALL}xyz
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="client_id"

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

02{my little secret}I=
----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 调用.

The response to this POST will contain an access-token that can be used to make REST calls to the /discovery endpoint.

{
    "refresh-token":  "AAABsvRw-mAAWHr8XOY2lVOKZNLJ{BAR}xkSAA", 
    "resource": "https://api.office.com/discovery/", 
    "pwd_exp": "3062796", 
    "pwd_url": "https://portal.microsoftonline.com/ChangePassword.aspx", 
    "expires_in": "3599", 
    "access-token": "ey_0_J0eXAiOiJjsp6PpUhSjpXlm0{F00}-j0aLiFg", 
    "scope": "Contacts.Read", 
    "token-type": "Bearer", 
    "not_before": "1422385173", 
    "expires_on": "1422389073"
}

现在,使用此 access-token,查询 /Services 端点以了解 Office 365 中该用户还有哪些可用的内容.

Now, using this access-token, query the /Services endpoint to find out what else is available in Office 365 for this user.

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_J0eXAiOiJjsp6PpUhSjpXlm0{F00}-j0aLiFg
----WebKitFormBoundaryE19zNvXGzXaLvS5D

结果将包括一组服务结构,描述各个端点和每个端点的能力.

The result will include an array of Service structures, describing the various endpoints and capabilities of each endpoint.

{
    "@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 舞蹈.但在这种情况下,我们可以作弊 - 只需 POST 我们最初从 Azure 收到的相同 CODE - 使用上述相同的 HTTP 请求,仅更改 resourcescope 字段使用上述服务结构中的 serviceResourceIdcapability .像这样:

Now comes the tricky part... At this point, we know the endpoints that we really want to authenticate to - some of which are tenant-specific. Normally you'd think we need to play the OAuth2 dance all over again with each of these endpoints. But in this case, we can cheat a little - and simply POST the same CODE that we originally received from Azure - using the same HTTP request above, only altering the resource and the scope fields using the serviceResourceId and capability from the Service structure above. Like this:

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-ALL}xyz
----WebKitFormBoundaryE19zNvXGzXaLvS5C
Content-Disposition: form-data; name="client_id"

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

02{my little secret}I=
----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

然后对另外两个做同样的事情:

then do the same for the other two:

...
----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 的响应,为您提供每个端点的刷新令牌和访问令牌.所有这一切仅以单用户身份验证为代价.:)

All three of these calls will result in a response like the first POST above, providing you with a refresh-token and an access-token for each of the respective endpoints. All of this for the price of only a single user authentication. :)

中提琴!谜团解开 - 您可以为 O365 编写多租户应用程序.:)

Viola! Mystery solved - you CAN write multi-tenant applications for O365. :)

这篇关于为 SharePoint Online O365 构建多租户应用程序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆