超苦労したのでメモっておきます。
まず、Office 365のテナントからいけるAzure ADにアプリケーションを追加します。追加したら、CliendIDとClientSecret(期限つきのキーのやつ)とTenantID(エンドポイントを表示したときに見れるやつ)をとっておきます。
次に、ASP.NET MVCのプロジェクトに以下のライブラリを追加します。
- Microsoft.IdentityModel.Clients.ActiveDirectory
- 2016/7/22現在、最新版をとってくるとエラーになるので3.10.xを入れること!(はまったポイント)
- Microsoft.Office365.Discovery
- Microsoft.Office365.OutlookServices
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.Cookies
- Microsoft.Owin.Security.OpenIdConnect
そして、OWINのStartupクラスを作ってAzure ADとの連携に必要な決まり文句を書きます。
using System; using System.Threading.Tasks; using Microsoft.Owin; using Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.OpenIdConnect; using Microsoft.IdentityModel.Clients.ActiveDirectory; using System.IdentityModel.Claims; using System.Web; using OwinSecurityError.Models; [assembly: OwinStartup(typeof(OwinSecurityError.Startup))] namespace OwinSecurityError { publicclass Startup { publicvoid Configuration(IAppBuilder app) { app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); app.UseCookieAuthentication(new CookieAuthenticationOptions()); app.UseOpenIdConnectAuthentication( new OpenIdConnectAuthenticationOptions { ClientId = Consts.ClientId, ClientSecret = Consts.ClientSecret, Authority = "https://login.windows.net/O365のテナント名.onmicrosoft.com/", Notifications = new OpenIdConnectAuthenticationNotifications() { //// If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. AuthorizationCodeReceived = async (context) => { var code = context.Code; var credential = new ClientCredential(Consts.ClientId, Consts.ClientSecret); var userObjectId = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value; var authContext = new AuthenticationContext(Consts.Authority, new ADALTokenCache(userObjectId)); await authContext.AcquireTokenByAuthorizationCodeAsync(code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, "https://graph.windows.net"); }, RedirectToIdentityProvider = (context) => { // This ensures that the address used for sign in and sign out is picked up dynamically from the request// this allows you to deploy your app (to Azure Web Sites, for example)without having to change settings// Remember that the base URL of the address used here must be provisioned in Azure AD beforehand.string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase; context.ProtocolMessage.RedirectUri = appBaseUrl + "/"; context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl; return Task.FromResult(0); }, AuthenticationFailed = (context) => { // Suppress the exception if you don't want to see the error context.HandleResponse(); return Task.FromResult(0); } }, }); } } staticclass Consts { publicstaticstring ClientId { get; } = "クライアントID"; publicstaticstring ClientSecret { get; } = "クライアントシークレット"; publicstaticstring Authority { get; } = "https://login.windows.net/テナントID"; } }
ADALTokenCacheがポイントで、こいつでTokenをキャッシュしておきます。正しい実装はデータベースとかに値をキャッシュするのですが、今回はサンプルなのでインメモリで保持してます(再デプロイとかで消えるので実運用ではだめよ)
using Microsoft.IdentityModel.Clients.ActiveDirectory; using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace OwinSecurityError.Models { publicclass ADALTokenCache : TokenCache { privatestatic Dictionary<string, byte[]> Cache { get; } = new Dictionary<string, byte[]>(); privatestring User { get; } public ADALTokenCache(string user) { this.User = user; this.BeforeAccess = this.BeforeAccessNotification; this.AfterAccess = this.AfterAccessNotification; this.BeforeWrite = this.BeforeWriteNotification; this.DeserializeIfContains(); } privatevoid BeforeWriteNotification(TokenCacheNotificationArgs args) { } privatevoid AfterAccessNotification(TokenCacheNotificationArgs args) { lock (Cache) { Cache[this.User] = this.Serialize(); } } privatevoid BeforeAccessNotification(TokenCacheNotificationArgs args) { this.DeserializeIfContains(); } privatevoid DeserializeIfContains() { lock (Cache) { if (Cache.ContainsKey(this.User)) { this.Deserialize(Cache[this.User]); } } } } }
あとは、コントローラあたりでOutlookServiceClientあたりを作ってやればOKです。この作り方も曲者で認証情報やらをこねくり回す必要があります。コードはこんな感じ。
using Microsoft.IdentityModel.Clients.ActiveDirectory; using Microsoft.Office365.Discovery; using Microsoft.Office365.OutlookServices; using OwinSecurityError.Models; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using System.Web; using System.Web.Mvc; namespace OwinSecurityError.Controllers { [Authorize] publicclass HomeController : Controller { public async Task<ActionResult> Index() { var client = await EnsureOutlookServicesClientCreatedAsync("Calendar"); var user = await client.Me.ExecuteAsync(); var date = DateTimeOffset.UtcNow - TimeSpan.FromDays(1); var view = client.Me.Calendar.GetCalendarView(date, date + TimeSpan.FromDays(2)); var pages = await view.ExecuteAsync(); var result = new List<string>(); while (true) { result.AddRange(pages.CurrentPage.Select(x => $"{x.Location.DisplayName} {x.Subject}")); if (!pages.MorePagesAvailable) { break; } await pages.GetNextPageAsync(); } this.ViewBag.Title = user.DisplayName; return View(result); } privatestatic async Task<OutlookServicesClient> EnsureOutlookServicesClientCreatedAsync(string capabilityName) { var signInUserId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value; var userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; AuthenticationContext authContext = new AuthenticationContext(Consts.Authority, new ADALTokenCache(signInUserId)); try { var discClient = new DiscoveryClient(new Uri("https://api.office.com/discovery/v1.0/me/"), async () => { var authResult = await authContext.AcquireTokenSilentAsync("https://api.office.com/discovery/", new ClientCredential(Consts.ClientId, Consts.ClientSecret), new UserIdentifier(userObjectId, UserIdentifierType.UniqueId)); return authResult.AccessToken; }); var dcr = await discClient.DiscoverCapabilityAsync(capabilityName); returnnew OutlookServicesClient(dcr.ServiceEndpointUri, async () => { var authResult = await authContext.AcquireTokenSilentAsync(dcr.ServiceResourceId, new ClientCredential(Consts.ClientId, Consts.ClientSecret), new UserIdentifier(userObjectId, UserIdentifierType.UniqueId)); return authResult.AccessToken; }); } catch (AdalException ex) { if (ex.ErrorCode == AdalError.FailedToAcquireTokenSilently) { authContext.TokenCache.Clear(); throw; } returnnull; } } } }
クライアントを作ったら、後はAPIはそれっぽくいけるのでCalendarから値をとったりしてます。
こんな感じのViewを定義すればリストで会議室と予定が表示されます。
@model List<string> @{ ViewBag.Title = "Index"; } <h2>Index</h2><ul> @foreach (var item in this.Model) { <li>@item</li> } </ul>
超苦労した!