- Classic pages you said?
- Yes! You read it right. MS Graph API from classic SharePoint page. However please read it first:
That’s not an official or recommended way. That’s just a proof of concept, which uses some tenant features introduced with SPFx 1.6. That’s something I decided to try out when SPFx 1.6 was out. Use it on your own risk.
When to use it? On classic pages if you don’t have an option to execute SPFx code.
So what if you want to call some MS Graph APIs from your classic SharePoint page? No problem then.
Before doing actual coding, we should check that we meet all prerequisites:
- You have SPFx 1.6 features, which work without issues in your tenant. You can test it by creating a simple SPFx web part, which uses MS Graph. Upload it to the app catalog, approve the request to MS Graph and see it actually returns MS Graph data
If above works, you have everything needed for our experiments.
Add permissions to your SharePoint CE Web App
If you have at least one API Permission request in your SharePoint tenant admin from a webpart, which was built with SPFx 1.6, you should have a new App Registration in your tenant called SharePoint Client Extensibility Web Application (SharePoint CE Web App). All permissions granted to Web APIs through the admin portal or PowerShell go to this app registration (service principal actually).
In my sample I’m going to read all the groups from my tenant, so I should provide Group.Read.All permission to SharePoint CE Web App. How to do that? Well, you have two options:
- Create a helper SPFx web part, which requests desired permissions through the webApiPermissionRequests node. Upload the web part and approve the request.
- OR add it programmatically with help of .NET Microsoft.Azure.ActiveDirectory.GraphClient library (don’t confuse with MS Graph, that’s also graph, but for Azure AD)
I’m not looking for easy ways and I use the latter approach. The source code, which adds required permissions you can find here at GitHub. It was created as part of the blog post Diving into AadHttpClient (with hacking!). Read it if you are curious about how everything works internally.
Here is the code which adds required MS Graph permissions:
public static async Task AddSpfxMSGraphPermissions(ActiveDirectoryClient client, string scope)
{
var spoCeServicePrinicipal = await client.ServicePrincipals
.Where(sp => sp.AppId == GlobalConstants.SpoCeApplicationId).ExecuteSingleAsync();
var msGraphServicePrincipal = await client.ServicePrincipals
.Where(sp => sp.AppId == GlobalConstants.MsGrpaphApplicationId).ExecuteSingleAsync();
var grants = await client.Oauth2PermissionGrants.ExecuteAsync();
OAuth2PermissionGrant existingGrant = null;
foreach (IOAuth2PermissionGrant grant in grants.CurrentPage)
{
if (grant.ClientId == spoCeServicePrinicipal.ObjectId &&
grant.ResourceId == msGraphServicePrincipal.ObjectId)
{
existingGrant = (OAuth2PermissionGrant) grant;
}
}
if (existingGrant != null)
{
existingGrant.Scope = scope;
await existingGrant.UpdateAsync();
}
else
{
var auth2PermissionGrant = new OAuth2PermissionGrant
{
ClientId = spoCeServicePrinicipal.ObjectId,
ConsentType = "AllPrincipals",
PrincipalId = null,
ExpiryTime = DateTime.Now.AddYears(10),
ResourceId = msGraphServicePrincipal.ObjectId,
Scope = scope
};
await client.Oauth2PermissionGrants.AddOAuth2PermissionGrantAsync(auth2PermissionGrant);
}
}
This is how you should call it:
AddSpfxMSGraphPermissions(client, "Group.Read.All").Wait();
where the client is ActiveDirectoryClient. You also should add “User.Read” permissions to Windows Azure Active Directory, because by default that permissions are missing, at the same time they are required. The only difference in code, that it uses different Id for directory object (in GitHub sources that’s method called “AddWindowsAdPermissions”). GlobalConstants.SpoCeApplicationId is your SharePoint CE Web App Id. You can find it from Azure portal, Azure AD, under app registration section.
We have prepared everything, let’s start coding.
Implement adal in your classic page
NOTE: the code I use in graph-adal.js uses some features (fetch API and Promises), which are not supported in IE\Edge. If you want to test it in these browsers, use polyfills.
For this sample I’ve added Script Editor web part to my classic page. Added two script references as well:
<script src="https://tenant.sharepoint.com/sites/dev/Shared%20Documents/adal.js"></script>
<script src="https://tenant.sharepoint.com/sites/dev/Shared%20Documents/graph-adal.js"></script>
adal.js was extracted from latest adal-angular npm module (1.17 as of now). graph-adal.js is our main file, where all magic will take place.
How adal.js works: you should call login() API first, then you can use acquireToken API. The issue is, that Login API uses popups or full page redirect. That’s something unwanted, because we have our user logged in SharePoint. Ideally we should leverage SSO without redirects. That can be done by using “internal” adal.js api, to be precise “_renewToken” method. This method creates a hidden iframe and extracts access token from hash data.
Let’s first setup our AuthenticationContext object:
let clientId = _spPageContextInfo.spfx3rdPartyServicePrincipalId;
let authContext = new AuthenticationContext({
clientId: clientId,
tenant: _spPageContextInfo.aadTenantId,
redirectUri: window.location.origin + '/_forms/spfxsinglesignon.aspx'
});
A few important notes here. For tokens renewals and generation SPFx 1.6 uses SharePoint CE Web App principal. That’s why you should use _spPageContextInfo.spfx3rdPartyServicePrincipalId (yes, it can be easily obtained from page context) as your clientId param. Check out redirectUrl as well. I use the same url used in SPFx 1.6. That url is hosted on the same domain, that’s why you can leverage SSO. What is also important, this page (spfxsinglesignon.aspx) has it’s own copy of adal.js and calls method handleWindowCallback from adal.js. This method saves access token and calls our callback function.
Now examine silentLogin method:
function silentLogin() {
return new Promise(function (resolve, reject) {
authContext._renewToken(clientId, function (message, token) {
if (!token) {
let err = new Error(message);
console.log(err);
reject(err);
}
else {
console.log(token);
let user = authContext.getCachedUser();
resolve(user);
}
}, authContext.RESPONSE_TYPE.ID_TOKEN_TOKEN);
});
}
Instead of full page redirect, we execute _renewToken, which creates a hidden iframe to spfxsinglesignon.aspx. That page saves id token and access token and finally calls our callback function. This is how we request id token and resolve our logged in user finally.
Now as we have logged in user, we can easily get an access token for any third party API:
function getToken(resource) {
return new Promise(function (resolve, reject) {
authContext.acquireToken(resource, function (message, token) {
if (!token) {
let err = new Error(message);
console.log(err);
reject(err);
}
else {
console.log(token);
resolve(token);
}
});
});
}
In our case that’s MS Graph API. The final call to MS Graph is below:
silentLogin()
.then(function (user) {
console.log(user);
return getToken('https://graph.microsoft.com');
})
.then(function (graphAccessToken) {
return fetch('https://graph.microsoft.com/v1.0/groups', {
headers: {
'Authorization': 'Bearer ' + graphAccessToken
}
})
})
.then(function (data) {
return data.json();
})
.then(function (result) {
console.log(result);
})
.catch(console.log);
The full code is available at GitHub here.
If you are sure, that you have some approved MS Graph APIs in your tenant, you can simply test the code on a classic page by copying adal.js and adal-graph.js to a library. Then create a script editor, reference those files and change lines, which fetch graph groups to your approved MS Graph API. Alternatively you can just test silentLogin function by commenting out everything related to MS Graph. You don’t have to change parameters for AuthenticationContext, because they are extracted from page context.
Check it out how it works live:
Conclusion
What do you think about this “feature”? I am not sure if it’s applicable to real solutions. However now you should even more care about things you install in your farm, especially from third party vendors. With described approach any third party code can easily test, which APIs are available and finally use those API. What will they do with that information? Who knows…