├── .gitignore ├── LICENSE ├── ReadMe.md ├── SharePoint-Add-in-REST-OData-BasicDataOperations.sln ├── SharePoint-Add-in-REST-OData-BasicDataOperations ├── AppIcon.png ├── AppManifest.xml └── SharePoint-Add-in-REST-OData-BasicDataOperations.csproj ├── SharePoint-Add-in-REST-OData-BasicDataOperationsWeb ├── Pages │ ├── Default.aspx │ ├── Default.aspx.cs │ └── Default.aspx.designer.cs ├── Properties │ └── AssemblyInfo.cs ├── SharePoint-Add-in-REST-OData-BasicDataOperationsWeb.csproj ├── SharePointContext.cs ├── TokenHelper.cs ├── Web.Debug.config ├── Web.Release.config ├── Web.config └── packages.config └── description └── fig1.gif /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studo 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | 106 | # MightyMoose 107 | *.mm.* 108 | AutoTest.Net/ 109 | 110 | # Web workbench (sass) 111 | .sass-cache/ 112 | 113 | # Installshield output folder 114 | [Ee]xpress/ 115 | 116 | # DocProject is a documentation generator add-in 117 | DocProject/buildhelp/ 118 | DocProject/Help/*.HxT 119 | DocProject/Help/*.HxC 120 | DocProject/Help/*.hhc 121 | DocProject/Help/*.hhk 122 | DocProject/Help/*.hhp 123 | DocProject/Help/Html2 124 | DocProject/Help/html 125 | 126 | # Click-Once directory 127 | publish/ 128 | 129 | # Publish Web Output 130 | *.[Pp]ublish.xml 131 | *.azurePubxml 132 | # TODO: Comment the next line if you want to checkin your web deploy settings 133 | # but database connection strings (with potential passwords) will be unencrypted 134 | *.pubxml 135 | *.publishproj 136 | 137 | # NuGet Packages 138 | *.nupkg 139 | # The packages folder can be ignored because of Package Restore 140 | **/packages/* 141 | # except build/, which is used as an MSBuild target. 142 | !**/packages/build/ 143 | # Uncomment if necessary however generally it will be regenerated when needed 144 | #!**/packages/repositories.config 145 | 146 | # Windows Azure Build Output 147 | csx/ 148 | *.build.csdef 149 | 150 | # Windows Store app package directory 151 | AppPackages/ 152 | 153 | # Others 154 | *.[Cc]ache 155 | ClientBin/ 156 | [Ss]tyle[Cc]op.* 157 | ~$* 158 | *~ 159 | *.dbmdl 160 | *.dbproj.schemaview 161 | *.pfx 162 | *.publishsettings 163 | node_modules/ 164 | bower_components/ 165 | 166 | # RIA/Silverlight projects 167 | Generated_Code/ 168 | 169 | # Backup & report files from converting an old project file 170 | # to a newer Visual Studio version. Backup files are not needed, 171 | # because we have git ;-) 172 | _UpgradeReport_Files/ 173 | Backup*/ 174 | UpgradeLog*.XML 175 | UpgradeLog*.htm 176 | 177 | # SQL Server files 178 | *.mdf 179 | *.ldf 180 | 181 | # Business Intelligence projects 182 | *.rdl.data 183 | *.bim.layout 184 | *.bim_*.settings 185 | 186 | # Microsoft Fakes 187 | FakesAssemblies/ 188 | 189 | # Node.js Tools for Visual Studio 190 | .ntvs_analysis.dat 191 | 192 | # Visual Studio 6 build log 193 | *.plg 194 | 195 | # Visual Studio 6 workspace options file 196 | *.opt 197 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Office 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | products: 4 | - office-sp 5 | - office-365 6 | languages: 7 | - csharp 8 | urlFragment: crud-operations-sharepoint 9 | description: "Use the SharePoint REST/OData endpoints to perform operations on lists and list items from a SharePoint Add-in." 10 | extensions: 11 | contentType: samples 12 | technologies: 13 | - Add-ins 14 | createdDate: "8/8/2015 11:33:40 AM" 15 | --- 16 | 17 | # Basic CRUD operations in SharePoint Add-ins using the REST/OData APIs 18 | 19 | > SharePoint add-in model is considered as a legacy option for extending SharePoint user interface. Please see [SharePoint Framework documentation](https://aka.ms/spfx) and the [SharePoint Framework samples](https://aka.ms/spfx-webparts) for the future proven option to extend SharePoint Online. Possible backend services should be using Azure Active Directly based registration and related app models. 20 | 21 | ## Summary 22 | Use the SharePoint REST/OData endpoints to perform create, read, update, and delete operations on lists and list items from a SharePoint Add-in. 23 | 24 | ### Applies to 25 | - SharePoint Online and on-premise SharePoint 2013 and later 26 | 27 | ## Prerequisites 28 | This sample requires the following: 29 | 30 | 31 | - A SharePoint 2013 (or later) development environment that is configured for add-in isolation and OAuth. (A SharePoint Online Developer Site is automatically configured. For an on premise development environment, see [Set up an on-premises development environment for SharePoint Add-ins](https://msdn.microsoft.com/library/office/fp179923.aspx) and [Use an Office 365 SharePoint site to authorize provider-hosted add-ins on an on-premises SharePoint site](https://msdn.microsoft.com/library/office/dn155905.aspx).) 32 | 33 | 34 | - Visual Studio and the Office Developer Tools for Visual Studio installed on your developer computer 35 | 36 | 37 | - Basic familiarity with RESTful web services and OData 38 | 39 | ## Description of the code ## 40 | The code that uses the REST APIs is located in the Default.aspx.cs file of the SharePoint-Add-in-REST-OData-BasicDataOperationsWeb project. The Default.aspx page of the add-in appears after you install and launch the add-in and looks similar to the following. 41 | 42 | ![The add-in start page with a table listing all the list on the site by name and ID.](/description/fig1.gif) 43 | 44 | 45 | 46 | The sample demonstrates the following: 47 | 48 | 49 | - How to read and write data to and from a SharePoint host web. This data conforms with the OData protocol to the REST endpoints where the list and list item entities are exposed. 50 | 51 | 52 | 53 | - How to parse Atom-formatted XML returned from these endpoints and how to construct JSON-formatted representations of the list and list item entities so that you can perform Create and Update operations on them. 54 | 55 | 56 | - Best practices for retrieving form digest and eTag values that are required for Create and Update operations on lists and list items. 57 | 58 | 59 | ## To use the sample # 60 | 61 | 12. Open **Visual Studio** as an administrator. 62 | 13. Open the .sln file. 63 | 13. In **Solution Explorer**, highlight the SharePoint add-in project and replace the **Site URL** property with the URL of your SharePoint developer site. 64 | 14. Press F5. 65 | 15. After the add-in installs, the consent page opens. Click **Trust It**. 66 | 16. Enter a string in the text box beside the **Add List** button and click the button. In a moment, the page refreshes and the new list is in the table. 67 | 17. Click the ID of the list, and then click **Retrieve List Items**. There will initially be no items on the list. Some additional buttons will appear. 68 | 18. Add a string to the text box beside the **Add Item** button and press the button. The new item will appear in the table in the row for the list. 69 | 19. Add a string to the text box beside the **Change List Title** button and press the button. The title will change in the table. 70 | 20. Press the **Delete the List** button and the list is deleted. 71 | 72 | **Do not delete any of the built-in SharePoint lists. If you mistakenly do so, recover the list from the SharePoint Recycle Bin.** 73 | 74 | ## Troubleshooting 75 | 76 | 77 | 78 | 79 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 94 | 95 | 96 |
Problem 80 | Solution
Visual Studio does not open the browser after you press the F5 key.Set the SharePoint Add-in project as the startup project.
HTTP error 405 Method not allowed.Locate the applicationhost.config file in %userprofile%\Documents\IISExpress\config. 90 |

Locate the handler entry for StaticFile, and add the verbs 91 | GET, HEAD, POST, DEBUG, and 92 | TRACE.

93 |
97 | 98 | ## Questions and comments 99 | 100 | We'd love to get your feedback on this sample. You can send your questions and suggestions to us in the [Issues](https://github.com/OfficeDev/SharePoint-Add-in-REST-OData-BasicDataOperations/issues) section of this repository. 101 | 102 | ## Additional resources 103 | 104 | [Get to know the SharePoint 2013 REST service](https://msdn.microsoft.com/library/fp142380.aspx). 105 | 106 | [Open Data Protocol](http://www.odata.org/) 107 | 108 | [OData: JavaScript Object Notation (JSON) Format](http://www.odata.org/developers/protocols/json-format) 109 | 110 | [OData: AtomPub Format](http://www.odata.org/developers/protocols/atom-format). 111 | 112 | ### Copyright 113 | 114 | Copyright (c) Microsoft. All rights reserved. 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperations.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.40629.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharePoint-Add-in-REST-OData-BasicDataOperations", "SharePoint-Add-in-REST-OData-BasicDataOperations\SharePoint-Add-in-REST-OData-BasicDataOperations.csproj", "{CDE76E8E-DCB8-47E7-A63A-70A0CDB88999}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharePoint-Add-in-REST-OData-BasicDataOperationsWeb", "SharePoint-Add-in-REST-OData-BasicDataOperationsWeb\SharePoint-Add-in-REST-OData-BasicDataOperationsWeb.csproj", "{32170F89-0BF8-4017-9846-D2C0D81581AE}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {CDE76E8E-DCB8-47E7-A63A-70A0CDB88999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {CDE76E8E-DCB8-47E7-A63A-70A0CDB88999}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {CDE76E8E-DCB8-47E7-A63A-70A0CDB88999}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 19 | {CDE76E8E-DCB8-47E7-A63A-70A0CDB88999}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {CDE76E8E-DCB8-47E7-A63A-70A0CDB88999}.Release|Any CPU.Build.0 = Release|Any CPU 21 | {CDE76E8E-DCB8-47E7-A63A-70A0CDB88999}.Release|Any CPU.Deploy.0 = Release|Any CPU 22 | {32170F89-0BF8-4017-9846-D2C0D81581AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {32170F89-0BF8-4017-9846-D2C0D81581AE}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {32170F89-0BF8-4017-9846-D2C0D81581AE}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {32170F89-0BF8-4017-9846-D2C0D81581AE}.Release|Any CPU.Build.0 = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperations/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SharePoint-Add-in-REST-OData-BasicDataOperations/b700eb7c6114ffe3288f72398ea55db53dc3ea0e/SharePoint-Add-in-REST-OData-BasicDataOperations/AppIcon.png -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperations/AppManifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 9 | 10 | SharePoint-Add-in-REST-OData-BasicDataOperations 11 | ~remoteAppUrl/Pages/Default.aspx?{StandardTokens} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperations/SharePoint-Add-in-REST-OData-BasicDataOperations.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {CDE76E8E-DCB8-47E7-A63A-70A0CDB88999} 8 | Library 9 | Properties 10 | SharePoint_Add_in_REST_OData_BasicDataOperations 11 | SharePoint-Add-in-REST-OData-BasicDataOperations 12 | v4.5 13 | 15.0 14 | 512 15 | {C1CDDADD-2546-481F-9697-4EA41081F2FC};{14822709-B5A1-4724-98CA-57A101D1B079};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 16 | 11.0 17 | 11.1 18 | False 19 | SharePointApp 20 | {b2ab3bd3-bc76-401a-be6a-6e748719b823} 21 | {696a47f2-80d8-45ec-a6f7-38ea5b62336a} 22 | {147697fd-fe77-4291-903b-e84172518a69} 23 | {4d3a78d9-a3f6-4d91-9eaf-6f003a345bd4} 24 | {98576287-576d-451d-9e57-e796a9ee5a9f} 25 | 26 | 27 | true 28 | full 29 | false 30 | bin\Debug\ 31 | DEBUG;TRACE 32 | prompt 33 | 4 34 | false 35 | 36 | 37 | pdbonly 38 | true 39 | bin\Release\ 40 | TRACE 41 | prompt 42 | 4 43 | false 44 | 45 | 46 | 47 | manifest-icon 48 | 49 | 50 | 51 | 52 | Designer 53 | 54 | 55 | 56 | 57 | {32170F89-0BF8-4017-9846-D2C0D81581AE} 58 | SharePoint-Add-in-REST-OData-BasicDataOperationsWeb 59 | True 60 | Web 61 | SharePointWebProjectOutput 62 | SharePoint-Add-in-REST-OData-BasicDataOperationsWeb 63 | False 64 | 65 | 66 | 67 | 68 | 10.0 69 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 70 | 71 | 72 | -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperationsWeb/Pages/Default.aspx: -------------------------------------------------------------------------------- 1 | <%-- Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See full license at the bottom of this file. --%> 2 | 3 | <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="SharePoint_Add_in_REST_OData_BasicDataOperationsWeb.Default" %> 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |

Site Lists (REST)

15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 | 24 |
25 | 26 | List Title 27 | 28 |
29 |
30 |
31 |
32 | 33 | 34 | 35 | <%-- 36 | SharePoint Add-in REST/OData Basic Data Operations, https://github.com/OfficeDev/SharePoint-Add-in-REST-OData-BasicDataOperations 37 | 38 | Copyright (c) Microsoft Corporation 39 | All rights reserved. 40 | 41 | MIT License: 42 | Permission is hereby granted, free of charge, to any person obtaining 43 | a copy of this software and associated documentation files (the 44 | "Software"), to deal in the Software without restriction, including 45 | without limitation the rights to use, copy, modify, merge, publish, 46 | distribute, sublicense, and/or sell copies of the Software, and to 47 | permit persons to whom the Software is furnished to do so, subject to 48 | the following conditions: 49 | 50 | The above copyright notice and this permission notice shall be 51 | included in all copies or substantial portions of the Software. 52 | 53 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 54 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 55 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 56 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 57 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 58 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 59 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 60 | 61 | --%> -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperationsWeb/Pages/Default.aspx.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See full license at the bottom of this file. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Web; 9 | using System.Web.UI; 10 | using System.Web.UI.WebControls; 11 | using System.Xml; 12 | 13 | namespace SharePoint_Add_in_REST_OData_BasicDataOperationsWeb 14 | { 15 | public partial class Default : System.Web.UI.Page 16 | { 17 | SharePointContextToken contextToken; 18 | string accessToken; 19 | Uri sharepointUrl; 20 | 21 | //Create a namespace manager for parsing the ATOM XML returned by the queries. 22 | XmlNamespaceManager xmlnspm = new XmlNamespaceManager(new NameTable()); 23 | 24 | protected void Page_PreInit(object sender, EventArgs e) 25 | { 26 | Uri redirectUrl; 27 | switch (SharePointContextProvider.CheckRedirectionStatus(Context, out redirectUrl)) 28 | { 29 | case RedirectionStatus.Ok: 30 | return; 31 | case RedirectionStatus.ShouldRedirect: 32 | Response.Redirect(redirectUrl.AbsoluteUri, endResponse: true); 33 | break; 34 | case RedirectionStatus.CanNotRedirect: 35 | Response.Write("An error occurred while processing your request."); 36 | Response.End(); 37 | break; 38 | } 39 | } 40 | 41 | // The Page_load method gets the context token and the access token. The access token is used by all of the data retrieval methods. 42 | protected void Page_Load(object sender, EventArgs e) 43 | { 44 | string contextTokenString = TokenHelper.GetContextTokenFromRequest(Request); 45 | 46 | if (contextTokenString != null) 47 | { 48 | contextToken = 49 | TokenHelper.ReadAndValidateContextToken(contextTokenString, Request.Url.Authority); 50 | 51 | sharepointUrl = new Uri(Request.QueryString["SPHostUrl"]); 52 | accessToken = 53 | TokenHelper.GetAccessToken(contextToken, sharepointUrl.Authority).AccessToken; 54 | 55 | // In a production add-in, you should cache the access token somewhere, such as in a database 56 | // or ASP.NET Session Cache. (Do not put it in a cookie.) Your code should also check to see 57 | // if it is expired before using it (and use the refresh token to get a new one when needed). 58 | // For more information, see the MSDN topic at https://msdn.microsoft.com/library/office/dn762763.aspx 59 | // For simplicity, this sample does not follow these practices. 60 | AddListButton.CommandArgument = accessToken; 61 | RefreshListButton.CommandArgument = accessToken; 62 | RetrieveListButton.CommandArgument = accessToken; 63 | AddItemButton.CommandArgument = accessToken; 64 | DeleteListButton.CommandArgument = accessToken; 65 | ChangeListTitleButton.CommandArgument = accessToken; 66 | RetrieveLists(accessToken); 67 | 68 | } 69 | else if (!IsPostBack) 70 | { 71 | Response.Write("Could not find a context token."); 72 | } 73 | } 74 | 75 | 76 | private void RetrieveLists(string accessToken) 77 | { 78 | if (IsPostBack) 79 | { 80 | sharepointUrl = new Uri(Request.QueryString["SPHostUrl"]); 81 | } 82 | 83 | AddItemButton.Visible = false; 84 | AddListItemBox.Visible = false; 85 | DeleteListButton.Visible = false; 86 | ChangeListTitleButton.Visible = false; 87 | ChangeListTitleBox.Visible = false; 88 | RetrieveListNameBox.Enabled = true; 89 | ListTable.Rows[0].Cells[1].Text = "List ID"; 90 | 91 | //Add needed namespaces to the namespace manager. 92 | xmlnspm.AddNamespace("atom", "http://www.w3.org/2005/Atom"); 93 | xmlnspm.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices"); 94 | xmlnspm.AddNamespace("m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"); 95 | 96 | //Execute a REST request for all of the site's lists. 97 | HttpWebRequest listRequest = 98 | (HttpWebRequest)HttpWebRequest.Create(sharepointUrl.ToString() + "/_api/Web/lists"); 99 | listRequest.Method = "GET"; 100 | listRequest.Accept = "application/atom+xml"; 101 | listRequest.ContentType = "application/atom+xml;type=entry"; 102 | listRequest.Headers.Add("Authorization", "Bearer " + accessToken); 103 | HttpWebResponse listResponse = (HttpWebResponse)listRequest.GetResponse(); 104 | StreamReader listReader = new StreamReader(listResponse.GetResponseStream()); 105 | var listXml = new XmlDocument(); 106 | listXml.LoadXml(listReader.ReadToEnd()); 107 | 108 | var titleList = listXml.SelectNodes("//atom:entry/atom:content/m:properties/d:Title", xmlnspm); 109 | var idList = listXml.SelectNodes("//atom:entry/atom:content/m:properties/d:Id", xmlnspm); 110 | 111 | int listCounter = 0; 112 | foreach (XmlNode title in titleList) 113 | { 114 | TableRow tableRow = new TableRow(); 115 | LiteralControl idClick = new LiteralControl(); 116 | //Use Javascript to populate the RetrieveListNameBox control with the list id. 117 | string clickScript = "" + idList[listCounter].InnerXml + ""; 118 | idClick.Text = clickScript; 119 | TableCell tableCell1 = new TableCell(); 120 | tableCell1.Controls.Add(new LiteralControl(title.InnerXml)); 121 | TableCell tableCell2 = new TableCell(); 122 | tableCell2.Controls.Add(idClick); 123 | tableRow.Cells.Add(tableCell1); 124 | tableRow.Cells.Add(tableCell2); 125 | ListTable.Rows.Add(tableRow); 126 | listCounter++; 127 | } 128 | } 129 | 130 | private void RetrieveListItems(string accessToken, Guid listId) 131 | { 132 | if (IsPostBack) 133 | { 134 | sharepointUrl = new Uri(Request.QueryString["SPHostUrl"]); 135 | } 136 | 137 | //Adjust the visibility of controls on the page in light of the list-specific context. 138 | AddItemButton.Visible = true; 139 | AddListItemBox.Visible = true; 140 | DeleteListButton.Visible = true; 141 | ChangeListTitleButton.Visible = true; 142 | ChangeListTitleBox.Visible = true; 143 | RetrieveListNameBox.Enabled = false; 144 | ListTable.Rows[0].Cells[1].Text = "List Items"; 145 | 146 | //Add needed namespaces to the namespace manager. 147 | xmlnspm.AddNamespace("atom", "http://www.w3.org/2005/Atom"); 148 | xmlnspm.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices"); 149 | xmlnspm.AddNamespace("m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"); 150 | 151 | //Execute a REST request to get the list name. 152 | HttpWebRequest listRequest = 153 | (HttpWebRequest)HttpWebRequest.Create(sharepointUrl.ToString() + "/_api/Web/lists(guid'" + listId + "')"); 154 | listRequest.Method = "GET"; 155 | listRequest.Accept = "application/atom+xml"; 156 | listRequest.ContentType = "application/atom+xml;type=entry"; 157 | listRequest.Headers.Add("Authorization", "Bearer " + accessToken); 158 | HttpWebResponse listResponse = (HttpWebResponse)listRequest.GetResponse(); 159 | StreamReader listReader = new StreamReader(listResponse.GetResponseStream()); 160 | var listXml = new XmlDocument(); 161 | listXml.LoadXml(listReader.ReadToEnd()); 162 | 163 | var listNameNode = listXml.SelectSingleNode("//atom:entry/atom:content/m:properties/d:Title", xmlnspm); 164 | string listName = listNameNode.InnerXml; 165 | 166 | //Execute a REST request to get all of the list's items. 167 | HttpWebRequest itemRequest = 168 | (HttpWebRequest)HttpWebRequest.Create(sharepointUrl.ToString() + "/_api/Web/lists(guid'" + listId + "')/Items"); 169 | itemRequest.Method = "GET"; 170 | itemRequest.Accept = "application/atom+xml"; 171 | itemRequest.ContentType = "application/atom+xml;type=entry"; 172 | itemRequest.Headers.Add("Authorization", "Bearer " + accessToken); 173 | HttpWebResponse itemResponse = (HttpWebResponse)itemRequest.GetResponse(); 174 | StreamReader itemReader = new StreamReader(itemResponse.GetResponseStream()); 175 | var itemXml = new XmlDocument(); 176 | itemXml.LoadXml(itemReader.ReadToEnd()); 177 | 178 | var itemList = itemXml.SelectNodes("//atom:entry/atom:content/m:properties/d:Title", xmlnspm); 179 | 180 | TableRow tableRow = new TableRow(); 181 | TableCell tableCell1 = new TableCell(); 182 | tableCell1.Controls.Add(new LiteralControl(listName)); 183 | TableCell tableCell2 = new TableCell(); 184 | 185 | foreach (XmlNode itemTitle in itemList) 186 | { 187 | tableCell2.Text += itemTitle.InnerXml + "
"; 188 | } 189 | 190 | tableRow.Cells.Add(tableCell1); 191 | tableRow.Cells.Add(tableCell2); 192 | ListTable.Rows.Add(tableRow); 193 | } 194 | 195 | private void AddList(string accessToken, string newListName) 196 | { 197 | if (IsPostBack) 198 | { 199 | sharepointUrl = new Uri(Request.QueryString["SPHostUrl"]); 200 | } 201 | 202 | try 203 | { 204 | 205 | 206 | //Add pertinent namespace to the namespace manager. 207 | xmlnspm.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices"); 208 | 209 | //Execute a REST request to get the form digest. All POST requests that change the state of resources on the host 210 | //Web require the form digest in the request header. 211 | HttpWebRequest contextinfoRequest = 212 | (HttpWebRequest)HttpWebRequest.Create(sharepointUrl.ToString() + "/_api/contextinfo"); 213 | contextinfoRequest.Method = "POST"; 214 | contextinfoRequest.ContentType = "text/xml;charset=utf-8"; 215 | contextinfoRequest.ContentLength = 0; 216 | contextinfoRequest.Headers.Add("Authorization", "Bearer " + accessToken); 217 | 218 | HttpWebResponse contextinfoResponse = (HttpWebResponse)contextinfoRequest.GetResponse(); 219 | StreamReader contextinfoReader = new StreamReader(contextinfoResponse.GetResponseStream(), System.Text.Encoding.UTF8); 220 | var formDigestXML = new XmlDocument(); 221 | formDigestXML.LoadXml(contextinfoReader.ReadToEnd()); 222 | var formDigestNode = formDigestXML.SelectSingleNode("//d:FormDigestValue", xmlnspm); 223 | string formDigest = formDigestNode.InnerXml; 224 | 225 | //Execute a REST request to add a list that has the user-supplied name. 226 | //The body of the REST request is ASCII encoded and inserted into the request stream. 227 | string listPostBody = "{'__metadata':{'type':'SP.List'}, 'Title':'" + newListName + "', 'BaseTemplate': 100}"; 228 | byte[] listPostData = System.Text.Encoding.ASCII.GetBytes(listPostBody); 229 | 230 | HttpWebRequest listRequest = 231 | (HttpWebRequest)HttpWebRequest.Create(sharepointUrl.ToString() + "/_api/lists"); 232 | listRequest.Method = "POST"; 233 | listRequest.ContentLength = listPostBody.Length; 234 | listRequest.ContentType = "application/json;odata=verbose"; 235 | listRequest.Accept = "application/json;odata=verbose"; 236 | listRequest.Headers.Add("Authorization", "Bearer " + accessToken); 237 | listRequest.Headers.Add("X-RequestDigest", formDigest); 238 | Stream listRequestStream = listRequest.GetRequestStream(); 239 | listRequestStream.Write(listPostData, 0, listPostData.Length); 240 | listRequestStream.Close(); 241 | HttpWebResponse listResponse = (HttpWebResponse)listRequest.GetResponse(); 242 | 243 | RetrieveLists(accessToken); 244 | } 245 | catch (Exception e) 246 | { 247 | AddListNameBox.Text = e.Message; 248 | } 249 | } 250 | 251 | private void AddListItem(string accessToken, Guid listId, string newItemName) 252 | { 253 | if (IsPostBack) 254 | { 255 | sharepointUrl = new Uri(Request.QueryString["SPHostUrl"]); 256 | } 257 | 258 | try 259 | { 260 | 261 | //Add pertinent namespaces to the namespace manager. 262 | xmlnspm.AddNamespace("atom", "http://www.w3.org/2005/Atom"); 263 | xmlnspm.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices"); 264 | xmlnspm.AddNamespace("m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"); 265 | 266 | 267 | //Execute a REST request to get the form digest. All POST requests that change the state of resources on the host 268 | //Web require the form digest in the request header. 269 | HttpWebRequest contextinfoRequest = 270 | (HttpWebRequest)HttpWebRequest.Create(sharepointUrl.ToString() + "/_api/contextinfo"); 271 | contextinfoRequest.Method = "POST"; 272 | contextinfoRequest.ContentType = "text/xml;charset=utf-8"; 273 | contextinfoRequest.ContentLength = 0; 274 | contextinfoRequest.Headers.Add("Authorization", "Bearer " + accessToken); 275 | 276 | HttpWebResponse contextinfoResponse = (HttpWebResponse)contextinfoRequest.GetResponse(); 277 | StreamReader contextinfoReader = new StreamReader(contextinfoResponse.GetResponseStream(), System.Text.Encoding.UTF8); 278 | var formDigestXML = new XmlDocument(); 279 | formDigestXML.LoadXml(contextinfoReader.ReadToEnd()); 280 | var formDigestNode = formDigestXML.SelectSingleNode("//d:FormDigestValue", xmlnspm); 281 | string formDigest = formDigestNode.InnerXml; 282 | 283 | //Execute a REST request to get the list name and the entity type name for the list. 284 | HttpWebRequest listRequest = 285 | (HttpWebRequest)HttpWebRequest.Create(sharepointUrl.ToString() + "/_api/Web/lists(guid'" + listId + "')"); 286 | listRequest.Method = "GET"; 287 | listRequest.Accept = "application/atom+xml"; 288 | listRequest.ContentType = "application/atom+xml;type=entry"; 289 | listRequest.Headers.Add("Authorization", "Bearer " + accessToken); 290 | HttpWebResponse listResponse = (HttpWebResponse)listRequest.GetResponse(); 291 | StreamReader listReader = new StreamReader(listResponse.GetResponseStream()); 292 | var listXml = new XmlDocument(); 293 | listXml.LoadXml(listReader.ReadToEnd()); 294 | 295 | //The entity type name is the required type when you construct a request to add a list item. 296 | var entityTypeNode = listXml.SelectSingleNode("//atom:entry/atom:content/m:properties/d:ListItemEntityTypeFullName", xmlnspm); 297 | var listNameNode = listXml.SelectSingleNode("//atom:entry/atom:content/m:properties/d:Title", xmlnspm); 298 | string entityTypeName = entityTypeNode.InnerXml; 299 | string listName = listNameNode.InnerXml; 300 | 301 | //Execute a REST request to add an item to the list. 302 | string itemPostBody = "{'__metadata':{'type':'" + entityTypeName + "'}, 'Title':'" + newItemName + "'}"; 303 | Byte[] itemPostData = System.Text.Encoding.ASCII.GetBytes(itemPostBody); 304 | 305 | HttpWebRequest itemRequest = 306 | (HttpWebRequest)HttpWebRequest.Create(sharepointUrl.ToString() + "/_api/Web/lists(guid'" + listId + "')/Items"); 307 | itemRequest.Method = "POST"; 308 | itemRequest.ContentLength = itemPostBody.Length; 309 | itemRequest.ContentType = "application/json;odata=verbose"; 310 | itemRequest.Accept = "application/json;odata=verbose"; 311 | itemRequest.Headers.Add("Authorization", "Bearer " + accessToken); 312 | itemRequest.Headers.Add("X-RequestDigest", formDigest); 313 | Stream itemRequestStream = itemRequest.GetRequestStream(); 314 | 315 | itemRequestStream.Write(itemPostData, 0, itemPostData.Length); 316 | itemRequestStream.Close(); 317 | 318 | HttpWebResponse itemResponse = (HttpWebResponse)itemRequest.GetResponse(); 319 | RetrieveListItems(accessToken, listId); 320 | } 321 | catch (Exception e) 322 | { 323 | AddListItemBox.Text = e.Message; 324 | } 325 | } 326 | 327 | private void ChangeListTitle(string accessToken, Guid listId, string newListTitle) 328 | { 329 | if (IsPostBack) 330 | { 331 | sharepointUrl = new Uri(Request.QueryString["SPHostUrl"]); 332 | } 333 | 334 | //Add pertinent namespace to the namespace manager. 335 | xmlnspm.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices"); 336 | 337 | //Execute a REST request to get the form digest. All POST requests that change the state of resources on the host 338 | //Web require the form digest in the request header. 339 | HttpWebRequest contextinfoRequest = 340 | (HttpWebRequest)HttpWebRequest.Create(sharepointUrl.ToString() + "/_api/contextinfo"); 341 | contextinfoRequest.Method = "POST"; 342 | contextinfoRequest.ContentType = "text/xml;charset=utf-8"; 343 | contextinfoRequest.ContentLength = 0; 344 | contextinfoRequest.Headers.Add("Authorization", "Bearer " + accessToken); 345 | 346 | HttpWebResponse contextinfoResponse = (HttpWebResponse)contextinfoRequest.GetResponse(); 347 | StreamReader contextinfoReader = new StreamReader(contextinfoResponse.GetResponseStream(), System.Text.Encoding.UTF8); 348 | var formDigestXML = new XmlDocument(); 349 | formDigestXML.LoadXml(contextinfoReader.ReadToEnd()); 350 | var formDigestNode = formDigestXML.SelectSingleNode("//d:FormDigestValue", xmlnspm); 351 | string formDigest = formDigestNode.InnerXml; 352 | 353 | //Execute a REST request to get the ETag value, which needs to be sent with the delete request. 354 | HttpWebRequest getListEtagRequest = 355 | (HttpWebRequest)HttpWebRequest.Create(sharepointUrl.ToString() + "/_api/Web/lists(guid'" + listId + "')"); 356 | getListEtagRequest.Method = "GET"; 357 | getListEtagRequest.Accept = "application/atom+xml"; 358 | getListEtagRequest.ContentType = "application/atom+xml;type=entry"; 359 | getListEtagRequest.Headers.Add("Authorization", "Bearer " + accessToken); 360 | HttpWebResponse listETagResponse = (HttpWebResponse)getListEtagRequest.GetResponse(); 361 | string eTag = listETagResponse.Headers["ETag"]; 362 | 363 | //Execute a REST request to change the list title 364 | string listPostBody = "{'__metadata':{'type':'SP.List'}, 'Title':'" + newListTitle + "'}"; 365 | byte[] listPostData = System.Text.Encoding.ASCII.GetBytes(listPostBody); 366 | 367 | HttpWebRequest listRequest = 368 | (HttpWebRequest)HttpWebRequest.Create(sharepointUrl.ToString() + "/_api/lists(guid'" + listId + "')"); 369 | listRequest.Method = "POST"; 370 | listRequest.ContentLength = listPostBody.Length; 371 | listRequest.ContentType = "application/json;odata=verbose"; 372 | listRequest.Accept = "application/json;odata=verbose"; 373 | listRequest.Headers.Add("Authorization", "Bearer " + accessToken); 374 | listRequest.Headers.Add("X-RequestDigest", formDigest); 375 | listRequest.Headers.Add("If-Match", eTag); 376 | listRequest.Headers.Add("X-Http-Method", "MERGE"); 377 | Stream listRequestStream = listRequest.GetRequestStream(); 378 | listRequestStream.Write(listPostData, 0, listPostData.Length); 379 | listRequestStream.Close(); 380 | HttpWebResponse listResponse = (HttpWebResponse)listRequest.GetResponse(); 381 | 382 | RetrieveListItems(accessToken, listId); 383 | } 384 | 385 | private void DeleteList(string accessToken, Guid listId) 386 | { 387 | if (IsPostBack) 388 | { 389 | sharepointUrl = new Uri(Request.QueryString["SPHostUrl"]); 390 | } 391 | 392 | //Add pertinent namespace to the namespace manager. 393 | xmlnspm.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices"); 394 | 395 | //Execute a REST request to get the form digest. All POST requests that change the state of resources on the host 396 | //Web require the form digest in the request header. 397 | HttpWebRequest contextinfoRequest = 398 | (HttpWebRequest)HttpWebRequest.Create(sharepointUrl.ToString() + "/_api/contextinfo"); 399 | contextinfoRequest.Method = "POST"; 400 | contextinfoRequest.ContentType = "text/xml;charset=utf-8"; 401 | contextinfoRequest.ContentLength = 0; 402 | contextinfoRequest.Headers.Add("Authorization", "Bearer " + accessToken); 403 | 404 | HttpWebResponse contextinfoResponse = (HttpWebResponse)contextinfoRequest.GetResponse(); 405 | StreamReader contextinfoReader = new StreamReader(contextinfoResponse.GetResponseStream(), System.Text.Encoding.UTF8); 406 | var formDigestXML = new XmlDocument(); 407 | formDigestXML.LoadXml(contextinfoReader.ReadToEnd()); 408 | var formDigestNode = formDigestXML.SelectSingleNode("//d:FormDigestValue", xmlnspm); 409 | string formDigest = formDigestNode.InnerXml; 410 | 411 | //Execute a REST request to get the ETag value, which needs to be sent with the delete request. 412 | HttpWebRequest getListEtagRequest = 413 | (HttpWebRequest)HttpWebRequest.Create(sharepointUrl.ToString() + "/_api/Web/lists(guid'" + listId + "')"); 414 | getListEtagRequest.Method = "GET"; 415 | getListEtagRequest.Accept = "application/atom+xml"; 416 | getListEtagRequest.ContentType = "application/atom+xml;type=entry"; 417 | getListEtagRequest.Headers.Add("Authorization", "Bearer " + accessToken); 418 | HttpWebResponse listETagResponse = (HttpWebResponse)getListEtagRequest.GetResponse(); 419 | string eTag = listETagResponse.Headers["ETag"]; 420 | 421 | //Execute a REST request to delete the list. 422 | HttpWebRequest deleteListRequest = 423 | (HttpWebRequest)HttpWebRequest.Create(sharepointUrl.ToString() + "/_api/Web/lists(guid'" + listId + "')"); 424 | deleteListRequest.Method = "POST"; 425 | deleteListRequest.ContentLength = 0; 426 | deleteListRequest.ContentType = "text/xml;charset=utf-8"; 427 | deleteListRequest.Headers.Add("X-RequestDigest", formDigest); 428 | deleteListRequest.Headers.Add("If-Match", eTag); 429 | deleteListRequest.Headers.Add("Authorization", "Bearer " + accessToken); 430 | deleteListRequest.Headers.Add("X-Http-Method", "DELETE"); 431 | HttpWebResponse deleteListResponse = (HttpWebResponse)deleteListRequest.GetResponse(); 432 | RetrieveListNameBox.Text = ""; 433 | RetrieveLists(accessToken); 434 | } 435 | 436 | protected void AddList_Click(object sender, EventArgs e) 437 | { 438 | string commandAccessToken = ((Button)sender).CommandArgument; 439 | if (AddListNameBox.Text != "") 440 | { 441 | AddList(commandAccessToken, AddListNameBox.Text); 442 | } 443 | else 444 | { 445 | AddListNameBox.Text = "Enter a list title"; 446 | } 447 | } 448 | 449 | protected void RefreshList_Click(object sender, EventArgs e) 450 | { 451 | string commandAccessToken = ((Button)sender).CommandArgument; 452 | RetrieveLists(commandAccessToken); 453 | } 454 | 455 | protected void RetrieveListButton_Click(object sender, EventArgs e) 456 | { 457 | string commandAccessToken = ((Button)sender).CommandArgument; 458 | Guid listId = new Guid(); 459 | if (Guid.TryParse(RetrieveListNameBox.Text, out listId)) 460 | { 461 | RetrieveListItems(commandAccessToken, listId); 462 | } 463 | else 464 | { 465 | RetrieveListNameBox.Text = "Enter a List GUID"; 466 | } 467 | } 468 | 469 | protected void AddItemButton_Click(object sender, EventArgs e) 470 | { 471 | string commandAccessToken = ((Button)sender).CommandArgument; 472 | Guid listId = new Guid(RetrieveListNameBox.Text); 473 | if (AddListItemBox.Text != "") 474 | { 475 | AddListItem(commandAccessToken, listId, AddListItemBox.Text); 476 | } 477 | else 478 | { 479 | AddListItemBox.Text = "Enter an item title"; 480 | } 481 | } 482 | 483 | protected void DeleteListButton_Click(object sender, EventArgs e) 484 | { 485 | string commandAccessToken = ((Button)sender).CommandArgument; 486 | Guid listId = new Guid(RetrieveListNameBox.Text); 487 | DeleteList(commandAccessToken, listId); 488 | } 489 | 490 | protected void ChangeListTitleButton_Click(object sender, EventArgs e) 491 | { 492 | string commandAccessToken = ((Button)sender).CommandArgument; 493 | Guid listId = new Guid(RetrieveListNameBox.Text); 494 | if (ChangeListTitleBox.Text != "") 495 | { 496 | ChangeListTitle(commandAccessToken, listId, ChangeListTitleBox.Text); 497 | } 498 | else 499 | { 500 | ChangeListTitleBox.Text = "Enter a new list title"; 501 | } 502 | } 503 | } 504 | } 505 | 506 | /* 507 | SharePoint Add-in REST/OData Basic Data Operations, https://github.com/OfficeDev/SharePoint-Add-in-REST-OData-BasicDataOperations 508 | 509 | Copyright (c) Microsoft Corporation 510 | All rights reserved. 511 | 512 | MIT License: 513 | Permission is hereby granted, free of charge, to any person obtaining 514 | a copy of this software and associated documentation files (the 515 | "Software"), to deal in the Software without restriction, including 516 | without limitation the rights to use, copy, modify, merge, publish, 517 | distribute, sublicense, and/or sell copies of the Software, and to 518 | permit persons to whom the Software is furnished to do so, subject to 519 | the following conditions: 520 | 521 | The above copyright notice and this permission notice shall be 522 | included in all copies or substantial portions of the Software. 523 | 524 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 525 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 526 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 527 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 528 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 529 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 530 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 531 | 532 | */ 533 | -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperationsWeb/Pages/Default.aspx.designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | 10 | namespace SharePoint_Add_in_REST_OData_BasicDataOperationsWeb { 11 | 12 | 13 | public partial class Default { 14 | 15 | /// 16 | /// form1 control. 17 | /// 18 | /// 19 | /// Auto-generated field. 20 | /// To modify move field declaration from designer file to code-behind file. 21 | /// 22 | protected global::System.Web.UI.HtmlControls.HtmlForm form1; 23 | 24 | /// 25 | /// ScriptManager1 control. 26 | /// 27 | /// 28 | /// Auto-generated field. 29 | /// To modify move field declaration from designer file to code-behind file. 30 | /// 31 | protected global::System.Web.UI.ScriptManager ScriptManager1; 32 | 33 | /// 34 | /// ListManagementPanel control. 35 | /// 36 | /// 37 | /// Auto-generated field. 38 | /// To modify move field declaration from designer file to code-behind file. 39 | /// 40 | protected global::System.Web.UI.UpdatePanel ListManagementPanel; 41 | 42 | /// 43 | /// RefreshListButton control. 44 | /// 45 | /// 46 | /// Auto-generated field. 47 | /// To modify move field declaration from designer file to code-behind file. 48 | /// 49 | protected global::System.Web.UI.WebControls.Button RefreshListButton; 50 | 51 | /// 52 | /// AddListButton control. 53 | /// 54 | /// 55 | /// Auto-generated field. 56 | /// To modify move field declaration from designer file to code-behind file. 57 | /// 58 | protected global::System.Web.UI.WebControls.Button AddListButton; 59 | 60 | /// 61 | /// AddListNameBox control. 62 | /// 63 | /// 64 | /// Auto-generated field. 65 | /// To modify move field declaration from designer file to code-behind file. 66 | /// 67 | protected global::System.Web.UI.WebControls.TextBox AddListNameBox; 68 | 69 | /// 70 | /// RetrieveListButton control. 71 | /// 72 | /// 73 | /// Auto-generated field. 74 | /// To modify move field declaration from designer file to code-behind file. 75 | /// 76 | protected global::System.Web.UI.WebControls.Button RetrieveListButton; 77 | 78 | /// 79 | /// RetrieveListNameBox control. 80 | /// 81 | /// 82 | /// Auto-generated field. 83 | /// To modify move field declaration from designer file to code-behind file. 84 | /// 85 | protected global::System.Web.UI.WebControls.TextBox RetrieveListNameBox; 86 | 87 | /// 88 | /// AddItemButton control. 89 | /// 90 | /// 91 | /// Auto-generated field. 92 | /// To modify move field declaration from designer file to code-behind file. 93 | /// 94 | protected global::System.Web.UI.WebControls.Button AddItemButton; 95 | 96 | /// 97 | /// AddListItemBox control. 98 | /// 99 | /// 100 | /// Auto-generated field. 101 | /// To modify move field declaration from designer file to code-behind file. 102 | /// 103 | protected global::System.Web.UI.WebControls.TextBox AddListItemBox; 104 | 105 | /// 106 | /// DeleteListButton control. 107 | /// 108 | /// 109 | /// Auto-generated field. 110 | /// To modify move field declaration from designer file to code-behind file. 111 | /// 112 | protected global::System.Web.UI.WebControls.Button DeleteListButton; 113 | 114 | /// 115 | /// ChangeListTitleButton control. 116 | /// 117 | /// 118 | /// Auto-generated field. 119 | /// To modify move field declaration from designer file to code-behind file. 120 | /// 121 | protected global::System.Web.UI.WebControls.Button ChangeListTitleButton; 122 | 123 | /// 124 | /// ChangeListTitleBox control. 125 | /// 126 | /// 127 | /// Auto-generated field. 128 | /// To modify move field declaration from designer file to code-behind file. 129 | /// 130 | protected global::System.Web.UI.WebControls.TextBox ChangeListTitleBox; 131 | 132 | /// 133 | /// ListTable control. 134 | /// 135 | /// 136 | /// Auto-generated field. 137 | /// To modify move field declaration from designer file to code-behind file. 138 | /// 139 | protected global::System.Web.UI.WebControls.Table ListTable; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperationsWeb/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("SharePoint_Add_in_REST_OData_BasicDataOperationsWeb")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("SharePoint_Add_in_REST_OData_BasicDataOperationsWeb")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("0e1420f0-04e8-449d-98d0-3d2ae006b95d")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Revision and Build Numbers 33 | // by using the '*' as shown below: 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperationsWeb/SharePoint-Add-in-REST-OData-BasicDataOperationsWeb.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 8 | 9 | 2.0 10 | {32170F89-0BF8-4017-9846-D2C0D81581AE} 11 | {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} 12 | Library 13 | Properties 14 | SharePoint_Add_in_REST_OData_BasicDataOperationsWeb 15 | SharePoint-Add-in-REST-OData-BasicDataOperationsWeb 16 | v4.5 17 | true 18 | 44333 19 | 20 | 21 | 22 | 15.0 23 | 24 | 25 | true 26 | full 27 | false 28 | bin\ 29 | DEBUG;TRACE 30 | prompt 31 | 4 32 | 33 | 34 | pdbonly 35 | true 36 | bin\ 37 | TRACE 38 | prompt 39 | 4 40 | 41 | 42 | 43 | 44 | True 45 | 46 | 47 | True 48 | 49 | 50 | True 51 | 52 | 53 | True 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Web.config 78 | 79 | 80 | Web.config 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | Default.aspx 90 | ASPXCodeBehind 91 | 92 | 93 | Default.aspx 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 10.0 102 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | True 112 | True 113 | 19739 114 | / 115 | http://localhost:19739/ 116 | False 117 | False 118 | 119 | 120 | False 121 | 122 | 123 | 124 | 125 | 132 | -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperationsWeb/SharePointContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See full license at the bottom of this file. 2 | 3 | using Microsoft.IdentityModel.S2S.Protocols.OAuth2; 4 | using Microsoft.IdentityModel.Tokens; 5 | using Microsoft.SharePoint.Client; 6 | using System; 7 | using System.Net; 8 | using System.Security.Principal; 9 | using System.Web; 10 | using System.Web.Configuration; 11 | 12 | namespace SharePoint_Add_in_REST_OData_BasicDataOperationsWeb 13 | { 14 | /// 15 | /// Encapsulates all the information from SharePoint. 16 | /// 17 | public abstract class SharePointContext 18 | { 19 | public const string SPHostUrlKey = "SPHostUrl"; 20 | public const string SPAppWebUrlKey = "SPAppWebUrl"; 21 | public const string SPLanguageKey = "SPLanguage"; 22 | public const string SPClientTagKey = "SPClientTag"; 23 | public const string SPProductNumberKey = "SPProductNumber"; 24 | 25 | protected static readonly TimeSpan AccessTokenLifetimeTolerance = TimeSpan.FromMinutes(5.0); 26 | 27 | private readonly Uri spHostUrl; 28 | private readonly Uri spAppWebUrl; 29 | private readonly string spLanguage; 30 | private readonly string spClientTag; 31 | private readonly string spProductNumber; 32 | 33 | // 34 | protected Tuple userAccessTokenForSPHost; 35 | protected Tuple userAccessTokenForSPAppWeb; 36 | protected Tuple appOnlyAccessTokenForSPHost; 37 | protected Tuple appOnlyAccessTokenForSPAppWeb; 38 | 39 | /// 40 | /// Gets the SharePoint host url from QueryString of the specified HTTP request. 41 | /// 42 | /// The specified HTTP request. 43 | /// The SharePoint host url. Returns null if the HTTP request doesn't contain the SharePoint host url. 44 | public static Uri GetSPHostUrl(HttpRequestBase httpRequest) 45 | { 46 | if (httpRequest == null) 47 | { 48 | throw new ArgumentNullException("httpRequest"); 49 | } 50 | 51 | string spHostUrlString = TokenHelper.EnsureTrailingSlash(httpRequest.QueryString[SPHostUrlKey]); 52 | Uri spHostUrl; 53 | if (Uri.TryCreate(spHostUrlString, UriKind.Absolute, out spHostUrl) && 54 | (spHostUrl.Scheme == Uri.UriSchemeHttp || spHostUrl.Scheme == Uri.UriSchemeHttps)) 55 | { 56 | return spHostUrl; 57 | } 58 | 59 | return null; 60 | } 61 | 62 | /// 63 | /// Gets the SharePoint host url from QueryString of the specified HTTP request. 64 | /// 65 | /// The specified HTTP request. 66 | /// The SharePoint host url. Returns null if the HTTP request doesn't contain the SharePoint host url. 67 | public static Uri GetSPHostUrl(HttpRequest httpRequest) 68 | { 69 | return GetSPHostUrl(new HttpRequestWrapper(httpRequest)); 70 | } 71 | 72 | /// 73 | /// The SharePoint host url. 74 | /// 75 | public Uri SPHostUrl 76 | { 77 | get { return this.spHostUrl; } 78 | } 79 | 80 | /// 81 | /// The SharePoint app web url. 82 | /// 83 | public Uri SPAppWebUrl 84 | { 85 | get { return this.spAppWebUrl; } 86 | } 87 | 88 | /// 89 | /// The SharePoint language. 90 | /// 91 | public string SPLanguage 92 | { 93 | get { return this.spLanguage; } 94 | } 95 | 96 | /// 97 | /// The SharePoint client tag. 98 | /// 99 | public string SPClientTag 100 | { 101 | get { return this.spClientTag; } 102 | } 103 | 104 | /// 105 | /// The SharePoint product number. 106 | /// 107 | public string SPProductNumber 108 | { 109 | get { return this.spProductNumber; } 110 | } 111 | 112 | /// 113 | /// The user access token for the SharePoint host. 114 | /// 115 | public abstract string UserAccessTokenForSPHost 116 | { 117 | get; 118 | } 119 | 120 | /// 121 | /// The user access token for the SharePoint app web. 122 | /// 123 | public abstract string UserAccessTokenForSPAppWeb 124 | { 125 | get; 126 | } 127 | 128 | /// 129 | /// The app only access token for the SharePoint host. 130 | /// 131 | public abstract string AppOnlyAccessTokenForSPHost 132 | { 133 | get; 134 | } 135 | 136 | /// 137 | /// The app only access token for the SharePoint app web. 138 | /// 139 | public abstract string AppOnlyAccessTokenForSPAppWeb 140 | { 141 | get; 142 | } 143 | 144 | /// 145 | /// Constructor. 146 | /// 147 | /// The SharePoint host url. 148 | /// The SharePoint app web url. 149 | /// The SharePoint language. 150 | /// The SharePoint client tag. 151 | /// The SharePoint product number. 152 | protected SharePointContext(Uri spHostUrl, Uri spAppWebUrl, string spLanguage, string spClientTag, string spProductNumber) 153 | { 154 | if (spHostUrl == null) 155 | { 156 | throw new ArgumentNullException("spHostUrl"); 157 | } 158 | 159 | if (string.IsNullOrEmpty(spLanguage)) 160 | { 161 | throw new ArgumentNullException("spLanguage"); 162 | } 163 | 164 | if (string.IsNullOrEmpty(spClientTag)) 165 | { 166 | throw new ArgumentNullException("spClientTag"); 167 | } 168 | 169 | if (string.IsNullOrEmpty(spProductNumber)) 170 | { 171 | throw new ArgumentNullException("spProductNumber"); 172 | } 173 | 174 | this.spHostUrl = spHostUrl; 175 | this.spAppWebUrl = spAppWebUrl; 176 | this.spLanguage = spLanguage; 177 | this.spClientTag = spClientTag; 178 | this.spProductNumber = spProductNumber; 179 | } 180 | 181 | /// 182 | /// Creates a user ClientContext for the SharePoint host. 183 | /// 184 | /// A ClientContext instance. 185 | public ClientContext CreateUserClientContextForSPHost() 186 | { 187 | return CreateClientContext(this.SPHostUrl, this.UserAccessTokenForSPHost); 188 | } 189 | 190 | /// 191 | /// Creates a user ClientContext for the SharePoint app web. 192 | /// 193 | /// A ClientContext instance. 194 | public ClientContext CreateUserClientContextForSPAppWeb() 195 | { 196 | return CreateClientContext(this.SPAppWebUrl, this.UserAccessTokenForSPAppWeb); 197 | } 198 | 199 | /// 200 | /// Creates app only ClientContext for the SharePoint host. 201 | /// 202 | /// A ClientContext instance. 203 | public ClientContext CreateAppOnlyClientContextForSPHost() 204 | { 205 | return CreateClientContext(this.SPHostUrl, this.AppOnlyAccessTokenForSPHost); 206 | } 207 | 208 | /// 209 | /// Creates an app only ClientContext for the SharePoint app web. 210 | /// 211 | /// A ClientContext instance. 212 | public ClientContext CreateAppOnlyClientContextForSPAppWeb() 213 | { 214 | return CreateClientContext(this.SPAppWebUrl, this.AppOnlyAccessTokenForSPAppWeb); 215 | } 216 | 217 | /// 218 | /// Gets the database connection string from SharePoint for autohosted app. 219 | /// 220 | /// The database connection string. Returns null if the app is not autohosted or there is no database. 221 | public string GetDatabaseConnectionString() 222 | { 223 | string dbConnectionString = null; 224 | 225 | using (ClientContext clientContext = CreateAppOnlyClientContextForSPHost()) 226 | { 227 | if (clientContext != null) 228 | { 229 | var result = AppInstance.RetrieveAppDatabaseConnectionString(clientContext); 230 | 231 | clientContext.ExecuteQuery(); 232 | 233 | dbConnectionString = result.Value; 234 | } 235 | } 236 | 237 | if (dbConnectionString == null) 238 | { 239 | const string LocalDBInstanceForDebuggingKey = "LocalDBInstanceForDebugging"; 240 | 241 | var dbConnectionStringSettings = WebConfigurationManager.ConnectionStrings[LocalDBInstanceForDebuggingKey]; 242 | 243 | dbConnectionString = dbConnectionStringSettings != null ? dbConnectionStringSettings.ConnectionString : null; 244 | } 245 | 246 | return dbConnectionString; 247 | } 248 | 249 | /// 250 | /// Determines if the specified access token is valid. 251 | /// It considers an access token as not valid if it is null, or it has expired. 252 | /// 253 | /// The access token to verify. 254 | /// True if the access token is valid. 255 | protected static bool IsAccessTokenValid(Tuple accessToken) 256 | { 257 | return accessToken != null && 258 | !string.IsNullOrEmpty(accessToken.Item1) && 259 | accessToken.Item2 > DateTime.UtcNow; 260 | } 261 | 262 | /// 263 | /// Creates a ClientContext with the specified SharePoint site url and the access token. 264 | /// 265 | /// The site url. 266 | /// The access token. 267 | /// A ClientContext instance. 268 | private static ClientContext CreateClientContext(Uri spSiteUrl, string accessToken) 269 | { 270 | if (spSiteUrl != null && !string.IsNullOrEmpty(accessToken)) 271 | { 272 | return TokenHelper.GetClientContextWithAccessToken(spSiteUrl.AbsoluteUri, accessToken); 273 | } 274 | 275 | return null; 276 | } 277 | } 278 | 279 | /// 280 | /// Redirection status. 281 | /// 282 | public enum RedirectionStatus 283 | { 284 | Ok, 285 | ShouldRedirect, 286 | CanNotRedirect 287 | } 288 | 289 | /// 290 | /// Provides SharePointContext instances. 291 | /// 292 | public abstract class SharePointContextProvider 293 | { 294 | private static SharePointContextProvider current; 295 | 296 | /// 297 | /// The current SharePointContextProvider instance. 298 | /// 299 | public static SharePointContextProvider Current 300 | { 301 | get { return SharePointContextProvider.current; } 302 | } 303 | 304 | /// 305 | /// Initializes the default SharePointContextProvider instance. 306 | /// 307 | static SharePointContextProvider() 308 | { 309 | if (!TokenHelper.IsHighTrustApp()) 310 | { 311 | SharePointContextProvider.current = new SharePointAcsContextProvider(); 312 | } 313 | else 314 | { 315 | SharePointContextProvider.current = new SharePointHighTrustContextProvider(); 316 | } 317 | } 318 | 319 | /// 320 | /// Registers the specified SharePointContextProvider instance as current. 321 | /// It should be called by Application_Start() in Global.asax. 322 | /// 323 | /// The SharePointContextProvider to be set as current. 324 | public static void Register(SharePointContextProvider provider) 325 | { 326 | if (provider == null) 327 | { 328 | throw new ArgumentNullException("provider"); 329 | } 330 | 331 | SharePointContextProvider.current = provider; 332 | } 333 | 334 | /// 335 | /// Checks if it is necessary to redirect to SharePoint for user to authenticate. 336 | /// 337 | /// The HTTP context. 338 | /// The redirect url to SharePoint if the status is ShouldRedirect. Null if the status is Ok or CanNotRedirect. 339 | /// Redirection status. 340 | public static RedirectionStatus CheckRedirectionStatus(HttpContextBase httpContext, out Uri redirectUrl) 341 | { 342 | if (httpContext == null) 343 | { 344 | throw new ArgumentNullException("httpContext"); 345 | } 346 | 347 | redirectUrl = null; 348 | 349 | if (SharePointContextProvider.Current.GetSharePointContext(httpContext) != null) 350 | { 351 | return RedirectionStatus.Ok; 352 | } 353 | 354 | const string SPHasRedirectedToSharePointKey = "SPHasRedirectedToSharePoint"; 355 | 356 | if (!string.IsNullOrEmpty(httpContext.Request.QueryString[SPHasRedirectedToSharePointKey])) 357 | { 358 | return RedirectionStatus.CanNotRedirect; 359 | } 360 | 361 | Uri spHostUrl = SharePointContext.GetSPHostUrl(httpContext.Request); 362 | 363 | if (spHostUrl == null) 364 | { 365 | return RedirectionStatus.CanNotRedirect; 366 | } 367 | 368 | if (StringComparer.OrdinalIgnoreCase.Equals(httpContext.Request.HttpMethod, "POST")) 369 | { 370 | return RedirectionStatus.CanNotRedirect; 371 | } 372 | 373 | Uri requestUrl = httpContext.Request.Url; 374 | 375 | var queryNameValueCollection = HttpUtility.ParseQueryString(requestUrl.Query); 376 | 377 | // Removes the values that are included in {StandardTokens}, as {StandardTokens} will be inserted at the beginning of the query string. 378 | queryNameValueCollection.Remove(SharePointContext.SPHostUrlKey); 379 | queryNameValueCollection.Remove(SharePointContext.SPAppWebUrlKey); 380 | queryNameValueCollection.Remove(SharePointContext.SPLanguageKey); 381 | queryNameValueCollection.Remove(SharePointContext.SPClientTagKey); 382 | queryNameValueCollection.Remove(SharePointContext.SPProductNumberKey); 383 | 384 | // Adds SPHasRedirectedToSharePoint=1. 385 | queryNameValueCollection.Add(SPHasRedirectedToSharePointKey, "1"); 386 | 387 | UriBuilder returnUrlBuilder = new UriBuilder(requestUrl); 388 | returnUrlBuilder.Query = queryNameValueCollection.ToString(); 389 | 390 | // Inserts StandardTokens. 391 | const string StandardTokens = "{StandardTokens}"; 392 | string returnUrlString = returnUrlBuilder.Uri.AbsoluteUri; 393 | returnUrlString = returnUrlString.Insert(returnUrlString.IndexOf("?") + 1, StandardTokens + "&"); 394 | 395 | // Constructs redirect url. 396 | string redirectUrlString = TokenHelper.GetAppContextTokenRequestUrl(spHostUrl.AbsoluteUri, Uri.EscapeDataString(returnUrlString)); 397 | 398 | redirectUrl = new Uri(redirectUrlString, UriKind.Absolute); 399 | 400 | return RedirectionStatus.ShouldRedirect; 401 | } 402 | 403 | /// 404 | /// Checks if it is necessary to redirect to SharePoint for user to authenticate. 405 | /// 406 | /// The HTTP context. 407 | /// The redirect url to SharePoint if the status is ShouldRedirect. Null if the status is Ok or CanNotRedirect. 408 | /// Redirection status. 409 | public static RedirectionStatus CheckRedirectionStatus(HttpContext httpContext, out Uri redirectUrl) 410 | { 411 | return CheckRedirectionStatus(new HttpContextWrapper(httpContext), out redirectUrl); 412 | } 413 | 414 | /// 415 | /// Creates a SharePointContext instance with the specified HTTP request. 416 | /// 417 | /// The HTTP request. 418 | /// The SharePointContext instance. Returns null if errors occur. 419 | public SharePointContext CreateSharePointContext(HttpRequestBase httpRequest) 420 | { 421 | if (httpRequest == null) 422 | { 423 | throw new ArgumentNullException("httpRequest"); 424 | } 425 | 426 | // SPHostUrl 427 | Uri spHostUrl = SharePointContext.GetSPHostUrl(httpRequest); 428 | if (spHostUrl == null) 429 | { 430 | return null; 431 | } 432 | 433 | // SPAppWebUrl 434 | string spAppWebUrlString = TokenHelper.EnsureTrailingSlash(httpRequest.QueryString[SharePointContext.SPAppWebUrlKey]); 435 | Uri spAppWebUrl; 436 | if (!Uri.TryCreate(spAppWebUrlString, UriKind.Absolute, out spAppWebUrl) || 437 | !(spAppWebUrl.Scheme == Uri.UriSchemeHttp || spAppWebUrl.Scheme == Uri.UriSchemeHttps)) 438 | { 439 | spAppWebUrl = null; 440 | } 441 | 442 | // SPLanguage 443 | string spLanguage = httpRequest.QueryString[SharePointContext.SPLanguageKey]; 444 | if (string.IsNullOrEmpty(spLanguage)) 445 | { 446 | return null; 447 | } 448 | 449 | // SPClientTag 450 | string spClientTag = httpRequest.QueryString[SharePointContext.SPClientTagKey]; 451 | if (string.IsNullOrEmpty(spClientTag)) 452 | { 453 | return null; 454 | } 455 | 456 | // SPProductNumber 457 | string spProductNumber = httpRequest.QueryString[SharePointContext.SPProductNumberKey]; 458 | if (string.IsNullOrEmpty(spProductNumber)) 459 | { 460 | return null; 461 | } 462 | 463 | return CreateSharePointContext(spHostUrl, spAppWebUrl, spLanguage, spClientTag, spProductNumber, httpRequest); 464 | } 465 | 466 | /// 467 | /// Creates a SharePointContext instance with the specified HTTP request. 468 | /// 469 | /// The HTTP request. 470 | /// The SharePointContext instance. Returns null if errors occur. 471 | public SharePointContext CreateSharePointContext(HttpRequest httpRequest) 472 | { 473 | return CreateSharePointContext(new HttpRequestWrapper(httpRequest)); 474 | } 475 | 476 | /// 477 | /// Gets a SharePointContext instance associated with the specified HTTP context. 478 | /// 479 | /// The HTTP context. 480 | /// The SharePointContext instance. Returns null if not found and a new instance can't be created. 481 | public SharePointContext GetSharePointContext(HttpContextBase httpContext) 482 | { 483 | if (httpContext == null) 484 | { 485 | throw new ArgumentNullException("httpContext"); 486 | } 487 | 488 | Uri spHostUrl = SharePointContext.GetSPHostUrl(httpContext.Request); 489 | if (spHostUrl == null) 490 | { 491 | return null; 492 | } 493 | 494 | SharePointContext spContext = LoadSharePointContext(httpContext); 495 | 496 | if (spContext == null || !ValidateSharePointContext(spContext, httpContext)) 497 | { 498 | spContext = CreateSharePointContext(httpContext.Request); 499 | 500 | if (spContext != null) 501 | { 502 | SaveSharePointContext(spContext, httpContext); 503 | } 504 | } 505 | 506 | return spContext; 507 | } 508 | 509 | /// 510 | /// Gets a SharePointContext instance associated with the specified HTTP context. 511 | /// 512 | /// The HTTP context. 513 | /// The SharePointContext instance. Returns null if not found and a new instance can't be created. 514 | public SharePointContext GetSharePointContext(HttpContext httpContext) 515 | { 516 | return GetSharePointContext(new HttpContextWrapper(httpContext)); 517 | } 518 | 519 | /// 520 | /// Creates a SharePointContext instance. 521 | /// 522 | /// The SharePoint host url. 523 | /// The SharePoint app web url. 524 | /// The SharePoint language. 525 | /// The SharePoint client tag. 526 | /// The SharePoint product number. 527 | /// The HTTP request. 528 | /// The SharePointContext instance. Returns null if errors occur. 529 | protected abstract SharePointContext CreateSharePointContext(Uri spHostUrl, Uri spAppWebUrl, string spLanguage, string spClientTag, string spProductNumber, HttpRequestBase httpRequest); 530 | 531 | /// 532 | /// Validates if the given SharePointContext can be used with the specified HTTP context. 533 | /// 534 | /// The SharePointContext. 535 | /// The HTTP context. 536 | /// True if the given SharePointContext can be used with the specified HTTP context. 537 | protected abstract bool ValidateSharePointContext(SharePointContext spContext, HttpContextBase httpContext); 538 | 539 | /// 540 | /// Loads the SharePointContext instance associated with the specified HTTP context. 541 | /// 542 | /// The HTTP context. 543 | /// The SharePointContext instance. Returns null if not found. 544 | protected abstract SharePointContext LoadSharePointContext(HttpContextBase httpContext); 545 | 546 | /// 547 | /// Saves the specified SharePointContext instance associated with the specified HTTP context. 548 | /// null is accepted for clearing the SharePointContext instance associated with the HTTP context. 549 | /// 550 | /// The SharePointContext instance to be saved, or null. 551 | /// The HTTP context. 552 | protected abstract void SaveSharePointContext(SharePointContext spContext, HttpContextBase httpContext); 553 | } 554 | 555 | #region ACS 556 | 557 | /// 558 | /// Encapsulates all the information from SharePoint in ACS mode. 559 | /// 560 | public class SharePointAcsContext : SharePointContext 561 | { 562 | private readonly string contextToken; 563 | private readonly SharePointContextToken contextTokenObj; 564 | 565 | /// 566 | /// The context token. 567 | /// 568 | public string ContextToken 569 | { 570 | get { return this.contextTokenObj.ValidTo > DateTime.UtcNow ? this.contextToken : null; } 571 | } 572 | 573 | /// 574 | /// The context token's "CacheKey" claim. 575 | /// 576 | public string CacheKey 577 | { 578 | get { return this.contextTokenObj.ValidTo > DateTime.UtcNow ? this.contextTokenObj.CacheKey : null; } 579 | } 580 | 581 | /// 582 | /// The context token's "refreshtoken" claim. 583 | /// 584 | public string RefreshToken 585 | { 586 | get { return this.contextTokenObj.ValidTo > DateTime.UtcNow ? this.contextTokenObj.RefreshToken : null; } 587 | } 588 | 589 | public override string UserAccessTokenForSPHost 590 | { 591 | get 592 | { 593 | return GetAccessTokenString(ref this.userAccessTokenForSPHost, 594 | () => TokenHelper.GetAccessToken(this.contextTokenObj, this.SPHostUrl.Authority)); 595 | } 596 | } 597 | 598 | public override string UserAccessTokenForSPAppWeb 599 | { 600 | get 601 | { 602 | if (this.SPAppWebUrl == null) 603 | { 604 | return null; 605 | } 606 | 607 | return GetAccessTokenString(ref this.userAccessTokenForSPAppWeb, 608 | () => TokenHelper.GetAccessToken(this.contextTokenObj, this.SPAppWebUrl.Authority)); 609 | } 610 | } 611 | 612 | public override string AppOnlyAccessTokenForSPHost 613 | { 614 | get 615 | { 616 | return GetAccessTokenString(ref this.appOnlyAccessTokenForSPHost, 617 | () => TokenHelper.GetAppOnlyAccessToken(TokenHelper.SharePointPrincipal, this.SPHostUrl.Authority, TokenHelper.GetRealmFromTargetUrl(this.SPHostUrl))); 618 | } 619 | } 620 | 621 | public override string AppOnlyAccessTokenForSPAppWeb 622 | { 623 | get 624 | { 625 | if (this.SPAppWebUrl == null) 626 | { 627 | return null; 628 | } 629 | 630 | return GetAccessTokenString(ref this.appOnlyAccessTokenForSPAppWeb, 631 | () => TokenHelper.GetAppOnlyAccessToken(TokenHelper.SharePointPrincipal, this.SPAppWebUrl.Authority, TokenHelper.GetRealmFromTargetUrl(this.SPAppWebUrl))); 632 | } 633 | } 634 | 635 | public SharePointAcsContext(Uri spHostUrl, Uri spAppWebUrl, string spLanguage, string spClientTag, string spProductNumber, string contextToken, SharePointContextToken contextTokenObj) 636 | : base(spHostUrl, spAppWebUrl, spLanguage, spClientTag, spProductNumber) 637 | { 638 | if (string.IsNullOrEmpty(contextToken)) 639 | { 640 | throw new ArgumentNullException("contextToken"); 641 | } 642 | 643 | if (contextTokenObj == null) 644 | { 645 | throw new ArgumentNullException("contextTokenObj"); 646 | } 647 | 648 | this.contextToken = contextToken; 649 | this.contextTokenObj = contextTokenObj; 650 | } 651 | 652 | /// 653 | /// Ensures the access token is valid and returns it. 654 | /// 655 | /// The access token to verify. 656 | /// The token renewal handler. 657 | /// The access token string. 658 | private static string GetAccessTokenString(ref Tuple accessToken, Func tokenRenewalHandler) 659 | { 660 | RenewAccessTokenIfNeeded(ref accessToken, tokenRenewalHandler); 661 | 662 | return IsAccessTokenValid(accessToken) ? accessToken.Item1 : null; 663 | } 664 | 665 | /// 666 | /// Renews the access token if it is not valid. 667 | /// 668 | /// The access token to renew. 669 | /// The token renewal handler. 670 | private static void RenewAccessTokenIfNeeded(ref Tuple accessToken, Func tokenRenewalHandler) 671 | { 672 | if (IsAccessTokenValid(accessToken)) 673 | { 674 | return; 675 | } 676 | 677 | try 678 | { 679 | OAuth2AccessTokenResponse oAuth2AccessTokenResponse = tokenRenewalHandler(); 680 | 681 | DateTime expiresOn = oAuth2AccessTokenResponse.ExpiresOn; 682 | 683 | if ((expiresOn - oAuth2AccessTokenResponse.NotBefore) > AccessTokenLifetimeTolerance) 684 | { 685 | // Make the access token get renewed a bit earlier than the time when it expires 686 | // so that the calls to SharePoint with it will have enough time to complete successfully. 687 | expiresOn -= AccessTokenLifetimeTolerance; 688 | } 689 | 690 | accessToken = Tuple.Create(oAuth2AccessTokenResponse.AccessToken, expiresOn); 691 | } 692 | catch (WebException) 693 | { 694 | } 695 | } 696 | } 697 | 698 | /// 699 | /// Default provider for SharePointAcsContext. 700 | /// 701 | public class SharePointAcsContextProvider : SharePointContextProvider 702 | { 703 | private const string SPContextKey = "SPContext"; 704 | private const string SPCacheKeyKey = "SPCacheKey"; 705 | 706 | protected override SharePointContext CreateSharePointContext(Uri spHostUrl, Uri spAppWebUrl, string spLanguage, string spClientTag, string spProductNumber, HttpRequestBase httpRequest) 707 | { 708 | string contextTokenString = TokenHelper.GetContextTokenFromRequest(httpRequest); 709 | if (string.IsNullOrEmpty(contextTokenString)) 710 | { 711 | return null; 712 | } 713 | 714 | SharePointContextToken contextToken = null; 715 | try 716 | { 717 | contextToken = TokenHelper.ReadAndValidateContextToken(contextTokenString, httpRequest.Url.Authority); 718 | } 719 | catch (WebException) 720 | { 721 | return null; 722 | } 723 | catch (AudienceUriValidationFailedException) 724 | { 725 | return null; 726 | } 727 | 728 | return new SharePointAcsContext(spHostUrl, spAppWebUrl, spLanguage, spClientTag, spProductNumber, contextTokenString, contextToken); 729 | } 730 | 731 | protected override bool ValidateSharePointContext(SharePointContext spContext, HttpContextBase httpContext) 732 | { 733 | SharePointAcsContext spAcsContext = spContext as SharePointAcsContext; 734 | 735 | if (spAcsContext != null) 736 | { 737 | Uri spHostUrl = SharePointContext.GetSPHostUrl(httpContext.Request); 738 | string contextToken = TokenHelper.GetContextTokenFromRequest(httpContext.Request); 739 | HttpCookie spCacheKeyCookie = httpContext.Request.Cookies[SPCacheKeyKey]; 740 | string spCacheKey = spCacheKeyCookie != null ? spCacheKeyCookie.Value : null; 741 | 742 | return spHostUrl == spAcsContext.SPHostUrl && 743 | !string.IsNullOrEmpty(spAcsContext.CacheKey) && 744 | spCacheKey == spAcsContext.CacheKey && 745 | !string.IsNullOrEmpty(spAcsContext.ContextToken) && 746 | (string.IsNullOrEmpty(contextToken) || contextToken == spAcsContext.ContextToken); 747 | } 748 | 749 | return false; 750 | } 751 | 752 | protected override SharePointContext LoadSharePointContext(HttpContextBase httpContext) 753 | { 754 | return httpContext.Session[SPContextKey] as SharePointAcsContext; 755 | } 756 | 757 | protected override void SaveSharePointContext(SharePointContext spContext, HttpContextBase httpContext) 758 | { 759 | SharePointAcsContext spAcsContext = spContext as SharePointAcsContext; 760 | 761 | if (spAcsContext != null) 762 | { 763 | HttpCookie spCacheKeyCookie = new HttpCookie(SPCacheKeyKey) 764 | { 765 | Value = spAcsContext.CacheKey, 766 | Secure = true, 767 | HttpOnly = true 768 | }; 769 | 770 | httpContext.Response.AppendCookie(spCacheKeyCookie); 771 | } 772 | 773 | httpContext.Session[SPContextKey] = spAcsContext; 774 | } 775 | } 776 | 777 | #endregion ACS 778 | 779 | #region HighTrust 780 | 781 | /// 782 | /// Encapsulates all the information from SharePoint in HighTrust mode. 783 | /// 784 | public class SharePointHighTrustContext : SharePointContext 785 | { 786 | private readonly WindowsIdentity logonUserIdentity; 787 | 788 | /// 789 | /// The Windows identity for the current user. 790 | /// 791 | public WindowsIdentity LogonUserIdentity 792 | { 793 | get { return this.logonUserIdentity; } 794 | } 795 | 796 | public override string UserAccessTokenForSPHost 797 | { 798 | get 799 | { 800 | return GetAccessTokenString(ref this.userAccessTokenForSPHost, 801 | () => TokenHelper.GetS2SAccessTokenWithWindowsIdentity(this.SPHostUrl, this.LogonUserIdentity)); 802 | } 803 | } 804 | 805 | public override string UserAccessTokenForSPAppWeb 806 | { 807 | get 808 | { 809 | if (this.SPAppWebUrl == null) 810 | { 811 | return null; 812 | } 813 | 814 | return GetAccessTokenString(ref this.userAccessTokenForSPAppWeb, 815 | () => TokenHelper.GetS2SAccessTokenWithWindowsIdentity(this.SPAppWebUrl, this.LogonUserIdentity)); 816 | } 817 | } 818 | 819 | public override string AppOnlyAccessTokenForSPHost 820 | { 821 | get 822 | { 823 | return GetAccessTokenString(ref this.appOnlyAccessTokenForSPHost, 824 | () => TokenHelper.GetS2SAccessTokenWithWindowsIdentity(this.SPHostUrl, null)); 825 | } 826 | } 827 | 828 | public override string AppOnlyAccessTokenForSPAppWeb 829 | { 830 | get 831 | { 832 | if (this.SPAppWebUrl == null) 833 | { 834 | return null; 835 | } 836 | 837 | return GetAccessTokenString(ref this.appOnlyAccessTokenForSPAppWeb, 838 | () => TokenHelper.GetS2SAccessTokenWithWindowsIdentity(this.SPAppWebUrl, null)); 839 | } 840 | } 841 | 842 | public SharePointHighTrustContext(Uri spHostUrl, Uri spAppWebUrl, string spLanguage, string spClientTag, string spProductNumber, WindowsIdentity logonUserIdentity) 843 | : base(spHostUrl, spAppWebUrl, spLanguage, spClientTag, spProductNumber) 844 | { 845 | if (logonUserIdentity == null) 846 | { 847 | throw new ArgumentNullException("logonUserIdentity"); 848 | } 849 | 850 | this.logonUserIdentity = logonUserIdentity; 851 | } 852 | 853 | /// 854 | /// Ensures the access token is valid and returns it. 855 | /// 856 | /// The access token to verify. 857 | /// The token renewal handler. 858 | /// The access token string. 859 | private static string GetAccessTokenString(ref Tuple accessToken, Func tokenRenewalHandler) 860 | { 861 | RenewAccessTokenIfNeeded(ref accessToken, tokenRenewalHandler); 862 | 863 | return IsAccessTokenValid(accessToken) ? accessToken.Item1 : null; 864 | } 865 | 866 | /// 867 | /// Renews the access token if it is not valid. 868 | /// 869 | /// The access token to renew. 870 | /// The token renewal handler. 871 | private static void RenewAccessTokenIfNeeded(ref Tuple accessToken, Func tokenRenewalHandler) 872 | { 873 | if (IsAccessTokenValid(accessToken)) 874 | { 875 | return; 876 | } 877 | 878 | DateTime expiresOn = DateTime.UtcNow.Add(TokenHelper.HighTrustAccessTokenLifetime); 879 | 880 | if (TokenHelper.HighTrustAccessTokenLifetime > AccessTokenLifetimeTolerance) 881 | { 882 | // Make the access token get renewed a bit earlier than the time when it expires 883 | // so that the calls to SharePoint with it will have enough time to complete successfully. 884 | expiresOn -= AccessTokenLifetimeTolerance; 885 | } 886 | 887 | accessToken = Tuple.Create(tokenRenewalHandler(), expiresOn); 888 | } 889 | } 890 | 891 | /// 892 | /// Default provider for SharePointHighTrustContext. 893 | /// 894 | public class SharePointHighTrustContextProvider : SharePointContextProvider 895 | { 896 | private const string SPContextKey = "SPContext"; 897 | 898 | protected override SharePointContext CreateSharePointContext(Uri spHostUrl, Uri spAppWebUrl, string spLanguage, string spClientTag, string spProductNumber, HttpRequestBase httpRequest) 899 | { 900 | WindowsIdentity logonUserIdentity = httpRequest.LogonUserIdentity; 901 | if (logonUserIdentity == null || !logonUserIdentity.IsAuthenticated || logonUserIdentity.IsGuest || logonUserIdentity.User == null) 902 | { 903 | return null; 904 | } 905 | 906 | return new SharePointHighTrustContext(spHostUrl, spAppWebUrl, spLanguage, spClientTag, spProductNumber, logonUserIdentity); 907 | } 908 | 909 | protected override bool ValidateSharePointContext(SharePointContext spContext, HttpContextBase httpContext) 910 | { 911 | SharePointHighTrustContext spHighTrustContext = spContext as SharePointHighTrustContext; 912 | 913 | if (spHighTrustContext != null) 914 | { 915 | Uri spHostUrl = SharePointContext.GetSPHostUrl(httpContext.Request); 916 | WindowsIdentity logonUserIdentity = httpContext.Request.LogonUserIdentity; 917 | 918 | return spHostUrl == spHighTrustContext.SPHostUrl && 919 | logonUserIdentity != null && 920 | logonUserIdentity.IsAuthenticated && 921 | !logonUserIdentity.IsGuest && 922 | logonUserIdentity.User == spHighTrustContext.LogonUserIdentity.User; 923 | } 924 | 925 | return false; 926 | } 927 | 928 | protected override SharePointContext LoadSharePointContext(HttpContextBase httpContext) 929 | { 930 | return httpContext.Session[SPContextKey] as SharePointHighTrustContext; 931 | } 932 | 933 | protected override void SaveSharePointContext(SharePointContext spContext, HttpContextBase httpContext) 934 | { 935 | httpContext.Session[SPContextKey] = spContext as SharePointHighTrustContext; 936 | } 937 | } 938 | 939 | #endregion HighTrust 940 | } 941 | 942 | /* 943 | SharePoint Add-in REST/OData Basic Data Operations, https://github.com/OfficeDev/SharePoint-Add-in-REST-OData-BasicDataOperations 944 | 945 | Copyright (c) Microsoft Corporation 946 | All rights reserved. 947 | 948 | MIT License: 949 | Permission is hereby granted, free of charge, to any person obtaining 950 | a copy of this software and associated documentation files (the 951 | "Software"), to deal in the Software without restriction, including 952 | without limitation the rights to use, copy, modify, merge, publish, 953 | distribute, sublicense, and/or sell copies of the Software, and to 954 | permit persons to whom the Software is furnished to do so, subject to 955 | the following conditions: 956 | 957 | The above copyright notice and this permission notice shall be 958 | included in all copies or substantial portions of the Software. 959 | 960 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 961 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 962 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 963 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 964 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 965 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 966 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 967 | 968 | */ -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperationsWeb/TokenHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See full license at the bottom of this file. 2 | 3 | using Microsoft.IdentityModel; 4 | using Microsoft.IdentityModel.S2S.Protocols.OAuth2; 5 | using Microsoft.IdentityModel.S2S.Tokens; 6 | using Microsoft.SharePoint.Client; 7 | using Microsoft.SharePoint.Client.EventReceivers; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Collections.ObjectModel; 11 | using System.Globalization; 12 | using System.IdentityModel.Selectors; 13 | using System.IdentityModel.Tokens; 14 | using System.IO; 15 | using System.Linq; 16 | using System.Net; 17 | using System.Security.Cryptography.X509Certificates; 18 | using System.Security.Principal; 19 | using System.ServiceModel; 20 | using System.Text; 21 | using System.Web; 22 | using System.Web.Configuration; 23 | using System.Web.Script.Serialization; 24 | using AudienceRestriction = Microsoft.IdentityModel.Tokens.AudienceRestriction; 25 | using AudienceUriValidationFailedException = Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException; 26 | using SecurityTokenHandlerConfiguration = Microsoft.IdentityModel.Tokens.SecurityTokenHandlerConfiguration; 27 | using X509SigningCredentials = Microsoft.IdentityModel.SecurityTokenService.X509SigningCredentials; 28 | 29 | namespace SharePoint_Add_in_REST_OData_BasicDataOperationsWeb 30 | { 31 | public static class TokenHelper 32 | { 33 | #region public fields 34 | 35 | /// 36 | /// SharePoint principal. 37 | /// 38 | public const string SharePointPrincipal = "00000003-0000-0ff1-ce00-000000000000"; 39 | 40 | /// 41 | /// Lifetime of HighTrust access token, 12 hours. 42 | /// 43 | public static readonly TimeSpan HighTrustAccessTokenLifetime = TimeSpan.FromHours(12.0); 44 | 45 | #endregion public fields 46 | 47 | #region public methods 48 | 49 | /// 50 | /// Retrieves the context token string from the specified request by looking for well-known parameter names in the 51 | /// POSTed form parameters and the querystring. Returns null if no context token is found. 52 | /// 53 | /// HttpRequest in which to look for a context token 54 | /// The context token string 55 | public static string GetContextTokenFromRequest(HttpRequest request) 56 | { 57 | return GetContextTokenFromRequest(new HttpRequestWrapper(request)); 58 | } 59 | 60 | /// 61 | /// Retrieves the context token string from the specified request by looking for well-known parameter names in the 62 | /// POSTed form parameters and the querystring. Returns null if no context token is found. 63 | /// 64 | /// HttpRequest in which to look for a context token 65 | /// The context token string 66 | public static string GetContextTokenFromRequest(HttpRequestBase request) 67 | { 68 | string[] paramNames = { "AppContext", "AppContextToken", "AccessToken", "SPAppToken" }; 69 | foreach (string paramName in paramNames) 70 | { 71 | if (!string.IsNullOrEmpty(request.Form[paramName])) 72 | { 73 | return request.Form[paramName]; 74 | } 75 | if (!string.IsNullOrEmpty(request.QueryString[paramName])) 76 | { 77 | return request.QueryString[paramName]; 78 | } 79 | } 80 | return null; 81 | } 82 | 83 | /// 84 | /// Validate that a specified context token string is intended for this application based on the parameters 85 | /// specified in web.config. Parameters used from web.config used for validation include ClientId, 86 | /// HostedAppHostNameOverride, HostedAppHostName, ClientSecret, and Realm (if it is specified). If HostedAppHostNameOverride is present, 87 | /// it will be used for validation. Otherwise, if the is not 88 | /// null, it is used for validation instead of the web.config's HostedAppHostName. If the token is invalid, an 89 | /// exception is thrown. If the token is valid, TokenHelper's static STS metadata url is updated based on the token contents 90 | /// and a JsonWebSecurityToken based on the context token is returned. 91 | /// 92 | /// The context token to validate 93 | /// The URL authority, consisting of Domain Name System (DNS) host name or IP address and the port number, to use for token audience validation. 94 | /// If null, HostedAppHostName web.config setting is used instead. HostedAppHostNameOverride web.config setting, if present, will be used 95 | /// for validation instead of . 96 | /// A JsonWebSecurityToken based on the context token. 97 | public static SharePointContextToken ReadAndValidateContextToken(string contextTokenString, string appHostName = null) 98 | { 99 | JsonWebSecurityTokenHandler tokenHandler = CreateJsonWebSecurityTokenHandler(); 100 | SecurityToken securityToken = tokenHandler.ReadToken(contextTokenString); 101 | JsonWebSecurityToken jsonToken = securityToken as JsonWebSecurityToken; 102 | SharePointContextToken token = SharePointContextToken.Create(jsonToken); 103 | 104 | string stsAuthority = (new Uri(token.SecurityTokenServiceUri)).Authority; 105 | int firstDot = stsAuthority.IndexOf('.'); 106 | 107 | GlobalEndPointPrefix = stsAuthority.Substring(0, firstDot); 108 | AcsHostUrl = stsAuthority.Substring(firstDot + 1); 109 | 110 | tokenHandler.ValidateToken(jsonToken); 111 | 112 | string[] acceptableAudiences; 113 | if (!String.IsNullOrEmpty(HostedAppHostNameOverride)) 114 | { 115 | acceptableAudiences = HostedAppHostNameOverride.Split(';'); 116 | } 117 | else if (appHostName == null) 118 | { 119 | acceptableAudiences = new[] { HostedAppHostName }; 120 | } 121 | else 122 | { 123 | acceptableAudiences = new[] { appHostName }; 124 | } 125 | 126 | bool validationSuccessful = false; 127 | string realm = Realm ?? token.Realm; 128 | foreach (var audience in acceptableAudiences) 129 | { 130 | string principal = GetFormattedPrincipal(ClientId, audience, realm); 131 | if (StringComparer.OrdinalIgnoreCase.Equals(token.Audience, principal)) 132 | { 133 | validationSuccessful = true; 134 | break; 135 | } 136 | } 137 | 138 | if (!validationSuccessful) 139 | { 140 | throw new AudienceUriValidationFailedException( 141 | String.Format(CultureInfo.CurrentCulture, 142 | "\"{0}\" is not the intended audience \"{1}\"", String.Join(";", acceptableAudiences), token.Audience)); 143 | } 144 | 145 | return token; 146 | } 147 | 148 | /// 149 | /// Retrieves an access token from ACS to call the source of the specified context token at the specified 150 | /// targetHost. The targetHost must be registered for the principal that sent the context token. 151 | /// 152 | /// Context token issued by the intended access token audience 153 | /// Url authority of the target principal 154 | /// An access token with an audience matching the context token's source 155 | public static OAuth2AccessTokenResponse GetAccessToken(SharePointContextToken contextToken, string targetHost) 156 | { 157 | string targetPrincipalName = contextToken.TargetPrincipalName; 158 | 159 | // Extract the refreshToken from the context token 160 | string refreshToken = contextToken.RefreshToken; 161 | 162 | if (String.IsNullOrEmpty(refreshToken)) 163 | { 164 | return null; 165 | } 166 | 167 | string targetRealm = Realm ?? contextToken.Realm; 168 | 169 | return GetAccessToken(refreshToken, 170 | targetPrincipalName, 171 | targetHost, 172 | targetRealm); 173 | } 174 | 175 | /// 176 | /// Uses the specified authorization code to retrieve an access token from ACS to call the specified principal 177 | /// at the specified targetHost. The targetHost must be registered for target principal. If specified realm is 178 | /// null, the "Realm" setting in web.config will be used instead. 179 | /// 180 | /// Authorization code to exchange for access token 181 | /// Name of the target principal to retrieve an access token for 182 | /// Url authority of the target principal 183 | /// Realm to use for the access token's nameid and audience 184 | /// Redirect URI registerd for this app 185 | /// An access token with an audience of the target principal 186 | public static OAuth2AccessTokenResponse GetAccessToken( 187 | string authorizationCode, 188 | string targetPrincipalName, 189 | string targetHost, 190 | string targetRealm, 191 | Uri redirectUri) 192 | { 193 | if (targetRealm == null) 194 | { 195 | targetRealm = Realm; 196 | } 197 | 198 | string resource = GetFormattedPrincipal(targetPrincipalName, targetHost, targetRealm); 199 | string clientId = GetFormattedPrincipal(ClientId, null, targetRealm); 200 | 201 | // Create request for token. The RedirectUri is null here. This will fail if redirect uri is registered 202 | OAuth2AccessTokenRequest oauth2Request = 203 | OAuth2MessageFactory.CreateAccessTokenRequestWithAuthorizationCode( 204 | clientId, 205 | ClientSecret, 206 | authorizationCode, 207 | redirectUri, 208 | resource); 209 | 210 | // Get token 211 | OAuth2S2SClient client = new OAuth2S2SClient(); 212 | OAuth2AccessTokenResponse oauth2Response; 213 | try 214 | { 215 | oauth2Response = 216 | client.Issue(AcsMetadataParser.GetStsUrl(targetRealm), oauth2Request) as OAuth2AccessTokenResponse; 217 | } 218 | catch (WebException wex) 219 | { 220 | using (StreamReader sr = new StreamReader(wex.Response.GetResponseStream())) 221 | { 222 | string responseText = sr.ReadToEnd(); 223 | throw new WebException(wex.Message + " - " + responseText, wex); 224 | } 225 | } 226 | 227 | return oauth2Response; 228 | } 229 | 230 | /// 231 | /// Uses the specified refresh token to retrieve an access token from ACS to call the specified principal 232 | /// at the specified targetHost. The targetHost must be registered for target principal. If specified realm is 233 | /// null, the "Realm" setting in web.config will be used instead. 234 | /// 235 | /// Refresh token to exchange for access token 236 | /// Name of the target principal to retrieve an access token for 237 | /// Url authority of the target principal 238 | /// Realm to use for the access token's nameid and audience 239 | /// An access token with an audience of the target principal 240 | public static OAuth2AccessTokenResponse GetAccessToken( 241 | string refreshToken, 242 | string targetPrincipalName, 243 | string targetHost, 244 | string targetRealm) 245 | { 246 | if (targetRealm == null) 247 | { 248 | targetRealm = Realm; 249 | } 250 | 251 | string resource = GetFormattedPrincipal(targetPrincipalName, targetHost, targetRealm); 252 | string clientId = GetFormattedPrincipal(ClientId, null, targetRealm); 253 | 254 | OAuth2AccessTokenRequest oauth2Request = OAuth2MessageFactory.CreateAccessTokenRequestWithRefreshToken(clientId, ClientSecret, refreshToken, resource); 255 | 256 | // Get token 257 | OAuth2S2SClient client = new OAuth2S2SClient(); 258 | OAuth2AccessTokenResponse oauth2Response; 259 | try 260 | { 261 | oauth2Response = 262 | client.Issue(AcsMetadataParser.GetStsUrl(targetRealm), oauth2Request) as OAuth2AccessTokenResponse; 263 | } 264 | catch (WebException wex) 265 | { 266 | using (StreamReader sr = new StreamReader(wex.Response.GetResponseStream())) 267 | { 268 | string responseText = sr.ReadToEnd(); 269 | throw new WebException(wex.Message + " - " + responseText, wex); 270 | } 271 | } 272 | 273 | return oauth2Response; 274 | } 275 | 276 | /// 277 | /// Retrieves an app-only access token from ACS to call the specified principal 278 | /// at the specified targetHost. The targetHost must be registered for target principal. If specified realm is 279 | /// null, the "Realm" setting in web.config will be used instead. 280 | /// 281 | /// Name of the target principal to retrieve an access token for 282 | /// Url authority of the target principal 283 | /// Realm to use for the access token's nameid and audience 284 | /// An access token with an audience of the target principal 285 | public static OAuth2AccessTokenResponse GetAppOnlyAccessToken( 286 | string targetPrincipalName, 287 | string targetHost, 288 | string targetRealm) 289 | { 290 | 291 | if (targetRealm == null) 292 | { 293 | targetRealm = Realm; 294 | } 295 | 296 | string resource = GetFormattedPrincipal(targetPrincipalName, targetHost, targetRealm); 297 | string clientId = GetFormattedPrincipal(ClientId, HostedAppHostName, targetRealm); 298 | 299 | OAuth2AccessTokenRequest oauth2Request = OAuth2MessageFactory.CreateAccessTokenRequestWithClientCredentials(clientId, ClientSecret, resource); 300 | oauth2Request.Resource = resource; 301 | 302 | // Get token 303 | OAuth2S2SClient client = new OAuth2S2SClient(); 304 | 305 | OAuth2AccessTokenResponse oauth2Response; 306 | try 307 | { 308 | oauth2Response = 309 | client.Issue(AcsMetadataParser.GetStsUrl(targetRealm), oauth2Request) as OAuth2AccessTokenResponse; 310 | } 311 | catch (WebException wex) 312 | { 313 | using (StreamReader sr = new StreamReader(wex.Response.GetResponseStream())) 314 | { 315 | string responseText = sr.ReadToEnd(); 316 | throw new WebException(wex.Message + " - " + responseText, wex); 317 | } 318 | } 319 | 320 | return oauth2Response; 321 | } 322 | 323 | /// 324 | /// Creates a client context based on the properties of a remote event receiver 325 | /// 326 | /// Properties of a remote event receiver 327 | /// A ClientContext ready to call the web where the event originated 328 | public static ClientContext CreateRemoteEventReceiverClientContext(SPRemoteEventProperties properties) 329 | { 330 | Uri sharepointUrl; 331 | if (properties.ListEventProperties != null) 332 | { 333 | sharepointUrl = new Uri(properties.ListEventProperties.WebUrl); 334 | } 335 | else if (properties.ItemEventProperties != null) 336 | { 337 | sharepointUrl = new Uri(properties.ItemEventProperties.WebUrl); 338 | } 339 | else if (properties.WebEventProperties != null) 340 | { 341 | sharepointUrl = new Uri(properties.WebEventProperties.FullUrl); 342 | } 343 | else 344 | { 345 | return null; 346 | } 347 | 348 | if (IsHighTrustApp()) 349 | { 350 | return GetS2SClientContextWithWindowsIdentity(sharepointUrl, null); 351 | } 352 | 353 | return CreateAcsClientContextForUrl(properties, sharepointUrl); 354 | } 355 | 356 | /// 357 | /// Creates a client context based on the properties of an app event 358 | /// 359 | /// Properties of an app event 360 | /// True to target the app web, false to target the host web 361 | /// A ClientContext ready to call the app web or the parent web 362 | public static ClientContext CreateAppEventClientContext(SPRemoteEventProperties properties, bool useAppWeb) 363 | { 364 | if (properties.AppEventProperties == null) 365 | { 366 | return null; 367 | } 368 | 369 | Uri sharepointUrl = useAppWeb ? properties.AppEventProperties.AppWebFullUrl : properties.AppEventProperties.HostWebFullUrl; 370 | if (IsHighTrustApp()) 371 | { 372 | return GetS2SClientContextWithWindowsIdentity(sharepointUrl, null); 373 | } 374 | 375 | return CreateAcsClientContextForUrl(properties, sharepointUrl); 376 | } 377 | 378 | /// 379 | /// Retrieves an access token from ACS using the specified authorization code, and uses that access token to 380 | /// create a client context 381 | /// 382 | /// Url of the target SharePoint site 383 | /// Authorization code to use when retrieving the access token from ACS 384 | /// Redirect URI registerd for this app 385 | /// A ClientContext ready to call targetUrl with a valid access token 386 | public static ClientContext GetClientContextWithAuthorizationCode( 387 | string targetUrl, 388 | string authorizationCode, 389 | Uri redirectUri) 390 | { 391 | return GetClientContextWithAuthorizationCode(targetUrl, SharePointPrincipal, authorizationCode, GetRealmFromTargetUrl(new Uri(targetUrl)), redirectUri); 392 | } 393 | 394 | /// 395 | /// Retrieves an access token from ACS using the specified authorization code, and uses that access token to 396 | /// create a client context 397 | /// 398 | /// Url of the target SharePoint site 399 | /// Name of the target SharePoint principal 400 | /// Authorization code to use when retrieving the access token from ACS 401 | /// Realm to use for the access token's nameid and audience 402 | /// Redirect URI registerd for this app 403 | /// A ClientContext ready to call targetUrl with a valid access token 404 | public static ClientContext GetClientContextWithAuthorizationCode( 405 | string targetUrl, 406 | string targetPrincipalName, 407 | string authorizationCode, 408 | string targetRealm, 409 | Uri redirectUri) 410 | { 411 | Uri targetUri = new Uri(targetUrl); 412 | 413 | string accessToken = 414 | GetAccessToken(authorizationCode, targetPrincipalName, targetUri.Authority, targetRealm, redirectUri).AccessToken; 415 | 416 | return GetClientContextWithAccessToken(targetUrl, accessToken); 417 | } 418 | 419 | /// 420 | /// Uses the specified access token to create a client context 421 | /// 422 | /// Url of the target SharePoint site 423 | /// Access token to be used when calling the specified targetUrl 424 | /// A ClientContext ready to call targetUrl with the specified access token 425 | public static ClientContext GetClientContextWithAccessToken(string targetUrl, string accessToken) 426 | { 427 | ClientContext clientContext = new ClientContext(targetUrl); 428 | 429 | clientContext.AuthenticationMode = ClientAuthenticationMode.Anonymous; 430 | clientContext.FormDigestHandlingEnabled = false; 431 | clientContext.ExecutingWebRequest += 432 | delegate(object oSender, WebRequestEventArgs webRequestEventArgs) 433 | { 434 | webRequestEventArgs.WebRequestExecutor.RequestHeaders["Authorization"] = 435 | "Bearer " + accessToken; 436 | }; 437 | 438 | return clientContext; 439 | } 440 | 441 | /// 442 | /// Retrieves an access token from ACS using the specified context token, and uses that access token to create 443 | /// a client context 444 | /// 445 | /// Url of the target SharePoint site 446 | /// Context token received from the target SharePoint site 447 | /// Url authority of the hosted app. If this is null, the value in the HostedAppHostName 448 | /// of web.config will be used instead 449 | /// A ClientContext ready to call targetUrl with a valid access token 450 | public static ClientContext GetClientContextWithContextToken( 451 | string targetUrl, 452 | string contextTokenString, 453 | string appHostUrl) 454 | { 455 | SharePointContextToken contextToken = ReadAndValidateContextToken(contextTokenString, appHostUrl); 456 | 457 | Uri targetUri = new Uri(targetUrl); 458 | 459 | string accessToken = GetAccessToken(contextToken, targetUri.Authority).AccessToken; 460 | 461 | return GetClientContextWithAccessToken(targetUrl, accessToken); 462 | } 463 | 464 | /// 465 | /// Returns the SharePoint url to which the app should redirect the browser to request consent and get back 466 | /// an authorization code. 467 | /// 468 | /// Absolute Url of the SharePoint site 469 | /// Space-delimited permissions to request from the SharePoint site in "shorthand" format 470 | /// (e.g. "Web.Read Site.Write") 471 | /// Url of the SharePoint site's OAuth authorization page 472 | public static string GetAuthorizationUrl(string contextUrl, string scope) 473 | { 474 | return string.Format( 475 | "{0}{1}?IsDlg=1&client_id={2}&scope={3}&response_type=code", 476 | EnsureTrailingSlash(contextUrl), 477 | AuthorizationPage, 478 | ClientId, 479 | scope); 480 | } 481 | 482 | /// 483 | /// Returns the SharePoint url to which the app should redirect the browser to request consent and get back 484 | /// an authorization code. 485 | /// 486 | /// Absolute Url of the SharePoint site 487 | /// Space-delimited permissions to request from the SharePoint site in "shorthand" format 488 | /// (e.g. "Web.Read Site.Write") 489 | /// Uri to which SharePoint should redirect the browser to after consent is 490 | /// granted 491 | /// Url of the SharePoint site's OAuth authorization page 492 | public static string GetAuthorizationUrl(string contextUrl, string scope, string redirectUri) 493 | { 494 | return string.Format( 495 | "{0}{1}?IsDlg=1&client_id={2}&scope={3}&response_type=code&redirect_uri={4}", 496 | EnsureTrailingSlash(contextUrl), 497 | AuthorizationPage, 498 | ClientId, 499 | scope, 500 | redirectUri); 501 | } 502 | 503 | /// 504 | /// Returns the SharePoint url to which the app should redirect the browser to request a new context token. 505 | /// 506 | /// Absolute Url of the SharePoint site 507 | /// Uri to which SharePoint should redirect the browser to with a context token 508 | /// Url of the SharePoint site's context token redirect page 509 | public static string GetAppContextTokenRequestUrl(string contextUrl, string redirectUri) 510 | { 511 | return string.Format( 512 | "{0}{1}?client_id={2}&redirect_uri={3}", 513 | EnsureTrailingSlash(contextUrl), 514 | RedirectPage, 515 | ClientId, 516 | redirectUri); 517 | } 518 | 519 | /// 520 | /// Retrieves an S2S access token signed by the application's private certificate on behalf of the specified 521 | /// WindowsIdentity and intended for the SharePoint at the targetApplicationUri. If no Realm is specified in 522 | /// web.config, an auth challenge will be issued to the targetApplicationUri to discover it. 523 | /// 524 | /// Url of the target SharePoint site 525 | /// Windows identity of the user on whose behalf to create the access token 526 | /// An access token with an audience of the target principal 527 | public static string GetS2SAccessTokenWithWindowsIdentity( 528 | Uri targetApplicationUri, 529 | WindowsIdentity identity) 530 | { 531 | string realm = string.IsNullOrEmpty(Realm) ? GetRealmFromTargetUrl(targetApplicationUri) : Realm; 532 | 533 | JsonWebTokenClaim[] claims = identity != null ? GetClaimsWithWindowsIdentity(identity) : null; 534 | 535 | return GetS2SAccessTokenWithClaims(targetApplicationUri.Authority, realm, claims); 536 | } 537 | 538 | /// 539 | /// Retrieves an S2S client context with an access token signed by the application's private certificate on 540 | /// behalf of the specified WindowsIdentity and intended for application at the targetApplicationUri using the 541 | /// targetRealm. If no Realm is specified in web.config, an auth challenge will be issued to the 542 | /// targetApplicationUri to discover it. 543 | /// 544 | /// Url of the target SharePoint site 545 | /// Windows identity of the user on whose behalf to create the access token 546 | /// A ClientContext using an access token with an audience of the target application 547 | public static ClientContext GetS2SClientContextWithWindowsIdentity( 548 | Uri targetApplicationUri, 549 | WindowsIdentity identity) 550 | { 551 | string realm = string.IsNullOrEmpty(Realm) ? GetRealmFromTargetUrl(targetApplicationUri) : Realm; 552 | 553 | JsonWebTokenClaim[] claims = identity != null ? GetClaimsWithWindowsIdentity(identity) : null; 554 | 555 | string accessToken = GetS2SAccessTokenWithClaims(targetApplicationUri.Authority, realm, claims); 556 | 557 | return GetClientContextWithAccessToken(targetApplicationUri.ToString(), accessToken); 558 | } 559 | 560 | /// 561 | /// Get authentication realm from SharePoint 562 | /// 563 | /// Url of the target SharePoint site 564 | /// String representation of the realm GUID 565 | public static string GetRealmFromTargetUrl(Uri targetApplicationUri) 566 | { 567 | WebRequest request = WebRequest.Create(targetApplicationUri + "/_vti_bin/client.svc"); 568 | request.Headers.Add("Authorization: Bearer "); 569 | 570 | try 571 | { 572 | using (request.GetResponse()) 573 | { 574 | } 575 | } 576 | catch (WebException e) 577 | { 578 | if (e.Response == null) 579 | { 580 | return null; 581 | } 582 | 583 | string bearerResponseHeader = e.Response.Headers["WWW-Authenticate"]; 584 | if (string.IsNullOrEmpty(bearerResponseHeader)) 585 | { 586 | return null; 587 | } 588 | 589 | const string bearer = "Bearer realm=\""; 590 | int bearerIndex = bearerResponseHeader.IndexOf(bearer, StringComparison.Ordinal); 591 | if (bearerIndex < 0) 592 | { 593 | return null; 594 | } 595 | 596 | int realmIndex = bearerIndex + bearer.Length; 597 | 598 | if (bearerResponseHeader.Length >= realmIndex + 36) 599 | { 600 | string targetRealm = bearerResponseHeader.Substring(realmIndex, 36); 601 | 602 | Guid realmGuid; 603 | 604 | if (Guid.TryParse(targetRealm, out realmGuid)) 605 | { 606 | return targetRealm; 607 | } 608 | } 609 | } 610 | return null; 611 | } 612 | 613 | /// 614 | /// Determines if this is a high trust app. 615 | /// 616 | /// True if this is a high trust app. 617 | public static bool IsHighTrustApp() 618 | { 619 | return SigningCredentials != null; 620 | } 621 | 622 | /// 623 | /// Ensures that the specified URL ends with '/' if it is not null or empty. 624 | /// 625 | /// The url. 626 | /// The url ending with '/' if it is not null or empty. 627 | public static string EnsureTrailingSlash(string url) 628 | { 629 | if (!string.IsNullOrEmpty(url) && url[url.Length - 1] != '/') 630 | { 631 | return url + "/"; 632 | } 633 | 634 | return url; 635 | } 636 | 637 | #endregion 638 | 639 | #region private fields 640 | 641 | // 642 | // Configuration Constants 643 | // 644 | 645 | private const string AuthorizationPage = "_layouts/15/OAuthAuthorize.aspx"; 646 | private const string RedirectPage = "_layouts/15/AppRedirect.aspx"; 647 | private const string AcsPrincipalName = "00000001-0000-0000-c000-000000000000"; 648 | private const string AcsMetadataEndPointRelativeUrl = "metadata/json/1"; 649 | private const string S2SProtocol = "OAuth2"; 650 | private const string DelegationIssuance = "DelegationIssuance1.0"; 651 | private const string NameIdentifierClaimType = JsonWebTokenConstants.ReservedClaims.NameIdentifier; 652 | private const string TrustedForImpersonationClaimType = "trustedfordelegation"; 653 | private const string ActorTokenClaimType = JsonWebTokenConstants.ReservedClaims.ActorToken; 654 | 655 | // 656 | // Environment Constants 657 | // 658 | 659 | private static string GlobalEndPointPrefix = "accounts"; 660 | private static string AcsHostUrl = "accesscontrol.windows.net"; 661 | 662 | // 663 | // Hosted app configuration 664 | // 665 | private static readonly string ClientId = string.IsNullOrEmpty(WebConfigurationManager.AppSettings.Get("ClientId")) ? WebConfigurationManager.AppSettings.Get("HostedAppName") : WebConfigurationManager.AppSettings.Get("ClientId"); 666 | private static readonly string IssuerId = string.IsNullOrEmpty(WebConfigurationManager.AppSettings.Get("IssuerId")) ? ClientId : WebConfigurationManager.AppSettings.Get("IssuerId"); 667 | private static readonly string HostedAppHostNameOverride = WebConfigurationManager.AppSettings.Get("HostedAppHostNameOverride"); 668 | private static readonly string HostedAppHostName = WebConfigurationManager.AppSettings.Get("HostedAppHostName"); 669 | private static readonly string ClientSecret = string.IsNullOrEmpty(WebConfigurationManager.AppSettings.Get("ClientSecret")) ? WebConfigurationManager.AppSettings.Get("HostedAppSigningKey") : WebConfigurationManager.AppSettings.Get("ClientSecret"); 670 | private static readonly string SecondaryClientSecret = WebConfigurationManager.AppSettings.Get("SecondaryClientSecret"); 671 | private static readonly string Realm = WebConfigurationManager.AppSettings.Get("Realm"); 672 | private static readonly string ServiceNamespace = WebConfigurationManager.AppSettings.Get("Realm"); 673 | 674 | private static readonly string ClientSigningCertificatePath = WebConfigurationManager.AppSettings.Get("ClientSigningCertificatePath"); 675 | private static readonly string ClientSigningCertificatePassword = WebConfigurationManager.AppSettings.Get("ClientSigningCertificatePassword"); 676 | private static readonly X509Certificate2 ClientCertificate = (string.IsNullOrEmpty(ClientSigningCertificatePath) || string.IsNullOrEmpty(ClientSigningCertificatePassword)) ? null : new X509Certificate2(ClientSigningCertificatePath, ClientSigningCertificatePassword); 677 | private static readonly X509SigningCredentials SigningCredentials = (ClientCertificate == null) ? null : new X509SigningCredentials(ClientCertificate, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest); 678 | 679 | #endregion 680 | 681 | #region private methods 682 | 683 | private static ClientContext CreateAcsClientContextForUrl(SPRemoteEventProperties properties, Uri sharepointUrl) 684 | { 685 | string contextTokenString = properties.ContextToken; 686 | 687 | if (String.IsNullOrEmpty(contextTokenString)) 688 | { 689 | return null; 690 | } 691 | 692 | SharePointContextToken contextToken = ReadAndValidateContextToken(contextTokenString, OperationContext.Current.IncomingMessageHeaders.To.Host); 693 | string accessToken = GetAccessToken(contextToken, sharepointUrl.Authority).AccessToken; 694 | 695 | return GetClientContextWithAccessToken(sharepointUrl.ToString(), accessToken); 696 | } 697 | 698 | private static string GetAcsMetadataEndpointUrl() 699 | { 700 | return Path.Combine(GetAcsGlobalEndpointUrl(), AcsMetadataEndPointRelativeUrl); 701 | } 702 | 703 | private static string GetFormattedPrincipal(string principalName, string hostName, string realm) 704 | { 705 | if (!String.IsNullOrEmpty(hostName)) 706 | { 707 | return String.Format(CultureInfo.InvariantCulture, "{0}/{1}@{2}", principalName, hostName, realm); 708 | } 709 | 710 | return String.Format(CultureInfo.InvariantCulture, "{0}@{1}", principalName, realm); 711 | } 712 | 713 | private static string GetAcsPrincipalName(string realm) 714 | { 715 | return GetFormattedPrincipal(AcsPrincipalName, new Uri(GetAcsGlobalEndpointUrl()).Host, realm); 716 | } 717 | 718 | private static string GetAcsGlobalEndpointUrl() 719 | { 720 | return String.Format(CultureInfo.InvariantCulture, "https://{0}.{1}/", GlobalEndPointPrefix, AcsHostUrl); 721 | } 722 | 723 | private static JsonWebSecurityTokenHandler CreateJsonWebSecurityTokenHandler() 724 | { 725 | JsonWebSecurityTokenHandler handler = new JsonWebSecurityTokenHandler(); 726 | handler.Configuration = new SecurityTokenHandlerConfiguration(); 727 | handler.Configuration.AudienceRestriction = new AudienceRestriction(AudienceUriMode.Never); 728 | handler.Configuration.CertificateValidator = X509CertificateValidator.None; 729 | 730 | List securityKeys = new List(); 731 | securityKeys.Add(Convert.FromBase64String(ClientSecret)); 732 | if (!string.IsNullOrEmpty(SecondaryClientSecret)) 733 | { 734 | securityKeys.Add(Convert.FromBase64String(SecondaryClientSecret)); 735 | } 736 | 737 | List securityTokens = new List(); 738 | securityTokens.Add(new MultipleSymmetricKeySecurityToken(securityKeys)); 739 | 740 | handler.Configuration.IssuerTokenResolver = 741 | SecurityTokenResolver.CreateDefaultSecurityTokenResolver( 742 | new ReadOnlyCollection(securityTokens), 743 | false); 744 | SymmetricKeyIssuerNameRegistry issuerNameRegistry = new SymmetricKeyIssuerNameRegistry(); 745 | foreach (byte[] securitykey in securityKeys) 746 | { 747 | issuerNameRegistry.AddTrustedIssuer(securitykey, GetAcsPrincipalName(ServiceNamespace)); 748 | } 749 | handler.Configuration.IssuerNameRegistry = issuerNameRegistry; 750 | return handler; 751 | } 752 | 753 | private static string GetS2SAccessTokenWithClaims( 754 | string targetApplicationHostName, 755 | string targetRealm, 756 | IEnumerable claims) 757 | { 758 | return IssueToken( 759 | ClientId, 760 | IssuerId, 761 | targetRealm, 762 | SharePointPrincipal, 763 | targetRealm, 764 | targetApplicationHostName, 765 | true, 766 | claims, 767 | claims == null); 768 | } 769 | 770 | private static JsonWebTokenClaim[] GetClaimsWithWindowsIdentity(WindowsIdentity identity) 771 | { 772 | JsonWebTokenClaim[] claims = new JsonWebTokenClaim[] 773 | { 774 | new JsonWebTokenClaim(NameIdentifierClaimType, identity.User.Value.ToLower()), 775 | new JsonWebTokenClaim("nii", "urn:office:idp:activedirectory") 776 | }; 777 | return claims; 778 | } 779 | 780 | private static string IssueToken( 781 | string sourceApplication, 782 | string issuerApplication, 783 | string sourceRealm, 784 | string targetApplication, 785 | string targetRealm, 786 | string targetApplicationHostName, 787 | bool trustedForDelegation, 788 | IEnumerable claims, 789 | bool appOnly = false) 790 | { 791 | if (null == SigningCredentials) 792 | { 793 | throw new InvalidOperationException("SigningCredentials was not initialized"); 794 | } 795 | 796 | #region Actor token 797 | 798 | string issuer = string.IsNullOrEmpty(sourceRealm) ? issuerApplication : string.Format("{0}@{1}", issuerApplication, sourceRealm); 799 | string nameid = string.IsNullOrEmpty(sourceRealm) ? sourceApplication : string.Format("{0}@{1}", sourceApplication, sourceRealm); 800 | string audience = string.Format("{0}/{1}@{2}", targetApplication, targetApplicationHostName, targetRealm); 801 | 802 | List actorClaims = new List(); 803 | actorClaims.Add(new JsonWebTokenClaim(JsonWebTokenConstants.ReservedClaims.NameIdentifier, nameid)); 804 | if (trustedForDelegation && !appOnly) 805 | { 806 | actorClaims.Add(new JsonWebTokenClaim(TrustedForImpersonationClaimType, "true")); 807 | } 808 | 809 | // Create token 810 | JsonWebSecurityToken actorToken = new JsonWebSecurityToken( 811 | issuer: issuer, 812 | audience: audience, 813 | validFrom: DateTime.UtcNow, 814 | validTo: DateTime.UtcNow.Add(HighTrustAccessTokenLifetime), 815 | signingCredentials: SigningCredentials, 816 | claims: actorClaims); 817 | 818 | string actorTokenString = new JsonWebSecurityTokenHandler().WriteTokenAsString(actorToken); 819 | 820 | if (appOnly) 821 | { 822 | // App-only token is the same as actor token for delegated case 823 | return actorTokenString; 824 | } 825 | 826 | #endregion Actor token 827 | 828 | #region Outer token 829 | 830 | List outerClaims = null == claims ? new List() : new List(claims); 831 | outerClaims.Add(new JsonWebTokenClaim(ActorTokenClaimType, actorTokenString)); 832 | 833 | JsonWebSecurityToken jsonToken = new JsonWebSecurityToken( 834 | nameid, // outer token issuer should match actor token nameid 835 | audience, 836 | DateTime.UtcNow, 837 | DateTime.UtcNow.Add(HighTrustAccessTokenLifetime), 838 | outerClaims); 839 | 840 | string accessToken = new JsonWebSecurityTokenHandler().WriteTokenAsString(jsonToken); 841 | 842 | #endregion Outer token 843 | 844 | return accessToken; 845 | } 846 | 847 | #endregion 848 | 849 | #region AcsMetadataParser 850 | 851 | // This class is used to get MetaData document from the global STS endpoint. It contains 852 | // methods to parse the MetaData document and get endpoints and STS certificate. 853 | public static class AcsMetadataParser 854 | { 855 | public static X509Certificate2 GetAcsSigningCert(string realm) 856 | { 857 | JsonMetadataDocument document = GetMetadataDocument(realm); 858 | 859 | if (null != document.keys && document.keys.Count > 0) 860 | { 861 | JsonKey signingKey = document.keys[0]; 862 | 863 | if (null != signingKey && null != signingKey.keyValue) 864 | { 865 | return new X509Certificate2(Encoding.UTF8.GetBytes(signingKey.keyValue.value)); 866 | } 867 | } 868 | 869 | throw new Exception("Metadata document does not contain ACS signing certificate."); 870 | } 871 | 872 | public static string GetDelegationServiceUrl(string realm) 873 | { 874 | JsonMetadataDocument document = GetMetadataDocument(realm); 875 | 876 | JsonEndpoint delegationEndpoint = document.endpoints.SingleOrDefault(e => e.protocol == DelegationIssuance); 877 | 878 | if (null != delegationEndpoint) 879 | { 880 | return delegationEndpoint.location; 881 | } 882 | throw new Exception("Metadata document does not contain Delegation Service endpoint Url"); 883 | } 884 | 885 | private static JsonMetadataDocument GetMetadataDocument(string realm) 886 | { 887 | string acsMetadataEndpointUrlWithRealm = String.Format(CultureInfo.InvariantCulture, "{0}?realm={1}", 888 | GetAcsMetadataEndpointUrl(), 889 | realm); 890 | byte[] acsMetadata; 891 | using (WebClient webClient = new WebClient()) 892 | { 893 | 894 | acsMetadata = webClient.DownloadData(acsMetadataEndpointUrlWithRealm); 895 | } 896 | string jsonResponseString = Encoding.UTF8.GetString(acsMetadata); 897 | 898 | JavaScriptSerializer serializer = new JavaScriptSerializer(); 899 | JsonMetadataDocument document = serializer.Deserialize(jsonResponseString); 900 | 901 | if (null == document) 902 | { 903 | throw new Exception("No metadata document found at the global endpoint " + acsMetadataEndpointUrlWithRealm); 904 | } 905 | 906 | return document; 907 | } 908 | 909 | public static string GetStsUrl(string realm) 910 | { 911 | JsonMetadataDocument document = GetMetadataDocument(realm); 912 | 913 | JsonEndpoint s2sEndpoint = document.endpoints.SingleOrDefault(e => e.protocol == S2SProtocol); 914 | 915 | if (null != s2sEndpoint) 916 | { 917 | return s2sEndpoint.location; 918 | } 919 | 920 | throw new Exception("Metadata document does not contain STS endpoint url"); 921 | } 922 | 923 | private class JsonMetadataDocument 924 | { 925 | public string serviceName { get; set; } 926 | public List endpoints { get; set; } 927 | public List keys { get; set; } 928 | } 929 | 930 | private class JsonEndpoint 931 | { 932 | public string location { get; set; } 933 | public string protocol { get; set; } 934 | public string usage { get; set; } 935 | } 936 | 937 | private class JsonKeyValue 938 | { 939 | public string type { get; set; } 940 | public string value { get; set; } 941 | } 942 | 943 | private class JsonKey 944 | { 945 | public string usage { get; set; } 946 | public JsonKeyValue keyValue { get; set; } 947 | } 948 | } 949 | 950 | #endregion 951 | } 952 | 953 | /// 954 | /// A JsonWebSecurityToken generated by SharePoint to authenticate to a 3rd party application and allow callbacks using a refresh token 955 | /// 956 | public class SharePointContextToken : JsonWebSecurityToken 957 | { 958 | public static SharePointContextToken Create(JsonWebSecurityToken contextToken) 959 | { 960 | return new SharePointContextToken(contextToken.Issuer, contextToken.Audience, contextToken.ValidFrom, contextToken.ValidTo, contextToken.Claims); 961 | } 962 | 963 | public SharePointContextToken(string issuer, string audience, DateTime validFrom, DateTime validTo, IEnumerable claims) 964 | : base(issuer, audience, validFrom, validTo, claims) 965 | { 966 | } 967 | 968 | public SharePointContextToken(string issuer, string audience, DateTime validFrom, DateTime validTo, IEnumerable claims, SecurityToken issuerToken, JsonWebSecurityToken actorToken) 969 | : base(issuer, audience, validFrom, validTo, claims, issuerToken, actorToken) 970 | { 971 | } 972 | 973 | public SharePointContextToken(string issuer, string audience, DateTime validFrom, DateTime validTo, IEnumerable claims, SigningCredentials signingCredentials) 974 | : base(issuer, audience, validFrom, validTo, claims, signingCredentials) 975 | { 976 | } 977 | 978 | public string NameId 979 | { 980 | get 981 | { 982 | return GetClaimValue(this, "nameid"); 983 | } 984 | } 985 | 986 | /// 987 | /// The principal name portion of the context token's "appctxsender" claim 988 | /// 989 | public string TargetPrincipalName 990 | { 991 | get 992 | { 993 | string appctxsender = GetClaimValue(this, "appctxsender"); 994 | 995 | if (appctxsender == null) 996 | { 997 | return null; 998 | } 999 | 1000 | return appctxsender.Split('@')[0]; 1001 | } 1002 | } 1003 | 1004 | /// 1005 | /// The context token's "refreshtoken" claim 1006 | /// 1007 | public string RefreshToken 1008 | { 1009 | get 1010 | { 1011 | return GetClaimValue(this, "refreshtoken"); 1012 | } 1013 | } 1014 | 1015 | /// 1016 | /// The context token's "CacheKey" claim 1017 | /// 1018 | public string CacheKey 1019 | { 1020 | get 1021 | { 1022 | string appctx = GetClaimValue(this, "appctx"); 1023 | if (appctx == null) 1024 | { 1025 | return null; 1026 | } 1027 | 1028 | ClientContext ctx = new ClientContext("http://tempuri.org"); 1029 | Dictionary dict = (Dictionary)ctx.ParseObjectFromJsonString(appctx); 1030 | string cacheKey = (string)dict["CacheKey"]; 1031 | 1032 | return cacheKey; 1033 | } 1034 | } 1035 | 1036 | /// 1037 | /// The context token's "SecurityTokenServiceUri" claim 1038 | /// 1039 | public string SecurityTokenServiceUri 1040 | { 1041 | get 1042 | { 1043 | string appctx = GetClaimValue(this, "appctx"); 1044 | if (appctx == null) 1045 | { 1046 | return null; 1047 | } 1048 | 1049 | ClientContext ctx = new ClientContext("http://tempuri.org"); 1050 | Dictionary dict = (Dictionary)ctx.ParseObjectFromJsonString(appctx); 1051 | string securityTokenServiceUri = (string)dict["SecurityTokenServiceUri"]; 1052 | 1053 | return securityTokenServiceUri; 1054 | } 1055 | } 1056 | 1057 | /// 1058 | /// The realm portion of the context token's "audience" claim 1059 | /// 1060 | public string Realm 1061 | { 1062 | get 1063 | { 1064 | string aud = Audience; 1065 | if (aud == null) 1066 | { 1067 | return null; 1068 | } 1069 | 1070 | string tokenRealm = aud.Substring(aud.IndexOf('@') + 1); 1071 | 1072 | return tokenRealm; 1073 | } 1074 | } 1075 | 1076 | private static string GetClaimValue(JsonWebSecurityToken token, string claimType) 1077 | { 1078 | if (token == null) 1079 | { 1080 | throw new ArgumentNullException("token"); 1081 | } 1082 | 1083 | foreach (JsonWebTokenClaim claim in token.Claims) 1084 | { 1085 | if (StringComparer.Ordinal.Equals(claim.ClaimType, claimType)) 1086 | { 1087 | return claim.Value; 1088 | } 1089 | } 1090 | 1091 | return null; 1092 | } 1093 | 1094 | } 1095 | 1096 | /// 1097 | /// Represents a security token which contains multiple security keys that are generated using symmetric algorithms. 1098 | /// 1099 | public class MultipleSymmetricKeySecurityToken : SecurityToken 1100 | { 1101 | /// 1102 | /// Initializes a new instance of the MultipleSymmetricKeySecurityToken class. 1103 | /// 1104 | /// An enumeration of Byte arrays that contain the symmetric keys. 1105 | public MultipleSymmetricKeySecurityToken(IEnumerable keys) 1106 | : this(UniqueId.CreateUniqueId(), keys) 1107 | { 1108 | } 1109 | 1110 | /// 1111 | /// Initializes a new instance of the MultipleSymmetricKeySecurityToken class. 1112 | /// 1113 | /// The unique identifier of the security token. 1114 | /// An enumeration of Byte arrays that contain the symmetric keys. 1115 | public MultipleSymmetricKeySecurityToken(string tokenId, IEnumerable keys) 1116 | { 1117 | if (keys == null) 1118 | { 1119 | throw new ArgumentNullException("keys"); 1120 | } 1121 | 1122 | if (String.IsNullOrEmpty(tokenId)) 1123 | { 1124 | throw new ArgumentException("Value cannot be a null or empty string.", "tokenId"); 1125 | } 1126 | 1127 | foreach (byte[] key in keys) 1128 | { 1129 | if (key.Length <= 0) 1130 | { 1131 | throw new ArgumentException("The key length must be greater then zero.", "keys"); 1132 | } 1133 | } 1134 | 1135 | id = tokenId; 1136 | effectiveTime = DateTime.UtcNow; 1137 | securityKeys = CreateSymmetricSecurityKeys(keys); 1138 | } 1139 | 1140 | /// 1141 | /// Gets the unique identifier of the security token. 1142 | /// 1143 | public override string Id 1144 | { 1145 | get 1146 | { 1147 | return id; 1148 | } 1149 | } 1150 | 1151 | /// 1152 | /// Gets the cryptographic keys associated with the security token. 1153 | /// 1154 | public override ReadOnlyCollection SecurityKeys 1155 | { 1156 | get 1157 | { 1158 | return securityKeys.AsReadOnly(); 1159 | } 1160 | } 1161 | 1162 | /// 1163 | /// Gets the first instant in time at which this security token is valid. 1164 | /// 1165 | public override DateTime ValidFrom 1166 | { 1167 | get 1168 | { 1169 | return effectiveTime; 1170 | } 1171 | } 1172 | 1173 | /// 1174 | /// Gets the last instant in time at which this security token is valid. 1175 | /// 1176 | public override DateTime ValidTo 1177 | { 1178 | get 1179 | { 1180 | // Never expire 1181 | return DateTime.MaxValue; 1182 | } 1183 | } 1184 | 1185 | /// 1186 | /// Returns a value that indicates whether the key identifier for this instance can be resolved to the specified key identifier. 1187 | /// 1188 | /// A SecurityKeyIdentifierClause to compare to this instance 1189 | /// true if keyIdentifierClause is a SecurityKeyIdentifierClause and it has the same unique identifier as the Id property; otherwise, false. 1190 | public override bool MatchesKeyIdentifierClause(SecurityKeyIdentifierClause keyIdentifierClause) 1191 | { 1192 | if (keyIdentifierClause == null) 1193 | { 1194 | throw new ArgumentNullException("keyIdentifierClause"); 1195 | } 1196 | 1197 | // Since this is a symmetric token and we do not have IDs to distinguish tokens, we just check for the 1198 | // presence of a SymmetricIssuerKeyIdentifier. The actual mapping to the issuer takes place later 1199 | // when the key is matched to the issuer. 1200 | if (keyIdentifierClause is SymmetricIssuerKeyIdentifierClause) 1201 | { 1202 | return true; 1203 | } 1204 | return base.MatchesKeyIdentifierClause(keyIdentifierClause); 1205 | } 1206 | 1207 | #region private members 1208 | 1209 | private List CreateSymmetricSecurityKeys(IEnumerable keys) 1210 | { 1211 | List symmetricKeys = new List(); 1212 | foreach (byte[] key in keys) 1213 | { 1214 | symmetricKeys.Add(new InMemorySymmetricSecurityKey(key)); 1215 | } 1216 | return symmetricKeys; 1217 | } 1218 | 1219 | private string id; 1220 | private DateTime effectiveTime; 1221 | private List securityKeys; 1222 | 1223 | #endregion 1224 | } 1225 | } 1226 | 1227 | /* 1228 | SharePoint Add-in REST/OData Basic Data Operations, https://github.com/OfficeDev/SharePoint-Add-in-REST-OData-BasicDataOperations 1229 | 1230 | Copyright (c) Microsoft Corporation 1231 | All rights reserved. 1232 | 1233 | MIT License: 1234 | Permission is hereby granted, free of charge, to any person obtaining 1235 | a copy of this software and associated documentation files (the 1236 | "Software"), to deal in the Software without restriction, including 1237 | without limitation the rights to use, copy, modify, merge, publish, 1238 | distribute, sublicense, and/or sell copies of the Software, and to 1239 | permit persons to whom the Software is furnished to do so, subject to 1240 | the following conditions: 1241 | 1242 | The above copyright notice and this permission notice shall be 1243 | included in all copies or substantial portions of the Software. 1244 | 1245 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 1246 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 1247 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 1248 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 1249 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 1250 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 1251 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 1252 | 1253 | */ -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperationsWeb/Web.Debug.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 29 | 30 | -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperationsWeb/Web.Release.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | 30 | 31 | -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperationsWeb/Web.config: -------------------------------------------------------------------------------- 1 |  2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /SharePoint-Add-in-REST-OData-BasicDataOperationsWeb/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /description/fig1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SharePoint-Add-in-REST-OData-BasicDataOperations/b700eb7c6114ffe3288f72398ea55db53dc3ea0e/description/fig1.gif --------------------------------------------------------------------------------