環境 :
Office 365 (Preview)
Visual Studio 2012
Microsoft Office Developer Tools for Visual Studio 2012 Preview

SharePoint Add-ins 開発

こんにちは。

次期開発基盤である SharePoint Add-ins (SharePoint アドイン, 旧 App for SharePoint) の手法を紹介していきます。
SharePoint Add-ins を使って SharePoint と連携する処理 (例 : アイテムの取得、更新など) をプログラミングする際は、「SharePoint Add-ins の動作と概要」で解説したように、サーバー サイドで連携する方法と、クライアント サイド (JavaScript) で連携する方法があります。今回は、サーバー サイドの連携方法について、実際のサンプル コードを含めて解説します。(といっても、後述するように、今回は、プロジェクトが生成するコードの解説がほとんどです。)

また、現在の Preview 版では日本語で一部が使えない機能があるため、以降、英語版を使用しますが、ご容赦ください。(Permission 設定などを使わないなら、概ね、日本語環境でも問題ありません。)
2012/11/19 追記 : このバグは、Microsoft Office Developer Tools for Visual Studio 2012 Preview 2 で修正されました。

 

プロジェクトの作成

今回は、Remote App のアプリケーションを作成し、Remote App のサーバー側のプログラム (C#) から SharePoint サイトのタイトルを取得して表示する簡単な Page アプリケーション (ASP.NET アプリケーション) を構築します。

まず、SharePoint Add-ins (SharePoint アドイン, 旧 App for SharePoint) のプロジェクトを新規作成します。

ウィザードが表示されるので、今回は、配置方法として、Provider-hosted を選択します。Provider-hosted の動作については、「SharePoint Add-ins の動作と概要」を参照してください。(追記 : 2014/06 以降 Autohosted はサポートされません。このため、内容を Provider-hosted に変更しました。。。)

補足 : 「SharePoint Add-ins の動作と概要」で解説したように、上図のデバッグ用のサイト (SharePoint サイト) として、オンプレミスの SharePoint Server ではなく、Office 365 のサイト (Sharepoint Online のサイト) を使用してください。(On-Premise の場合、いくつかの事前設定が必要です。)

以上で、テンプレートからプロジェクトが自動作成されます。
実は、このプロジェクトのテンプレートで、既に、必要な認証の処理と、.NET CSOM (Client Side Object Model) を使ったプログラム コードが作成されています。(デバッグ実行をおこなうと、ちゃんと動作します。)
以降では、この内容 (流れ) について解説します。

まず、準備として、Add-ins プロジェクトのマニフェスト ファイル (AppManifest.xml) のコードを開いてください (下記)。
ユーザーが SharePoint Add-ins をインストールすると Site Contents としてアプリケーションが登録されますが、この登録されたアプリケーションをクリックした際に飛ぶ URL が、下記の <StartPage /> 要素 (太字) になります。
この <StartPage /> 要素に記述されている ~remoteAppUrl、{StandardTokens} はトークンと呼ばれる約束語で、実行時には、~remoteAppUrl は実際の配置先のアプリケーションの URI が設定され、{StandardTokens} には必要な情報が Query String として渡されます。
この {StandardTokens} については、このあと解説しますので、おぼえておいてください。

<?xml version="1.0" encoding="utf-8" ?>
<App xmlns="..." . . .>
  <Properties>
    <Title>SharePointApp1</Title>
    <StartPage>~remoteAppUrl/Pages/Default.aspx?{StandardTokens}</StartPage>
  </Properties>
  <AppPrincipal>
    <AutoDeployedWebApplication/>
  </AppPrincipal>
  . . .

 

OAuth 2 による認証・認可と SharePoint へのアクセス

Remote App (サーバー側) から SharePoint (今回の場合、SharePoint Online) にアクセスして必要な情報の取得や更新をおこなう場合、まずは認証処理をおこなって、SharePoint にログインする必要があります。この認証処理では、OAuth 2 による server to server 認証を使用します。
後述するように、この認証処理は API がおこなってくれるので、プログラマーが細かくプログラミングする必要はありません。しかし、流れを理解しておくことは重要なので、以下に記載しておきます。

Office 365 Preview では、以前このブログでも解説した Azure Active Directory (AAD) が使用されています。Add-ins がインストールされると、裏側では、Azure Active Directory に専用の Service Principal を作成します。

補足 : 作成された Service Principal がゴミとして残ってしまう場合があるので、定期的に消しておくと良いでしょう。確認方法と削除方法については、「SharePoint Add-insで token が null になる場合の対処」に掲載しました。(2013/02/27追記)

インストールされた Remote App (アプリ) が呼びだされる際に、SharePoint は、その SharePoint サイトのログインに必要な情報 (トークン) を HTTP POST を使ってアプリに渡します。
このトークンは、AppContext、AppContextToken、AccessToken、SPAppToken のいずれかのパラメーターに、JWT (Json 形式のトークン) として格納されています。厳密には、このパラメーターに . (dot) で区切られた配列として文字列が渡され、その配列の要素の 1 つとして、エンコードされた JWT が設定されています。(このデコード プログラムを「OAuth 2 Token の Decode」に掲載しました。)
つまり、Remote App 側では、まず、この渡されたパラメーターを Parse して、JWT を取り出します。

渡される JWT は、以下のフォーマットの Json 文字列です。
なお、下記で identifier (識別子) は、以前 紹介した Azure Active Directory で使用した <AppPrincipalID>@<TenantId> 形式の識別子です。

{
  "aud":"<audience identifier (即ち、今回の場合、Remote App の identifier)>",
  "iss":"<issuer identifier>",
  "nbf":<starting time>,
  "exp":<expiration time>,
  "appctxsender":"<appctx の sender の identifier>",
  "appctx":"<後述>",
  "refreshtoken":"<OAuth refresh token>",
  "isbrowserhostedapp":"true"
}

上記の appctx (アプリケーション コンテキスト) には、下記のフォーマットの JSON がエスケープされた文字列として設定されています。

{
  "CacheKey":"<Cache key>",
  "SecurityTokenServiceUri":"https://accounts.accesscontrol.windows.net/tokens/OAuth/2"
}

実際の JWT のサンプルは、以下のような感じです。(本来、こうした内容は他人に見せるものではありませんが、この後のプログラミングにも影響するので、あえて人柱となって記載します。)
なお、Refresh token は、さすがに長いので省略しています。

{
  "aud":"8c3672a6-15c2-489b-813b-a6424cf6d98e/localhost:44301@e4b292c8-102a-4035-a1b1-1666c90b94cb",
  "iss":"00000001-0000-0000-c000-000000000000@e4b292c8-102a-4035-a1b1-1666c90b94cb",
  "nbf":1352096980,
  "exp":1352140180,
  "appctxsender":"00000003-0000-0ff1-ce00-000000000000@e4b292c8-102a-4035-a1b1-1666c90b94cb",
  "appctx":"{
    \"CacheKey\":\"pDp3sFAezpQ/frI+9mLCB3aL6mfSmdzWUFZ9wN0S8qo=\",
    \"SecurityTokenServiceUri\":\"https://accounts.accesscontrol.windows.net/tokens/OAuth/2\"
  }",
  "refreshtoken":"IAAAAD1DB3 ...",
  "isbrowserhostedapp":"true"
}

つぎに、上記の OAuth refresh token を使って、STS に対し、OAuth の access token を要求します。
そして、受け取った access token を使用し、下記の Http header を付与することで、SharePoint の REST サービスや CSOM による呼び出しが可能になります。(SharePoint 2013 では、CSOM も最終的に、REST を呼び出しています。)

Authorization : Bearer <access token>

なお、上述した {StandardTokens} (上記の AppManifest.xml 参照) には、下記のフォーマットのクエリー文字列が設定されます。

SPHostUrl=<使用している Host web (SharePoint) の URL>

つまり、このクエリー文字列から、現在使用している SharePoint の URL (Host web の Url) が取得できるため、以降は、この URL に対して REST (または、CSOM) でアクセスします。

SharePoint Add-ins のテンプレートが作成するプロジェクトでは、上記の手続きが、最新の Windows Identity Foundation (WIF) でプログラミングされています。(正確には、WIF 1.1 のライブラリーである Microsoft.IdentityModel.Extensions.dll と、Microsoft.IdentityModel.dll の runtime version 2 を使用しています。)
例えば、これらのライブラリーを使って OAuth の Access Token を取得するには、下記の通りプログラミングできます。(実際にプロジェクトが生成するコードでは、Validation チェックや例外処理などが含まれた複雑なコードです。)

. . .
using System.Web.Configuration;
using System.IdentityModel.Tokens;
using System.IdentityModel.Selectors;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.S2S.Tokens;
using Microsoft.IdentityModel.S2S.Protocols.OAuth2;
. . .

protected void Page_Load(object sender, EventArgs e)
{
  // 実際の開発では、Audience の Validation チェックなどをおこなうこと
  // (今回は省略してます)
  string hostWeb = Page.Request["SPHostUrl"];
  string contextToken = Page.Request.Form["SPAppToken"];

  string clientId = WebConfigurationManager.AppSettings.Get("ClientId");
  string clientSecret = WebConfigurationManager.AppSettings.Get("ClientSecret");

  // ready JsonWebSecurityTokenHandler
  JsonWebSecurityTokenHandler tokenHandler =
    new JsonWebSecurityTokenHandler();
  tokenHandler.Configuration = new SecurityTokenHandlerConfiguration();
  tokenHandler.Configuration.AudienceRestriction =
    new AudienceRestriction(AudienceUriMode.Never);
  tokenHandler.Configuration.CertificateValidator =
    X509CertificateValidator.None;
  byte[] clientSecretByte = Convert.FromBase64String(clientSecret);
  tokenHandler.Configuration.IssuerTokenResolver =
    SecurityTokenResolver.CreateDefaultSecurityTokenResolver(
    new ReadOnlyCollection<SecurityToken>(new List<SecurityToken>(
      new SecurityToken[]
      {
        new SimpleSymmetricKeySecurityToken(clientSecretByte)
      })),
    false);

  // Create JsonWebSecurityToken from JWT string
  JsonWebSecurityToken jsonToken =
    tokenHandler.ReadToken(contextToken)
    as JsonWebSecurityToken;

  // Get principalName from JWT
  var appctxsender = (from c in jsonToken.Claims
    where c.ClaimType == "appctxsender"
    select c.Value).FirstOrDefault();
  string principalName = appctxsender.Split('@')[0];

  // Get OAuth refresh token from JWT
  var refreshToken = (from c in jsonToken.Claims
    where c.ClaimType == "refreshtoken"
    select c.Value).FirstOrDefault();

  // Get app realm from JWT
  string aud = jsonToken.Audience;
  string realm = aud.Substring(aud.IndexOf('@') + 1);

  // Get host web authority
  string hostAuthority = (new Uri(hostWeb)).Authority;

  // Create OAuth resource id
  string resource = String.Format(
    CultureInfo.InvariantCulture,
    "{0}/{1}@{2}",
    principalName, hostAuthority, realm);

  // Create OAuth client id
  string oauthClientId = String.Format(
    CultureInfo.InvariantCulture,
    "{0}@{1}",
    clientId, realm);

  // Get sts endpoint for OAuth S2S
  JsonMetadataDocument jdoc = GetJsonMetadataFromAcs(realm);
  JsonEndpoint s2s = (from c in jdoc.endpoints
    where c.protocol == "OAuth2"
    select c).FirstOrDefault();
  string stsOAuth2Url = s2s.location;

  // Get access token from refresh token
  OAuth2S2SClient client = new OAuth2S2SClient();
  OAuth2AccessTokenRequest oauthReq =
    OAuth2MessageFactory.CreateAccessTokenRequestWithRefreshToken(
      oauthClientId,
      clientSecret,
      refreshToken,
      resource);
  OAuth2AccessTokenResponse oauthRes =
    client.Issue(
      stsOAuth2Url,
      oauthReq) as OAuth2AccessTokenResponse;
  string accessToken = oauthRes.AccessToken;

  Response.Write("Access Token is " + accessToken);
}

private JsonMetadataDocument GetJsonMetadataFromAcs(string realm)
{
  string endpoint = String.Format(
    CultureInfo.InvariantCulture,
    "{0}?realm={1}",
    "https://accounts.accesscontrol.windows.net/metadata/json/1",
    realm);
  byte[] metadata;
  using (WebClient webClient = new WebClient())
  {
    metadata = webClient.DownloadData(endpoint);
  }
  string jsonString = Encoding.UTF8.GetString(metadata);
  JavaScriptSerializer serializer = new JavaScriptSerializer();
  JsonMetadataDocument document =
    serializer.Deserialize<JsonMetadataDocument>(jsonString);
  return document;
}

. . .

// These classes are used for Json serialization.
// (Mapped class)
public class JsonMetadataDocument
{
  public string serviceName { get; set; }
  public List<JsonEndpoint> endpoints { get; set; }
  public List<JsonKey> keys { get; set; }
}

public class JsonEndpoint
{
  public string location { get; set; }
  public string protocol { get; set; }
  public string usage { get; set; }
}

public class JsonKeyValue
{
  public string type { get; set; }
  public string value { get; set; }
}

public class JsonKey
{
  public string usage { get; set; }
  public JsonKeyValue keyValue { get; set; }
}
. . .

このコードでは、毎回、トークンを取り直していますが、取得したトークンを Cookie や Cache などに保持して再利用しても構いません。(その場合には、前述の Cache Key が使用できます。)

補足 : 取得した Access token は 12 時間で無効になります。長時間の処理などでトークンが無効になった場合は、「MSDN : Tips and FAQs OAuth and remote apps for SharePoint 2013」に記載されているように、 /_layouts/15/AppRedirect.aspx を使ってトークンを取り直すことができます。(AppRedirect.aspx は remote app にリダイレクトし、その際に、新しい JWT を POST します。)

なお、上記のコードを見ておわかりの通り、オンプレミスの SharePoint に認証する場合、上記のコードは、そのまま使用できないので注意してください。(面倒ですが、コードは認証方法によって異なってきます。上記のコードは Office 365 の認証基盤、すなわち、Azure Active Directory の認証基盤へ接続しています。)

上記で取得した accessToken を使って REST で Web の Title を取得するには、下記の通りプログラミングします。(あらかじめ、System.Net.Http.dll、System.Net.Http.Formatting.dll の参照を追加し、NuGet から Json.NET を取得します。)
なお、下記はサンプルですので、実際のプログラミングでは、非同期をうまく活用してください。(つまり、真似しないでください。)

. . .
using System.Threading.Tasks;
using System.Net.Http;
using System.Net.Http.Formatting;
using Newtonsoft.Json.Linq;

. . .

// Get web title
HttpClient restCl = new HttpClient();
restCl.DefaultRequestHeaders.Add(
  "Accept",
  "application/json; odata=verbose");
restCl.DefaultRequestHeaders.Add(
  "Authorization",
  " Bearer " + accessToken);
HttpResponseMessage restRes =
  restCl.GetAsync(hostWeb + "/_api/web/Title").Result;
JObject jreturn =
  restRes.Content.ReadAsAsync<JObject>(
    new[] { new JsonMediaTypeFormatter() }).Result;
JObject jd = (JObject)jreturn["d"];
string title = ((JValue)jd["Title"]).Value.ToString();
Response.Write("Web title is " + title);
. . .

補足 : 上記は、SharePoint 2013 から使用可能な新しい REST サービスです。(CSOM は、内部で、この REST サービスを使用します。実体は、client.svc です。) 従来の REST サービスも、そのまま使用できます。

同様に、accessToken を使って .NET CSOM (Client Side Object Model) で Web の Title を取得するには、下記の通りプログラミングします。

. . .
using Microsoft.SharePoint.Client;

. . .

// Get web title
ClientContext clCtx = new ClientContext(hostWeb);
clCtx.AuthenticationMode = ClientAuthenticationMode.Anonymous;
clCtx.FormDigestHandlingEnabled = false;
clCtx.ExecutingWebRequest +=
    delegate(object oSender, WebRequestEventArgs webRequestEventArgs)
    {
        webRequestEventArgs.WebRequestExecutor.RequestHeaders["Authorization"]
            = "Bearer " + accessToken;
    };
clCtx.Load(clCtx.Web, web => web.Title);
clCtx.ExecuteQuery();
Response.Write("Web title is " + clCtx.Web.Title);
. . .

なお、SharePoint Add-ins のプロジェクトに含まれている TokenHelper クラス のメソッドには、こうした認証処理を実装した多数のメソッドが提供されていて、これらのメソッドを組み合わせることで簡単に認証処理が実装できるようになっています。
例えば、上記の処理は、以下の通り簡単に記述できます。(これが、SharePoint Add-ins のプロジェクト作成時に生成されるコードです。)

つまり、上記のコードの記述は不要ですが (下記の短いコードで充分です)、内部でどのような処理をおこなっているか理解しておくことは重要ですので、上記の流れをおぼえておきましょう !

protected void Page_Load(object sender, EventArgs e)
{
  var contextToken =
    TokenHelper.GetContextTokenFromRequest(Page.Request);
  var hostWeb = Page.Request["SPHostUrl"];
  using (var clientContext =
    TokenHelper.GetClientContextWithContextToken(
    hostWeb, contextToken, Request.Url.Authority))
  {
    clientContext.Load(
      clientContext.Web,
      web => web.Title);
    clientContext.ExecuteQuery();
    Response.Write(
      "Web title is " + clientContext.Web.Title);
  }
}

 

Permission と App Context

SharePoint Add-ins の動作と概要」でも解説しましたが、さいごに、Permission の設定をおこなってください。
例えば、今回は、SharePoint サイトのタイトルを読み込んで表示するだけなので、AppManifest.xml をダブルクリックして、表示されるウィンドウで、下記の通り、「Web」スコープの「Read」権限を要求します。

設定をおこなうと、AppManifest.xml には下記 (太字) の通り設定されます。(もちろん、要領がつかめてきたら、AppManifest.xml の XML テキストを直接編集して構いません。)

<App xmlns="..." . . .>
  . . .

  <AppPermissionRequests>
    <AppPermissionRequest
      Scope="http://sharepoint/content/sitecollection/web"
      Right="Read" />
  </AppPermissionRequests>

</App>

SharePoint Add-ins の動作と概要」でも解説した通り、Permission 設定をおこなうと、Add-ins のインストール時に、この Add-ins が要求している Permission を付与するかどうかを確認する画面が表示されます。この画面で管理者が権限を付与 (Trust) すると、SharePoint には、Add-ins と Permission の情報が登録されます。そして、Add-ins が上記の Refresh token を使用した方法で SharePoint にアクセスする場合、OAuth のトークンには App Context と呼ばれるアプリケーションの情報が含まれていて、この Context の情報を元に、アクセスしている Add-ins が、その操作の権限があるかチェックします。(権限のない Add-ins に対しては、Permission エラーが表示されます。)
また、上記の Refresh token を使用した方法では、OAuth のトークンに User Context も含まれており、どのユーザーが操作した内容であるかも SharePoint 側で認識されています。例えば、アイテムを作成すると、作成者 (CreatedBy) としてユーザーの情報も設定され、そのアイテムの振る舞い (権限の動作など) も、そのユーザーが画面から登録したアイテムと同じように動作します。
User Context を含まず、App Context のみの Access Token を取得したい場合は、プロジェクトの TokenHelper.GetAppOnlyAccessToken メソッドが使用できます。(どのように処理されているか参照してみてください。)

注意 : App のみの AccessToken を使用 (TokenHelper.GetAppOnlyAccessToken を使用) する際は、AppManifest.xml に、下記の通り AllowAppOnlyPolicy 属性を設定します。
<AppPermissionRequests AllowAppOnlyPolicy="true">
  . . .
</AppPermissionRequests>

なお、SharePoint Store の Add-ins の場合は、AppManifest.xml に、Locale 情報も忘れずに設定しておきましょう。Locale に関する詳細は、チームブログの「Locale support information is required for all apps in the SharePoint Store」を参照してください。(今後、この Locale の説明は省略します。)

 

動作確認

以上で完了です。

Visual Studio で F5 キー (または、[Debug] - [Start Debugging] メニュー) でデバッグ実行をおこなうと、アプリが localhost で起動し、デバッグ用に使用している SharePoint サイトに、このアプリが配置 (登録) されます。(この際、SharePoint へのログインが必要となります。また、「SharePoint Add-ins の動作と概要」でも解説した Permission の要求画面が表示されます。)
SharePoint サイトにインストールされると、下図の通り、アプリケーションは、[Site Contents] に表示されます。ここは、リスト、ライブラリー、外部からインストールしたアプリケーションなど、サイトでインストールされたすべてのコンテンツが表示されています。

上図で、インストールされた「SharePointApp1」をクリックすると、下図の通り、Remote App (Default.aspx) が表示されます。

補足 : SharePoint が提供している chrome control と呼ばれるものを使用すると、SharePoint と同じブランディング (UI レイアウト) のページを表示できます。(元の Host web に戻るためのリンクなども表示されます。)

デバッグ実行をした際は、既定で、ローカルの IIS Express (localhost) が使用されますが、Add-ins を Azure など実際のクラウド環境に配置 (展開) してデバッグする場合は、「SharePoint Add-ins の Autohosted から Provider-hosted への移行」を参照してください。

補足 : デバッグ実行時は、ローカル (localhost) の IIS Express の SSL (https) が使用されます。このため、この SSL で使用されている開発用の証明書をブラウザー (Internet Explorer) の信頼されたルート (Trusted Root Certification Authorities) に登録してください。(登録しないと、証明書のエラーが表示され、トークンが正しく POST されない場合があります。)

補足 (2013/07 追記) : ここで紹介した認証の流れを使って、SharePoint の外の Native Application などから Access Token を取得して SharePoint に接続することも可能です。詳細は、「Native Application で SharePoint Online に Login して REST サービスなどを呼び出すプログラミング」に記載しました。

 

※ 変更履歴 :

2015/05/05  App for SharePoint (SharePoint 用アプリ) を SharePoint Add-ins (SharePoint アドイン) に名称変更