PowerShell Register Provider-Hosted Add-in/App

My current client uses provider-hosted add-ins with SharePoint 2013 on-premises. We have a centralized server infrastructure – provider-host – for the add-ins, where we deploy the add-in/app logic and then deploy the APP files to different SharePoint 2013 environments.

Why? The add-ins we’ve developed use CSOM to effect changes in the environment they’re deployed (SharePoint). We have one team developing the provider-hosted add-ins, and another team testing the add-ins within their development environments. This post is not about lifecycle deployment of SharePoint Provider-Hosted Add-ins – besides, we have integration, staging, test, and production hosts for this purpose – but about a nifty PowerShell script to reuse APP files across environments.

So, the scenario goes like this…

We have an integration farm with the provider-hosted add-ins deployed (and working). Developers download these add-ins from the integration SharePoint farm app catalog and save the APP files locally. They then upload these APP files into the app catalog of their local development SharePoint farm. Each development farm has a registered Security Token Issuer, using the same issuer ID as the integration farm. The development farms also have a trusted root certificate for the High-Trust between the provider-host and SharePoint, also the same as integration. The remaining step is to ensure that each add-in deployed to the development farm has the same client/app ID as that registered in the integration farm.

The typical process to register a shared add-in would be to crack open the APP file (just a zip file), look in the manifest.xml file and pull the client ID, and then call https://site/_layouts/15/appregnew.aspx. However, I wanted a script that avoided all that nonsense, and here it is:


if ((Get-PSSnapin -Name "Microsoft.SharePoint.PowerShell" -ErrorAction SilentlyContinue) -eq $null) {
    Add-PSSnapin "Microsoft.SharePoint.PowerShell";

$zipStream = $null;
$streamReader = $null;
try {
    Write-Verbose "Looking for AppManifest in APP Zip";
    [System.Reflection.Assembly]::LoadWithPartialName('System.IO.Compression') | Out-Null;
    if (![System.IO.File]::Exists($appPath)) { throw "$appPath does not exist"; }
    $zipBytes = [System.IO.File]::ReadAllBytes($appPath);
    $zipStream = New-Object System.IO.Memorystream;
    $zipStream.Write($zipBytes, 0, $zipBytes.Length);
    $zipArchive = New-Object System.IO.Compression.ZipArchive($zipStream);
    $zipEntry = $zipArchive.GetEntry("AppManifest.xml");
    $streamReader = New-Object System.IO.StreamReader($zipEntry.Open());
    $manifest = New-Object System.Xml.XmlDocument;
    Write-Verbose "Looking for ClientID";
    $ns = New-Object System.Xml.XmlNamespaceManager($manifest.NameTable);
    $ns.AddNamespace("x", "http://schemas.microsoft.com/sharepoint/2012/app/manifest");
    $node = $manifest.SelectSingleNode("/x:App/x:AppPrincipal/x:RemoteWebApplication", $ns);
    $clientId = $node.Attributes["ClientId"].Value;
    $node = $manifest.SelectSingleNode("/x:App/x:Properties/x:Title", $ns);
    $appTitle = $node.InnerText;
    Write-Verbose "Found app with title $appTitle and clientID $clientId";
    Write-Verbose "Registering App ClientId with SharePoint";
    $web = Get-SPWeb $webUrl;
    $realm = Get-SPAuthenticationRealm -ServiceContext $web.Site;
    $fullAppId = $clientId + '@' + $realm;
    Register-SPAppPrincipal -DisplayName $appTitle -NameIdentifier $fullAppId -Site $web;

} catch {
    Write-Host -ForegroundColor Red $_.Exception;
} finally {
    if ($streamReader -ne $null) { $streamReader.Close(); }
    if ($zipStream -ne $null) { $zipStream.Close(); }

The script takes a full path to the APP file and a web URL to register the add-in. As you can see from the code, the script replicates the manual steps of unzipping the APP (in memory), pulls out the client ID and calls Register-SPAppPrincipal to register the add-in.