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

时间:2014-12-04 20:37:44

标签: sharepoint azure oauth-2.0 office365 multi-tenant

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

正确注册应用程序并在Azure和Office中设置用户的许多机制虽然有些复杂,但却能够充分利用时间投资。

即使Azure的基本OAuth2协议使用也相对顺利。但是,凭借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身份验证的下一步是POST返回/ token端点,以获取要在所有后续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"

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对每个租赁都有不同的端点。您尚未实际登录用户,但您已经需要有关您尚未拥有的用户的信息..

如果我部署的应用程序版本假设'contoso'作为租户名称(如上所述),则只有contoso租户中的用户才能成功使用我的应用程序读取SharePoint数据。一旦fabrikam中的另一个用户尝试使用它,我POST/token端点的POST将要求获得错误网站的许可......并且存在问题。

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

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

1 个答案:

答案 0 :(得分:5)

我想在上面的评论中简要提到解决方案的细节 - 这对于在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-ALL}xyz

现在我们回到/token端点以获取要在后续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-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调用。

{
    "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中此用户可用的其他内容。

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

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

{
    "@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舞蹈。但在这种情况下,我们可以稍微作弊 - 并简单地使用上面相同的HTTP请求POST我们最初从Azure收到的相同CODE,只使用{更改resourcescope字段来自上述服务结构的{1}}和serviceResourceId。像这样:

capability

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

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

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

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

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

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

中提琴!神秘解决了 - 您可以为O365编写多租户应用程序。 :)