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

Commit

Permalink
Merge pull request #15 from anujab/master
Browse files Browse the repository at this point in the history
Adding support for subscription renewal
  • Loading branch information
jasonjoh committed May 23, 2018
2 parents d6e3c5c + 76e90c8 commit 8514561
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 113 deletions.
16 changes: 6 additions & 10 deletions GraphWebhooks/Controllers/NotificationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ 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);
// Check client state to verify the message is from Microsoft Graph.
var subscription = SubscriptionCache.GetSubscriptionCache().GetSubscriptionInfo(current.SubscriptionId);

// This sample only works with subscriptions that are still cached.
if (subscription != null)
Expand All @@ -70,11 +70,11 @@ public async Task<ActionResult> Listen()
}
}
}

if (notifications.Count > 0)
{

// Query for the changed messages.
// Query for the changed messages.
await GetChangedMessagesAsync(notifications.Values);
}
}
Expand All @@ -97,13 +97,9 @@ public async Task GetChangedMessagesAsync(IEnumerable<Notification> notification

foreach (var notification in notifications)
{
var subscription = SubscriptionStore.GetSubscriptionInfo(notification.SubscriptionId);

// Extract base URL from client state to use as redirect
// url for token request
string baseUrl = notification.ClientState.Split('+')[1];
SubscriptionDetails subscription = SubscriptionCache.GetSubscriptionCache().GetSubscriptionInfo(notification.SubscriptionId);

var graphClient = GraphHelper.GetAuthenticatedClient(subscription.UserId, baseUrl);
var graphClient = GraphHelper.GetAuthenticatedClient(subscription.UserId, subscription.RedirectUrl);

// Get the message
var message = await graphClient.Me.Messages[notification.ResourceData.Id].Request()
Expand Down
50 changes: 12 additions & 38 deletions GraphWebhooks/Controllers/SubscriptionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
using GraphWebhooks.Helpers;
using GraphWebhooks.Models;
using Microsoft.Identity.Client;
using Microsoft.Graph;
using System;
using System.Configuration;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web.Mvc;

Expand All @@ -27,62 +24,39 @@ public ActionResult Index()
public async Task<ActionResult> CreateSubscription()
{
string baseUrl = $"{Request.Url.Scheme}://{Request.Url.Authority}";
string userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;

var graphClient = GraphHelper.GetAuthenticatedClient(userObjectId, baseUrl);

var subscription = new Subscription
{
Resource = "me/mailFolders('Inbox')/messages",
ChangeType = "created",
NotificationUrl = ConfigurationManager.AppSettings["ida:NotificationUrl"],
// Include baseUrl as part of state (so we can use this in the notification
// to get an access token)
ClientState = $"{Guid.NewGuid().ToString()}+{baseUrl}",
ExpirationDateTime = DateTime.UtcNow + new TimeSpan(0, 0, 15, 0) // shorter duration useful for testing
};

try
{
var newSubscription = await graphClient.Subscriptions.Request().AddAsync(subscription);
var subscription = await SubscriptionHelper.CreateSubscription(baseUrl);

SubscriptionViewModel viewModel = new SubscriptionViewModel
SubscriptionViewModel viewModel = new SubscriptionViewModel()
{
Subscription = newSubscription
Subscription = subscription
};

// 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(newSubscription.Id,
newSubscription.ClientState,
ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value);

// This sample just saves the current subscription ID to the session so we can delete it later.
Session["SubscriptionId"] = newSubscription.Id;
return View("Subscription", viewModel);
}
catch (Exception ex)
catch (Exception e)
{
ViewBag.Message = BuildErrorMessage(ex);
return View("Error", ex);
ViewBag.Message = BuildErrorMessage(e);
return View("Error", e);
}
}

// Delete the current webhooks subscription and sign out the user.
[Authorize]
public async Task<ActionResult> DeleteSubscription()
{
string subscriptionId = (string)Session["SubscriptionId"];
string baseUrl = $"{Request.Url.Scheme}://{Request.Url.Authority}";
string userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;

var graphClient = GraphHelper.GetAuthenticatedClient(userObjectId, baseUrl);
var subscriptions = SubscriptionCache.GetSubscriptionCache().DeleteAllSubscriptions();

try
{
await graphClient.Subscriptions[subscriptionId].Request().DeleteAsync();
Session.Remove("SubscriptionId");
foreach (var subscription in subscriptions)
{
await SubscriptionHelper.DeleteSubscription(subscription.Key, baseUrl);
}

return RedirectToAction("SignOut", "Account");
}
catch (Exception ex)
Expand Down
4 changes: 3 additions & 1 deletion GraphWebhooks/GraphWebhooks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,10 @@
<Compile Include="Global.asax.cs">
<DependentUpon>Global.asax</DependentUpon>
</Compile>
<Compile Include="Helpers\SubscriptionCache.cs" />
<Compile Include="Helpers\SubscriptionDetails.cs" />
<Compile Include="Helpers\SubscriptionHelper.cs" />
<Compile Include="Helpers\GraphHelper.cs" />
<Compile Include="Helpers\SubscriptionStore.cs" />
<Compile Include="Models\Message.cs" />
<Compile Include="Models\Notification.cs" />
<Compile Include="Models\Subscription.cs" />
Expand Down
89 changes: 89 additions & 0 deletions GraphWebhooks/Helpers/SubscriptionCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Collections.Generic;
using System.Runtime.Caching;
using System.Timers;

namespace GraphWebhooks.Helpers
{
public class SubscriptionCache
{
static SubscriptionCache cache = null;

private static ObjectCache objCache = MemoryCache.Default;
private static CacheItemPolicy defaultPolicy = new CacheItemPolicy();

Timer timer;
private SubscriptionCache()
{
// Renew subscriptions every 10 minute.
Timer renewalTimer = new Timer(10 * 60 * 1000)
{
AutoReset = false
};
renewalTimer.Elapsed += OnRenewal;
renewalTimer.Start();
timer = renewalTimer;
}

public static SubscriptionCache GetSubscriptionCache()
{
if(cache != null)
{
return cache;
}

cache = new SubscriptionCache();
return cache;
}


private async void OnRenewal(object sender, ElapsedEventArgs e)
{
var subscriptionstore = objCache.Get("subscription_store") as Dictionary<string, SubscriptionDetails>;

foreach (var item in subscriptionstore)
{
var response = await SubscriptionHelper.CheckSubscription(item.Key, item.Value.UserId, item.Value.RedirectUrl);
if (response != null)
{
await SubscriptionHelper.RenewSubscription(item.Key, item.Value.UserId, item.Value.RedirectUrl);
}
else
{
await SubscriptionHelper.CreateSubscription(item.Value.UserId, item.Value.RedirectUrl);
}
}

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 (objCache["subscription_store"] == null)
{
var subscriptionstore = new Dictionary<string, SubscriptionDetails>();
subscriptionstore.Add(subscriptionDetails.SubscriptionId, subscriptionDetails);

objCache.Set(new CacheItem("subscription_store", subscriptionstore), defaultPolicy);
}
else
{
var subscriptionstore = objCache.Get("subscription_store") as Dictionary<string, SubscriptionDetails>;
subscriptionstore.Add(subscriptionDetails.SubscriptionId, subscriptionDetails);
}
}

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

public Dictionary<string, SubscriptionDetails> DeleteAllSubscriptions()
{
return objCache.Remove("subscription_store") as Dictionary<string, SubscriptionDetails>;
}
}
}
23 changes: 23 additions & 0 deletions GraphWebhooks/Helpers/SubscriptionDetails.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license.
* See LICENSE in the source repository root for complete license information.
*/

namespace GraphWebhooks.Helpers
{
public class SubscriptionDetails
{
public string SubscriptionId { get; set; }
public string ClientState { get; set; }
public string UserId { get; set; }
public string RedirectUrl { get; set; }

internal SubscriptionDetails(string subscriptionId, string clientState, string userId, string redirectUrl)
{
SubscriptionId = subscriptionId;
ClientState = clientState;
UserId = userId;
RedirectUrl = redirectUrl;
}
}
}
74 changes: 74 additions & 0 deletions GraphWebhooks/Helpers/SubscriptionHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using Microsoft.Graph;
using System;
using System.Configuration;
using System.Security.Claims;
using System.Threading.Tasks;

namespace GraphWebhooks.Helpers
{
public class SubscriptionHelper
{
internal static string CurrentUserId
{
get
{
return ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
}
}

internal static async Task<Subscription> CreateSubscription(string baseUrl, string userId = null)
{
var graphClient = GraphHelper.GetAuthenticatedClient(string.IsNullOrEmpty(userId) ? CurrentUserId : userId, baseUrl);

var 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, 15, 0) // shorter duration useful for testing
};

var newSubscription = await graphClient.Subscriptions.Request().AddAsync(subscription);

// This sample temporarily stores the current subscription ID, client state, and user object 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.
var subscriptionDetails = new SubscriptionDetails(
newSubscription.Id,
newSubscription.ClientState,
CurrentUserId,
baseUrl);

SubscriptionCache.GetSubscriptionCache().SaveSubscriptionInfo(subscriptionDetails);

return newSubscription;
}


internal static async Task<Subscription> RenewSubscription(string subscriptionId, string userId, string baseUrl)
{
var graphClient = GraphHelper.GetAuthenticatedClient(userId, baseUrl);

Subscription subscription = new Subscription
{
ExpirationDateTime = DateTime.UtcNow + new TimeSpan(0, 0, 15, 0) // shorter duration useful for testing
};

return await graphClient.Subscriptions[subscriptionId].Request().UpdateAsync(subscription);
}

internal static async Task<Subscription> CheckSubscription(string subscriptionId, string userId, string baseUrl)
{
var graphClient = GraphHelper.GetAuthenticatedClient(userId, baseUrl);
return await graphClient.Subscriptions[subscriptionId].Request().GetAsync();
}

internal static async Task DeleteSubscription(string subscriptionId, string baseUrl)
{
var graphClient = GraphHelper.GetAuthenticatedClient(CurrentUserId, baseUrl);
await graphClient.Subscriptions[subscriptionId].Request().DeleteAsync();
}
}
}
44 changes: 0 additions & 44 deletions GraphWebhooks/Helpers/SubscriptionStore.cs

This file was deleted.

2 changes: 1 addition & 1 deletion GraphWebhooks/PrivateSettings.example.config
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
<add key="ida:ClientSecret" value="ENTER_YOUR_SECRET" />
<add key="ida:NotificationUrl" value="ENTER_YOUR_URL/notification/listen" />
<add key="ida:AADInstance" value="https://login.microsoftonline.com" />
<add key="ida:ResourceId" value="https://graph.microsoft.com" />
<add key="ida:AppScopes" value="User.Read Mail.Read" />
</appSettings>
Loading

0 comments on commit 8514561

Please sign in to comment.