The purpose of this article is to provide step by step instructions for setting up a low-trust provider-hosted SharePoint add-in where the external application is hosted in IIS. The external application will be setup to run CRUD (Create/Read/Update/Delete) operations against SharePoint without the need to start the application from within SharePoint.
2.1.1.1Microsoft Online Services Sign-In Assistant
- Download Microsoft Online Services Sign-In Assistant PowerShell module from: https://www.microsoft.com/en-us/download/details.aspx?id=41950
- Install the module on your SharePoint 2013 environment
2.1.1.2Microsoft Azure Active Directory
- Download Microsoft Azure Active Directory PowerShell module from:
http://connect.microsoft.com/site1164/Downloads/DownloadDetails.aspx?DownloadID=59185
- Install the module on your SharePoint 2013 environment
- On your SharePoint 2013 server, open IIS Manager
- Click on your server name
- Double-click on Server Certificates
- Click on Create Self-Signed Certificate on the right hand side
- Enter a name for the certificate, then hit OK
- Right-click the certificate, then Export
- Select a location to save the certificate, and enter a password
- Click OK
- On your SharePoint 2013 server, open SharePoint Management Shell as an administrator
- Run the following PowerShell script
$certPrKPath = "c:\location_of_your_.pfx_file"
$certPassword = "your_password"
$stsCertificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $certPrKPath, $certPassword, 20
Set-SPSecurityTokenServiceConfig -ImportSigningCertificate $stsCertificate -confirm:$false
- On your SharePoint 2013 server, go to the following path: C:\windows\system32\windowspowershell\V1.0\modules
- Create a new folder named: Connect-SP-Farm-To-AAD
- Copy the following script into a text file and save it with the name Connect-SP-Farm-To-AAD.psm1
function Connect-SPFarmToAAD {
param(
[Parameter(Mandatory)][String] $AADDomain,
[Parameter(Mandatory)][String] $SharePointOnlineUrl,
[Parameter()][String] $SharePointWeb,
[Parameter()][System.Management.Automation.PSCredential] $O365Credentials,
[Parameter()][Switch] $RemoveExistingACS,
[Parameter()][Switch] $RemoveExistingSTS,
[Parameter()][Switch] $RemoveExistingSPOProxy,
[Parameter()][Switch] $RemoveExistingAADCredentials,
[Parameter()][Switch] $AllowOverHttp
)
if (-not $O365Credentials) {
$O365Credentials = Get-Credential -Message "Admin credentials for $AADDomain"
}
Add-PSSnapin Microsoft.SharePoint.PowerShell
Import-Module -Name MSOnline
Import-Module MSOnlineExtended -force -verbose
New-Variable -Option Constant -Name SP_APPPRINCIPALID -Value '00000003-0000-0ff1-ce00-000000000000' | Out-Null
New-Variable -Option Constant -Name ACS_APPPRINCIPALID -Value '00000001-0000-0000-c000-000000000000' | Out-Null
New-Variable -Option Constant -Name ACS_APPPROXY_NAME -Value ACS
New-Variable -Option Constant -Name SPO_MANAGEMENT_APPPROXY_NAME -Value 'SPO Add-in Management Proxy'
New-Variable -Option Constant -Name ACS_STS_NAME -Value ACS-STS
New-Variable -Option Constant -Name AAD_METADATAEP_FSTRING -Value 'https://accounts.accesscontrol.windows.net/{0}/metadata/json/1'
New-Variable -Option Constant -Name SP_METADATAEP_FSTRING -Value '{0}/_layouts/15/metadata/json/1'
if ([String]::IsNullOrEmpty($SharePointWeb)) {
$SharePointWeb = Get-SPSite | Select-Object -First 1 | Get-SPWeb | Select-Object -First 1 | % Url
}
$ACSMetadataEndpoint = $AAD_METADATAEP_FSTRING -f $AADDomain
$ACSMetadata = Invoke-RestMethod -Uri $ACSMetadataEndpoint
$AADRealmId = $ACSMetadata.realm
Set-SPAuthenticationRealm -ServiceContext $SharePointWeb -Realm $AADRealmId
$LocalSTS = Get-SPSecurityTokenServiceConfig
$LocalSTS.NameIdentifier = '{0}@{1}' -f $SP_APPPRINCIPALID,$AADRealmId
$LocalSTS.Update()
if ($AllowOverHttp.IsPresent -and $AllowOverHttp -eq $True) {
$serviceConfig = Get-SPSecurityTokenServiceConfig
$serviceConfig.AllowOAuthOverHttp = $true
$serviceConfig.AllowMetadataOverHttp = $true
$serviceConfig.Update()
}
if ($RemoveExistingACS.IsPresent -and $RemoveExistingACS -eq $True) {
Get-SPServiceApplicationProxy | ? DisplayName -EQ $ACS_APPPROXY_NAME | Remove-SPServiceApplicationProxy -RemoveData -Confirm:$false
}
if (-not (Get-SPServiceApplicationProxy | ? DisplayName -EQ $ACS_APPPROXY_NAME)) {
$AzureACSProxy = New-SPAzureAccessControlServiceApplicationProxy -Name $ACS_APPPROXY_NAME -MetadataServiceEndpointUri $ACSMetadataEndpoint -DefaultProxyGroup
}
if ($RemoveExistingSTS.IsPresent) {
Get-SPTrustedSecurityTokenIssuer | ? Name -EQ $ACS_STS_NAME | Remove-SPTrustedSecurityTokenIssuer -Confirm:$false
}
if (-not (Get-SPTrustedSecurityTokenIssuer | ? DisplayName -EQ $ACS_STS_NAME)) {
$AzureACSSTS = New-SPTrustedSecurityTokenIssuer -Name $ACS_STS_NAME -IsTrustBroker -MetadataEndPoint $ACSMetadataEndpoint
}
$ACSProxy = Get-SPServiceApplicationProxy | ? Name -EQ $ACS_APPPROXY_NAME
$ACSProxy.DiscoveryConfiguration.SecurityTokenServiceName = $ACS_APPPRINCIPALID
$ACSProxy.Update()
$SPMetadata = Invoke-RestMethod -Uri ($SP_METADATAEP_FSTRING -f $SharePointWeb)
$SPSigningKey = $SPMetadata.keys | ? usage -EQ "Signing" | % keyValue
$CertValue = $SPSigningKey.value
Connect-MsolService -Credential $O365Credentials
if ($RemoveExistingAADCredentials.IsPresent -and $RemoveExistingAADCredentials -eq $true) {
$msolserviceprincipal = Get-MsolServicePrincipal -AppPrincipalId $SP_APPPRINCIPALID
[Guid[]] $ExistingKeyIds = Get-MsolServicePrincipalCredential -ObjectId $msolserviceprincipal.ObjectId -ReturnKeyValues $false | % {if ($_.Type -ne "Other") {$_.KeyId}}
Remove-MsolServicePrincipalCredential -AppPrincipalId $SP_APPPRINCIPALID -KeyIds $ExistingKeyIds
}
New-MsolServicePrincipalCredential -AppPrincipalId $SP_APPPRINCIPALID -Type Asymmetric -Value $CertValue -Usage Verify
$indexHostName = $SharePointWeb.IndexOf('://') + 3
$HostName = $SharePointWeb.Substring($indexHostName)
$NewSPN = '{0}/{1}' -f $SP_APPPRINCIPALID, $HostName
$SPAppPrincipal = Get-MsolServicePrincipal -AppPrincipalId $SP_APPPRINCIPALID
if ($SPAppPrincipal.ServicePrincipalNames -notcontains $NewSPN) {
$SPAppPrincipal.ServicePrincipalNames.Add($NewSPN)
Set-MsolServicePrincipal -AppPrincipalId $SPAppPrincipal.AppPrincipalId -ServicePrincipalNames $SPAppPrincipal.ServicePrincipalNames
}
if ($RemoveExistingSPOProxy.IsPresent -and $RemoveExistingSPOProxy -eq $True) {
Get-SPServiceApplicationProxy | ? DisplayName -EQ $SPO_MANAGEMENT_APPPROXY_NAME | Remove-SPServiceApplicationProxy -RemoveData -Confirm:$false
}
if (-not (Get-SPServiceApplicationProxy | ? DisplayName -EQ $SPO_MANAGEMENT_APPPROXY_NAME)) {
$spoproxy = New-SPOnlineApplicationPrincipalManagementServiceApplicationProxy -Name $SPO_MANAGEMENT_APPPROXY_NAME -OnlineTenantUri $SharePointOnlineUrl -DefaultProxyGroup
}
}
- Copy Connect-SP-Farm-To-AAD.psm1 to C:\Windows\System32\WindowsPowerShell\v1.0\Modules\Connect-SP-Farm-To-AAD
- Open the SharePoint Management Shell as an administrator and run the following cmdlet to verify that the Connect-SP-Farm-To-AAD module is listed:
Get-Module –listavailable
- Run the following cmdlet to import the module:
import-module Connect-SP-Farm-To-AAD
- Run the following cmdlet to verify that the Connect-SPFarmToAAD function is listed as part of the module:
Get-Command -module Connect-SP-Farm-To-AAD
- Run the following cmdlet to verify that the Connect-SPFarmToAAD function is loaded.
ls function:\ | where {$_.Name -eq "Connect-SPFarmToAAD"}
- In the same SharePoint Management Shell windows that you have opened from last step (2.1.4), run the following script as a farm admin:
Note: you will be prompted for the tenant admin creds. So have them handy.
Connect-SPFarmToAAD -AADDomain 'your_company_name.onmicrosoft.com' -SharePointOnlineUrl https://your_company_name.sharepoint.com -SharePointWeb http://your_on-prem_web_application -AllowOverHttp
Example:
Connect-SPFarmToAAD -AADDomain 'MyO365Domain.onmicrosoft.com' -SharePointOnlineUrl https://MyO365Domain.sharepoint.com -SharePointWeb http://ecmspotsbx –AllowOverHttp
Notes:
- SharePointWeb: is the on-prem web application which hosts the site collection which we want the external application to access
- If your SharePointWeb is on https and its certificate is not a self-signed certificate, then you do not need: -AllowOverHttp
- On the server to host the external application, open IIS Manager
- Expand the server name
- Right click Sites, then click Add Website…
- Fill-in the fields as below, then hit OK:
- Site name: enter a name for your site
- Host name: enter the same site’s name
- Physical path: create a new folder under C:\inetpub\wwwroot, name the folder the same name you chose for the site
- Enable Directory Browsing
- Click on the IIS site that we just created
- Double click Directory Browsing
- Click Enable on the right hand side
- Open Notepad as administrator
- Navigate to: C:\Windows\System32\Drivers\etc
- Select All Files at the right-bottom corner
- Open the Hosts file
- Add an entry for the IIS site as follows:
127.0.0.1 your_site_name
- In IIS Manager, under Sites, click on your site
- On the right hand side, click on Browse your_site_name on *:80 (http)
- Make sure that you do not get any errors
- You should get a page similar to the one below:
- Go to the site collection which the external application will access
- Add the following at the end of the site collection’s URL, right after the site collection’s name: /_layouts/15/appregnew.aspx
Example: http://ecmspotsbx/sites/DevSC/_layouts/15/appregnew.aspx
- You will get the add-in registration page
- For the App Id and App Secret, click on Generate
- For the Title, enter a title for your add-in
- For the App Domain, enter the host name you entered when creating the IIS site in step: 2.2.1 -> 4 above.
- Leave the Redirect URL empty
- Click Create
- You will get an app identifier successfully created page
- Save the App Id, App Secret and App Domain
- Go to the site collection which the external application will access
- Add the following at the end of the site collection’s URL, right after the site collection’s name: /_layouts/15/appinv.aspx
Example: http://ecmspotsbx/sites/DevSC/_layouts/15/appinv.aspx
- Enter your App Id, then hit Lookup.
- You will get your app’s registration information:
- Add the following permissions to Permission Request XML
<AppPermissionRequests AllowAppOnlyPolicy="true">
<AppPermissionRequest Scope="http://sharepoint/content/sitecollection" Right="FullControl" />
</AppPermissionRequests>
This will grant the add-in full control access over the site collection, and will tell SharePoint that the add-in will use only the add-in’s permissions and that it will not use the permissions of the user running the add-in.
- Click Create
- SharePoint will ask you if you want to grant these permissions to the add-in
- Hit Trust It
- Go to the site collection which the external application will access
- Add the following at the end of the site collection’s URL, right after the site collection’s name: /_layouts/15/appprincipals.aspx
Example: http://ecmspotsbx/sites/DevSC/_layouts/15/appprincipals.aspx
- Find your add-in
- Save the ID after the “@” sign. This is your Realm.
- On the server where we created the IIS site in step 2.2 above, open Visual Studio 2015 or above as administrator.
- File -> New -> Project
- Select Office/SharePoint -> Apps -> App for SharePoint
Note: Depending on your version of Visual Studio, the path can be:
Office/SharePoint -> Office Add-ins -> SharePoint Add-in
- Enter a name for your project and hit OK
- Enter the URL of the site collection which the external application will access
- Select Provider-hosted
- Click Next
- Choose SharePoint 2013
- Click Next
- Choose ASP.Net Web Forms Application
- Click Next
- Choose Use Windows Azure Access Control Service (for SharePoint cloud apps)
- Click Finish
- In Solution Explorer, right-click the app project, then hit Remove
- Visual Studio will tell you that the app project will be removed
- Hit OK
- In Solution Explorer, right-click the web project, then hit properties
- Under Web -> Servers:
- Change IIS Express to local IIS
- In Project Url, replace localhost with the IIS site’s name we created in step 2.2 -> 2.2.1 above
- Click Create Virtual Directory
- You will get a message that the virtual directory has been created successfully
- Hit OK
- Hit Save
- Go to IIS
- Right-click Sites, then hit Refresh
- Expand your IIS site
- Make sure that you can see your virtual directory
2.4.5.1Update the web.config
- In Solution Explorer, under the web project, open web.config
- Update the ClientId and ClientSecret entries with the values we saved in step 2.3 -> 2.3.1 -> 10 above. Make sure that there are no spaces in the value.
- Add the following entries right after the ClientSecret entry:
<add key="HostedAppHostName" value=""/>
<add key="Realm" value=""/>
<add key="SPHostUrl" value=""/>
<add key="SPAppWebUrl" value=""/>
- For HostedAppHostName value: enter the App Domain we saved in step 2.3 -> 2.3.1 -> 10 above
Example: LowTrustAddIn
- For Realm value: enter the realm we saved in step 2.3 -> 2.3.3 -> 4 above
Example: fa2e957d-d348-4cf8-94cf-dae799dfbbda
- For SPHostUrl value: enter the URL of the site collection which the external application will access
Example: http://ecmspotsbx/sites/DevSC
- For SPAppWebUrl value: enter the Project URL of step 2.4.3 above followed by: /Pages/Default.aspx
Example: http://LowTrustAddIn/LowTrustAddInWeb/Pages/Default.aspx
- Your appSettings should look similar to this:
2.4.5.2Update Default.aspx
- In Solution Explorer, under the web project -> pages, open Default.aspx
- Remove everything between <body></body>
- Add the following between <body></body>:
<a name="_Hlk499710851"><</a>form id="form1" runat="server">
<div style="margin-bottom: 10px;">
<b>List:</b>
<asp:Label runat="server" ID="lblListUrl" />
</div>
<div style="margin-bottom: 10px;">
<b>User:</b>
<asp:Label runat="server" ID="lblUserName" />
</div>
<div style="margin-bottom: 10px;">
<b>ID:</b>
<asp:TextBox ID="txtItemId" runat="server"></asp:TextBox>
<b>Status:</b>
<asp:TextBox ID="txtItemStatus" runat="server"></asp:TextBox>
<asp:Button ID="btnUpdateItem" runat="server" Text="Update Status" OnClick="btnUpdateItem_Click" />
</div>
<div>
<span style="color: green"><asp:Label runat="server" ID="lblOperationSuccess" /></span>
<span style="color: red"><asp:Label runat="server" ID="lblOperationFailure" /></span>
</div>
</form>
- Your final Default.aspx should look similar to this:
2.4.5.3Update Default.aspx.cs
- In Solution Explorer, under the web project -> pages, open Default.aspx.cs
- Remove the Page_PreInit() method
- Add a reference to the following namespaces:
- System.Web.Configuration
- Microsoft.IdentityModel.SecurityTokenService
- Microsoft.SharePoint.Client
using System.Web.Configuration;
using Microsoft.IdentityModel.SecurityTokenService;
using Microsoft.SharePoint.Client;
- Add the following before the Page_Load() method:
private string _siteUrl = "http://ecmspotsbx/sites/DevSC/TS1";
private string _listName = "Test List";
private string _fieldName = "Status";
private List _list;
private ClientContext _clientContext;
- _siteUrl: The URL of the site hosting the list we want to update
The site must be within the site collection where we registered the add-in in step 2.3 above.
- _listName: The Name of the list we want to update
- _fieldName: The list field’s name we want to update
- Replace everything in the Page_Load() method with the following:
lblOperationSuccess.Text = "";
lblOperationFailure.Text = "";
lblListUrl.Text = _listName + " @ " + _siteUrl;
_clientContext = GetClientContextForUrl(_siteUrl);
var web = _clientContext.Web;
_clientContext.Load(web, w => w.CurrentUser);
_clientContext.ExecuteQuery();
lblUserName.Text = web.CurrentUser.Title;
_list = _clientContext.Web.Lists.GetByTitle(_listName);
- Add the btnUpdateItem_Click() method after the Page_Load() method:
protected void btnUpdateItem_Click(object sender, EventArgs e)
{
try
{
var itemId = txtItemId.Text;
var itemStatus = txtItemStatus.Text;
var listItem = _list.GetItemById(itemId);
listItem[_fieldName] = itemStatus;
listItem.Update();
_clientContext.ExecuteQuery();
lblOperationSuccess.Text = "Operation has been completed successfully!";
}
catch (Exception ex)
{
lblOperationFailure.Text = "Operation failed! " + ex.Message;
}
}
- Add the GetClientContextForUrl() method after the btnUpdateItem_Click() method:
private ClientContext GetClientContextForUrl(string url)
{
var targetWeb = new Uri(url);
var targetRealm = WebConfigurationManager.AppSettings.Get("Realm");
try
{
var responseToken = TokenHelper.GetAppOnlyAccessToken(TokenHelper.SharePointPrincipal, targetWeb.Authority, targetRealm).AccessToken;
var clientContext = TokenHelper.GetClientContextWithAccessToken(targetWeb.ToString(), responseToken);
return clientContext;
}
catch (RequestFailedException rex)
{
var clientId = WebConfigurationManager.AppSettings.Get("ClientId");
var hostedAppHostName = WebConfigurationManager.AppSettings.Get("HostedAppHostName");
var spHostUrl = WebConfigurationManager.AppSettings.Get("SPHostUrl");
var message = string.Format(@"Token request failed:
1) Verify your App Identifier on {3}/_layouts/15/appPrincipals.aspx it should be '{0}@{1}'.
2) Verify your App Domain on {3}/_layouts/15/appInv.aspx is '{2}' for the App Id '{0}'.",
clientId, targetRealm, hostedAppHostName, spHostUrl);
throw new RequestFailedException(message, rex);
}
}
- In Solution Explorer, right-click the web project, then hit Publish
- For Select a Publish target, select Custom
- Hit Next
- Enter a profile name
Example: LowTrustAddIn
- For Publish method: select File System
- For Target location: this is your visual studio project location
Example: C:\Users\wfawzi\Documents\visual studio 2015\Projects\LowTrustAddIn\LowTrustAddInWeb
- Hit Publish
Pre-requisites:
- The list which we entered its name in step 2.4.5.3 -> 4 must exist on the site we entered its URL in the same step
- The list must have a single-line-of-text column named “Status”
- The list must have at least one item
- Start the app:
In a browser navigate to the SPAppWebUrl URL of step 2.4.5.1 above.
Example: http://lowtrustaddin/LowTrustAddInWeb/Pages/Default.aspx
- You should get a page similar to the following:
- Enter the ID of the item you would like to update its status
- Enter the new status
- Hit Update Status
- You should get a message that the operation has been completed successfully
- Check your list
- You should find that the item’s status has been updated
3References
- Use an Office 365 SharePoint site to authorize provider-hosted add-ins on an on-premises SharePoint site
https://docs.microsoft.com/en-us/sharepoint/dev/sp-add-ins/use-an-office-365-sharepoint-site-to-authorize-provider-hosted-add-ins-on-an-on
- Context Token OAuth flow for SharePoint Add-ins
https://docs.microsoft.com/en-us/sharepoint/dev/sp-add-ins/context-token-oauth-flow-for-sharepoint-add-ins
- Add-in authorization policy types in SharePoint
https://docs.microsoft.com/en-us/sharepoint/dev/sp-add-ins/add-in-authorization-policy-types-in-sharepoint
https://threewill.com/making-app-only-sharepoint-calls-without-launched-sharepoint/