From dad214623d5aa8bc29b2a069bad5be6b93a48bb6 Mon Sep 17 00:00:00 2001 From: anujab Date: Mon, 11 Sep 2017 20:37:04 -0400 Subject: [PATCH] Adding support for subscription renewal --- .../Controllers/NotificationController.cs | 6 +- .../Controllers/SubscriptionController.cs | 86 +++------------- GraphWebhooks/GraphWebhooks.csproj | 5 +- GraphWebhooks/Helpers/HttpHelper.cs | 38 +++++++ GraphWebhooks/Helpers/SubscriptionCache.cs | 98 +++++++++++++++++++ GraphWebhooks/Helpers/SubscriptionDetails.cs | 26 +++++ GraphWebhooks/Helpers/SubscriptionHelper.cs | 87 ++++++++++++++++ GraphWebhooks/Helpers/SubscriptionStore.cs | 42 -------- 8 files changed, 271 insertions(+), 117 deletions(-) create mode 100644 GraphWebhooks/Helpers/HttpHelper.cs create mode 100644 GraphWebhooks/Helpers/SubscriptionCache.cs create mode 100644 GraphWebhooks/Helpers/SubscriptionDetails.cs create mode 100644 GraphWebhooks/Helpers/SubscriptionHelper.cs delete mode 100644 GraphWebhooks/Helpers/SubscriptionStore.cs diff --git a/GraphWebhooks/Controllers/NotificationController.cs b/GraphWebhooks/Controllers/NotificationController.cs index 81f5975..2a8391f 100644 --- a/GraphWebhooks/Controllers/NotificationController.cs +++ b/GraphWebhooks/Controllers/NotificationController.cs @@ -59,7 +59,7 @@ public async Task Listen() Notification current = JsonConvert.DeserializeObject(notification.ToString()); // Check client state to verify the message is from Microsoft Graph. - SubscriptionStore subscription = SubscriptionStore.GetSubscriptionInfo(current.SubscriptionId); + SubscriptionDetails subscription = SubscriptionCache.GetSubscriptionCache().GetSubscriptionInfo(current.SubscriptionId); // This sample only works with subscriptions that are still cached. if (subscription != null) @@ -73,7 +73,7 @@ public async Task Listen() } } } - + if (notifications.Count > 0) { @@ -101,7 +101,7 @@ public async Task GetChangedMessagesAsync(IEnumerable notification string serviceRootUrl = "https://graph.microsoft.com/v1.0/"; foreach (var notification in notifications) { - SubscriptionStore subscription = SubscriptionStore.GetSubscriptionInfo(notification.SubscriptionId); + SubscriptionDetails subscription = SubscriptionCache.GetSubscriptionCache().GetSubscriptionInfo(notification.SubscriptionId); string accessToken; try { diff --git a/GraphWebhooks/Controllers/SubscriptionController.cs b/GraphWebhooks/Controllers/SubscriptionController.cs index 88aed3f..9d4cb9b 100644 --- a/GraphWebhooks/Controllers/SubscriptionController.cs +++ b/GraphWebhooks/Controllers/SubscriptionController.cs @@ -14,6 +14,7 @@ using System.Threading.Tasks; using GraphWebhooks.Helpers; using System.Security.Claims; +using System.Collections.Generic; namespace GraphWebhooks.Controllers { @@ -29,13 +30,10 @@ public ActionResult Index() [Authorize] public async Task CreateSubscription() { - string subscriptionsEndpoint = "https://graph.microsoft.com/v1.0/subscriptions/"; - string accessToken; + HttpResponseMessage response; try { - - // Get an access token. - accessToken = await AuthHelper.GetAccessTokenAsync(); + response = await SubscriptionHelper.CreateSubscription(); } catch (Exception e) { @@ -43,90 +41,36 @@ public async Task CreateSubscription() return View("Error", e); } - // Build the request. - HttpClient client = new HttpClient(); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - - // This sample subscribes to get notifications when the user receives an email. - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, subscriptionsEndpoint); - Subscription subscription = new Subscription + if (!response.IsSuccessStatusCode) { - Resource = "me/mailFolders('Inbox')/messages", - ChangeType = "created", - NotificationUrl = ConfigurationManager.AppSettings["ida:NotificationUrl"], - ClientState = Guid.NewGuid().ToString(), - //ExpirationDateTime = DateTime.UtcNow + new TimeSpan(0, 0, 4230, 0) // current maximum timespan for messages - ExpirationDateTime = DateTime.UtcNow + new TimeSpan(0, 0, 15, 0) // shorter duration useful for testing - }; - - string contentString = JsonConvert.SerializeObject(subscription, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); - request.Content = new StringContent(contentString, System.Text.Encoding.UTF8, "application/json"); + return RedirectToAction("Index", "Error", new { message = response.StatusCode, debug = await response.Content.ReadAsStringAsync() }); + } - // Send the `POST subscriptions` request and parse the response. - HttpResponseMessage response = await client.SendAsync(request); - if (response.IsSuccessStatusCode) + string stringResult = await response.Content.ReadAsStringAsync(); + SubscriptionViewModel viewModel = new SubscriptionViewModel() { - string stringResult = await response.Content.ReadAsStringAsync(); - SubscriptionViewModel viewModel = new SubscriptionViewModel - { - Subscription = JsonConvert.DeserializeObject(stringResult) - }; - - // This sample temporarily stores the current subscription ID, client state, user object ID, and tenant ID. - // This info is required so the NotificationController, which is not authenticated, can retrieve an access token from the cache and validate the subscription. - // Production apps typically use some method of persistent storage. - SubscriptionStore.SaveSubscriptionInfo(viewModel.Subscription.Id, - viewModel.Subscription.ClientState, - ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value, - ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value); + Subscription = JsonConvert.DeserializeObject(stringResult) + }; - // This sample just saves the current subscription ID to the session so we can delete it later. - Session["SubscriptionId"] = viewModel.Subscription.Id; - return View("Subscription", viewModel); - } - else - { - return RedirectToAction("Index", "Error", new { message = response.StatusCode, debug = await response.Content.ReadAsStringAsync() }); - } + return View("Subscription", viewModel); } // Delete the current webhooks subscription and sign out the user. [Authorize] public async Task DeleteSubscription() { - string subscriptionsEndpoint = "https://graph.microsoft.com/v1.0/subscriptions/"; - string subscriptionId = (string)Session["SubscriptionId"]; - string accessToken; - try - { - - // Get an access token. - accessToken = await AuthHelper.GetAccessTokenAsync(); - } - catch (Exception e) - { - ViewBag.Message = BuildErrorMessage(e); - return View("Error", e); - } + var subscriptions = SubscriptionCache.GetSubscriptionCache().DeleteAllSubscriptions(); - if (!string.IsNullOrEmpty(subscriptionId)) + foreach (var subscription in subscriptions) { + HttpResponseMessage response = await SubscriptionHelper.DeleteSubscription(subscription.Key); - // Build the request. - HttpClient client = new HttpClient(); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, subscriptionsEndpoint + subscriptionId); - - // Send the `DELETE subscriptions/id` request. - HttpResponseMessage response = await client.SendAsync(request); if (!response.IsSuccessStatusCode) { return RedirectToAction("Index", "Error", new { message = response.StatusCode, debug = response.Content.ReadAsStringAsync() }); } } + return RedirectToAction("SignOut", "Account"); } diff --git a/GraphWebhooks/GraphWebhooks.csproj b/GraphWebhooks/GraphWebhooks.csproj index fcfdb25..08778e9 100644 --- a/GraphWebhooks/GraphWebhooks.csproj +++ b/GraphWebhooks/GraphWebhooks.csproj @@ -177,7 +177,10 @@ Global.asax - + + + + diff --git a/GraphWebhooks/Helpers/HttpHelper.cs b/GraphWebhooks/Helpers/HttpHelper.cs new file mode 100644 index 0000000..41f4c49 --- /dev/null +++ b/GraphWebhooks/Helpers/HttpHelper.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using System.Web; + +namespace GraphWebhooks.Helpers +{ + public class HttpHelper + { + internal static async Task SendAsync(string endpoint, HttpMethod httpMethod, object content = null) + { + // Get an access token. + string accessToken = await AuthHelper.GetAccessTokenAsync(); + + // Build the request. + using (HttpClient client = new HttpClient()) + { + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + // This sample subscribes to get notifications when the user receives an email. + HttpRequestMessage request = new HttpRequestMessage(httpMethod, endpoint); + + if (content != null) + { + string contentString = JsonConvert.SerializeObject(content, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + request.Content = new StringContent(contentString, System.Text.Encoding.UTF8, "application/json"); + } + + return await client.SendAsync(request); + } + } + } +} \ No newline at end of file diff --git a/GraphWebhooks/Helpers/SubscriptionCache.cs b/GraphWebhooks/Helpers/SubscriptionCache.cs new file mode 100644 index 0000000..cf97582 --- /dev/null +++ b/GraphWebhooks/Helpers/SubscriptionCache.cs @@ -0,0 +1,98 @@ +using GraphWebhooks.Models; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Threading.Tasks; +using System.Timers; +using System.Web; + +namespace GraphWebhooks.Helpers +{ + public class SubscriptionCache + { + static SubscriptionCache cache = null; + + Timer timer; + private SubscriptionCache() + { + // Renew subscriptions every 10 minute. + Timer renewalTimer = new Timer(10 * 60 * 1000) + { + AutoReset = false + }; + renewalTimer.Elapsed += OnRenewal; + renewalTimer.Start(); + this.timer = renewalTimer; + } + + public static SubscriptionCache GetSubscriptionCache() + { + if(cache != null) + { + return cache; + } + + cache = new SubscriptionCache(); + return cache; + } + + + private async void OnRenewal(object sender, ElapsedEventArgs e) + { + Dictionary subscriptionstore = HttpRuntime.Cache.Get("subscription_store") as Dictionary; + + foreach (var item in subscriptionstore) + { + var response = await SubscriptionHelper.CheckSubscription(item.Key); + if (response.IsSuccessStatusCode) + { + await SubscriptionHelper.RenewSubscription(item.Key as string); + } + else + { + await SubscriptionHelper.CreateSubscription(); + } + } + + timer.Start(); + } + + + // This sample temporarily stores the current subscription ID, client state, user object ID, and tenant ID. + // This info is required so the NotificationController can retrieve an access token from the cache and validate the subscription. + // Production apps typically use some method of persistent storage. + public void SaveSubscriptionInfo(SubscriptionDetails subscriptionDetails) + { + if (HttpRuntime.Cache["subscription_store"] == null) + { + Dictionary subscriptionstore = new Dictionary(); + subscriptionstore.Add(subscriptionDetails.SubscriptionId, subscriptionDetails); + HttpRuntime.Cache.Add("subscription_store", + subscriptionstore, + null, DateTime.MaxValue, new TimeSpan(24, 0, 0), System.Web.Caching.CacheItemPriority.NotRemovable, null); + } + else + { + Dictionary subscriptionstore = HttpRuntime.Cache.Get("subscription_store") as Dictionary; + subscriptionstore.Add(subscriptionDetails.SubscriptionId, subscriptionDetails); + } + } + + public SubscriptionDetails GetSubscriptionInfo(string subscriptionId) + { + Dictionary subscriptionstore = HttpRuntime.Cache.Get("subscription_store") as Dictionary; + return subscriptionstore[subscriptionId]; + } + + public Dictionary DeleteAllSubscriptions() + { + return HttpRuntime.Cache.Remove("subscription_store") as Dictionary; + + } + } +} \ No newline at end of file diff --git a/GraphWebhooks/Helpers/SubscriptionDetails.cs b/GraphWebhooks/Helpers/SubscriptionDetails.cs new file mode 100644 index 0000000..ac5e3c7 --- /dev/null +++ b/GraphWebhooks/Helpers/SubscriptionDetails.cs @@ -0,0 +1,26 @@ +/* + * Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. + * See LICENSE in the source repository root for complete license information. + */ + +using System; +using System.Web; + +namespace GraphWebhooks.Helpers +{ + public class SubscriptionDetails + { + public string SubscriptionId { get; set; } + public string ClientState { get; set; } + public string UserId { get; set; } + public string TenantId { get; set; } + + internal SubscriptionDetails(string subscriptionId, string clientState, string userId, string tenantId) + { + SubscriptionId = subscriptionId; + ClientState = clientState; + UserId = userId; + TenantId = tenantId; + } + } +} \ No newline at end of file diff --git a/GraphWebhooks/Helpers/SubscriptionHelper.cs b/GraphWebhooks/Helpers/SubscriptionHelper.cs new file mode 100644 index 0000000..751d019 --- /dev/null +++ b/GraphWebhooks/Helpers/SubscriptionHelper.cs @@ -0,0 +1,87 @@ +using GraphWebhooks.Models; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; +using System.Web; + +namespace GraphWebhooks.Helpers +{ + public class SubscriptionHelper + { + internal static async Task CreateSubscription() + { + string subscriptionsEndpoint = "https://graph.microsoft.com/v1.0/subscriptions/"; + + Subscription subscription = new Subscription + { + Resource = "me/mailFolders('Inbox')/messages", + ChangeType = "created", + NotificationUrl = ConfigurationManager.AppSettings["ida:NotificationUrl"], + ClientState = Guid.NewGuid().ToString(), + //ExpirationDateTime = DateTime.UtcNow + new TimeSpan(0, 0, 4230, 0) // current maximum timespan for messages + ExpirationDateTime = DateTime.UtcNow + new TimeSpan(0, 0, 15, 0) // shorter duration useful for testing + }; + + // Send the `POST subscriptions` request and parse the response. + HttpResponseMessage response = await HttpHelper.SendAsync(subscriptionsEndpoint, HttpMethod.Post, subscription); + + if (!response.IsSuccessStatusCode) + { + return response; + } + + string stringResult = await response.Content.ReadAsStringAsync(); + var createdSubscription = JsonConvert.DeserializeObject(stringResult); + var subscriptionDetails = new SubscriptionDetails( + createdSubscription.Id, + createdSubscription.ClientState, + ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value, + ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value); + + // This sample temporarily stores the current subscription ID, client state, user object ID, and tenant ID. + // This info is required so the NotificationController, which is not authenticated, can retrieve an access token from the cache and validate the subscription. + // Production apps typically use some method of persistent storage. + SubscriptionCache.GetSubscriptionCache().SaveSubscriptionInfo(subscriptionDetails); + + return response; + } + + + internal static async Task RenewSubscription(string subscriptionId) + { + + string subscriptionsEndpoint = "https://graph.microsoft.com/v1.0/subscriptions/" + subscriptionId; + + Subscription subscription = new Subscription + { + ExpirationDateTime = DateTime.UtcNow + new TimeSpan(0, 0, 5, 0) // shorter duration useful for testing + }; + + // Send the `POST subscriptions` request and parse the response. + HttpResponseMessage response = await HttpHelper.SendAsync(subscriptionsEndpoint, new HttpMethod("PATCH"), subscription); + response.EnsureSuccessStatusCode(); + return response; + } + + internal static async Task CheckSubscription(string subscriptionId) + { + string subscriptionsEndpoint = "https://graph.microsoft.com/v1.0/subscriptions/" + subscriptionId; + + HttpResponseMessage response = await HttpHelper.SendAsync(subscriptionsEndpoint, HttpMethod.Get); + return response; + } + + internal static async Task DeleteSubscription(string subscriptionId) + { + string subscriptionsEndpoint = "https://graph.microsoft.com/v1.0/subscriptions/" + subscriptionId; + + HttpResponseMessage response = await HttpHelper.SendAsync(subscriptionsEndpoint, HttpMethod.Delete); + return response; + } + } +} \ No newline at end of file diff --git a/GraphWebhooks/Helpers/SubscriptionStore.cs b/GraphWebhooks/Helpers/SubscriptionStore.cs deleted file mode 100644 index 6f4a05a..0000000 --- a/GraphWebhooks/Helpers/SubscriptionStore.cs +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. - * See LICENSE in the source repository root for complete license information. - */ - -using System; -using System.Web; - -namespace GraphWebhooks.Helpers -{ - public class SubscriptionStore - { - public string SubscriptionId { get; set; } - public string ClientState { get; set; } - public string UserId { get; set; } - public string TenantId { get; set; } - - private SubscriptionStore(string subscriptionId, Tuple parameters) - { - SubscriptionId = subscriptionId; - ClientState = parameters.Item1; - UserId = parameters.Item2; - TenantId = parameters.Item3; - } - - // This sample temporarily stores the current subscription ID, client state, user object ID, and tenant ID. - // This info is required so the NotificationController can retrieve an access token from the cache and validate the subscription. - // Production apps typically use some method of persistent storage. - public static void SaveSubscriptionInfo(string subscriptionId, string clientState, string userId, string tenantId) - { - HttpRuntime.Cache.Insert("subscriptionId_" + subscriptionId, - Tuple.Create(clientState, userId, tenantId), - null, DateTime.MaxValue, new TimeSpan(24, 0, 0), System.Web.Caching.CacheItemPriority.NotRemovable, null); - } - - public static SubscriptionStore GetSubscriptionInfo(string subscriptionId) - { - Tuple subscriptionParams = HttpRuntime.Cache.Get("subscriptionId_" + subscriptionId) as Tuple; - return new SubscriptionStore(subscriptionId, subscriptionParams); - } - } -} \ No newline at end of file