Build a multi-tenant app for SharePoint Online O365

I am trying to create a multi-tenant application for Office 365 that focuses on SharePoint Online and authenticates through Azure using OAuth2. The issue is related to SharePoint access through the Azure logical interface, but is only detected when using this API for authentication using OAuth2.

Many of the mechanics of properly registering an application and setting up users in Azure and Office, although somewhat complex, are gaining the right investment time.

Even the basic use of the OAuth2 protocol with Azure works relatively smoothly. However, it prevents me from making my application truly multi-tenant due to the SharePoint resource setting. This apparently requires my application to find out the URL of the root node of the end user site before they complete the login sequence. I do not see how this is possible. Someone, please point me in the right direction.

Here is an example 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 a user authenticates, this leads to a redirect, provided that it looks like this:

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

Fine! The next step in three-legged authentication is POST back to the endpoint / token to get the actual token token that will 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 

and here where it gets sticky. The resource parameter is required and should point to the endpoint of the user you want to access. For Exchange or Azure, the endpoint is always the same. ( https://graph.windows.net or https://outlook.office365.com ). However, SharePoint has different endpoints for each lease. You have not registered a user yet, but you already need information about a user that you do not already have.

If I distribute a version of my application that suggests "contoso" as the tenant name (as mentioned above), only users in contoso tenancy can use my application 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 will be tr.

How can I determine the correct endpoint to POST to the endpoint /token before the user actually logs in? Is there any hidden information that is provided to me that I can use? Is it possible to detect any discovery for the SharePoint root URL? Or even better, is there an endpoint that I can convey as a resource that can be representative of the tenantโ€™s home (something like https://office.microsoft.com/sharepoint )? Then this could be gleaned from the returned user_id JWT token. This would be similar to other services and quite simple for the client. However, I do not see this.

Without a definitive answer to these questions or a workaround to these questions, I must assume that it is not possible to write a multi-tenant application that authenticates with SharePoint Online O365 ... and that just doesn't seem right. Someone please help!

+7
sharepoint azure multi-tenant office365
source share
1 answer

I want to add the detail to the solution mentioned briefly in my comment above. This will be important for those who develop applications with multiple tenants in Office 365, especially if the application will ever access SharePoint sites, including OneDrive.

The procedure here is a little non-standard from the point of view of OAuth 2.0, but it makes sense in a world with several tenants. The key reuses the first CODE returned from Azure. Follow me here:

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 

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

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

Now we go back to the /token endpoint to get the actual Token token that will be used in subsequent REST calls. Again, this is just classic OAuth2 ... but look at how we use the /Discovery endpoint as a resource - instead of any of the endpoints that we will actually use to collect data. In addition, we request the 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 

The response to this POST will contain an access-token that can be used to call REST 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" } 

Now, using this access-token , request 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 contain 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/" } ] } 

Now the hard part is coming ... At the moment, we know the endpoints that we really want to authenticate - some of them are specific to tenants. Usually you think we need to play OAuth2 dance again from each of these endpoints. But in this case, we can fool a little - and just POST the same CODE that we originally received from Azure - using the same HTTP request above, only changing the resource and scope fields, using 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 

and

 ... ----WebKitFormBoundaryE19zNvXGzXaLvS5C Content-Disposition: form-data; name="scope" Contacts.Read ----WebKitFormBoundaryE19zNvXGzXaLvS5C Content-Disposition: form-data; name="resource" https://outlook.office365.com/ ----WebKitFormBoundaryE19zNvXGzXaLvS5C 

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

Viola! The mystery is solved - you CAN write multi-user applications for O365. :)

+5
source share

All Articles