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 Sh
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.
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:
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 calls back to your endpoint with a code:
https://myappdomain.com/v1/oauth2_redirect/?code=AAABAAAA...{ONE-CODE-To-RULE-THEM-ALL}xyz
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
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"
}
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/"
}
]
}
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
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 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. :)
Viola! Mystery solved - you CAN write multi-tenant applications for O365. :)