Building a multi-tenant app for SharePoint Online O365

后端 未结 1 1124
我在风中等你
我在风中等你 2021-02-04 15:44

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

1条回答
  •  太阳男子
    2021-02-04 16:08

    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. :)

    0 讨论(0)
提交回复
热议问题