Skip to content
This repository has been archived by the owner on Nov 16, 2023. It is now read-only.

Commit

Permalink
Adding support for subscription renewal
Browse files Browse the repository at this point in the history
  • Loading branch information
anujab committed Sep 12, 2017
1 parent eed3855 commit dad2146
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 117 deletions.
6 changes: 3 additions & 3 deletions GraphWebhooks/Controllers/NotificationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public async Task<ActionResult> Listen()
Notification current = JsonConvert.DeserializeObject<Notification>(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)
Expand All @@ -73,7 +73,7 @@ public async Task<ActionResult> Listen()
}
}
}

if (notifications.Count > 0)
{

Expand Down Expand Up @@ -101,7 +101,7 @@ public async Task GetChangedMessagesAsync(IEnumerable<Notification> 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
{
Expand Down
86 changes: 15 additions & 71 deletions GraphWebhooks/Controllers/SubscriptionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using System.Threading.Tasks;
using GraphWebhooks.Helpers;
using System.Security.Claims;
using System.Collections.Generic;

namespace GraphWebhooks.Controllers
{
Expand All @@ -29,104 +30,47 @@ public ActionResult Index()
[Authorize]
public async Task<ActionResult> 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)
{
ViewBag.Message = BuildErrorMessage(e);
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<Subscription>(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<Subscription>(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<ActionResult> 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");
}

Expand Down
5 changes: 4 additions & 1 deletion GraphWebhooks/GraphWebhooks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,10 @@
<Compile Include="Global.asax.cs">
<DependentUpon>Global.asax</DependentUpon>
</Compile>
<Compile Include="Helpers\SubscriptionStore.cs" />
<Compile Include="Helpers\HttpHelper.cs" />
<Compile Include="Helpers\SubscriptionCache.cs" />
<Compile Include="Helpers\SubscriptionDetails.cs" />
<Compile Include="Helpers\SubscriptionHelper.cs" />
<Compile Include="Models\Message.cs" />
<Compile Include="Models\Notification.cs" />
<Compile Include="Models\Subscription.cs" />
Expand Down
38 changes: 38 additions & 0 deletions GraphWebhooks/Helpers/HttpHelper.cs
Original file line number Diff line number Diff line change
@@ -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<HttpResponseMessage> 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);
}
}
}
}
98 changes: 98 additions & 0 deletions GraphWebhooks/Helpers/SubscriptionCache.cs
Original file line number Diff line number Diff line change
@@ -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<string, SubscriptionDetails> subscriptionstore = HttpRuntime.Cache.Get("subscription_store") as Dictionary<string, SubscriptionDetails>;

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<string, SubscriptionDetails> subscriptionstore = new Dictionary<string, SubscriptionDetails>();
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<string, SubscriptionDetails> subscriptionstore = HttpRuntime.Cache.Get("subscription_store") as Dictionary<string, SubscriptionDetails>;
subscriptionstore.Add(subscriptionDetails.SubscriptionId, subscriptionDetails);
}
}

public SubscriptionDetails GetSubscriptionInfo(string subscriptionId)
{
Dictionary<string, SubscriptionDetails> subscriptionstore = HttpRuntime.Cache.Get("subscription_store") as Dictionary<string, SubscriptionDetails>;
return subscriptionstore[subscriptionId];
}

public Dictionary<string, SubscriptionDetails> DeleteAllSubscriptions()
{
return HttpRuntime.Cache.Remove("subscription_store") as Dictionary<string, SubscriptionDetails>;

}
}
}
26 changes: 26 additions & 0 deletions GraphWebhooks/Helpers/SubscriptionDetails.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading

0 comments on commit dad2146

Please sign in to comment.