internal static async Task ManageSubscription(SubscriptionActivity currentSubscriptionActivity, string oid, string tid, string upn, IConfiguration config, IMsalAccountActivityStore msalAccountActivityStore, GraphServiceClient _graphServiceClient, IMsalTokenCacheProvider msalTokenCacheProvider) { string subscriptionId = null; string changeToken = null; string notiticationUrl = config.GetValue <string>("Files:SubscriptionService"); int subscriptionLifeTimeInMinutes = config.GetValue <int>("Files:SubscriptionLifeTime"); if (subscriptionLifeTimeInMinutes == 0) { subscriptionLifeTimeInMinutes = 15; } // Load the current subscription (if any) var currentSubscriptions = await _graphServiceClient.Subscriptions.Request().GetAsync(); var currentOneDriveSubscription = currentSubscriptions.FirstOrDefault(p => p.Resource == "me/drive/root"); // If present and still using the same subscription host then update the subscription expiration date if (currentOneDriveSubscription != null && currentOneDriveSubscription.NotificationUrl.Equals(notiticationUrl, StringComparison.InvariantCultureIgnoreCase)) { // Extend the expiration of the current subscription subscriptionId = currentOneDriveSubscription.Id; currentOneDriveSubscription.ExpirationDateTime = DateTimeOffset.UtcNow.AddMinutes(subscriptionLifeTimeInMinutes); currentOneDriveSubscription.ClientState = Constants.FilesSubscriptionServiceClientState; await _graphServiceClient.Subscriptions[currentOneDriveSubscription.Id].Request().UpdateAsync(currentOneDriveSubscription); // Check if the last change token was populated if (currentSubscriptionActivity == null) { currentSubscriptionActivity = await msalAccountActivityStore.GetSubscriptionActivityForUserSubscription(oid, tid, upn, subscriptionId); } if (currentSubscriptionActivity != null) { changeToken = currentSubscriptionActivity.LastChangeToken; } } else { // Add a new subscription var newSubscription = await _graphServiceClient.Subscriptions.Request().AddAsync(new Subscription() { ChangeType = "updated", NotificationUrl = notiticationUrl, Resource = "me/drive/root", ExpirationDateTime = DateTimeOffset.UtcNow.AddMinutes(subscriptionLifeTimeInMinutes), ClientState = Constants.FilesSubscriptionServiceClientState, LatestSupportedTlsVersion = "v1_2" }); subscriptionId = newSubscription.Id; } // Store the user principal name with the subscriptionid as that's the mechanism needed to connect change event with tenant/user var cacheEntriesToRemove = await msalAccountActivityStore.UpdateSubscriptionId(subscriptionId, oid, tid, upn); // If we've found old MSAL cache entries for which we've removed the respective MsalActivity records we should also // drop these from the MSAL cache itself if (cacheEntriesToRemove.Any()) { foreach (var cacheEntry in cacheEntriesToRemove) { await(msalTokenCacheProvider as IntegratedTokenCacheAdapter).RemoveKeyFromCache(cacheEntry); } } if (changeToken == null) { // Initialize the subscription and get the latest change token, to avoid getting back all the historical changes IDriveItemDeltaCollectionPage deltaCollection = await _graphServiceClient.Me.Drive.Root.Delta("latest").Request().GetAsync(); var deltaLink = deltaCollection.AdditionalData["@odata.deltaLink"]; if (!string.IsNullOrEmpty(deltaLink.ToString())) { changeToken = ProcessChanges.GetChangeTokenFromUrl(deltaLink.ToString()); } } // Store a record per user/subscription to track future delta queries if (currentSubscriptionActivity == null) { currentSubscriptionActivity = new SubscriptionActivity(oid, tid, upn) { SubscriptionId = subscriptionId, LastChangeToken = changeToken }; } currentSubscriptionActivity.LastChangeToken = changeToken; currentSubscriptionActivity.SubscriptionId = subscriptionId; await msalAccountActivityStore.UpsertSubscriptionActivity(currentSubscriptionActivity); }
public async Task Run([QueueTrigger(Constants.OneDriveFileNotificationsQueue)] string myQueueItem, ILogger log) { log.LogInformation($"C# Queue trigger function processed: {myQueueItem}"); var notification = JsonSerializer.Deserialize <ChangeNotification>(myQueueItem); // Get the most recently updated msal account information for this subscription var account = await _msalAccountActivityStore.GetMsalAccountActivityForSubscription(notification.SubscriptionId); if (account != null) { // Configure the confidential client to get an access token for the needed resource var app = ConfidentialClientApplicationBuilder.Create(_config.GetValue <string>("AzureAd:ClientId")) .WithClientSecret(_config.GetValue <string>("AzureAd:ClientSecret")) .WithAuthority($"{_config.GetValue<string>("AzureAd:Instance")}{_config.GetValue<string>("AzureAd:TenantId")}") .Build(); // Initialize the MSAL cache for the specific account var msalCache = new BackgroundWorkerTokenCacheAdapter(account.AccountCacheKey, _serviceProvider.GetService <IDistributedCache>(), _serviceProvider.GetService <ILogger <MsalDistributedTokenCacheAdapter> >(), _serviceProvider.GetService <IOptions <MsalDistributedTokenCacheAdapterOptions> >()); await msalCache.InitializeAsync(app.UserTokenCache); // Prepare an MsalAccount instance representing the user we want to get a token for var hydratedAccount = new MsalAccount { HomeAccountId = new AccountId( account.AccountIdentifier, account.AccountObjectId, account.AccountTenantId) }; try { // Use the confidential MSAL client to get a token for the user we need to impersonate var result = await app.AcquireTokenSilent(Constants.BasePermissionScopes, hydratedAccount) .ExecuteAsync() .ConfigureAwait(false); //log.LogInformation($"Token acquired: {result.AccessToken}"); // Configure the Graph SDK to use an auth provider that takes the token we've just requested var authenticationProvider = new DelegateAuthenticationProvider( (requestMessage) => { requestMessage.Headers.Authorization = new AuthenticationHeaderValue(CoreConstants.Headers.Bearer, result.AccessToken); return(Task.FromResult(0)); }); var graphClient = new GraphServiceClient(authenticationProvider); // Retrieve the last used subscription activity information for this user+subscription var subscriptionActivity = await _msalAccountActivityStore.GetSubscriptionActivityForUserSubscription(account.AccountObjectId, account.AccountTenantId, account.UserPrincipalName, notification.SubscriptionId); // Make graph call on behalf of the user: do the delta query providing the last used change token so only new changes are returned IDriveItemDeltaCollectionPage deltaCollection = await graphClient.Me.Drive.Root.Delta(subscriptionActivity.LastChangeToken).Request().GetAsync(); bool morePagesAvailable = false; do { // If there is a NextPageRequest, there are more pages morePagesAvailable = deltaCollection.NextPageRequest != null; foreach (var driveItem in deltaCollection.CurrentPage) { await ProcessDriveItemChanges(driveItem, log); } if (morePagesAvailable) { // Get the next page of results deltaCollection = await deltaCollection.NextPageRequest.GetAsync(); } }while (morePagesAvailable); // Get the last used change token var deltaLink = deltaCollection.AdditionalData["@odata.deltaLink"]; if (!string.IsNullOrEmpty(deltaLink.ToString())) { var token = GetChangeTokenFromUrl(deltaLink.ToString()); subscriptionActivity.LastChangeToken = token; } // Persist back the last used change token await _msalAccountActivityStore.UpsertSubscriptionActivity(subscriptionActivity); } catch (MsalUiRequiredException ex) { /* * If MsalUiRequiredException is thrown for an account, it means that a user interaction is required * thus the background worker wont be able to acquire a token silently for it. * The user of that account will have to access the web app to perform this interaction. * Examples that could cause this: MFA requirement, token expired or revoked, token cache deleted, etc */ await _msalAccountActivityStore.HandleIntegratedTokenAcquisitionFailure(account); log.LogError($"Could not acquire token for account {account.UserPrincipalName}."); log.LogError($"Error: {ex.Message}"); } catch (Exception ex) { throw ex; } } }