Monday, 14 May 2018

SPFx: Calling back to SharePoint from an AAD secured Azure Function on behalf of a user

This post is part of a series where we explore consuming Azure AD secured Azure Functions from SharePoint Framework components. Articles in the series:

1) SharePoint Framework: Calling AAD secured Azure Function on behalf of a user
2) SharePoint Framework: Calling Microsoft Graph API from an AAD secured Azure Function on behalf of a user
3) Calling back to SharePoint from an AAD secured Azure Function on behalf of a user (this post)

In the previous post, we were successfully able to call the Microsoft Graph API from an AAD secured Azure Function and return data back to the SharePoint Framework web part.

Now in this post, lets see how we can make a call back to SharePoint on behalf of the logged in user from the Azure Function. We will only focus on the code in the Azure Function here, to fully understand the set up and auth process, I recommend you check out the previous posts in the series.


Updates to the Azure AD app registration:


In order to make a call back to SharePoint, we will need to add a Client Secret to the Azure AD app registration. Skip this step if you have already done this as part of the previous post.



We will also need to add the Office 365 SharePoint Online permissions scope to the Azure AD app registration. For this post, I am selecting the "Read and write items in all Site Collections" delegated scope. You can select the scope according to the operations you want to perform in SharePoint


Don't forget to grant the permissions again as a subscription admin. This is so that each user does not have to do this individually:


Here is what we are going to do in the code:

When the Azure Function executes, we already have an access token sent by the SharePoint Framework AadHttpClient in the Authorization header. This access token has the "user_impersonation" scope which only allows it to access the Azure Function. It cannot be directly used to call back to SharePoint.

In order to obtain new access token that will work with SharePoint, we will have to request it using the existing access token.

using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.SharePoint.Client;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Security.Claims;
using System.Threading.Tasks;
namespace UserDetails
{
public static class CurrentInfoFromSharePoint
{
[FunctionName("CurrentInfoFromSharePoint")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)]HttpRequestMessage req, TraceWriter log)
{
string ClientId = "<client-id-of-custom-aad-app>";
string ClientSecret = "<client-secret-of-custom-aad-app>";
string spRootResourceUrl = "https://<your-tenant>.sharepoint.com";
string spSiteUrl = $"{spRootResourceUrl}/sites/Comms";
//Get the tenant id from the current claims
string tenantId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value;
string authority = $"https://login.microsoftonline.com/{tenantId}";
//Access token coming from SPFx AadHttpClient with "user_impersonation" Scope
var userImpersonationAccessToken = req.Headers.Authorization.Parameter;
//Exchange the SPFx access token with another Access token containing the delegated "AllSites.Manage" scope for the SharePoint resource
ClientCredential clientCred = new ClientCredential(ClientId, ClientSecret);
UserAssertion userAssertion = new UserAssertion(userImpersonationAccessToken);
//For production, use a Token Cache like Redis https://blogs.msdn.microsoft.com/mrochon/2016/09/19/using-redis-as-adal-token-cache/
var authContext = new AuthenticationContext(authority);
AuthenticationResult authResult = await authContext.AcquireTokenAsync(spRootResourceUrl, clientCred, userAssertion);
var spAccessToken = authResult.AccessToken;
//Get CSOM ClientContext using the SharePoint Access Token
var clientContext = GetAzureADAccessTokenAuthenticatedContext(spSiteUrl, spAccessToken);
//The usual CSOM stuff.
var web = clientContext.Web;
var currentUser = web.CurrentUser;
clientContext.Load(web);
clientContext.Load(currentUser);
clientContext.ExecuteQuery();
var result = new Dictionary<string, string>();
result.Add("Current Web in SharePoint", web.Title);
result.Add("Current User in SharePoint", currentUser.Title);
return req.CreateResponse(HttpStatusCode.OK, result, JsonMediaTypeFormatter.DefaultMediaType);
}
public static ClientContext GetAzureADAccessTokenAuthenticatedContext(string siteUrl, string accessToken)
{
var clientContext = new ClientContext(siteUrl);
clientContext.ExecutingWebRequest += (sender, args) =>
{
args.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken;
};
return clientContext;
}
}
}
Once this function is called from the SharePoint Framework, we are able to get the data back to the web part:


Thanks for reading!

SPFx: Calling Microsoft Graph API from an AAD secured Azure Function on behalf of a user

This post is part of a series where we explore consuming Azure AD secured Azure Functions from SharePoint Framework components. Articles in the series:

1) SharePoint Framework: Calling AAD secured Azure Function on behalf of a user
2) Calling Microsoft Graph API from an AAD secured Azure Function on behalf of a user (this post)
3) SharePoint Framework: Calling back to SharePoint from an AAD secured Azure Function on behalf of a user

In the previous post, we were successfully able to call an AAD secured Azure Function from a SharePoint Framework web part.

Now once we are in the Function, lets see how to make a call to the Microsoft Graph on behalf of the logged in user. We will only focus on the Azure Function here, to fully understand the set up and auth process, I recommend you check out the previous post.



Updates to the Azure AD app registration:


In order to call the Microsoft Graph, we will need to add a Client Secret to the Azure AD app registration.



We don't need to add any new permission scopes for the purposes of this post as we are going to make a call to the /v1.0/me endpoint which requires the User.Read permission. According to the SPFx docs, if we exchange the SPFx generated token for a MS Graph token, it will automatically have the User.Read.All permission scope.

If you want to do anything beyond this with the Microsoft Graph, you will need to add the relevant permissions scope and grant permissions to it. In the next post, we will see how to do this by granting permissions to the "Office 365 SharePoint Online" permissions scope.

Here is what we are going to do in this post:

When the Azure Function executes, we already have an access token sent by the SharePoint Framework AadHttpClient in the Authorization header. This access token has the "user_impersonation" scope which only allows it to access the Azure Function. It cannot be directly used to call the Microsoft Graph.

In order to obtain new access token that will work with the Microsoft Graph, we will have to request it using the existing access token. Once we have the new token, we are able to make a call to the Microsoft Graph:

using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading.Tasks;
namespace UserDetails
{
public static class CurrentUserFromGraph
{
private static HttpClient httpClient = new HttpClient();
[FunctionName("CurrentUserFromGraph")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)]HttpRequestMessage req, TraceWriter log)
{
string ClientId = "<client-id-of-custom-aad-app>";
string ClientSecret = "<client-secret-of-custom-aad-app>";
string msGraphResourceUrl = "https://graph.microsoft.com";
//Get the tenant id from the current claims
string tenantId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value;
string authority = $"https://login.microsoftonline.com/{tenantId}";
//Access token coming from SPFx AadHttpClient with "user_impersonation" Scope
var userImpersonationAccessToken = req.Headers.Authorization.Parameter;
//Exchange the SPFx access token with another Access token containing the delegated scope for Microsoft Graph
ClientCredential clientCred = new ClientCredential(ClientId, ClientSecret);
UserAssertion userAssertion = new UserAssertion(userImpersonationAccessToken);
//For production, use a Token Cache like Redis https://blogs.msdn.microsoft.com/mrochon/2016/09/19/using-redis-as-adal-token-cache/
var authContext = new AuthenticationContext(authority);
AuthenticationResult authResult = await authContext.AcquireTokenAsync(msGraphResourceUrl, clientCred, userAssertion);
var graphAccessToken = authResult.AccessToken;
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", graphAccessToken);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var request = new HttpRequestMessage(HttpMethod.Get, $"https://graph.microsoft.com/v1.0/me");
var response = await httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
var result = new Dictionary<string, string>();
result.Add("Current User through Microsoft Graph", content);
return req.CreateResponse(HttpStatusCode.OK, result, JsonMediaTypeFormatter.DefaultMediaType);
}
}
}
Once this function is called from our SharePoint Framework web part, we are able to get data back from the graph:


Thanks for reading!