/Samples/Core.Dialog/Core.DialogWeb/TokenHelper.cs

https://github.com/SharePoint/PnP · C# · 1223 lines · 812 code · 174 blank · 237 comment · 84 complexity · 644bbdf59f783752a1b597f9c51f9dd7 MD5 · raw file

  1. using Microsoft.IdentityModel;
  2. using Microsoft.IdentityModel.S2S.Protocols.OAuth2;
  3. using Microsoft.IdentityModel.S2S.Tokens;
  4. using Microsoft.SharePoint.Client;
  5. using Microsoft.SharePoint.Client.EventReceivers;
  6. using System;
  7. using System.Collections.Generic;
  8. using System.Collections.ObjectModel;
  9. using System.Globalization;
  10. using System.IdentityModel.Selectors;
  11. using System.IdentityModel.Tokens;
  12. using System.IO;
  13. using System.Linq;
  14. using System.Net;
  15. using System.Security.Cryptography.X509Certificates;
  16. using System.Security.Principal;
  17. using System.ServiceModel;
  18. using System.Text;
  19. using System.Web;
  20. using System.Web.Configuration;
  21. using System.Web.Script.Serialization;
  22. using AudienceRestriction = Microsoft.IdentityModel.Tokens.AudienceRestriction;
  23. using AudienceUriValidationFailedException = Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException;
  24. using SecurityTokenHandlerConfiguration = Microsoft.IdentityModel.Tokens.SecurityTokenHandlerConfiguration;
  25. using X509SigningCredentials = Microsoft.IdentityModel.SecurityTokenService.X509SigningCredentials;
  26. namespace Core.DialogWeb
  27. {
  28. public static class TokenHelper
  29. {
  30. #region public fields
  31. /// <summary>
  32. /// SharePoint principal.
  33. /// </summary>
  34. public const string SharePointPrincipal = "00000003-0000-0ff1-ce00-000000000000";
  35. /// <summary>
  36. /// Lifetime of HighTrust access token, 12 hours.
  37. /// </summary>
  38. public static readonly TimeSpan HighTrustAccessTokenLifetime = TimeSpan.FromHours(12.0);
  39. #endregion public fields
  40. #region public methods
  41. /// <summary>
  42. /// Retrieves the context token string from the specified request by looking for well-known parameter names in the
  43. /// POSTed form parameters and the querystring. Returns null if no context token is found.
  44. /// </summary>
  45. /// <param name="request">HttpRequest in which to look for a context token</param>
  46. /// <returns>The context token string</returns>
  47. public static string GetContextTokenFromRequest(HttpRequest request)
  48. {
  49. return GetContextTokenFromRequest(new HttpRequestWrapper(request));
  50. }
  51. /// <summary>
  52. /// Retrieves the context token string from the specified request by looking for well-known parameter names in the
  53. /// POSTed form parameters and the querystring. Returns null if no context token is found.
  54. /// </summary>
  55. /// <param name="request">HttpRequest in which to look for a context token</param>
  56. /// <returns>The context token string</returns>
  57. public static string GetContextTokenFromRequest(HttpRequestBase request)
  58. {
  59. string[] paramNames = { "AppContext", "AppContextToken", "AccessToken", "SPAppToken" };
  60. foreach (string paramName in paramNames)
  61. {
  62. if (!string.IsNullOrEmpty(request.Form[paramName]))
  63. {
  64. return request.Form[paramName];
  65. }
  66. if (!string.IsNullOrEmpty(request.QueryString[paramName]))
  67. {
  68. return request.QueryString[paramName];
  69. }
  70. }
  71. return null;
  72. }
  73. /// <summary>
  74. /// Validate that a specified context token string is intended for this application based on the parameters
  75. /// specified in web.config. Parameters used from web.config used for validation include ClientId,
  76. /// HostedAppHostNameOverride, HostedAppHostName, ClientSecret, and Realm (if it is specified). If HostedAppHostNameOverride is present,
  77. /// it will be used for validation. Otherwise, if the <paramref name="appHostName"/> is not
  78. /// null, it is used for validation instead of the web.config's HostedAppHostName. If the token is invalid, an
  79. /// exception is thrown. If the token is valid, TokenHelper's static STS metadata url is updated based on the token contents
  80. /// and a JsonWebSecurityToken based on the context token is returned.
  81. /// </summary>
  82. /// <param name="contextTokenString">The context token to validate</param>
  83. /// <param name="appHostName">The URL authority, consisting of Domain Name System (DNS) host name or IP address and the port number, to use for token audience validation.
  84. /// If null, HostedAppHostName web.config setting is used instead. HostedAppHostNameOverride web.config setting, if present, will be used
  85. /// for validation instead of <paramref name="appHostName"/> .</param>
  86. /// <returns>A JsonWebSecurityToken based on the context token.</returns>
  87. public static SharePointContextToken ReadAndValidateContextToken(string contextTokenString, string appHostName = null)
  88. {
  89. JsonWebSecurityTokenHandler tokenHandler = CreateJsonWebSecurityTokenHandler();
  90. SecurityToken securityToken = tokenHandler.ReadToken(contextTokenString);
  91. JsonWebSecurityToken jsonToken = securityToken as JsonWebSecurityToken;
  92. SharePointContextToken token = SharePointContextToken.Create(jsonToken);
  93. string stsAuthority = (new Uri(token.SecurityTokenServiceUri)).Authority;
  94. int firstDot = stsAuthority.IndexOf('.');
  95. GlobalEndPointPrefix = stsAuthority.Substring(0, firstDot);
  96. AcsHostUrl = stsAuthority.Substring(firstDot + 1);
  97. tokenHandler.ValidateToken(jsonToken);
  98. string[] acceptableAudiences;
  99. if (!String.IsNullOrEmpty(HostedAppHostNameOverride))
  100. {
  101. acceptableAudiences = HostedAppHostNameOverride.Split(';');
  102. }
  103. else if (appHostName == null)
  104. {
  105. acceptableAudiences = new[] { HostedAppHostName };
  106. }
  107. else
  108. {
  109. acceptableAudiences = new[] { appHostName };
  110. }
  111. bool validationSuccessful = false;
  112. string realm = Realm ?? token.Realm;
  113. foreach (var audience in acceptableAudiences)
  114. {
  115. string principal = GetFormattedPrincipal(ClientId, audience, realm);
  116. if (StringComparer.OrdinalIgnoreCase.Equals(token.Audience, principal))
  117. {
  118. validationSuccessful = true;
  119. break;
  120. }
  121. }
  122. if (!validationSuccessful)
  123. {
  124. throw new AudienceUriValidationFailedException(
  125. String.Format(CultureInfo.CurrentCulture,
  126. "\"{0}\" is not the intended audience \"{1}\"", String.Join(";", acceptableAudiences), token.Audience));
  127. }
  128. return token;
  129. }
  130. /// <summary>
  131. /// Retrieves an access token from ACS to call the source of the specified context token at the specified
  132. /// targetHost. The targetHost must be registered for the principal that sent the context token.
  133. /// </summary>
  134. /// <param name="contextToken">Context token issued by the intended access token audience</param>
  135. /// <param name="targetHost">Url authority of the target principal</param>
  136. /// <returns>An access token with an audience matching the context token's source</returns>
  137. public static OAuth2AccessTokenResponse GetAccessToken(SharePointContextToken contextToken, string targetHost)
  138. {
  139. string targetPrincipalName = contextToken.TargetPrincipalName;
  140. // Extract the refreshToken from the context token
  141. string refreshToken = contextToken.RefreshToken;
  142. if (String.IsNullOrEmpty(refreshToken))
  143. {
  144. return null;
  145. }
  146. string targetRealm = Realm ?? contextToken.Realm;
  147. return GetAccessToken(refreshToken,
  148. targetPrincipalName,
  149. targetHost,
  150. targetRealm);
  151. }
  152. /// <summary>
  153. /// Uses the specified authorization code to retrieve an access token from ACS to call the specified principal
  154. /// at the specified targetHost. The targetHost must be registered for target principal. If specified realm is
  155. /// null, the "Realm" setting in web.config will be used instead.
  156. /// </summary>
  157. /// <param name="authorizationCode">Authorization code to exchange for access token</param>
  158. /// <param name="targetPrincipalName">Name of the target principal to retrieve an access token for</param>
  159. /// <param name="targetHost">Url authority of the target principal</param>
  160. /// <param name="targetRealm">Realm to use for the access token's nameid and audience</param>
  161. /// <param name="redirectUri">Redirect URI registerd for this app</param>
  162. /// <returns>An access token with an audience of the target principal</returns>
  163. public static OAuth2AccessTokenResponse GetAccessToken(
  164. string authorizationCode,
  165. string targetPrincipalName,
  166. string targetHost,
  167. string targetRealm,
  168. Uri redirectUri)
  169. {
  170. if (targetRealm == null)
  171. {
  172. targetRealm = Realm;
  173. }
  174. string resource = GetFormattedPrincipal(targetPrincipalName, targetHost, targetRealm);
  175. string clientId = GetFormattedPrincipal(ClientId, null, targetRealm);
  176. // Create request for token. The RedirectUri is null here. This will fail if redirect uri is registered
  177. OAuth2AccessTokenRequest oauth2Request =
  178. OAuth2MessageFactory.CreateAccessTokenRequestWithAuthorizationCode(
  179. clientId,
  180. ClientSecret,
  181. authorizationCode,
  182. redirectUri,
  183. resource);
  184. // Get token
  185. OAuth2S2SClient client = new OAuth2S2SClient();
  186. OAuth2AccessTokenResponse oauth2Response;
  187. try
  188. {
  189. oauth2Response =
  190. client.Issue(AcsMetadataParser.GetStsUrl(targetRealm), oauth2Request) as OAuth2AccessTokenResponse;
  191. }
  192. catch (WebException wex)
  193. {
  194. using (StreamReader sr = new StreamReader(wex.Response.GetResponseStream()))
  195. {
  196. string responseText = sr.ReadToEnd();
  197. throw new WebException(wex.Message + " - " + responseText, wex);
  198. }
  199. }
  200. return oauth2Response;
  201. }
  202. /// <summary>
  203. /// Uses the specified refresh token to retrieve an access token from ACS to call the specified principal
  204. /// at the specified targetHost. The targetHost must be registered for target principal. If specified realm is
  205. /// null, the "Realm" setting in web.config will be used instead.
  206. /// </summary>
  207. /// <param name="refreshToken">Refresh token to exchange for access token</param>
  208. /// <param name="targetPrincipalName">Name of the target principal to retrieve an access token for</param>
  209. /// <param name="targetHost">Url authority of the target principal</param>
  210. /// <param name="targetRealm">Realm to use for the access token's nameid and audience</param>
  211. /// <returns>An access token with an audience of the target principal</returns>
  212. public static OAuth2AccessTokenResponse GetAccessToken(
  213. string refreshToken,
  214. string targetPrincipalName,
  215. string targetHost,
  216. string targetRealm)
  217. {
  218. if (targetRealm == null)
  219. {
  220. targetRealm = Realm;
  221. }
  222. string resource = GetFormattedPrincipal(targetPrincipalName, targetHost, targetRealm);
  223. string clientId = GetFormattedPrincipal(ClientId, null, targetRealm);
  224. OAuth2AccessTokenRequest oauth2Request = OAuth2MessageFactory.CreateAccessTokenRequestWithRefreshToken(clientId, ClientSecret, refreshToken, resource);
  225. // Get token
  226. OAuth2S2SClient client = new OAuth2S2SClient();
  227. OAuth2AccessTokenResponse oauth2Response;
  228. try
  229. {
  230. oauth2Response =
  231. client.Issue(AcsMetadataParser.GetStsUrl(targetRealm), oauth2Request) as OAuth2AccessTokenResponse;
  232. }
  233. catch (WebException wex)
  234. {
  235. using (StreamReader sr = new StreamReader(wex.Response.GetResponseStream()))
  236. {
  237. string responseText = sr.ReadToEnd();
  238. throw new WebException(wex.Message + " - " + responseText, wex);
  239. }
  240. }
  241. return oauth2Response;
  242. }
  243. /// <summary>
  244. /// Retrieves an app-only access token from ACS to call the specified principal
  245. /// at the specified targetHost. The targetHost must be registered for target principal. If specified realm is
  246. /// null, the "Realm" setting in web.config will be used instead.
  247. /// </summary>
  248. /// <param name="targetPrincipalName">Name of the target principal to retrieve an access token for</param>
  249. /// <param name="targetHost">Url authority of the target principal</param>
  250. /// <param name="targetRealm">Realm to use for the access token's nameid and audience</param>
  251. /// <returns>An access token with an audience of the target principal</returns>
  252. public static OAuth2AccessTokenResponse GetAppOnlyAccessToken(
  253. string targetPrincipalName,
  254. string targetHost,
  255. string targetRealm)
  256. {
  257. if (targetRealm == null)
  258. {
  259. targetRealm = Realm;
  260. }
  261. string resource = GetFormattedPrincipal(targetPrincipalName, targetHost, targetRealm);
  262. string clientId = GetFormattedPrincipal(ClientId, HostedAppHostName, targetRealm);
  263. OAuth2AccessTokenRequest oauth2Request = OAuth2MessageFactory.CreateAccessTokenRequestWithClientCredentials(clientId, ClientSecret, resource);
  264. oauth2Request.Resource = resource;
  265. // Get token
  266. OAuth2S2SClient client = new OAuth2S2SClient();
  267. OAuth2AccessTokenResponse oauth2Response;
  268. try
  269. {
  270. oauth2Response =
  271. client.Issue(AcsMetadataParser.GetStsUrl(targetRealm), oauth2Request) as OAuth2AccessTokenResponse;
  272. }
  273. catch (WebException wex)
  274. {
  275. using (StreamReader sr = new StreamReader(wex.Response.GetResponseStream()))
  276. {
  277. string responseText = sr.ReadToEnd();
  278. throw new WebException(wex.Message + " - " + responseText, wex);
  279. }
  280. }
  281. return oauth2Response;
  282. }
  283. /// <summary>
  284. /// Creates a client context based on the properties of a remote event receiver
  285. /// </summary>
  286. /// <param name="properties">Properties of a remote event receiver</param>
  287. /// <returns>A ClientContext ready to call the web where the event originated</returns>
  288. public static ClientContext CreateRemoteEventReceiverClientContext(SPRemoteEventProperties properties)
  289. {
  290. Uri sharepointUrl;
  291. if (properties.ListEventProperties != null)
  292. {
  293. sharepointUrl = new Uri(properties.ListEventProperties.WebUrl);
  294. }
  295. else if (properties.ItemEventProperties != null)
  296. {
  297. sharepointUrl = new Uri(properties.ItemEventProperties.WebUrl);
  298. }
  299. else if (properties.WebEventProperties != null)
  300. {
  301. sharepointUrl = new Uri(properties.WebEventProperties.FullUrl);
  302. }
  303. else
  304. {
  305. return null;
  306. }
  307. if (IsHighTrustApp())
  308. {
  309. return GetS2SClientContextWithWindowsIdentity(sharepointUrl, null);
  310. }
  311. return CreateAcsClientContextForUrl(properties, sharepointUrl);
  312. }
  313. /// <summary>
  314. /// Creates a client context based on the properties of an app event
  315. /// </summary>
  316. /// <param name="properties">Properties of an app event</param>
  317. /// <param name="useAppWeb">True to target the app web, false to target the host web</param>
  318. /// <returns>A ClientContext ready to call the app web or the parent web</returns>
  319. public static ClientContext CreateAppEventClientContext(SPRemoteEventProperties properties, bool useAppWeb)
  320. {
  321. if (properties.AppEventProperties == null)
  322. {
  323. return null;
  324. }
  325. Uri sharepointUrl = useAppWeb ? properties.AppEventProperties.AppWebFullUrl : properties.AppEventProperties.HostWebFullUrl;
  326. if (IsHighTrustApp())
  327. {
  328. return GetS2SClientContextWithWindowsIdentity(sharepointUrl, null);
  329. }
  330. return CreateAcsClientContextForUrl(properties, sharepointUrl);
  331. }
  332. /// <summary>
  333. /// Retrieves an access token from ACS using the specified authorization code, and uses that access token to
  334. /// create a client context
  335. /// </summary>
  336. /// <param name="targetUrl">Url of the target SharePoint site</param>
  337. /// <param name="authorizationCode">Authorization code to use when retrieving the access token from ACS</param>
  338. /// <param name="redirectUri">Redirect URI registerd for this app</param>
  339. /// <returns>A ClientContext ready to call targetUrl with a valid access token</returns>
  340. public static ClientContext GetClientContextWithAuthorizationCode(
  341. string targetUrl,
  342. string authorizationCode,
  343. Uri redirectUri)
  344. {
  345. return GetClientContextWithAuthorizationCode(targetUrl, SharePointPrincipal, authorizationCode, GetRealmFromTargetUrl(new Uri(targetUrl)), redirectUri);
  346. }
  347. /// <summary>
  348. /// Retrieves an access token from ACS using the specified authorization code, and uses that access token to
  349. /// create a client context
  350. /// </summary>
  351. /// <param name="targetUrl">Url of the target SharePoint site</param>
  352. /// <param name="targetPrincipalName">Name of the target SharePoint principal</param>
  353. /// <param name="authorizationCode">Authorization code to use when retrieving the access token from ACS</param>
  354. /// <param name="targetRealm">Realm to use for the access token's nameid and audience</param>
  355. /// <param name="redirectUri">Redirect URI registerd for this app</param>
  356. /// <returns>A ClientContext ready to call targetUrl with a valid access token</returns>
  357. public static ClientContext GetClientContextWithAuthorizationCode(
  358. string targetUrl,
  359. string targetPrincipalName,
  360. string authorizationCode,
  361. string targetRealm,
  362. Uri redirectUri)
  363. {
  364. Uri targetUri = new Uri(targetUrl);
  365. string accessToken =
  366. GetAccessToken(authorizationCode, targetPrincipalName, targetUri.Authority, targetRealm, redirectUri).AccessToken;
  367. return GetClientContextWithAccessToken(targetUrl, accessToken);
  368. }
  369. /// <summary>
  370. /// Uses the specified access token to create a client context
  371. /// </summary>
  372. /// <param name="targetUrl">Url of the target SharePoint site</param>
  373. /// <param name="accessToken">Access token to be used when calling the specified targetUrl</param>
  374. /// <returns>A ClientContext ready to call targetUrl with the specified access token</returns>
  375. public static ClientContext GetClientContextWithAccessToken(string targetUrl, string accessToken)
  376. {
  377. ClientContext clientContext = new ClientContext(targetUrl);
  378. clientContext.AuthenticationMode = ClientAuthenticationMode.Anonymous;
  379. clientContext.FormDigestHandlingEnabled = false;
  380. clientContext.ExecutingWebRequest +=
  381. delegate(object oSender, WebRequestEventArgs webRequestEventArgs)
  382. {
  383. webRequestEventArgs.WebRequestExecutor.RequestHeaders["Authorization"] =
  384. "Bearer " + accessToken;
  385. };
  386. return clientContext;
  387. }
  388. /// <summary>
  389. /// Retrieves an access token from ACS using the specified context token, and uses that access token to create
  390. /// a client context
  391. /// </summary>
  392. /// <param name="targetUrl">Url of the target SharePoint site</param>
  393. /// <param name="contextTokenString">Context token received from the target SharePoint site</param>
  394. /// <param name="appHostUrl">Url authority of the hosted app. If this is null, the value in the HostedAppHostName
  395. /// of web.config will be used instead</param>
  396. /// <returns>A ClientContext ready to call targetUrl with a valid access token</returns>
  397. public static ClientContext GetClientContextWithContextToken(
  398. string targetUrl,
  399. string contextTokenString,
  400. string appHostUrl)
  401. {
  402. SharePointContextToken contextToken = ReadAndValidateContextToken(contextTokenString, appHostUrl);
  403. Uri targetUri = new Uri(targetUrl);
  404. string accessToken = GetAccessToken(contextToken, targetUri.Authority).AccessToken;
  405. return GetClientContextWithAccessToken(targetUrl, accessToken);
  406. }
  407. /// <summary>
  408. /// Returns the SharePoint url to which the app should redirect the browser to request consent and get back
  409. /// an authorization code.
  410. /// </summary>
  411. /// <param name="contextUrl">Absolute Url of the SharePoint site</param>
  412. /// <param name="scope">Space-delimited permissions to request from the SharePoint site in "shorthand" format
  413. /// (e.g. "Web.Read Site.Write")</param>
  414. /// <returns>Url of the SharePoint site's OAuth authorization page</returns>
  415. public static string GetAuthorizationUrl(string contextUrl, string scope)
  416. {
  417. return string.Format(
  418. "{0}{1}?IsDlg=1&client_id={2}&scope={3}&response_type=code",
  419. EnsureTrailingSlash(contextUrl),
  420. AuthorizationPage,
  421. ClientId,
  422. scope);
  423. }
  424. /// <summary>
  425. /// Returns the SharePoint url to which the app should redirect the browser to request consent and get back
  426. /// an authorization code.
  427. /// </summary>
  428. /// <param name="contextUrl">Absolute Url of the SharePoint site</param>
  429. /// <param name="scope">Space-delimited permissions to request from the SharePoint site in "shorthand" format
  430. /// (e.g. "Web.Read Site.Write")</param>
  431. /// <param name="redirectUri">Uri to which SharePoint should redirect the browser to after consent is
  432. /// granted</param>
  433. /// <returns>Url of the SharePoint site's OAuth authorization page</returns>
  434. public static string GetAuthorizationUrl(string contextUrl, string scope, string redirectUri)
  435. {
  436. return string.Format(
  437. "{0}{1}?IsDlg=1&client_id={2}&scope={3}&response_type=code&redirect_uri={4}",
  438. EnsureTrailingSlash(contextUrl),
  439. AuthorizationPage,
  440. ClientId,
  441. scope,
  442. redirectUri);
  443. }
  444. /// <summary>
  445. /// Returns the SharePoint url to which the app should redirect the browser to request a new context token.
  446. /// </summary>
  447. /// <param name="contextUrl">Absolute Url of the SharePoint site</param>
  448. /// <param name="redirectUri">Uri to which SharePoint should redirect the browser to with a context token</param>
  449. /// <returns>Url of the SharePoint site's context token redirect page</returns>
  450. public static string GetAppContextTokenRequestUrl(string contextUrl, string redirectUri)
  451. {
  452. return string.Format(
  453. "{0}{1}?client_id={2}&redirect_uri={3}",
  454. EnsureTrailingSlash(contextUrl),
  455. RedirectPage,
  456. ClientId,
  457. redirectUri);
  458. }
  459. /// <summary>
  460. /// Retrieves an S2S access token signed by the application's private certificate on behalf of the specified
  461. /// WindowsIdentity and intended for the SharePoint at the targetApplicationUri. If no Realm is specified in
  462. /// web.config, an auth challenge will be issued to the targetApplicationUri to discover it.
  463. /// </summary>
  464. /// <param name="targetApplicationUri">Url of the target SharePoint site</param>
  465. /// <param name="identity">Windows identity of the user on whose behalf to create the access token</param>
  466. /// <returns>An access token with an audience of the target principal</returns>
  467. public static string GetS2SAccessTokenWithWindowsIdentity(
  468. Uri targetApplicationUri,
  469. WindowsIdentity identity)
  470. {
  471. string realm = string.IsNullOrEmpty(Realm) ? GetRealmFromTargetUrl(targetApplicationUri) : Realm;
  472. JsonWebTokenClaim[] claims = identity != null ? GetClaimsWithWindowsIdentity(identity) : null;
  473. return GetS2SAccessTokenWithClaims(targetApplicationUri.Authority, realm, claims);
  474. }
  475. /// <summary>
  476. /// Retrieves an S2S client context with an access token signed by the application's private certificate on
  477. /// behalf of the specified WindowsIdentity and intended for application at the targetApplicationUri using the
  478. /// targetRealm. If no Realm is specified in web.config, an auth challenge will be issued to the
  479. /// targetApplicationUri to discover it.
  480. /// </summary>
  481. /// <param name="targetApplicationUri">Url of the target SharePoint site</param>
  482. /// <param name="identity">Windows identity of the user on whose behalf to create the access token</param>
  483. /// <returns>A ClientContext using an access token with an audience of the target application</returns>
  484. public static ClientContext GetS2SClientContextWithWindowsIdentity(
  485. Uri targetApplicationUri,
  486. WindowsIdentity identity)
  487. {
  488. string realm = string.IsNullOrEmpty(Realm) ? GetRealmFromTargetUrl(targetApplicationUri) : Realm;
  489. JsonWebTokenClaim[] claims = identity != null ? GetClaimsWithWindowsIdentity(identity) : null;
  490. string accessToken = GetS2SAccessTokenWithClaims(targetApplicationUri.Authority, realm, claims);
  491. return GetClientContextWithAccessToken(targetApplicationUri.ToString(), accessToken);
  492. }
  493. /// <summary>
  494. /// Get authentication realm from SharePoint
  495. /// </summary>
  496. /// <param name="targetApplicationUri">Url of the target SharePoint site</param>
  497. /// <returns>String representation of the realm GUID</returns>
  498. public static string GetRealmFromTargetUrl(Uri targetApplicationUri)
  499. {
  500. WebRequest request = WebRequest.Create(targetApplicationUri + "/_vti_bin/client.svc");
  501. request.Headers.Add("Authorization: Bearer ");
  502. try
  503. {
  504. using (request.GetResponse())
  505. {
  506. }
  507. }
  508. catch (WebException e)
  509. {
  510. if (e.Response == null)
  511. {
  512. return null;
  513. }
  514. string bearerResponseHeader = e.Response.Headers["WWW-Authenticate"];
  515. if (string.IsNullOrEmpty(bearerResponseHeader))
  516. {
  517. return null;
  518. }
  519. const string bearer = "Bearer realm=\"";
  520. int bearerIndex = bearerResponseHeader.IndexOf(bearer, StringComparison.Ordinal);
  521. if (bearerIndex < 0)
  522. {
  523. return null;
  524. }
  525. int realmIndex = bearerIndex + bearer.Length;
  526. if (bearerResponseHeader.Length >= realmIndex + 36)
  527. {
  528. string targetRealm = bearerResponseHeader.Substring(realmIndex, 36);
  529. Guid realmGuid;
  530. if (Guid.TryParse(targetRealm, out realmGuid))
  531. {
  532. return targetRealm;
  533. }
  534. }
  535. }
  536. return null;
  537. }
  538. /// <summary>
  539. /// Determines if this is a high trust app.
  540. /// </summary>
  541. /// <returns>True if this is a high trust app.</returns>
  542. public static bool IsHighTrustApp()
  543. {
  544. return SigningCredentials != null;
  545. }
  546. /// <summary>
  547. /// Ensures that the specified URL ends with '/' if it is not null or empty.
  548. /// </summary>
  549. /// <param name="url">The url.</param>
  550. /// <returns>The url ending with '/' if it is not null or empty.</returns>
  551. public static string EnsureTrailingSlash(string url)
  552. {
  553. if (!string.IsNullOrEmpty(url) && url[url.Length - 1] != '/')
  554. {
  555. return url + "/";
  556. }
  557. return url;
  558. }
  559. #endregion
  560. #region private fields
  561. //
  562. // Configuration Constants
  563. //
  564. private const string AuthorizationPage = "_layouts/15/OAuthAuthorize.aspx";
  565. private const string RedirectPage = "_layouts/15/AppRedirect.aspx";
  566. private const string AcsPrincipalName = "00000001-0000-0000-c000-000000000000";
  567. private const string AcsMetadataEndPointRelativeUrl = "metadata/json/1";
  568. private const string S2SProtocol = "OAuth2";
  569. private const string DelegationIssuance = "DelegationIssuance1.0";
  570. private const string NameIdentifierClaimType = JsonWebTokenConstants.ReservedClaims.NameIdentifier;
  571. private const string TrustedForImpersonationClaimType = "trustedfordelegation";
  572. private const string ActorTokenClaimType = JsonWebTokenConstants.ReservedClaims.ActorToken;
  573. //
  574. // Environment Constants
  575. //
  576. private static string GlobalEndPointPrefix = "accounts";
  577. private static string AcsHostUrl = "accesscontrol.windows.net";
  578. //
  579. // Hosted app configuration
  580. //
  581. private static readonly string ClientId = string.IsNullOrEmpty(WebConfigurationManager.AppSettings.Get("ClientId")) ? WebConfigurationManager.AppSettings.Get("HostedAppName") : WebConfigurationManager.AppSettings.Get("ClientId");
  582. private static readonly string IssuerId = string.IsNullOrEmpty(WebConfigurationManager.AppSettings.Get("IssuerId")) ? ClientId : WebConfigurationManager.AppSettings.Get("IssuerId");
  583. private static readonly string HostedAppHostNameOverride = WebConfigurationManager.AppSettings.Get("HostedAppHostNameOverride");
  584. private static readonly string HostedAppHostName = WebConfigurationManager.AppSettings.Get("HostedAppHostName");
  585. private static readonly string ClientSecret = string.IsNullOrEmpty(WebConfigurationManager.AppSettings.Get("ClientSecret")) ? WebConfigurationManager.AppSettings.Get("HostedAppSigningKey") : WebConfigurationManager.AppSettings.Get("ClientSecret");
  586. private static readonly string SecondaryClientSecret = WebConfigurationManager.AppSettings.Get("SecondaryClientSecret");
  587. private static readonly string Realm = WebConfigurationManager.AppSettings.Get("Realm");
  588. private static readonly string ServiceNamespace = WebConfigurationManager.AppSettings.Get("Realm");
  589. private static readonly string ClientSigningCertificatePath = WebConfigurationManager.AppSettings.Get("ClientSigningCertificatePath");
  590. private static readonly string ClientSigningCertificatePassword = WebConfigurationManager.AppSettings.Get("ClientSigningCertificatePassword");
  591. private static readonly X509Certificate2 ClientCertificate = (string.IsNullOrEmpty(ClientSigningCertificatePath) || string.IsNullOrEmpty(ClientSigningCertificatePassword)) ? null : new X509Certificate2(ClientSigningCertificatePath, ClientSigningCertificatePassword);
  592. private static readonly X509SigningCredentials SigningCredentials = (ClientCertificate == null) ? null : new X509SigningCredentials(ClientCertificate, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest);
  593. #endregion
  594. #region private methods
  595. private static ClientContext CreateAcsClientContextForUrl(SPRemoteEventProperties properties, Uri sharepointUrl)
  596. {
  597. string contextTokenString = properties.ContextToken;
  598. if (String.IsNullOrEmpty(contextTokenString))
  599. {
  600. return null;
  601. }
  602. SharePointContextToken contextToken = ReadAndValidateContextToken(contextTokenString, OperationContext.Current.IncomingMessageHeaders.To.Host);
  603. string accessToken = GetAccessToken(contextToken, sharepointUrl.Authority).AccessToken;
  604. return GetClientContextWithAccessToken(sharepointUrl.ToString(), accessToken);
  605. }
  606. private static string GetAcsMetadataEndpointUrl()
  607. {
  608. return Path.Combine(GetAcsGlobalEndpointUrl(), AcsMetadataEndPointRelativeUrl);
  609. }
  610. private static string GetFormattedPrincipal(string principalName, string hostName, string realm)
  611. {
  612. if (!String.IsNullOrEmpty(hostName))
  613. {
  614. return String.Format(CultureInfo.InvariantCulture, "{0}/{1}@{2}", principalName, hostName, realm);
  615. }
  616. return String.Format(CultureInfo.InvariantCulture, "{0}@{1}", principalName, realm);
  617. }
  618. private static string GetAcsPrincipalName(string realm)
  619. {
  620. return GetFormattedPrincipal(AcsPrincipalName, new Uri(GetAcsGlobalEndpointUrl()).Host, realm);
  621. }
  622. private static string GetAcsGlobalEndpointUrl()
  623. {
  624. return String.Format(CultureInfo.InvariantCulture, "https://{0}.{1}/", GlobalEndPointPrefix, AcsHostUrl);
  625. }
  626. private static JsonWebSecurityTokenHandler CreateJsonWebSecurityTokenHandler()
  627. {
  628. JsonWebSecurityTokenHandler handler = new JsonWebSecurityTokenHandler();
  629. handler.Configuration = new SecurityTokenHandlerConfiguration();
  630. handler.Configuration.AudienceRestriction = new AudienceRestriction(AudienceUriMode.Never);
  631. handler.Configuration.CertificateValidator = X509CertificateValidator.None;
  632. List<byte[]> securityKeys = new List<byte[]>();
  633. securityKeys.Add(Convert.FromBase64String(ClientSecret));
  634. if (!string.IsNullOrEmpty(SecondaryClientSecret))
  635. {
  636. securityKeys.Add(Convert.FromBase64String(SecondaryClientSecret));
  637. }
  638. List<SecurityToken> securityTokens = new List<SecurityToken>();
  639. securityTokens.Add(new MultipleSymmetricKeySecurityToken(securityKeys));
  640. handler.Configuration.IssuerTokenResolver =
  641. SecurityTokenResolver.CreateDefaultSecurityTokenResolver(
  642. new ReadOnlyCollection<SecurityToken>(securityTokens),
  643. false);
  644. SymmetricKeyIssuerNameRegistry issuerNameRegistry = new SymmetricKeyIssuerNameRegistry();
  645. foreach (byte[] securitykey in securityKeys)
  646. {
  647. issuerNameRegistry.AddTrustedIssuer(securitykey, GetAcsPrincipalName(ServiceNamespace));
  648. }
  649. handler.Configuration.IssuerNameRegistry = issuerNameRegistry;
  650. return handler;
  651. }
  652. private static string GetS2SAccessTokenWithClaims(
  653. string targetApplicationHostName,
  654. string targetRealm,
  655. IEnumerable<JsonWebTokenClaim> claims)
  656. {
  657. return IssueToken(
  658. ClientId,
  659. IssuerId,
  660. targetRealm,
  661. SharePointPrincipal,
  662. targetRealm,
  663. targetApplicationHostName,
  664. true,
  665. claims,
  666. claims == null);
  667. }
  668. private static JsonWebTokenClaim[] GetClaimsWithWindowsIdentity(WindowsIdentity identity)
  669. {
  670. JsonWebTokenClaim[] claims = new JsonWebTokenClaim[]
  671. {
  672. new JsonWebTokenClaim(NameIdentifierClaimType, identity.User.Value.ToLower()),
  673. new JsonWebTokenClaim("nii", "urn:office:idp:activedirectory")
  674. };
  675. return claims;
  676. }
  677. private static string IssueToken(
  678. string sourceApplication,
  679. string issuerApplication,
  680. string sourceRealm,
  681. string targetApplication,
  682. string targetRealm,
  683. string targetApplicationHostName,
  684. bool trustedForDelegation,
  685. IEnumerable<JsonWebTokenClaim> claims,
  686. bool appOnly = false)
  687. {
  688. if (null == SigningCredentials)
  689. {
  690. throw new InvalidOperationException("SigningCredentials was not initialized");
  691. }
  692. #region Actor token
  693. string issuer = string.IsNullOrEmpty(sourceRealm) ? issuerApplication : string.Format("{0}@{1}", issuerApplication, sourceRealm);
  694. string nameid = string.IsNullOrEmpty(sourceRealm) ? sourceApplication : string.Format("{0}@{1}", sourceApplication, sourceRealm);
  695. string audience = string.Format("{0}/{1}@{2}", targetApplication, targetApplicationHostName, targetRealm);
  696. List<JsonWebTokenClaim> actorClaims = new List<JsonWebTokenClaim>();
  697. actorClaims.Add(new JsonWebTokenClaim(JsonWebTokenConstants.ReservedClaims.NameIdentifier, nameid));
  698. if (trustedForDelegation && !appOnly)
  699. {
  700. actorClaims.Add(new JsonWebTokenClaim(TrustedForImpersonationClaimType, "true"));
  701. }
  702. // Create token
  703. JsonWebSecurityToken actorToken = new JsonWebSecurityToken(
  704. issuer: issuer,
  705. audience: audience,
  706. validFrom: DateTime.UtcNow,
  707. validTo: DateTime.UtcNow.Add(HighTrustAccessTokenLifetime),
  708. signingCredentials: SigningCredentials,
  709. claims: actorClaims);
  710. string actorTokenString = new JsonWebSecurityTokenHandler().WriteTokenAsString(actorToken);
  711. if (appOnly)
  712. {
  713. // App-only token is the same as actor token for delegated case
  714. return actorTokenString;
  715. }
  716. #endregion Actor token
  717. #region Outer token
  718. List<JsonWebTokenClaim> outerClaims = null == claims ? new List<JsonWebTokenClaim>() : new List<JsonWebTokenClaim>(claims);
  719. outerClaims.Add(new JsonWebTokenClaim(ActorTokenClaimType, actorTokenString));
  720. JsonWebSecurityToken jsonToken = new JsonWebSecurityToken(
  721. nameid, // outer token issuer should match actor token nameid
  722. audience,
  723. DateTime.UtcNow,
  724. DateTime.UtcNow.Add(HighTrustAccessTokenLifetime),
  725. outerClaims);
  726. string accessToken = new JsonWebSecurityTokenHandler().WriteTokenAsString(jsonToken);
  727. #endregion Outer token
  728. return accessToken;
  729. }
  730. #endregion
  731. #region AcsMetadataParser
  732. // This class is used to get MetaData document from the global STS endpoint. It contains
  733. // methods to parse the MetaData document and get endpoints and STS certificate.
  734. public static class AcsMetadataParser
  735. {
  736. public static X509Certificate2 GetAcsSigningCert(string realm)
  737. {
  738. JsonMetadataDocument document = GetMetadataDocument(realm);
  739. if (null != document.keys && document.keys.Count > 0)
  740. {
  741. JsonKey signingKey = document.keys[0];
  742. if (null != signingKey && null != signingKey.keyValue)
  743. {
  744. return new X509Certificate2(Encoding.UTF8.GetBytes(signingKey.keyValue.value));
  745. }
  746. }
  747. throw new Exception("Metadata document does not contain ACS signing certificate.");
  748. }
  749. public static string GetDelegationServiceUrl(string realm)
  750. {
  751. JsonMetadataDocument document = GetMetadataDocument(realm);
  752. JsonEndpoint delegationEndpoint = document.endpoints.SingleOrDefault(e => e.protocol == DelegationIssuance);
  753. if (null != delegationEndpoint)
  754. {
  755. return delegationEndpoint.location;
  756. }
  757. throw new Exception("Metadata document does not contain Delegation Service endpoint Url");
  758. }
  759. private static JsonMetadataDocument GetMetadataDocument(string realm)
  760. {
  761. string acsMetadataEndpointUrlWithRealm = String.Format(CultureInfo.InvariantCulture, "{0}?realm={1}",
  762. GetAcsMetadataEndpointUrl(),
  763. realm);
  764. byte[] acsMetadata;
  765. using (WebClient webClient = new WebClient())
  766. {
  767. acsMetadata = webClient.DownloadData(acsMetadataEndpointUrlWithRealm);
  768. }
  769. string jsonResponseString = Encoding.UTF8.GetString(acsMetadata);
  770. JavaScriptSerializer serializer = new JavaScriptSerializer();
  771. JsonMetadataDocument document = serializer.Deserialize<JsonMetadataDocument>(jsonResponseString);
  772. if (null == document)
  773. {
  774. throw new Exception("No metadata document found at the global endpoint " + acsMetadataEndpointUrlWithRealm);
  775. }
  776. return document;
  777. }
  778. public static string GetStsUrl(string realm)
  779. {
  780. JsonMetadataDocument document = GetMetadataDocument(realm);
  781. JsonEndpoint s2sEndpoint = document.endpoints.SingleOrDefault(e => e.protocol == S2SProtocol);
  782. if (null != s2sEndpoint)
  783. {
  784. return s2sEndpoint.location;
  785. }
  786. throw new Exception("Metadata document does not contain STS endpoint url");
  787. }
  788. private class JsonMetadataDocument
  789. {
  790. public string serviceName { get; set; }
  791. public List<JsonEndpoint> endpoints { get; set; }
  792. public List<JsonKey> keys { get; set; }
  793. }
  794. private class JsonEndpoint
  795. {
  796. public string location { get; set; }
  797. public string protocol { get; set; }
  798. public string usage { get; set; }
  799. }
  800. private class JsonKeyValue
  801. {
  802. public string type { get; set; }
  803. public string value { get; set; }
  804. }
  805. private class JsonKey
  806. {
  807. public string usage { get; set; }
  808. public JsonKeyValue keyValue { get; set; }
  809. }
  810. }
  811. #endregion
  812. }
  813. /// <summary>
  814. /// A JsonWebSecurityToken generated by SharePoint to authenticate to a 3rd party application and allow callbacks using a refresh token
  815. /// </summary>
  816. public class SharePointContextToken : JsonWebSecurityToken
  817. {
  818. public static SharePointContextToken Create(JsonWebSecurityToken contextToken)
  819. {
  820. return new SharePointContextToken(contextToken.Issuer, contextToken.Audience, contextToken.ValidFrom, contextToken.ValidTo, contextToken.Claims);
  821. }
  822. public SharePointContextToken(string issuer, string audience, DateTime validFrom, DateTime validTo, IEnumerable<JsonWebTokenClaim> claims)
  823. : base(issuer, audience, validFrom, validTo, claims)
  824. {
  825. }
  826. public SharePointContextToken(string issuer, string audience, DateTime validFrom, DateTime validTo, IEnumerable<JsonWebTokenClaim> claims, SecurityToken issuerToken, JsonWebSecurityToken actorToken)
  827. : base(issuer, audience, validFrom, validTo, claims, issuerToken, actorToken)
  828. {
  829. }
  830. public SharePointContextToken(string issuer, string audience, DateTime validFrom, DateTime validTo, IEnumerable<JsonWebTokenClaim> claims, SigningCredentials signingCredentials)
  831. : base(issuer, audience, validFrom, validTo, claims, signingCredentials)
  832. {
  833. }
  834. public string NameId
  835. {
  836. get
  837. {
  838. return GetClaimValue(this, "nameid");
  839. }
  840. }
  841. /// <summary>
  842. /// The principal name portion of the context token's "appctxsender" claim
  843. /// </summary>
  844. public string TargetPrincipalName
  845. {
  846. get
  847. {
  848. string appctxsender = GetClaimValue(this, "appctxsender");
  849. if (appctxsender == null)
  850. {
  851. return null;
  852. }
  853. return appctxsender.Split('@')[0];
  854. }
  855. }
  856. /// <summary>
  857. /// The context token's "refreshtoken" claim
  858. /// </summary>
  859. public string RefreshToken
  860. {
  861. get
  862. {
  863. return GetClaimValue(this, "refreshtoken");
  864. }
  865. }
  866. /// <summary>
  867. /// The context token's "CacheKey" claim
  868. /// </summary>
  869. public string CacheKey
  870. {
  871. get
  872. {
  873. string appctx = GetClaimValue(this, "appctx");
  874. if (appctx == null)
  875. {
  876. return null;
  877. }
  878. ClientContext ctx = new ClientContext("http://tempuri.org");
  879. Dictionary<string, object> dict = (Dictionary<string, object>)ctx.ParseObjectFromJsonString(appctx);
  880. string cacheKey = (string)dict["CacheKey"];
  881. return cacheKey;
  882. }
  883. }
  884. /// <summary>
  885. /// The context token's "SecurityTokenServiceUri" claim
  886. /// </summary>
  887. public string SecurityTokenServiceUri
  888. {
  889. get
  890. {
  891. string appctx = GetClaimValue(this, "appctx");
  892. if (appctx == null)
  893. {
  894. return null;
  895. }
  896. ClientContext ctx = new ClientContext("http://tempuri.org");
  897. Dictionary<string, object> dict = (Dictionary<string, object>)ctx.ParseObjectFromJsonString(appctx);
  898. string securityTokenServiceUri = (string)dict["SecurityTokenServiceUri"];
  899. return securityTokenServiceUri;
  900. }
  901. }
  902. /// <summary>
  903. /// The realm portion of the context token's "audience" claim
  904. /// </summary>
  905. public string Realm
  906. {
  907. get
  908. {
  909. string aud = Audience;
  910. if (aud == null)
  911. {
  912. return null;
  913. }
  914. string tokenRealm = aud.Substring(aud.IndexOf('@') + 1);
  915. return tokenRealm;
  916. }
  917. }
  918. private static string GetClaimValue(JsonWebSecurityToken token, string claimType)
  919. {
  920. if (token == null)
  921. {
  922. throw new ArgumentNullException("token");
  923. }
  924. foreach (JsonWebTokenClaim claim in token.Claims)
  925. {
  926. if (StringComparer.Ordinal.Equals(claim.ClaimType, claimType))
  927. {
  928. return claim.Value;
  929. }
  930. }
  931. return null;
  932. }
  933. }
  934. /// <summary>
  935. /// Represents a security token which contains multiple security keys that are generated using symmetric algorithms.
  936. /// </summary>
  937. public class MultipleSymmetricKeySecurityToken : SecurityToken
  938. {
  939. /// <summary>
  940. /// Initializes a new instance of the MultipleSymmetricKeySecurityToken class.
  941. /// </summary>
  942. /// <param name="keys">An enumeration of Byte arrays that contain the symmetric keys.</param>
  943. public MultipleSymmetricKeySecurityToken(IEnumerable<byte[]> keys)
  944. : this(UniqueId.CreateUniqueId(), keys)
  945. {
  946. }
  947. /// <summary>
  948. /// Initializes a new instance of the MultipleSymmetricKeySecurityToken class.
  949. /// </summary>
  950. /// <param name="tokenId">The unique identifier of the security token.</param>
  951. /// <param name="keys">An enumeration of Byte arrays that contain the symmetric keys.</param>
  952. public MultipleSymmetricKeySecurityToken(string tokenId, IEnumerable<byte[]> keys)
  953. {
  954. if (keys == null)
  955. {
  956. throw new ArgumentNullException("keys");
  957. }
  958. if (String.IsNullOrEmpty(tokenId))
  959. {
  960. throw new ArgumentException("Value cannot be a null or empty string.", "tokenId");
  961. }
  962. foreach (byte[] key in keys)
  963. {
  964. if (key.Length <= 0)
  965. {
  966. throw new ArgumentException("The key length must be greater then zero.", "keys");
  967. }
  968. }
  969. id = tokenId;
  970. effectiveTime = DateTime.UtcNow;
  971. securityKeys = CreateSymmetricSecurityKeys(keys);
  972. }
  973. /// <summary>
  974. /// Gets the unique identifier of the security token.
  975. /// </summary>
  976. public override string Id
  977. {
  978. get
  979. {
  980. return id;
  981. }
  982. }
  983. /// <summary>
  984. /// Gets the cryptographic keys associated with the security token.
  985. /// </summary>
  986. public override ReadOnlyCollection<SecurityKey> SecurityKeys
  987. {
  988. get
  989. {
  990. return securityKeys.AsReadOnly();
  991. }
  992. }
  993. /// <summary>
  994. /// Gets the first instant in time at which this security token is valid.
  995. /// </summary>
  996. public override DateTime ValidFrom
  997. {
  998. get
  999. {
  1000. return effectiveTime;
  1001. }
  1002. }
  1003. /// <summary>
  1004. /// Gets the last instant in time at which this security token is valid.
  1005. /// </summary>
  1006. public override DateTime ValidTo
  1007. {
  1008. get
  1009. {
  1010. // Never expire
  1011. return DateTime.MaxValue;
  1012. }
  1013. }
  1014. /// <summary>
  1015. /// Returns a value that indicates whether the key identifier for this instance can be resolved to the specified key identifier.
  1016. /// </summary>
  1017. /// <param name="keyIdentifierClause">A SecurityKeyIdentifierClause to compare to this instance</param>
  1018. /// <returns>true if keyIdentifierClause is a SecurityKeyIdentifierClause and it has the same unique identifier as the Id property; otherwise, false.</returns>
  1019. public override bool MatchesKeyIdentifierClause(SecurityKeyIdentifierClause keyIdentifierClause)
  1020. {
  1021. if (keyIdentifierClause == null)
  1022. {
  1023. throw new ArgumentNullException("keyIdentifierClause");
  1024. }
  1025. // Since this is a symmetric token and we do not have IDs to distinguish tokens, we just check for the
  1026. // presence of a SymmetricIssuerKeyIdentifier. The actual mapping to the issuer takes place later
  1027. // when the key is matched to the issuer.
  1028. if (keyIdentifierClause is SymmetricIssuerKeyIdentifierClause)
  1029. {
  1030. return true;
  1031. }
  1032. return base.MatchesKeyIdentifierClause(keyIdentifierClause);
  1033. }
  1034. #region private members
  1035. private List<SecurityKey> CreateSymmetricSecurityKeys(IEnumerable<byte[]> keys)
  1036. {
  1037. List<SecurityKey> symmetricKeys = new List<SecurityKey>();
  1038. foreach (byte[] key in keys)
  1039. {
  1040. symmetricKeys.Add(new InMemorySymmetricSecurityKey(key));
  1041. }
  1042. return symmetricKeys;
  1043. }
  1044. private string id;
  1045. private DateTime effectiveTime;
  1046. private List<SecurityKey> securityKeys;
  1047. #endregion
  1048. }
  1049. }