Hi devs, While the SharePoint Framework (aka SPFx) is getting more and more features, it becomes very handy to build engaging UI in the SharePoint context, at the Micro-services Era, it would be nice to let our solutions calling our various sets of APIs from various origins. We already have a large set of capabilities with the Microsoft Graph but we might also need to call API's outside of the Office 365 ecosystem (Line-of-Business or third parties...). Obviously, if the API processes sensitive data, it has to have at least a basic authorization mechasnism. In this post, I will show how we can achieve this using Azure Functions. If your Azure functions are contained in your tenant underlying Azure AD, there are some tricks that can be used to authenticate the current Office 365 user. (I blogged in the past about it here). If, instead, they are in an external Azure AD, or even if it is an API hosted on a totally different platform, we can use access keys in the tenant. The technique explained below will help you do that.
Our protected API
As I said above, we will use an Azure Function to simulate a protected API, but it can be any kind of API on any kind of platform. It will just be really easy to provision an new Azure Function with just a few clicks. Once logged in the Azure Portal, if you don't already have one, create a new Azure Functions app.
There is a requirement the API should meet to be called from your SPFx solution, it must allow Cross Origin Resource Sharing (CORS), If you use a public API, it will most likely satisfy this condition. In the case of our Azure Function, we have to tweak a dedicated setting. Switch to the Platform Features tab and select the CORS item.
In the Allowed Origins, remove the default values and add "*", you can also choose to allow only certain domains.
Now that we have set the needed configuration for our Function App, we can add our function. Add a new HttpTrigger in your preferred language, in this case I chose JavaScript, (IMHO, it is easier to work with SPFx, because we work directly with JSON, but remember, this API could be anything, and more than likely will not be dedicated for your SPFx solution).
I leave the sample code as it is (except for the body that I make a JSON object with a message property and append "from protected API" in the default message).
Click the Get function URL link in the right corner
Choose the default for Key (it will be the "password" to access our API, then copy/paste the URL, we will need it in our code. And that's it for our API !
Our SPFx extension
I opted here to create an simplistic SPFx Application Customizer extension that will show the message from the API in an Alert Dialog. Let's create our extension.
- Go for the now famous yo @microsoft/sharepoint.
- Answer the questions to choose a SharePoint Online target environment, extension and ApplicationCustomizer. In your Application Customizer code file, let's first create a constant outside of our class that will hold the API URL:
export const API_URL = 'https://my-func.azurewebsites.net/api/myapi';
Notice that I removed the ?code= ... but let's keep its value, we will need it later. Let's change the properties interface of our customizer to be
export interface IAzureFunctionMessageApplicationCustomizerProperties {
apiKeyPropertyKey: string;
}
In the onInit() method of our ApplicationCustomizer class, we write the following code
// Get the API key from tenant properties
this.context.spHttpClient
.get(`_api/web/GetStorageEntity('${this.properties.apiKeyPropertyKey}')`, SPHttpClient.configurations.v1)
.then((tenantPropResponse: Response) => tenantPropResponse.json())
.then((prop) => {
let userDisplayName = this.context.pageContext.user.displayName;
return this.context.httpClient.get(`${API_URL}?&name=${userDisplayName}&code=${prop.Value}`,
HttpClient.configurations.v1);
})
.then((apiResponse: Response) => apiResponse.json())
.then((apiPayload) => {
if (apiPayload.statusCode && apiPayload.statusCode != 200) {
Dialog.alert(`ERROR [${apiPayload.statusCode}] : ${apiPayload.error}`);
} else {
Dialog.alert(apiPayload.message);
}
})
.catch(error => {
Dialog.alert("ERROR: UNAUTHORIZED ACCESS");
console.log(error);
});
return Promise.resolve();
in the terminal, type the command
gulp serve --nobrowser
then go to your SharePoint Online site. in the URL append the following string:
?loadSPFX=true&debugManifestsFile=https://localhost:4321/temp/manifests.js&customActions={"b5d0b3ee-f1dc-4c96-b1ce-d81c8b21a139":{"location":"ClientSideExtension.ApplicationCustomizer","properties":{"apiKeyPropertyKey":"SPFxTestApiKey"}}}
Change the Id in bold by the Id you will find in your xxxx.manifest.json
If we try it now, we see
Indeed, you might have noticed in the code the first HTTP call we issue to the _api/web/GetStorageEntity endpoint.
We actually use here a recent feature in Office 365: Tenant-Scope properties. It's a location where administrators can store information that can be accessed by regular (properly logged in) users through JavaScript.
Store the API Access Key in SharePoint
We will use this location to store the access key to our API. To do so, we need to install the SharePoint Online Management Shell. Once installed, open a PowerShell console and type the following commands.
Connect-SPOService https://yourtenant-admin.sharepoint.com Set-SPOStorageEntity -Site https://yourtenant.sharepoint.com/sites/appcatalog -Key SPFxTestApiKey -Value "The API code" -Description "The key to the API" -Comments "..."
- The value of the URL argument should be the URL of your App Catalog.
- The value of the Value argument is the code we removed from the API URL earlier.
Now that we have the access key or the API stored as a tenant property, we can retry our extension.
Conclusion
Here we use the tenant properties to store the Access Key of our API. It is only one of the possibilities that Tenant properties offer. We can imagine using them to store License keys, configuration objects or any kind of information that must be available across the tenant. Moreover, here, we do store the API access key in clear directly in the tenant property bag. We might also imagine a solution in which we'd store a kind of pre-shared key or a "secret" that will never go on the wire in clear but instead will be used to compute a unique hash that will be recomputed by the API to check the incoming request is not a malicious one and is issued by an expected endpoint. From a security perspective, we can implement all kind of scenarios to better protect our APIs. Hope you will find this interesting and it will help you in your SPFx journey !
Please give your feedback !
Yannick