├── .gitattributes ├── .gitignore ├── LICENSE ├── Nuget.Config ├── README.md ├── SECURITY.md ├── WebJobs.Extensions.O365.sln ├── appveyor.yml ├── blog.md ├── samples └── GraphExtensionSamples │ ├── ExcelExamples.cs │ ├── GraphExtensionSamples.csproj │ ├── OneDriveExamples.cs │ ├── OutlookExamples.cs │ ├── TokenExamples.cs │ ├── WebhookSubscriptionExamples.cs │ └── WebhookTriggerExamples.cs ├── sign.snk ├── src ├── MicrosoftGraphBinding │ ├── Bindings │ │ ├── ExcelAsyncCollector.cs │ │ ├── ExcelAttribute.cs │ │ ├── GraphTokenBaseAttribute.cs │ │ ├── GraphWebhookChangeType.cs │ │ ├── GraphWebhookSubscriptionAction.cs │ │ ├── GraphWebhookSubscriptionAsyncCollector.cs │ │ ├── GraphWebhookSubscriptionAttribute.cs │ │ ├── GraphWebhookSubscriptionHandler.cs │ │ ├── GraphWebhookTriggerAttribute.cs │ │ ├── O365Constants.cs │ │ ├── O365Models.cs │ │ ├── OneDriveAsyncCollector.cs │ │ ├── OneDriveAttribute.cs │ │ ├── OneDriveWriteStream.cs │ │ ├── OutlookAsyncCollector.cs │ │ ├── OutlookAttribute.cs │ │ ├── WebhookTriggerBindingProvider.cs │ │ ├── WebhookTriggerData.cs │ │ └── WebhookTriggerListener.cs │ ├── Config │ │ ├── Converters │ │ │ ├── ExcelConverters.cs │ │ │ ├── GraphWebhookSubscriptionConverters.cs │ │ │ ├── OneDriveConverter.cs │ │ │ └── OutlookConverter.cs │ │ ├── GraphOptions.cs │ │ ├── GraphServiceClientManager.cs │ │ ├── GraphServiceClientProvider.cs │ │ ├── IGraphServiceClientProvider.cs │ │ ├── IGraphSubscriptionStore.cs │ │ ├── MicrosoftGraphExtensionConfigProvider.cs │ │ ├── MicrosoftGraphWebJobsBuilderExtensions.cs │ │ ├── MicrosoftGraphWebJobsStartup.cs │ │ ├── SubscriptionEntry.cs │ │ └── WebhookSubscriptionStore.cs │ ├── MicrosoftGraphBinding.csproj │ ├── Services │ │ ├── ExcelClient.cs │ │ ├── ExcelService.cs │ │ ├── OneDriveClient.cs │ │ ├── OneDriveService.cs │ │ ├── OutlookClient.cs │ │ └── OutlookService.cs │ └── stylecop.json └── TokenBinding │ ├── AadClient.cs │ ├── AuthTokenExtensionConfigProvider.cs │ ├── AuthTokenWebJobsBuilderExtensions.cs │ ├── AuthTokenWebJobsStartup.cs │ ├── Constants.cs │ ├── EasyAuthTokenClient.cs │ ├── EasyAuthTokenManager.cs │ ├── EasyAuthTokenStoreEntry.cs │ ├── IAadClient.cs │ ├── IEasyAuthClient.cs │ ├── TokenAttribute.cs │ ├── TokenBaseAttribute.cs │ ├── TokenBinding.csproj │ ├── TokenConverter.cs │ ├── TokenIdentityMode.cs │ └── TokenOptions.cs ├── test ├── MicrosoftGraph.Tests │ ├── ExcelMockUtilities.cs │ ├── ExcelTests.cs │ ├── MemorySubscriptionStore.cs │ ├── MicrosoftGraph.Tests.csproj │ ├── MockGraphServiceClientProvider.cs │ ├── MockTokenConverter.cs │ ├── OneDriveMockUtilities.cs │ ├── OneDriveTests.cs │ ├── OutlookMockUtilities.cs │ ├── OutlookTests.cs │ └── common │ │ └── CommonUtilities.cs └── TokenExtension.Tests │ ├── TokenExtension.Tests.csproj │ ├── TokenTests.cs │ └── common │ ├── FakeLocator.cs │ ├── OutputContainer.cs │ └── TestHelpers.cs └── update ├── README.md └── extensions.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | debug 3 | obj 4 | *.suo 5 | packages 6 | 7 | publishprofiles 8 | .vs 9 | *.user 10 | 11 | *.config 12 | appsettings* 13 | buildoutput 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) .NET Foundation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /Nuget.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure Functions bindings to O365 2 | 3 | **NOTE: This project is no longer maintained and is not recommended for new development. To integrate with the Microsoft Graph from Azure Functions, please instead see [this tutorial](https://docs.microsoft.com/en-us/graph/tutorials/azure-functions).** 4 | 5 | Prototype for some WebJobs extension bindings. 6 | 7 | This provides a sample Azure Function extensions for Office. 8 | 9 | ## This provides a few bindings: 10 | 11 | - [Outlook] - sends emails from an O365 account 12 | - [OneDrive] - reads/writes a Onedrive file 13 | - [Excel] - reads/writes an Excel table or worksheet. 14 | - [Token] - this has been extended to allow binding to the MS Graph SDK's Graph Service Client 15 | - [GraphWebhookSubscription] - creates, deletes, or refreshes a Graph Webhook. See https://github.com/microsoftgraph/aspnet-webhooks-rest-sample for graph webhooks sample. 16 | - [GraphWebhookTrigger] - the trigger that activates a function when a Graph Webhook is called for its datatype 17 | 18 | The bindings found in the Microsoft Graph extension use the same authentication process as those in the Token Extension. You can see how to use these bindings in the samples directory. 19 | 20 | ## For Authentication 21 | 22 | Authentication is built using Easy Auth's token store. (see https://cgillum.tech/2016/03/07/app-service-token-store/ ) 23 | The app has an AAD app registered that has been configured with access to the Graph API and given appropriate scopes. The bindings can access the client secret (via appsettings) and use that to perform token exchanges. 24 | 25 | The bindings can authenticate in 4 different ways: 26 | - UserFromId - the token is grabbed on behalf of a provided user id 27 | - UserFromToken - the token is grabbed on behalf of a provided user id token 28 | - UserFromRequest - the token is grabbed on behalf of the user id token found in the HTTP header `X-MS-TOKEN-AAD-ID-TOKEN` 29 | - ClientCredentials - uses the app's credentials to access AAD 30 | 31 | ## Source layout 32 | The samples directory contains examples of how to use the bindings. The code for both the Token and Microsoft Graph extensions can be found in the src directory, and in-memory tests can be found in the tests directory. 33 | 34 | ## Local Development 35 | First create a Functions app using the Functions CLI found https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local. Be sure to use the Version 2.x runtime. 36 | 37 | To install the Token extension, run the command `func extensions install --package Microsoft.Azure.WebJobs.Extensions.AuthTokens -v `. 38 | 39 | To install the Microsoft Graph extension, run the command `func extensions install --package Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph -v `. 40 | 41 | The easiest way to utilize most of the features of these features is to configure an Azure Functions app in the Portal, enable Authentication/Authorization, add the extension, and go through the configuration to enable the proper Microsoft Graph permissions. 42 | 43 | If you are making code changes to the extensions themselves and wish to test these locally, you can manually copy the .dll files that you build into your bin directory in your local function app's directory. 44 | 45 | App Settings to Modify in `local.settings.json`: 46 | - `WEBSITE_AUTH_CLIENT_ID` - Copy from your App Settings in Kudu from your configured app 47 | - `WEBSITE_AUTH_CLIENT_SECRET` - Copy from your App Settings in Kudu from your configured app 48 | - `WEBSITE_AUTH_OPENID_ISSUER` - (Optional, Required for a `ClientCredentials` `TokenIdentityMode`) Copy from your App Settings in Kudu from your configured app (or set to `https://sts.windows.net/` for Azure Active Directory) 49 | - `BYOB_TokenMap` - A valid local directory that you have read/write access to 50 | 51 | ## Current Version 52 | Latest Version: 1.0.0-beta6 53 | Portal Version: 1.0.0-beta5 54 | 55 | If you want to get the latest features and bugfixes, you can manually update to the latest version by following the instructions in the `update` folder. NOTE: If you update to a different version than the version in the portal, you cannot use the Token and Microsoft Graph templates. 56 | 57 | ## License 58 | 59 | This project is under the benevolent umbrella of the [.NET Foundation](http://www.dotnetfoundation.org/) and is licensed under [the MIT License](https://github.com/Azure/azure-webjobs-sdk/blob/master/LICENSE.txt) 60 | 61 | ## Contributing 62 | 63 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 64 | 65 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /WebJobs.Extensions.O365.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2005 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicrosoftGraphBinding", "src\MicrosoftGraphBinding\MicrosoftGraphBinding.csproj", "{B503849F-9A01-48E8-9401-76769F4CC745}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokenBinding", "src\TokenBinding\TokenBinding.csproj", "{397A114E-0075-48F8-AA01-11F3FDFDEC25}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{06065EFC-CB7A-49E4-B57F-B89B8CC01AA8}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokenExtension.Tests", "test\TokenExtension.Tests\TokenExtension.Tests.csproj", "{39053A38-67E7-46BB-AFD6-EFEF705ECE6C}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicrosoftGraph.Tests", "test\MicrosoftGraph.Tests\MicrosoftGraph.Tests.csproj", "{680406E6-3C6B-4006-81A3-B6AD88B78ABA}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3F0C104A-CC6F-4A80-8196-94C476495CF2}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{E0C76B7E-D030-4B52-A288-3790ABC4A72C}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphExtensionSamples", "samples\GraphExtensionSamples\GraphExtensionSamples.csproj", "{B0A7EE27-89D7-47BF-B132-2FC1FB9C5F14}" 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D9B31118-B878-4314-9B7C-0E8D887648CC}" 23 | ProjectSection(SolutionItems) = preProject 24 | README.md = README.md 25 | EndProjectSection 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Release|Any CPU = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {B503849F-9A01-48E8-9401-76769F4CC745}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {B503849F-9A01-48E8-9401-76769F4CC745}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {B503849F-9A01-48E8-9401-76769F4CC745}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {B503849F-9A01-48E8-9401-76769F4CC745}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {397A114E-0075-48F8-AA01-11F3FDFDEC25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {397A114E-0075-48F8-AA01-11F3FDFDEC25}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {397A114E-0075-48F8-AA01-11F3FDFDEC25}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {397A114E-0075-48F8-AA01-11F3FDFDEC25}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {39053A38-67E7-46BB-AFD6-EFEF705ECE6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {39053A38-67E7-46BB-AFD6-EFEF705ECE6C}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {39053A38-67E7-46BB-AFD6-EFEF705ECE6C}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {39053A38-67E7-46BB-AFD6-EFEF705ECE6C}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {680406E6-3C6B-4006-81A3-B6AD88B78ABA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {680406E6-3C6B-4006-81A3-B6AD88B78ABA}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {680406E6-3C6B-4006-81A3-B6AD88B78ABA}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {680406E6-3C6B-4006-81A3-B6AD88B78ABA}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {B0A7EE27-89D7-47BF-B132-2FC1FB9C5F14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {B0A7EE27-89D7-47BF-B132-2FC1FB9C5F14}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {B0A7EE27-89D7-47BF-B132-2FC1FB9C5F14}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {B0A7EE27-89D7-47BF-B132-2FC1FB9C5F14}.Release|Any CPU.Build.0 = Release|Any CPU 53 | EndGlobalSection 54 | GlobalSection(SolutionProperties) = preSolution 55 | HideSolutionNode = FALSE 56 | EndGlobalSection 57 | GlobalSection(NestedProjects) = preSolution 58 | {B503849F-9A01-48E8-9401-76769F4CC745} = {3F0C104A-CC6F-4A80-8196-94C476495CF2} 59 | {397A114E-0075-48F8-AA01-11F3FDFDEC25} = {3F0C104A-CC6F-4A80-8196-94C476495CF2} 60 | {39053A38-67E7-46BB-AFD6-EFEF705ECE6C} = {06065EFC-CB7A-49E4-B57F-B89B8CC01AA8} 61 | {680406E6-3C6B-4006-81A3-B6AD88B78ABA} = {06065EFC-CB7A-49E4-B57F-B89B8CC01AA8} 62 | {B0A7EE27-89D7-47BF-B132-2FC1FB9C5F14} = {E0C76B7E-D030-4B52-A288-3790ABC4A72C} 63 | EndGlobalSection 64 | GlobalSection(ExtensibilityGlobals) = postSolution 65 | SolutionGuid = {443E037E-BBF5-466D-831A-91A375A7FFDC} 66 | EndGlobalSection 67 | EndGlobal 68 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | pull_requests: 3 | do_not_increment_build_number: true 4 | branches: 5 | only: 6 | - master 7 | image: Visual Studio 2017 8 | install: 9 | - ps: >- 10 | $env:CommitHash = "$env:APPVEYOR_REPO_COMMIT" 11 | 12 | cache: '%USERPROFILE%\.nuget\packages' 13 | build_script: 14 | - ps: > 15 | dotnet --version 16 | 17 | $PackArguments = '-o', '..\buildoutput', '--no-build' 18 | 19 | If ($env:APPVEYOR_REPO_BRANCH -ne "master") 20 | { 21 | $PackArguments = $PackArguments + '--version-suffix', '"-$env:APPVEYOR_BUILD_NUMBER"' 22 | } 23 | 24 | dotnet build -v q 25 | 26 | dotnet pack .\src\TokenBinding\TokenBinding.csproj $PackArguments 27 | 28 | dotnet pack .\src\MicrosoftGraphBinding\MicrosoftGraphBinding.csproj $PackArguments 29 | 30 | test_script: 31 | - ps: >- 32 | dotnet test .\test\TokenExtension.Tests\ -v q --no-build 33 | 34 | dotnet test .\test\MicrosoftGraph.Tests\ -v q --no-build 35 | 36 | artifacts: 37 | - path: '**\*.nupkg' 38 | name: Binaries 39 | hosts: 40 | api.nuget.org: 93.184.221.200 -------------------------------------------------------------------------------- /samples/GraphExtensionSamples/ExcelExamples.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | namespace GraphExtensionSamples 4 | { 5 | using Microsoft.Azure.WebJobs; 6 | using Microsoft.AspNetCore.Http; 7 | using System.Collections.Generic; 8 | using Microsoft.Graph; 9 | 10 | public static class ExcelExamples 11 | { 12 | //Appending row(s) to an excel worksheet or table 13 | 14 | public static void AppendRowsToExcelSpreadsheetWithJaggedArray([Excel( 15 | Path = "TestSheet.xlsx", 16 | WorksheetName = "Sheet1", 17 | UserId = "%UserId%", 18 | Identity = TokenIdentityMode.UserFromId)] out object[][] output) 19 | { 20 | output = new object[2][]; 21 | output[0] = new object[] 22 | { 23 | "samplepartname", 42, 3.75, 24 | }; 25 | output[1] = new object[] 26 | { 27 | "part2", 73, 43.20, 28 | }; 29 | } 30 | 31 | [NoAutomaticTrigger] 32 | public static void AppendRowToExcelTableWithSinglePoco([Excel( 33 | Path = "TestSheet.xlsx", 34 | WorksheetName = "Sheet1", 35 | TableName = "Parts", 36 | UserId = "%UserId%", 37 | Identity = TokenIdentityMode.UserFromId)] out PartsTableRow output) 38 | { 39 | output = new PartsTableRow 40 | { 41 | Part = "samplepartname", 42 | Id = 42, 43 | Price = 3.75, 44 | }; 45 | } 46 | 47 | //Updating an excel table 48 | 49 | [NoAutomaticTrigger] 50 | public static void UpdateEntireExcelTableWithPocos( 51 | [Excel( 52 | Path = "TestSheet.xlsx", 53 | WorksheetName = "Sheet1", 54 | TableName = "Parts", 55 | UserId = "%UserId%", 56 | UpdateType = "Update", 57 | Identity = TokenIdentityMode.UserFromId)] out PartsTableRow[] output) 58 | { 59 | output = new PartsTableRow[2]; 60 | output[0] = (new PartsTableRow 61 | { 62 | Part = "part1", 63 | Id = 35, 64 | Price = 0.75 65 | }); 66 | output[1] = (new PartsTableRow 67 | { 68 | Part = "part2", 69 | Id = 73, 70 | Price = 42.37, 71 | }); 72 | } 73 | 74 | //Retrieving values from an excel table or worksheet 75 | public static void GetEntireExcelWorksheetAsJaggedStringArray([Excel( 76 | Path = "TestSheet.xlsx", 77 | WorksheetName = "Sheet1", 78 | UserId = "%UserId%", 79 | Identity = TokenIdentityMode.UserFromId)] string[][] rows) 80 | { 81 | //Perform any operations on the string[][], where each string[] is 82 | //a row in the worksheet. 83 | } 84 | 85 | public static void GetExcelTableAsWorkbookTable([Excel( 86 | Path = "TestSheet.xlsx", 87 | WorksheetName = "Sheet1", 88 | TableName = "sampletable", 89 | UserId = "%UserId%", 90 | Identity = TokenIdentityMode.UserFromId)] WorkbookTable table) 91 | { 92 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/81c50e72166152f9f84dc38b2516379b7a536300/src/Microsoft.Graph/Models/Generated/WorkbookTable.cs 93 | //for usage 94 | } 95 | 96 | public static void GetExcelTableAsPocoList([Excel( 97 | Path = "TestSheet.xlsx", 98 | WorksheetName = "Sheet1", 99 | TableName = "sampletable", 100 | UserId = "%UserId%", 101 | Identity = TokenIdentityMode.UserFromId)] PartsTableRow table) 102 | { 103 | //Note that each POCO object represents one row, and the values correspond to 104 | //the column titles that match the POCO's property names. 105 | } 106 | 107 | public class PartsTableRow 108 | { 109 | public string Part { get; set; } 110 | 111 | public int Id { get; set; } 112 | 113 | public double Price { get; set; } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /samples/GraphExtensionSamples/GraphExtensionSamples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/GraphExtensionSamples/OneDriveExamples.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace GraphExtensionSamples 5 | { 6 | using System; 7 | using System.IO; 8 | using Microsoft.Azure.WebJobs; 9 | using Microsoft.Graph; 10 | 11 | public static class OneDriveExamples 12 | { 13 | //NOTE: In the current release, if these bindings are being used in Azure Functions, you 14 | //should explicitly set a property Access to Read or Write, depending on which operation 15 | //you are trying to perform. In the future, this will be autopopulated based on whether it 16 | //is an input or an output binding. 17 | 18 | public static void ReadOneDriveFileAsByteArray([OneDrive( 19 | Identity = TokenIdentityMode.UserFromId, 20 | UserId = "sampleuserid", 21 | Path = "samplepath.txt")] byte[] array) 22 | { 23 | Console.Write(System.Text.Encoding.UTF8.GetString(array, 0, array.Length)); 24 | } 25 | 26 | 27 | //NOTE: These strings read the file assuming UTF-8 encoding. 28 | public static void ReadOneDriveFileAsString([OneDrive( 29 | Identity = TokenIdentityMode.UserFromId, 30 | UserId = "sampleuserid", 31 | Path = "samplepath.txt")] string fileText) 32 | { 33 | Console.Write(fileText); 34 | } 35 | 36 | //The binding also supports paths in the form of share links 37 | public static void ReadOneDriveFileAsByteArrayFromShareLink([OneDrive( 38 | Identity = TokenIdentityMode.UserFromId, 39 | UserId = "sampleuserid", 40 | Path = "https://microsoft-my.sharepoint.com/:t:/p/comcmaho/randomstringhere")] byte[] array) 41 | { 42 | Console.Write(System.Text.Encoding.UTF8.GetString(array, 0, array.Length)); 43 | } 44 | 45 | public static void GetDriveItem([OneDrive( 46 | Identity = TokenIdentityMode.UserFromId, 47 | UserId = "sampleuserid", 48 | Path = "samplepath.txt")] DriveItem array) 49 | { 50 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/81c50e72166152f9f84dc38b2516379b7a536300/src/Microsoft.Graph/Models/Generated/DriveItem.cs 51 | //for usage 52 | } 53 | 54 | public static void GetOneDriveStream([OneDrive( 55 | Identity = TokenIdentityMode.UserFromId, 56 | UserId = "sampleuserid", 57 | Path = "samplepath.txt")] Stream stream) 58 | { 59 | byte[] buffer = new byte[256]; 60 | stream.Read(buffer, 0, 256); 61 | } 62 | 63 | public static void GetOneDriveStreamWithWriteAccess([OneDrive(FileAccess.Write, 64 | Identity = TokenIdentityMode.UserFromId, 65 | UserId = "sampleuserid", 66 | Path = "samplepath.txt")] Stream stream) 67 | { 68 | string sampleText = "sampleText"; 69 | byte[] encodedText = System.Text.Encoding.UTF8.GetBytes(sampleText); 70 | stream.Write(encodedText, 0, encodedText.Length); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /samples/GraphExtensionSamples/OutlookExamples.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | namespace GraphExtensionSamples 4 | { 5 | using System.Collections.Generic; 6 | using Microsoft.Azure.WebJobs; 7 | using Microsoft.Graph; 8 | using Newtonsoft.Json.Linq; 9 | 10 | public static class OutlookExamples 11 | { 12 | //Sending messages 13 | 14 | public static void SendMailFromMessageObject([Outlook( 15 | Identity = TokenIdentityMode.UserFromId, 16 | UserId = "sampleuserid")] out Message message) 17 | { 18 | message = new Message(); 19 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/src/Microsoft.Graph/Models/Generated/Message.cs 20 | //for usage 21 | } 22 | 23 | public static void SendMailFromJObject([Outlook( 24 | Identity = TokenIdentityMode.UserFromId, 25 | UserId = "sampleuserid")] out JObject message) 26 | { 27 | message = new JObject(); 28 | //See https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/message for a json 29 | //representation. Note that a "recipient" or "recipients" field takes the place of the ToRecipient field in 30 | //the schema, with "recipient" being a single object and "recipients" being multiple 31 | } 32 | 33 | public static void SendMailFromPoco([Outlook( 34 | Identity = TokenIdentityMode.UserFromId, 35 | UserId = "sampleuserid")] out MessagePoco message) 36 | { 37 | message = new MessagePoco(); 38 | //See https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/message for a json 39 | //representation. Note that a "recipient" or "recipients" field takes the place of the ToRecipient field in 40 | //the schema, with "recipient" being a single object and "recipients" being multiple 41 | } 42 | 43 | public class MessagePoco 44 | { 45 | public string Subject { get; set; } 46 | public string Body { get; set; } 47 | public List Recipients { get; set; } 48 | } 49 | 50 | public class RecipientPoco 51 | { 52 | public string Address { get; set; } 53 | public string Name { get; set; } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /samples/GraphExtensionSamples/TokenExamples.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | namespace GraphExtensionSamples 4 | { 5 | using System; 6 | using Microsoft.Azure.WebJobs; 7 | 8 | public static class TokenExamples 9 | { 10 | [NoAutomaticTrigger] 11 | public static void GraphTokenFromId([Token( 12 | UserId = "%UserId%", 13 | IdentityProvider = "AAD", 14 | Identity = TokenIdentityMode.UserFromId, 15 | Resource = "https://graph.microsoft.com")] string token) 16 | { 17 | Console.Write("The Microsoft graph token for the user is: " + token); 18 | } 19 | 20 | [NoAutomaticTrigger] 21 | public static void GraphTokenFromUserToken([Token( 22 | Identity = TokenIdentityMode.UserFromToken, 23 | UserToken = "%UserToken%", 24 | IdentityProvider = "AAD", 25 | Resource = "https://graph.microsoft.com")] string token) 26 | { 27 | Console.Write("The microsoft graph token for the user is: " + token); 28 | } 29 | 30 | // NOTE: This would only work in a Function with an HTTP trigger with 31 | // requests having the header X-MS-TOKEN-AAD-ID-TOKEN 32 | [NoAutomaticTrigger] 33 | public static void GraphTokenFromHttpRequest([Token( 34 | Identity = TokenIdentityMode.UserFromRequest, 35 | IdentityProvider = "AAD", 36 | Resource = "https://graph.microsoft.com")] string token) 37 | { 38 | Console.Write("The microsoft graph token for the user is: " + token); 39 | } 40 | 41 | // This template uses application permissions and requires consent from an Azure Active Directory admin. 42 | // See https://go.microsoft.com/fwlink/?linkid=858780 43 | [NoAutomaticTrigger] 44 | public static void GraphTokenFromApplicationIdentity([Token( 45 | Identity = TokenIdentityMode.ClientCredentials, 46 | IdentityProvider = "AAD", 47 | Resource = "https://graph.microsoft.com")] string token) 48 | { 49 | Console.Write("The microsoft graph token for the application is: " + token); 50 | } 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /samples/GraphExtensionSamples/WebhookSubscriptionExamples.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | namespace GraphExtensionSamples 4 | { 5 | using System; 6 | using Microsoft.Azure.WebJobs; 7 | 8 | public static class WebhookSubscriptionExamples 9 | { 10 | //Note that this function will only pass if the ClientCredentials identity has permission to refresh all of the available webhooks 11 | public static void RefreshAllSubscriptions( 12 | [GraphWebhookSubscription] string[] subIds, 13 | [GraphWebhookSubscription( 14 | Action = GraphWebhookSubscriptionAction.Refresh, 15 | Identity = TokenIdentityMode.ClientCredentials)] ICollector refreshedSubscriptions) 16 | { 17 | foreach (var subId in subIds) 18 | { 19 | refreshedSubscriptions.Add(subId); 20 | } 21 | } 22 | 23 | //This more complicated function without setting up an application identity with lots of permissions 24 | public static async void RefreshAllSubscriptionsWithoutClientCredentials( 25 | [GraphWebhookSubscription] SubscriptionPoco[] existingSubscriptions, 26 | IBinder binder) 27 | { 28 | foreach (var subscription in existingSubscriptions) 29 | { 30 | // binding in code to allow dynamic identity 31 | var subscriptionsToRefresh = await binder.BindAsync>( 32 | new GraphWebhookSubscriptionAttribute() 33 | { 34 | Action = GraphWebhookSubscriptionAction.Refresh, 35 | Identity = TokenIdentityMode.UserFromId, 36 | UserId = subscription.UserId 37 | } 38 | ); 39 | { 40 | await subscriptionsToRefresh.AddAsync(subscription.Id); 41 | } 42 | } 43 | } 44 | 45 | public static void RefreshSubscriptionsSelectively( 46 | [GraphWebhookSubscription] SubscriptionPoco[] existingSubscriptions, 47 | [GraphWebhookSubscription( 48 | Action = GraphWebhookSubscriptionAction.Refresh, 49 | Identity = TokenIdentityMode.ClientCredentials)] ICollector refreshedSubscriptions) 50 | { 51 | foreach (var subscription in existingSubscriptions) 52 | { 53 | if(subscription.ChangeType.Equals("updated") && subscription.ODataType.Equals("#Microsoft.Graph.Message")) 54 | { 55 | refreshedSubscriptions.Add(subscription.Id); 56 | } 57 | } 58 | } 59 | 60 | public static void DeleteAllSubscriptions( 61 | [GraphWebhookSubscription(Identity = TokenIdentityMode.ClientCredentials)] string[] subIds, 62 | [GraphWebhookSubscription( 63 | Action = GraphWebhookSubscriptionAction.Delete, 64 | Identity = TokenIdentityMode.ClientCredentials)] ICollector deletedSubscriptions) 65 | { 66 | foreach (var subId in subIds) 67 | { 68 | deletedSubscriptions.Add(subId); 69 | } 70 | } 71 | 72 | public static void SubscribeToInbox([GraphWebhookSubscription( 73 | UserId = "%UserId%", 74 | Identity = TokenIdentityMode.UserFromId, 75 | SubscriptionResource = "me/mailFolders('Inbox')/messages", 76 | ChangeTypes = new GraphWebhookChangeType[] {GraphWebhookChangeType.Created, GraphWebhookChangeType.Updated }, 77 | Action = GraphWebhookSubscriptionAction.Create)] out string clientState) 78 | { 79 | clientState = Guid.NewGuid().ToString(); 80 | } 81 | 82 | public class SubscriptionPoco 83 | { 84 | public string UserId { get; set; } 85 | //All of the below are properties of the Subscription object that can be used in your custom POCO 86 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/81c50e72166152f9f84dc38b2516379b7a536300/src/Microsoft.Graph/Models/Generated/Subscription.cs 87 | //for usage 88 | public string Id { get; set; } 89 | public string ODataType { get; set; } 90 | public string Resource { get; set; } 91 | public string ChangeType { get; set; } 92 | public string ClientState { get; set; } 93 | public string NotificationUrl { get; set; } 94 | public DateTimeOffset? ExpirationDateTime { get; set; } 95 | } 96 | 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /samples/GraphExtensionSamples/WebhookTriggerExamples.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace GraphExtensionSamples 5 | { 6 | using Microsoft.Azure.WebJobs; 7 | using Microsoft.Graph; 8 | 9 | public static class WebhookTriggerExamples 10 | { 11 | //NOTE: There can only be one trigger per resource type. That means all graph webhook subscriptions for the 12 | //given resource type will go through the same function. 13 | 14 | public static void OnMessage([GraphWebhookTrigger(ResourceType = "#Microsoft.Graph.Message")] Message message) 15 | { 16 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/src/Microsoft.Graph/Models/Generated/Message.cs 17 | //for usage 18 | } 19 | 20 | public static void OnOneDriveChange([GraphWebhookTrigger(ResourceType = "#Microsoft.Graph.DriveItem")] DriveItem driveItem) 21 | { 22 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/src/Microsoft.Graph/Models/Generated/DriveItem.cs 23 | //for usage 24 | } 25 | 26 | public static void OnContactChange([GraphWebhookTrigger(ResourceType = "#Microsoft.Graph.Contact")] Contact contact) 27 | { 28 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/src/Microsoft.Graph/Models/Generated/Contact.cs 29 | //for usage 30 | } 31 | 32 | public static void OnEventChange([GraphWebhookTrigger(ResourceType = "#Microsoft.Graph.Event")] Event eventNotice) 33 | { 34 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/src/Microsoft.Graph/Models/Generated/Event.cs 35 | //for usage 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sign.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-functions-microsoftgraph-extension/0edb2e497e3f003943223a35f5c6f74dab78708e/sign.snk -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/ExcelAsyncCollector.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Collections.ObjectModel; 9 | using System.Linq; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.Azure.WebJobs; 13 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services; 14 | using Newtonsoft.Json.Linq; 15 | 16 | /// 17 | /// Collector class used to accumulate and then dispatch requests to MS Graph related to Excel 18 | /// 19 | internal class ExcelAsyncCollector : IAsyncCollector 20 | { 21 | private readonly ExcelService _service; 22 | private readonly ExcelAttribute _attribute; 23 | private readonly List _rows; 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// GraphServiceClient used to make calls to MS Graph 29 | /// ExcelAttribute containing necessary info about workbook, etc. 30 | public ExcelAsyncCollector(ExcelService manager, ExcelAttribute attribute) 31 | { 32 | _service = manager; 33 | _attribute = attribute; 34 | _rows = new List(); 35 | } 36 | 37 | /// 38 | /// Add a string representation of a JObject to the list of items that need to be processed 39 | /// 40 | /// JSON string to be added (contains table, worksheet, etc. data) 41 | /// Used to propagate notifications 42 | /// Task representing the addition of the item 43 | public Task AddAsync(string item, CancellationToken cancellationToken = default(CancellationToken)) 44 | { 45 | if (item == null) 46 | { 47 | throw new ArgumentNullException(nameof(item)); 48 | } 49 | JToken parsedToken = JToken.Parse(item); 50 | if (parsedToken is JObject) 51 | { 52 | _rows.Add(parsedToken as JObject); 53 | } 54 | else 55 | { 56 | //JavaScript Array, so add metadata manually 57 | var array = parsedToken as JArray; 58 | bool arrayIsNested = array.All(element => element is JArray); 59 | if (arrayIsNested) 60 | { 61 | var consolidatedRow = new JObject(); 62 | consolidatedRow[O365Constants.ValuesKey] = array; 63 | consolidatedRow[O365Constants.RowsKey] = array.Count; 64 | // No exception -- array is rectangular by default 65 | consolidatedRow[O365Constants.ColsKey] = array[0].Children().Count(); 66 | _rows.Add(consolidatedRow); 67 | } 68 | else 69 | { 70 | throw new InvalidOperationException("Only nested arrays are supported for Excel output bindings."); 71 | } 72 | 73 | } 74 | 75 | return Task.CompletedTask; 76 | } 77 | 78 | /// 79 | /// Send all of the items in the collector to Microsoft Graph API 80 | /// 81 | /// Used to propagate notifications 82 | /// Task representing the flushing of the collector 83 | public async Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) 84 | { 85 | if (_rows.Count == 0) 86 | { 87 | return; 88 | } 89 | // Distinguish between appending and updating 90 | if (this._attribute.UpdateType != null && this._attribute.UpdateType == "Update") 91 | { 92 | if (_rows.FirstOrDefault(row => row["column"] != null && row["value"] != null) != null) 93 | { 94 | foreach (var row in this._rows) 95 | { 96 | row[O365Constants.RowsKey] = 1; 97 | row[O365Constants.ColsKey] = row.Children().Count(); 98 | if (row["column"] != null && row["value"] != null) 99 | { 100 | await _service.UpdateColumnAsync(this._attribute, row, cancellationToken); 101 | } 102 | else 103 | { 104 | // Update whole worksheet 105 | await _service.UpdateWorksheetAsync(this._attribute, row, cancellationToken); 106 | } 107 | } 108 | } 109 | else if (_rows.Count > 0) 110 | { 111 | // Update whole worksheet at once 112 | JObject consolidatedRows = GetConsolidatedRows(_rows); 113 | await _service.UpdateWorksheetAsync(_attribute, consolidatedRows, cancellationToken); 114 | } 115 | } 116 | else 117 | { 118 | // DEFAULT: Append (rows to specific table) 119 | foreach (var row in this._rows) 120 | { 121 | await _service.AddRowAsync(this._attribute, row, cancellationToken); 122 | } 123 | } 124 | 125 | this._rows.Clear(); 126 | } 127 | 128 | public JObject GetConsolidatedRows(List rows) 129 | { 130 | JObject consolidatedRows = new JObject(); 131 | if (rows.Count > 0 && rows[0][O365Constants.ValuesKey] == null) 132 | { 133 | //Each row is a POCO, so make it one value, and consolidate into one row 134 | consolidatedRows[O365Constants.ValuesKey] = JArray.FromObject(rows); 135 | consolidatedRows[O365Constants.RowsKey] = rows.Count; 136 | // No exception -- array is rectangular by default 137 | consolidatedRows[O365Constants.ColsKey] = rows[0].Children().Count(); 138 | // Set POCO key to indicate that the values need to be ordered to match the header of the existing table 139 | consolidatedRows[O365Constants.POCOKey] = rows[0][O365Constants.POCOKey]; 140 | } 141 | else if (rows.Count == 1 && rows[0][O365Constants.ValuesKey] != null) 142 | { 143 | return rows[0]; 144 | } 145 | else 146 | { 147 | var rowsAsString = $"[ {string.Join(", ", rows.Select(jobj => jobj.ToString()))} ]"; 148 | throw new InvalidOperationException($"Could not consolidate the following rows: {rowsAsString}"); 149 | } 150 | return consolidatedRows; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/ExcelAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs 5 | { 6 | using Microsoft.Azure.WebJobs.Description; 7 | 8 | /// 9 | /// Attribute used to describe Excel files 10 | /// Files must be on OneDrive 11 | /// 12 | [Binding] 13 | public class ExcelAttribute : GraphTokenBaseAttribute 14 | { 15 | /// 16 | /// Gets or sets path FROM ONEDRIVE ROOT to Excel file 17 | /// e.g. "Documents/TestSheet.xlsx" 18 | /// 19 | [AutoResolve] 20 | public string Path { get; set; } 21 | 22 | /// 23 | /// Gets or sets name of Excel table 24 | /// 25 | [AutoResolve] 26 | public string TableName { get; set; } 27 | 28 | /// 29 | /// Gets or sets worksheet name 30 | /// 31 | [AutoResolve] 32 | public string WorksheetName { get; set; } 33 | 34 | /// 35 | /// Gets or sets used when reading/writing from/to file 36 | /// Append (row) or Update (whole worksheet, specific column) 37 | /// 38 | [AutoResolve] 39 | public string UpdateType { get; set; } 40 | } 41 | } -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/GraphTokenBaseAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs 5 | { 6 | using Microsoft.Azure.WebJobs.Description; 7 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph; 8 | 9 | public abstract class GraphTokenBaseAttribute : TokenBaseAttribute 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | public GraphTokenBaseAttribute() 15 | { 16 | this.Resource = O365Constants.GraphBaseUrl; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/GraphWebhookChangeType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs 5 | { 6 | /// 7 | /// Enum of changes that can be subscribed to 8 | /// 9 | public enum GraphWebhookChangeType 10 | { 11 | /// 12 | /// Webhook activated when a new item of the subscribed resource is CREATED 13 | /// 14 | Created, 15 | 16 | /// 17 | /// Webhook activated when a new item of the subscribed resource is UPDATED 18 | /// 19 | Updated, 20 | 21 | /// 22 | /// Webhook activated when a new item of the subscribed resource is DELETED 23 | /// 24 | Deleted, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/GraphWebhookSubscriptionAction.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs 5 | { 6 | /// 7 | /// Determines what action the output binding for GraphWebhookActionAttribute 8 | /// will perform. Note that different modes have different semantics for the strings 9 | /// received by the IAsyncCollector. 10 | /// 11 | public enum GraphWebhookSubscriptionAction 12 | { 13 | /// 14 | /// Creates a new webhook (string = clientState) 15 | /// 16 | Create, 17 | /// 18 | /// Deletes a webhook (string = subscriptionId) 19 | /// 20 | Delete, 21 | /// 22 | /// Refreshes a webhook (string = subscriptionId) 23 | /// 24 | Refresh, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/GraphWebhookSubscriptionAsyncCollector.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config; 12 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services; 13 | using Microsoft.Extensions.Logging; 14 | using Microsoft.Graph; 15 | 16 | /// 17 | /// Collector class used to accumulate client states and create new subscriptions and cache them 18 | /// 19 | internal class GraphWebhookSubscriptionAsyncCollector : IAsyncCollector 20 | { 21 | private readonly GraphServiceClientManager _clientManager; // already has token 22 | private readonly ILogger _log; 23 | private readonly IGraphSubscriptionStore _subscriptionStore; 24 | private readonly Uri _notificationUrl; 25 | private readonly GraphOptions _options; 26 | 27 | 28 | // User attribute that we're bound against. 29 | // Has key properties (e.g. what resource we're listening to) 30 | private readonly GraphWebhookSubscriptionAttribute _attribute; 31 | 32 | private List _values; 33 | 34 | public GraphWebhookSubscriptionAsyncCollector(GraphServiceClientManager clientManager, GraphOptions options, ILoggerFactory logFactory, IGraphSubscriptionStore subscriptionStore, Uri notificationUrl, GraphWebhookSubscriptionAttribute attribute) 35 | { 36 | _clientManager = clientManager; 37 | _log = logFactory?.CreateLogger(MicrosoftGraphExtensionConfigProvider.CreateBindingCategory("GraphWebhook")); 38 | _subscriptionStore = subscriptionStore; 39 | _notificationUrl = notificationUrl; 40 | _attribute = attribute; 41 | _options = options; 42 | _values = new List(); 43 | 44 | _attribute.Validate(); 45 | } 46 | 47 | public Task AddAsync(string value, CancellationToken cancellationToken) 48 | { 49 | _values.Add(value); 50 | return Task.CompletedTask; 51 | } 52 | 53 | public async Task FlushAsync(CancellationToken cancellationToken) 54 | { 55 | switch (_attribute.Action) 56 | { 57 | case GraphWebhookSubscriptionAction.Create: 58 | await CreateSubscriptionsFromClientStatesAsync(cancellationToken); 59 | break; 60 | case GraphWebhookSubscriptionAction.Delete: 61 | await DeleteSubscriptionsFromSubscriptionIds(cancellationToken); 62 | break; 63 | case GraphWebhookSubscriptionAction.Refresh: 64 | await RefreshSubscriptionsFromSubscriptionIds(cancellationToken); 65 | break; 66 | } 67 | 68 | _values.Clear(); 69 | } 70 | 71 | private async Task CreateSubscriptionsFromClientStatesAsync(CancellationToken cancellationToken) 72 | { 73 | var client = await _clientManager.GetMSGraphClientFromTokenAttributeAsync(_attribute, cancellationToken); 74 | var userInfo = await client.Me.Request().Select("Id").GetAsync(); 75 | 76 | var subscriptions = _values.Select(GetSubscription); 77 | foreach (var subscription in subscriptions) 78 | { 79 | _log.LogTrace($"Sending a request to {_notificationUrl} expecting a 200 response for a subscription to {subscription.Resource}"); 80 | var newSubscription = await client.Subscriptions.Request().AddAsync(subscription); 81 | await _subscriptionStore.SaveSubscriptionEntryAsync(newSubscription, userInfo.Id); 82 | } 83 | } 84 | 85 | private Subscription GetSubscription(string clientState) 86 | { 87 | clientState = clientState ?? Guid.NewGuid().ToString(); 88 | return new Subscription 89 | { 90 | Resource = _attribute.SubscriptionResource, 91 | ChangeType = ChangeTypeExtension.ConvertArrayToString(_attribute.ChangeTypes), 92 | NotificationUrl = _notificationUrl.AbsoluteUri, 93 | ExpirationDateTime = DateTime.UtcNow + _options.WebhookExpirationTimeSpan, 94 | ClientState = clientState, 95 | }; 96 | } 97 | 98 | private async Task DeleteSubscriptionsFromSubscriptionIds(CancellationToken token) 99 | { 100 | var client = await _clientManager.GetMSGraphClientFromTokenAttributeAsync(_attribute, token); 101 | var subscriptionIds = _values; 102 | await Task.WhenAll(subscriptionIds.Select(id => DeleteSubscription(client, id))); 103 | } 104 | 105 | private async Task DeleteSubscription(IGraphServiceClient client, string id) 106 | { 107 | try 108 | { 109 | await client.Subscriptions[id].Request().DeleteAsync(); 110 | _log.LogInformation($"Successfully deleted MS Graph subscription {id}."); 111 | } 112 | catch 113 | { 114 | _log.LogWarning($"Failed to delete MS Graph subscription {id}.\n Either it never existed or it has already expired."); 115 | } 116 | finally 117 | { 118 | // Regardless of whether or not deleting the Graph subscription succeeded, delete the file 119 | await _subscriptionStore.DeleteAsync(id); 120 | } 121 | } 122 | 123 | private async Task RefreshSubscriptionsFromSubscriptionIds(CancellationToken token) 124 | { 125 | var client = await _clientManager.GetMSGraphClientFromTokenAttributeAsync(_attribute, token); 126 | var subscriptionIds = _values; 127 | await Task.WhenAll(subscriptionIds.Select(id => RefreshSubscription(client, id))); 128 | } 129 | 130 | private async Task RefreshSubscription(IGraphServiceClient client, string id) 131 | { 132 | try 133 | { 134 | var subscription = new Subscription 135 | { 136 | ExpirationDateTime = DateTime.UtcNow + _options.WebhookExpirationTimeSpan, 137 | }; 138 | 139 | var result = await client.Subscriptions[id].Request().UpdateAsync(subscription); 140 | 141 | _log.LogInformation($"Successfully renewed MS Graph subscription {id}. \n Active until {subscription.ExpirationDateTime}"); 142 | } 143 | catch (Exception ex) 144 | { 145 | // If the subscription is expired, it can no longer be renewed, so delete the file 146 | var subscriptionEntry = await _subscriptionStore.GetSubscriptionEntryAsync(id); 147 | if (subscriptionEntry != null) 148 | { 149 | if(subscriptionEntry.Subscription.ExpirationDateTime < DateTime.UtcNow) 150 | { 151 | await _subscriptionStore.DeleteAsync(id); 152 | } else 153 | { 154 | _log.LogError(ex, "A non-expired subscription failed to renew"); 155 | } 156 | } else 157 | { 158 | _log.LogWarning("The subscription with id " + id + " was not present in the local subscription store."); 159 | } 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/GraphWebhookSubscriptionAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs 5 | { 6 | using System; 7 | using Microsoft.Azure.WebJobs.Description; 8 | 9 | // This is used to retrieve the locally stored subscriptions 10 | [Binding] 11 | public class GraphWebhookSubscriptionAttribute : GraphTokenBaseAttribute 12 | { 13 | private string _filter; 14 | 15 | /// 16 | /// Gets or sets the UserId to filter subscriptions on; Optional 17 | /// 18 | public string Filter 19 | { 20 | get 21 | { 22 | return _filter; 23 | } 24 | set 25 | { 26 | if (TokenIdentityMode.UserFromRequest.ToString().Equals(value, StringComparison.OrdinalIgnoreCase)) 27 | { 28 | Identity = TokenIdentityMode.UserFromRequest; 29 | } 30 | _filter = value; 31 | } 32 | } 33 | 34 | /// 35 | /// Resource for which we're subscribing to changes. 36 | /// ie: me/mailFolders('Inbox')/messages 37 | /// 38 | [AutoResolve] 39 | public string SubscriptionResource { get; set; } 40 | 41 | /// 42 | /// Gets or sets types of changes that we're subscribing to. 43 | /// This is specific to the resource 44 | /// 45 | public GraphWebhookChangeType[] ChangeTypes { get; set; } 46 | 47 | public GraphWebhookSubscriptionAction Action { get; set; } 48 | 49 | internal void Validate() 50 | { 51 | switch (Action) 52 | { 53 | case GraphWebhookSubscriptionAction.Create: 54 | if (string.IsNullOrEmpty(SubscriptionResource)) 55 | { 56 | throw new ArgumentException($"A value for listen must be provided in ${Action} mode."); 57 | } 58 | 59 | if (ChangeTypes == null || ChangeTypes.Length == 0) 60 | { 61 | ChangeTypes = new GraphWebhookChangeType[] { GraphWebhookChangeType.Created, GraphWebhookChangeType.Deleted, GraphWebhookChangeType.Updated }; 62 | } 63 | 64 | break; 65 | case GraphWebhookSubscriptionAction.Delete: 66 | case GraphWebhookSubscriptionAction.Refresh: 67 | if (!string.IsNullOrEmpty(SubscriptionResource)) 68 | { 69 | throw new ArgumentException($"No value should be provided for listen in {Action} mode."); 70 | } 71 | 72 | if (ChangeTypes != null && ChangeTypes.Length > 0) 73 | { 74 | throw new ArgumentException($"No values should be provided for changeTypes in ${Action} mode."); 75 | } 76 | 77 | break; 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/GraphWebhookSubscriptionHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Net; 10 | using System.Net.Http; 11 | using System.Text; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | using System.Web; 15 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config; 16 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services; 17 | using Microsoft.Extensions.Logging; 18 | using Newtonsoft.Json; 19 | using Newtonsoft.Json.Linq; 20 | 21 | // Handles the subscription validation and notification payloads 22 | internal class GraphWebhookSubscriptionHandler 23 | { 24 | private readonly IGraphSubscriptionStore _subscriptionStore; // already has token 25 | private readonly Uri _notificationUri; 26 | private readonly ILogger _log; 27 | private readonly GraphServiceClientManager _clientManager; 28 | private readonly WebhookTriggerBindingProvider _bindingProvider; 29 | 30 | public GraphWebhookSubscriptionHandler(GraphServiceClientManager clientManager, IGraphSubscriptionStore subscriptionStore, ILoggerFactory loggerFactory, Uri notificationUri, WebhookTriggerBindingProvider bindingProvider) 31 | { 32 | _clientManager = clientManager; 33 | _subscriptionStore = subscriptionStore; 34 | _log = loggerFactory?.CreateLogger(MicrosoftGraphExtensionConfigProvider.CreateBindingCategory("GraphWebhookSubscription")); 35 | _notificationUri = notificationUri; 36 | _bindingProvider = bindingProvider; 37 | } 38 | 39 | private async Task HandleNotificationsAsync(NotificationPayload notifications, CancellationToken cancellationToken) 40 | { 41 | // A single webhook might get notifications from different users. 42 | List resources = new List(); 43 | 44 | foreach (Notification notification in notifications.Value) 45 | { 46 | var subId = notification.SubscriptionId; 47 | var entry = await _subscriptionStore.GetSubscriptionEntryAsync(subId); 48 | if (entry == null) 49 | { 50 | _log.LogError($"No subscription exists in our store for subscription id: {subId}"); 51 | // mapping of subscription ID to principal ID does not exist in file system 52 | continue; 53 | } 54 | 55 | if (entry.Subscription.ClientState != notification.ClientState) 56 | { 57 | _log.LogTrace($"The subscription store's client state: {entry.Subscription.ClientState} did not match the notifications's client state: {notification.ClientState}"); 58 | // Stored client state does not match client state we just received 59 | continue; 60 | } 61 | 62 | // call onto Graph to fetch the resource 63 | var userId = entry.UserId; 64 | var graphClient = await _clientManager.GetMSGraphClientFromUserIdAsync(userId, cancellationToken); 65 | 66 | _log.LogTrace($"A graph client was obtained for subscription id: {subId}"); 67 | 68 | // Prepend with / if necessary 69 | if (notification.Resource[0] != '/') 70 | { 71 | notification.Resource = '/' + notification.Resource; 72 | } 73 | 74 | var url = graphClient.BaseUrl + notification.Resource; 75 | 76 | HttpRequestMessage request = new HttpRequestMessage 77 | { 78 | Method = HttpMethod.Get, 79 | RequestUri = new Uri(url), 80 | }; 81 | 82 | _log.LogTrace($"Making a GET request to {url} on behalf of subId: {subId}"); 83 | 84 | await graphClient.AuthenticationProvider.AuthenticateRequestAsync(request); // Add authentication header 85 | var response = await graphClient.HttpProvider.SendAsync(request); 86 | string responseContent = await response.Content.ReadAsStringAsync(); 87 | 88 | _log.LogTrace($"Recieved {responseContent} from request to {url}"); 89 | 90 | var actualPayload = JObject.Parse(responseContent); 91 | 92 | // Superimpose some common properties onto the JObject for easy access. 93 | actualPayload["ClientState"] = entry.Subscription.ClientState; 94 | 95 | // Drive items only payload without resource data 96 | string odataType = null; 97 | if (notification.ResourceData != null) 98 | { 99 | odataType = notification.ResourceData.ODataType; 100 | } 101 | else if (notification.Resource.StartsWith("/me/drive")) 102 | { 103 | odataType = "#Microsoft.Graph.DriveItem"; 104 | } 105 | 106 | actualPayload["@odata.type"] = odataType; 107 | 108 | var data = new WebhookTriggerData 109 | { 110 | client = graphClient, 111 | Payload = actualPayload, 112 | odataType = odataType, 113 | }; 114 | 115 | resources.Add(data); 116 | } 117 | 118 | _log.LogTrace($"Triggering {resources.Count} GraphWebhookTriggers"); 119 | Task[] webhookReceipts = resources.Select(item => _bindingProvider.PushDataAsync(item)).ToArray(); 120 | 121 | Task.WaitAll(webhookReceipts); 122 | _log.LogTrace($"Finished responding to notifications."); 123 | } 124 | 125 | // See here for subscribing and payload information. 126 | // https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/subscription_post_subscriptions 127 | public async Task ProcessAsync( 128 | HttpRequestMessage request, CancellationToken cancellationToken) 129 | { 130 | var nvc = HttpUtility.ParseQueryString(request.RequestUri.Query); 131 | 132 | string validationToken = nvc["validationToken"]; 133 | if (validationToken != null) 134 | { 135 | 136 | return HandleInitialValidation(validationToken); 137 | } 138 | 139 | return await HandleNotificationPayloadAsync(request, cancellationToken); 140 | } 141 | 142 | private HttpResponseMessage HandleInitialValidation(string validationToken) 143 | { 144 | _log.LogTrace($"Returning a 200 OK Response to a request to {_notificationUri} with a validation token of {validationToken}"); 145 | return new HttpResponseMessage(HttpStatusCode.OK) 146 | { 147 | Content = new StringContent(validationToken, Encoding.UTF8, "plain/text"), 148 | }; 149 | } 150 | 151 | private async Task HandleNotificationPayloadAsync(HttpRequestMessage request, CancellationToken cancellationToken) 152 | { 153 | string json = await request.Content.ReadAsStringAsync(); 154 | var notifications = JsonConvert.DeserializeObject(json); 155 | 156 | _log.LogTrace($"Received a notification payload of {json}"); 157 | // We have 30sec to reply to the payload. 158 | // So offload everything else (especially fetches back to the graph and executing the user function) 159 | Task.Run(() => HandleNotificationsAsync(notifications, cancellationToken)); 160 | 161 | // Still return a 200 so the service doesn't resend the notification. 162 | return new HttpResponseMessage(HttpStatusCode.OK); 163 | } 164 | 165 | 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/GraphWebhookTriggerAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs 5 | { 6 | using System; 7 | using Microsoft.Azure.WebJobs.Description; 8 | 9 | [Binding] 10 | public class GraphWebhookTriggerAttribute : Attribute 11 | { 12 | /// 13 | /// Gets or sets oData type of payload 14 | /// "#Microsoft.Graph.Message", "#Microsoft.Graph.DriveItem", "#Microsoft.Graph.Contact", "#Microsoft.Graph.Event" 15 | /// 16 | public string ResourceType { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/O365Constants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph 5 | { 6 | using System; 7 | 8 | /// 9 | /// Constants needed to communicate with Graph, find app settings, etc. 10 | /// 11 | internal static class O365Constants 12 | { 13 | /// 14 | /// Base URL used to make any rest API calls 15 | /// 16 | public const string GraphBaseUrl = "https://graph.microsoft.com"; 17 | 18 | /// 19 | /// JObject key that holds range values (I/O) 20 | /// 21 | public const string ValuesKey = "Microsoft.O365Bindings.values"; 22 | 23 | /// 24 | /// JObject key that holds number of rows in ValuesKey 25 | /// 26 | public const string RowsKey = "Microsoft.O365Bindings.rows"; 27 | 28 | /// 29 | /// JObject key that holds number of columns in ValuesKey (rectangular data) 30 | /// 31 | public const string ColsKey = "Microsoft.O365Bindings.columns"; 32 | 33 | /// 34 | /// JObject key to indicate whether or not data came from POCO objects (list, array, or otherwise) 35 | /// 36 | public const string POCOKey = "Microsoft.O365Bindings.POCO"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/O365Models.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using Microsoft.Graph; 9 | using Newtonsoft.Json; 10 | 11 | /// 12 | /// GraphServiceClient + time the token used to authenticate calls expires 13 | /// 14 | internal class CachedClient { 15 | internal IGraphServiceClient client; 16 | internal int expirationDate; 17 | } 18 | 19 | /// 20 | /// Class containing an array of notifications received in a single blast from MS Graph 21 | /// 22 | internal class NotificationPayload 23 | { 24 | /// 25 | /// Gets or sets the array of notifications received by the function app 26 | /// 27 | public Notification[] Value { get; set; } 28 | } 29 | 30 | /// 31 | /// Single notification from MS Graph indicating a resource that the user subscribed to has been created/updated/deleted 32 | /// Several of these might come in at once 33 | /// 34 | internal class Notification 35 | { 36 | /// 37 | /// Gets or sets the type of change. 38 | /// 39 | [JsonProperty(PropertyName = "changeType")] 40 | public string ChangeType { get; set; } 41 | 42 | /// 43 | /// Gets or sets the client state used to verify that the notification is from Microsoft Graph. 44 | /// Compare the value received with the notification to the value you sent with the subscription request. 45 | /// 46 | [JsonProperty(PropertyName = "clientState")] 47 | public string ClientState { get; set; } 48 | 49 | /// 50 | /// Gets or sets the endpoint of the resource that changed. 51 | /// For example, a message uses the format ../Users/{user-id}/Messages/{message-id} 52 | /// 53 | [JsonProperty(PropertyName = "resource")] 54 | public string Resource { get; set; } 55 | 56 | /// 57 | /// Gets or sets the UTC date and time when the webhooks subscription expires. 58 | /// 59 | [JsonProperty(PropertyName = "subscriptionExpirationDateTime")] 60 | public DateTimeOffset SubscriptionExpirationDateTime { get; set; } 61 | 62 | /// 63 | /// Gets or sets the unique identifier for the webhooks subscription. 64 | /// 65 | [JsonProperty(PropertyName = "subscriptionId")] 66 | public string SubscriptionId { get; set; } 67 | 68 | /// 69 | /// Gets or sets the properties of the changed resource. 70 | /// 71 | [JsonProperty(PropertyName = "resourceData")] 72 | public ResourceData ResourceData { get; set; } 73 | } 74 | 75 | /// 76 | /// From within a given Notification 77 | /// Message, Contact, and Calendar all contain this ResourceData. OneDrive does not. 78 | /// 79 | internal class ResourceData 80 | { 81 | /// 82 | /// Gets or sets the ID of the resource. 83 | /// 84 | [JsonProperty(PropertyName = "id")] 85 | public string Id { get; set; } 86 | 87 | /// 88 | /// Gets or sets the OData etag property. 89 | /// 90 | [JsonProperty(PropertyName = "@odata.etag")] 91 | public string ODataEtag { get; set; } 92 | 93 | /// 94 | /// Gets or sets the OData ID of the resource. This is the same value as the resource property. 95 | /// 96 | [JsonProperty(PropertyName = "@odata.id")] 97 | public string ODataId { get; set; } 98 | 99 | /// 100 | /// Gets or sets the OData type of the resource 101 | /// "#Microsoft.Graph.Message", "#Microsoft.Graph.Event", or "#Microsoft.Graph.Contact". 102 | /// 103 | [JsonProperty(PropertyName = "@odata.type")] 104 | public string ODataType { get; set; } 105 | } 106 | 107 | /// 108 | /// Helper class for change types 109 | /// 110 | internal class ChangeTypeExtension 111 | { 112 | /// 113 | /// Convert an array of ChangeTypes to a Microsoft Graph-friendly list 114 | /// 115 | /// Array of change types 116 | /// lowercase array of strings representing the change types 117 | public static string ConvertArrayToString(GraphWebhookChangeType[] array) 118 | { 119 | List result = new List(); 120 | foreach (GraphWebhookChangeType ct in array) 121 | { 122 | result.Add(ct.ToString().ToLower()); 123 | } 124 | 125 | return string.Join(", ", result); 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/OneDriveAsyncCollector.cs: -------------------------------------------------------------------------------- 1 | /// Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph 5 | { 6 | using System; 7 | using System.Collections.ObjectModel; 8 | using System.IO; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Microsoft.Azure.WebJobs; 12 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services; 13 | using Microsoft.Graph; 14 | 15 | /// 16 | /// Collector class used to accumulate and then dispatch requests to MS Graph related to OneDrive 17 | /// 18 | internal class OneDriveAsyncCollector : IAsyncCollector 19 | { 20 | private readonly OneDriveService _service; 21 | private readonly OneDriveAttribute _attribute; 22 | private readonly Collection _fileStreams; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// GraphServiceClient used to make calls to MS Graph 28 | /// OneDriveAttribute containing necessary info about file 29 | public OneDriveAsyncCollector(OneDriveService service, OneDriveAttribute attribute) 30 | { 31 | _service = service; 32 | _attribute = attribute; 33 | _fileStreams = new Collection(); 34 | } 35 | 36 | /// 37 | /// Add a stream representing a file to the list of objects needing to be processed 38 | /// 39 | /// Stream representing OneDrive file 40 | /// Used to propagate notifications 41 | /// Task whose resolution results in Stream being added to the collector 42 | public Task AddAsync(byte[] item, CancellationToken cancellationToken = default(CancellationToken)) 43 | { 44 | if (item == null) 45 | { 46 | throw new ArgumentNullException("No stream found"); 47 | } 48 | 49 | _fileStreams.Add(item); 50 | return Task.CompletedTask; 51 | } 52 | 53 | /// 54 | /// Execute methods associated with file streams 55 | /// 56 | /// Used to propagate notifications 57 | /// Task representing the file streams being uploaded 58 | public async Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) 59 | { 60 | // Upload all files 61 | foreach (var stream in _fileStreams) 62 | { 63 | await _service.UploadOneDriveContentsAsync(_attribute, new MemoryStream(stream), cancellationToken); 64 | } 65 | 66 | this._fileStreams.Clear(); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/OneDriveAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs 5 | { 6 | using System.IO; 7 | using Microsoft.Azure.WebJobs.Description; 8 | 9 | /// 10 | /// Binding to O365 OneDrive. 11 | /// 12 | [Binding] 13 | public class OneDriveAttribute : GraphTokenBaseAttribute 14 | { 15 | /// 16 | /// Gets or sets path FROM ONEDRIVE ROOT to file 17 | /// e.g. "Documents/testing.docx" 18 | /// 19 | [AutoResolve] 20 | public string Path { get; set; } 21 | 22 | public FileAccess? Access { get; set; } 23 | 24 | public OneDriveAttribute() 25 | { 26 | 27 | } 28 | 29 | public OneDriveAttribute(FileAccess access) 30 | { 31 | Access = access; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/OneDriveWriteStream.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph 5 | { 6 | using System; 7 | using System.IO; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services; 11 | using Microsoft.Graph; 12 | 13 | internal class OneDriveWriteStream : Stream 14 | { 15 | private readonly IGraphServiceClient _client; 16 | private readonly Stream _stream; 17 | private readonly string _path; 18 | 19 | public OneDriveWriteStream(IGraphServiceClient client, string path) 20 | { 21 | _client = client; 22 | _path = path; 23 | _stream = new MemoryStream(); 24 | } 25 | 26 | public override bool CanRead => false; 27 | 28 | public override bool CanSeek => false; 29 | 30 | public override bool CanWrite => true; 31 | 32 | public override long Length => _stream.Length; 33 | 34 | public override long Position { get => _stream.Position; set => throw new NotSupportedException("This stream cannot seek"); } 35 | 36 | public override void Flush() 37 | { 38 | //NO-OP since flush must be idempotent, and the upload operation is not 39 | } 40 | 41 | public override int Read(byte[] buffer, int offset, int count) 42 | { 43 | throw new NotSupportedException("This stream does not support read operations."); 44 | } 45 | 46 | public override long Seek(long offset, SeekOrigin origin) 47 | { 48 | throw new NotSupportedException("This stream does not support seek operations"); 49 | } 50 | 51 | public override void SetLength(long value) 52 | { 53 | throw new NotSupportedException("This stream does not support seek operations"); 54 | } 55 | 56 | public override void Close() 57 | { 58 | Task.Run(() => _client.UploadOneDriveItemAsync(_path, _stream, CancellationToken.None)).GetAwaiter().GetResult(); 59 | } 60 | 61 | public override void Write(byte[] buffer, int offset, int count) 62 | { 63 | _stream.Write(buffer, offset, count); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/OutlookAsyncCollector.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph 5 | { 6 | using System; 7 | using System.Collections.ObjectModel; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.Azure.WebJobs; 11 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services; 12 | using Microsoft.Graph; 13 | 14 | internal class OutlookAsyncCollector : IAsyncCollector 15 | { 16 | private readonly OutlookService _client; 17 | private readonly OutlookAttribute _attribute; 18 | private readonly Collection _messages; 19 | 20 | public OutlookAsyncCollector(OutlookService client, OutlookAttribute attribute) 21 | { 22 | _client = client; 23 | _attribute = attribute; 24 | _messages = new Collection(); 25 | } 26 | 27 | public Task AddAsync(Message item, CancellationToken cancellationToken = default(CancellationToken)) 28 | { 29 | if (item == null) 30 | { 31 | throw new ArgumentNullException("No message item"); 32 | } 33 | 34 | _messages.Add(item); 35 | return Task.CompletedTask; 36 | } 37 | 38 | public async Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) 39 | { 40 | foreach (var msg in _messages) 41 | { 42 | await _client.SendMessageAsync(_attribute, msg, cancellationToken); 43 | } 44 | 45 | _messages.Clear(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/OutlookAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs 5 | { 6 | using Microsoft.Azure.WebJobs.Description; 7 | 8 | /// 9 | /// Outlook Attribute inherits from TokenAttribute 10 | /// No additional info necessary, but remains separate class in order to maintain clarity 11 | /// 12 | [Binding] 13 | public class OutlookAttribute : GraphTokenBaseAttribute 14 | { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/WebhookTriggerData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph 5 | { 6 | using Microsoft.Graph; 7 | using Newtonsoft.Json.Linq; 8 | 9 | /// 10 | /// internal trigger payload for when a graph webhook fires. 11 | /// 12 | internal class WebhookTriggerData 13 | { 14 | /// 15 | /// Authenticated client that can call & access the resource; 16 | /// should be the same client that subscribed to the notification. 17 | /// Used to allow other O365 bindings to get on-behalf credentials 18 | /// 19 | public IGraphServiceClient client; 20 | 21 | /// 22 | /// Results of GET request made using the resource in the notification 23 | /// Ultimately what ends up being passed to the user's fx 24 | /// 25 | public JObject Payload; 26 | 27 | /// 28 | /// Type used for filtering notifications 29 | /// (Should also be in Payload) 30 | /// 31 | public string odataType; 32 | } 33 | } -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Bindings/WebhookTriggerListener.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph 5 | { 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.Azure.WebJobs.Host.Executors; 9 | using Microsoft.Azure.WebJobs.Host.Listeners; 10 | 11 | // Per-function. 12 | // The listener is "passive". It just tracks the context invoker used for calling functions. 13 | // This receives context that can invoke the function 14 | internal class WebhookTriggerListener : IListener 15 | { 16 | // The context contains an invoker that can call the user function 17 | private readonly ListenerFactoryContext _context; 18 | private readonly WebhookTriggerBindingProvider _parent; 19 | private readonly GraphWebhookTriggerAttribute _attribute; 20 | 21 | public WebhookTriggerListener(ListenerFactoryContext context, WebhookTriggerBindingProvider parent, GraphWebhookTriggerAttribute attribute) 22 | { 23 | this._context = context; 24 | this._parent = parent; 25 | this._attribute = attribute; 26 | } 27 | 28 | public void Cancel() 29 | { 30 | } 31 | 32 | public void Dispose() 33 | { 34 | } 35 | 36 | public Task StartAsync(CancellationToken cancellationToken) 37 | { 38 | this._parent.AddListener(this._attribute, this); 39 | return Task.CompletedTask; 40 | } 41 | 42 | public Task StopAsync(CancellationToken cancellationToken) 43 | { 44 | this._parent.StopListeners(); 45 | 46 | return Task.CompletedTask; 47 | } 48 | 49 | public ITriggeredFunctionExecutor Executor => this._context.Executor; 50 | } 51 | } -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Config/Converters/ExcelConverters.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config.Converters 5 | { 6 | using System.Linq; 7 | using System.Collections.Generic; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Newtonsoft.Json.Linq; 11 | using Microsoft.Graph; 12 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services; 13 | 14 | internal class ExcelConverters 15 | { 16 | internal class ExcelConverter : 17 | IAsyncConverter, 18 | IAsyncConverter, 19 | IAsyncConverter>, 20 | IConverter 21 | { 22 | private readonly ExcelService _service; 23 | 24 | public ExcelConverter(ExcelService service) 25 | { 26 | _service = service; 27 | } 28 | 29 | public string Convert(JObject input) 30 | { 31 | return input.ToString(); 32 | } 33 | 34 | async Task> IAsyncConverter>.ConvertAsync(ExcelAttribute attr, CancellationToken token) 35 | { 36 | return new ExcelAsyncCollector(_service, attr); 37 | } 38 | 39 | async Task IAsyncConverter.ConvertAsync(ExcelAttribute attr, CancellationToken cancellationToken) 40 | { 41 | return await _service.GetExcelRangeAsync(attr, cancellationToken); 42 | } 43 | 44 | async Task IAsyncConverter.ConvertAsync(ExcelAttribute input, CancellationToken cancellationToken) 45 | { 46 | return await _service.GetExcelTableAsync(input, cancellationToken); 47 | } 48 | } 49 | 50 | internal class ExcelGenericsConverter : IAsyncConverter, 51 | IConverter 52 | { 53 | private readonly ExcelService _service; 54 | 55 | /// 56 | /// Initializes a new instance of the class. 57 | /// 58 | /// O365Extension to which the result of the request for data will be returned 59 | public ExcelGenericsConverter(ExcelService service) 60 | { 61 | _service = service; 62 | } 63 | 64 | async Task IAsyncConverter.ConvertAsync(ExcelAttribute input, CancellationToken cancellationToken) 65 | { 66 | return await _service.GetExcelRangePOCOAsync(input, cancellationToken); 67 | } 68 | 69 | /// 70 | /// Convert from POCO -> string (either row or rows) 71 | /// 72 | /// POCO input from fx 73 | /// String representation of JSON 74 | public string Convert(T input) 75 | { 76 | // handle T[] 77 | if (typeof(T).IsArray) 78 | { 79 | var array = input as object[]; 80 | return ConvertEnumerable(array).ToString(); 81 | } 82 | else 83 | { 84 | // handle T 85 | JObject data = JObject.FromObject(input); 86 | data[O365Constants.POCOKey] = true; // Set Microsoft.O365Bindings.POCO flag to indicate that data is from POCO (vs. object[][]) 87 | 88 | return data.ToString(); 89 | } 90 | } 91 | 92 | /// 93 | /// Convert from List -> JObject 94 | /// 95 | /// POCO input from fx 96 | /// JObject with proper keys set 97 | public JObject Convert(List input) 98 | { 99 | return ConvertEnumerable(input); 100 | } 101 | 102 | private JObject ConvertEnumerable(IEnumerable input) 103 | { 104 | JObject jsonContent = new JObject(); 105 | 106 | JArray rowData = JArray.FromObject(input); 107 | 108 | // List -> JArray 109 | jsonContent[O365Constants.ValuesKey] = rowData; 110 | 111 | // Set rows, columns needed if updating entire worksheet 112 | jsonContent[O365Constants.RowsKey] = rowData.Count(); 113 | 114 | // No exception -- array is rectangular by default 115 | jsonContent[O365Constants.ColsKey] = rowData.First.Count(); 116 | 117 | // Set POCO key to indicate that the values need to be ordered to match the header of the existing table 118 | jsonContent[O365Constants.POCOKey] = true; 119 | 120 | return jsonContent; 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Config/Converters/GraphWebhookSubscriptionConverters.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config.Converters 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Reflection; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services; 13 | using Microsoft.Graph; 14 | using Newtonsoft.Json.Linq; 15 | 16 | internal class GraphWebhookSubscriptionConverters 17 | { 18 | internal class GraphWebhookSubscriptionConverter : 19 | IAsyncConverter, 20 | IAsyncConverter, 21 | IAsyncConverter 22 | { 23 | private readonly GraphServiceClientManager _clientManager; 24 | private readonly IGraphSubscriptionStore _subscriptionStore; 25 | private readonly GraphOptions _options; 26 | 27 | public GraphWebhookSubscriptionConverter(GraphServiceClientManager clientManager, GraphOptions options, IGraphSubscriptionStore subscriptionStore) 28 | { 29 | _options = options; 30 | _clientManager = clientManager; 31 | _subscriptionStore = subscriptionStore; 32 | } 33 | 34 | async Task IAsyncConverter.ConvertAsync(GraphWebhookSubscriptionAttribute input, CancellationToken cancellationToken) 35 | { 36 | return (await GetSubscriptionsFromAttributeAsync(input, cancellationToken)) 37 | .Select(entry => entry.Subscription) 38 | .ToArray(); 39 | } 40 | 41 | async Task IAsyncConverter.ConvertAsync(GraphWebhookSubscriptionAttribute input, CancellationToken cancellationToken) 42 | { 43 | return (await GetSubscriptionsFromAttributeAsync(input, cancellationToken)) 44 | .Select(entry => entry.Subscription.Id) 45 | .ToArray(); 46 | } 47 | 48 | async Task IAsyncConverter.ConvertAsync(GraphWebhookSubscriptionAttribute input, CancellationToken cancellationToken) 49 | { 50 | SubscriptionEntry[] subscriptions = await GetSubscriptionsFromAttributeAsync(input, cancellationToken); 51 | var serializedSubscriptions = new JArray(); 52 | foreach (var subscription in subscriptions) 53 | { 54 | serializedSubscriptions.Add(JObject.FromObject(subscription)); 55 | } 56 | return serializedSubscriptions; 57 | } 58 | 59 | protected async Task GetSubscriptionsFromAttributeAsync(GraphWebhookSubscriptionAttribute attribute, CancellationToken cancellationToken) 60 | { 61 | IEnumerable subscriptionEntries = await _subscriptionStore.GetAllSubscriptionsAsync(); 62 | if (TokenIdentityMode.UserFromRequest.ToString().Equals(attribute.Filter, StringComparison.OrdinalIgnoreCase)) 63 | { 64 | var dummyTokenAttribute = new TokenAttribute() 65 | { 66 | Resource = _options.GraphBaseUrl, 67 | Identity = TokenIdentityMode.UserFromToken, 68 | UserToken = attribute.UserToken, 69 | IdentityProvider = "AAD", 70 | }; 71 | var graph = await _clientManager.GetMSGraphClientFromTokenAttributeAsync(dummyTokenAttribute, cancellationToken); 72 | var user = await graph.Me.Request().GetAsync(); 73 | subscriptionEntries = subscriptionEntries.Where(entry => entry.UserId.Equals(user.Id)); 74 | } 75 | else if (attribute.Filter != null) 76 | { 77 | throw new InvalidOperationException($"There is no filter for {attribute.Filter}"); 78 | } 79 | return subscriptionEntries.ToArray(); 80 | } 81 | } 82 | 83 | internal class GenericGraphWebhookSubscriptionConverter : GraphWebhookSubscriptionConverter, 84 | IAsyncConverter 85 | { 86 | public GenericGraphWebhookSubscriptionConverter(GraphServiceClientManager clientManager, GraphOptions options, IGraphSubscriptionStore subscriptionStore) : base(clientManager, options, subscriptionStore) 87 | { 88 | } 89 | 90 | public async Task ConvertAsync(GraphWebhookSubscriptionAttribute input, CancellationToken cancellationToken) 91 | { 92 | return ConvertSubscriptionEntries(await this.GetSubscriptionsFromAttributeAsync(input, cancellationToken)); 93 | } 94 | 95 | //Converts a Subscription Entry into a "flattened" POCO representation where the properties 96 | //of the POCO can be UserId or any of the properties of Subscription 97 | public T[] ConvertSubscriptionEntries(SubscriptionEntry[] entries) 98 | { 99 | var pocoType = typeof(T); 100 | var subEntryType = typeof(Subscription); 101 | var subscriptionProperties = subEntryType.GetProperties(); 102 | var pocoProperties = pocoType.GetProperties(); 103 | 104 | T[] pocos = new T[entries.Length]; 105 | for (int i = 0; i < pocos.Length; i++) 106 | { 107 | pocos[i] = (T)Activator.CreateInstance(typeof(T), new object[] { }); 108 | } 109 | 110 | foreach (PropertyInfo pocoProperty in pocoProperties) 111 | { 112 | var subscriptionProperty = subEntryType.GetProperty(pocoProperty.Name, pocoProperty.PropertyType); 113 | if (subscriptionProperty != null) 114 | { 115 | for (int i = 0; i < pocos.Length; i++) 116 | { 117 | pocoProperty.SetValue(pocos[i], subscriptionProperty.GetValue(entries[i].Subscription)); 118 | } 119 | } 120 | } 121 | 122 | var pocoUserIdProperty = pocoType.GetProperty("UserId"); 123 | if (pocoUserIdProperty != null) 124 | { 125 | var subEntryUserIdProperty = typeof(SubscriptionEntry).GetProperty("UserId"); 126 | for (int i = 0; i < pocos.Length; i++) 127 | { 128 | pocoUserIdProperty.SetValue(pocos[i], subEntryUserIdProperty.GetValue(entries[i])); 129 | } 130 | } 131 | 132 | return pocos; 133 | } 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Config/Converters/OneDriveConverter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config.Converters 5 | { 6 | using System.IO; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services; 11 | using Microsoft.Graph; 12 | 13 | internal class OneDriveConverter : 14 | IAsyncConverter, 15 | IAsyncConverter, 16 | IAsyncConverter, 17 | IAsyncConverter, 18 | IAsyncConverter> 19 | { 20 | private readonly OneDriveService _service; 21 | 22 | public OneDriveConverter(OneDriveService service) 23 | { 24 | _service = service; 25 | } 26 | 27 | async Task IAsyncConverter.ConvertAsync(OneDriveAttribute input, CancellationToken cancellationToken) 28 | { 29 | return await _service.GetOneDriveContentsAsByteArrayAsync(input, cancellationToken); 30 | } 31 | 32 | async Task IAsyncConverter.ConvertAsync(OneDriveAttribute input, CancellationToken cancellationToken) 33 | { 34 | var byteArray = await _service.GetOneDriveContentsAsByteArrayAsync(input, cancellationToken); 35 | return Encoding.UTF8.GetString(byteArray); 36 | } 37 | 38 | async Task IAsyncConverter.ConvertAsync(OneDriveAttribute input, CancellationToken cancellationToken) 39 | { 40 | return await _service.GetOneDriveContentsAsStreamAsync(input, cancellationToken); 41 | } 42 | 43 | async Task IAsyncConverter.ConvertAsync(OneDriveAttribute input, CancellationToken cancellationToken) 44 | { 45 | return await _service.GetOneDriveItemAsync(input, cancellationToken); 46 | } 47 | 48 | public async Task> ConvertAsync(OneDriveAttribute input, CancellationToken cancellationToken) 49 | { 50 | return new OneDriveAsyncCollector(_service, input); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Config/Converters/OutlookConverter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config.Converters 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services; 11 | using Microsoft.Graph; 12 | using Newtonsoft.Json.Linq; 13 | 14 | internal class OutlookConverter : IConverter, IConverter, IAsyncConverter> 15 | { 16 | private OutlookService _outlookService; 17 | 18 | public OutlookConverter(OutlookService outlookService) 19 | { 20 | _outlookService = outlookService; 21 | } 22 | 23 | public Message Convert(JObject input) 24 | { 25 | // Set up recipient(s) 26 | List recipientList = new List(); 27 | 28 | JToken recipientToken = GetPropertyValueIgnoreCase(input, "recipient", throwException: false) 29 | ?? GetPropertyValueIgnoreCase(input, "recipients", throwException: false); 30 | 31 | if (recipientToken == null) 32 | { 33 | throw new InvalidOperationException("The object needs to have a 'recipient' or 'recipients' field."); 34 | } 35 | 36 | List recipients; 37 | 38 | // MS Graph Message expects a list of recipients 39 | if (recipientToken is JArray) 40 | { 41 | // JArray -> List 42 | recipients = recipientToken.ToObject>(); 43 | } 44 | else 45 | { 46 | // List with one JObject 47 | recipients = new List(); 48 | recipients.Add(recipientToken.ToObject()); 49 | } 50 | 51 | if (recipients.Count == 0) 52 | { 53 | throw new InvalidOperationException("At least one recipient must be provided."); 54 | } 55 | 56 | foreach (JObject recip in recipients) 57 | { 58 | Recipient recipient = new Recipient 59 | { 60 | EmailAddress = new EmailAddress 61 | { 62 | Address = GetPropertyValueIgnoreCase(recip, "address"), 63 | Name = GetPropertyValueIgnoreCase(recip, "name", throwException: false), 64 | }, 65 | }; 66 | recipientList.Add(recipient); 67 | } 68 | 69 | // Actually create message 70 | var msg = new Message 71 | { 72 | Body = new ItemBody 73 | { 74 | Content = GetPropertyValueIgnoreCase(input, "body"), 75 | ContentType = BodyType.Text, 76 | }, 77 | Subject = GetPropertyValueIgnoreCase(input, "subject"), 78 | ToRecipients = recipientList, 79 | }; 80 | 81 | return msg; 82 | } 83 | 84 | public Message Convert(string input) 85 | { 86 | return Convert(JObject.Parse(input)); 87 | } 88 | 89 | private static T GetPropertyValueIgnoreCase(JObject input, string key, bool throwException = true) 90 | { 91 | JToken value; 92 | if (!input.TryGetValue(key, StringComparison.OrdinalIgnoreCase, out value)) 93 | { 94 | if (throwException) 95 | { 96 | throw new InvalidOperationException($"The object needs to have a {key} field."); 97 | } 98 | return default(T); 99 | } 100 | return value.ToObject(); 101 | } 102 | 103 | public async Task> ConvertAsync(OutlookAttribute input, CancellationToken cancellationToken) 104 | { 105 | return new OutlookAsyncCollector(_outlookService, input); 106 | } 107 | } 108 | 109 | // This converter goes directly to Message instead of T -> JObject and composing 110 | // with JObject -> Message as composition conversions with OpenTypes are broken 111 | internal class OutlookGenericsConverter : IConverter 112 | { 113 | private readonly OutlookConverter _converter; 114 | 115 | public OutlookGenericsConverter(OutlookService service) 116 | { 117 | _converter = new OutlookConverter(service); 118 | } 119 | 120 | public Message Convert(T input) 121 | { 122 | return _converter.Convert(JObject.FromObject(input)); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Config/GraphOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config 7 | { 8 | public class GraphOptions 9 | { 10 | private const string defaultTokenMapLocation = "D:/home/data/byob_graphmap"; 11 | public string SubscriptionStoreLocationAppSettingName { get; set; } = "BYOB_TokenMap"; 12 | 13 | public string GraphBaseUrl { get; set; } = O365Constants.GraphBaseUrl; 14 | 15 | public string TokenMapLocation { get; set; } = defaultTokenMapLocation; 16 | 17 | public TimeSpan WebhookExpirationTimeSpan { get; set; } = new TimeSpan(0, 0, 4230, 0); 18 | 19 | public void SetAppSettings(INameResolver appSettings) 20 | { 21 | var settingsTokenMapLocation = appSettings.Resolve(SubscriptionStoreLocationAppSettingName); 22 | if (!string.IsNullOrEmpty(settingsTokenMapLocation) && TokenMapLocation == defaultTokenMapLocation) 23 | TokenMapLocation = settingsTokenMapLocation; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Config/GraphServiceClientManager.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services 5 | { 6 | using System; 7 | using System.Collections.Concurrent; 8 | using System.IdentityModel.Tokens.Jwt; 9 | using System.Linq; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config; 13 | using Microsoft.Graph; 14 | 15 | internal class GraphServiceClientManager 16 | { 17 | private readonly IAsyncConverter _tokenProvider; 18 | private readonly IGraphServiceClientProvider _clientProvider; 19 | private readonly GraphOptions _options; 20 | 21 | /// 22 | /// Map principal Id + scopes -> GraphServiceClient + token expiration date 23 | /// 24 | private ConcurrentDictionary _clients = new ConcurrentDictionary(); 25 | 26 | public GraphServiceClientManager(GraphOptions options, IAsyncConverter tokenProvider, IGraphServiceClientProvider clientProvider) 27 | { 28 | _tokenProvider = tokenProvider; 29 | _clientProvider = clientProvider; 30 | _options = options; 31 | } 32 | 33 | /// 34 | /// Retrieve audience from raw JWT 35 | /// 36 | /// JWT 37 | /// Token audience 38 | private static string GetTokenOID(string rawToken) 39 | { 40 | var jwt = new JwtSecurityToken(rawToken); 41 | var oidClaim = jwt.Claims.FirstOrDefault(claim => claim.Type == "oid"); 42 | if (oidClaim == null) 43 | { 44 | throw new InvalidOperationException("The graph token is missing an oid. Check your Microsoft Graph binding configuration."); 45 | } 46 | return oidClaim.Value; 47 | } 48 | 49 | /// 50 | /// Given a JWT, return the list of scopes in alphabetical order 51 | /// 52 | /// raw JWT 53 | /// string of scopes in alphabetical order, separated by a space 54 | private static string GetTokenOrderedScopes(string rawToken) 55 | { 56 | var jwt = new JwtSecurityToken(rawToken); 57 | var stringScopes = jwt.Claims.FirstOrDefault(claim => claim.Type == "scp")?.Value; 58 | if (stringScopes == null) 59 | { 60 | throw new InvalidOperationException("The graph token has no scopes. Ensure your application is properly configured to access the Microsoft Graph."); 61 | } 62 | var scopes = stringScopes.Split(' '); 63 | Array.Sort(scopes); 64 | return string.Join(" ", scopes); 65 | } 66 | 67 | /// 68 | /// Retrieve integer token expiration date 69 | /// 70 | /// raw JWT 71 | /// parsed expiration date 72 | private static int GetTokenExpirationDate(string rawToken) 73 | { 74 | var jwt = new JwtSecurityToken(rawToken); 75 | var stringTime = jwt.Claims.FirstOrDefault(claim => claim.Type == "exp").Value; 76 | int result; 77 | if (int.TryParse(stringTime, out result)) 78 | { 79 | return result; 80 | } 81 | else 82 | { 83 | return -1; 84 | } 85 | } 86 | 87 | /// 88 | /// Hydrate GraphServiceClient from a moniker (serialized TokenAttribute) 89 | /// 90 | /// string representing serialized TokenAttribute 91 | /// Authenticated GraphServiceClient 92 | public async Task GetMSGraphClientFromUserIdAsync(string userId, CancellationToken token) 93 | { 94 | var attr = new TokenAttribute 95 | { 96 | UserId = userId, 97 | Resource = _options.GraphBaseUrl, 98 | Identity = TokenIdentityMode.UserFromId, 99 | }; 100 | 101 | return await this.GetMSGraphClientFromTokenAttributeAsync(attr, token); 102 | } 103 | 104 | /// 105 | /// Either retrieve existing GSC or create a new one 106 | /// GSCs are cached using a combination of the user's principal ID and the scopes of the token used to authenticate 107 | /// 108 | /// Token attribute with either principal ID or ID token 109 | /// Authenticated GSC 110 | public virtual async Task GetMSGraphClientFromTokenAttributeAsync(TokenBaseAttribute attribute, CancellationToken cancellationToken) 111 | { 112 | string token = await this._tokenProvider.ConvertAsync(attribute, cancellationToken); 113 | string principalId = GetTokenOID(token); 114 | 115 | var key = string.Concat(principalId, " ", GetTokenOrderedScopes(token)); 116 | 117 | CachedClient cachedClient = null; 118 | 119 | // Check to see if there already exists a GSC associated with this principal ID and the token scopes. 120 | if (_clients.TryGetValue(key, out cachedClient)) 121 | { 122 | // Check if token is expired 123 | if (cachedClient.expirationDate < DateTimeOffset.Now.ToUnixTimeSeconds()) 124 | { 125 | // Need to update the client's token & expiration date 126 | // $$ todo -- just reset token instead of whole new authentication provider? 127 | _clientProvider.UpdateGraphServiceClientAuthToken(cachedClient.client, token); 128 | cachedClient.expirationDate = GetTokenExpirationDate(token); 129 | } 130 | 131 | return cachedClient.client; 132 | } 133 | else 134 | { 135 | cachedClient = new CachedClient 136 | { 137 | client = _clientProvider.CreateNewGraphServiceClient(token), 138 | expirationDate = GetTokenExpirationDate(token), 139 | }; 140 | _clients.TryAdd(key, cachedClient); 141 | return cachedClient.client; 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Config/GraphServiceClientProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config 5 | { 6 | using System; 7 | using System.Net.Http.Headers; 8 | using System.Threading.Tasks; 9 | using Microsoft.Graph; 10 | 11 | internal class GraphServiceClientProvider : IGraphServiceClientProvider 12 | { 13 | public IGraphServiceClient CreateNewGraphServiceClient(string token) 14 | { 15 | return new GraphServiceClient( 16 | new DelegateAuthenticationProvider( 17 | (requestMessage) => 18 | { 19 | requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token); 20 | return Task.CompletedTask; 21 | })); 22 | } 23 | 24 | public void UpdateGraphServiceClientAuthToken(IGraphServiceClient client, string token) 25 | { 26 | GraphServiceClient typedClient = client as GraphServiceClient; 27 | if (typedClient == null) 28 | { 29 | throw new InvalidOperationException($"Only {nameof(IGraphServiceClient)} of type {nameof(GraphServiceClient)} should be created with this client provider"); 30 | } 31 | 32 | typedClient.AuthenticationProvider = new DelegateAuthenticationProvider( 33 | (requestMessage) => 34 | { 35 | requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token); 36 | 37 | return Task.CompletedTask; 38 | }); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Config/IGraphServiceClientProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config 5 | { 6 | using Microsoft.Graph; 7 | 8 | internal interface IGraphServiceClientProvider 9 | { 10 | IGraphServiceClient CreateNewGraphServiceClient(string token); 11 | 12 | void UpdateGraphServiceClientAuthToken(IGraphServiceClient client, string token); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Config/IGraphSubscriptionStore.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | using System.Runtime.CompilerServices; 4 | 5 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100cd1dabd5a893b40e75dc901fe7293db4a3caf9cd4d3e3ed6178d49cd476969abe74a9e0b7f4a0bb15edca48758155d35a4f05e6e852fff1b319d103b39ba04acbadd278c2753627c95e1f6f6582425374b92f51cca3deb0d2aab9de3ecda7753900a31f70a236f163006beefffe282888f85e3c76d1205ec7dfef7fa472a17b1")] 6 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config 7 | { 8 | using System.Threading.Tasks; 9 | using Microsoft.Graph; 10 | 11 | internal interface IGraphSubscriptionStore 12 | { 13 | /// 14 | /// Saves the subscription in the token store, along with the user id for the subscription 15 | /// 16 | /// The subscription being saved 17 | /// The user id of the user who the subscription belongs to 18 | /// A task that represents the asynchronous operation. 19 | Task SaveSubscriptionEntryAsync(Subscription subscription, string userId); 20 | 21 | /// 22 | /// Retrieves all subscription entries in the store. 23 | /// 24 | /// A task that respresents the asynchronous operation. The task's result contains the 25 | /// subscription entries. 26 | Task GetAllSubscriptionsAsync(); 27 | 28 | /// 29 | /// Retrieves the subscription entry with the given subscription id. 30 | /// Returns null if the subscriptionId does not match any subscriptions in the store 31 | /// 32 | /// The subscription id of the entry to retrieve 33 | /// A task that respresents the asynchronous operation. The task's result contains the 34 | /// subscription entry with the subscription Id provided 35 | Task GetSubscriptionEntryAsync(string subscriptionId); 36 | 37 | /// 38 | /// Deletes the subscription entry in the store with the given subscription Id. 39 | /// NoOps if the subscriptionId does not match any subscriptions in the store 40 | /// 41 | /// The subscription id of the entry to delete 42 | /// A task that represents the asynchronous operation 43 | Task DeleteAsync(string subscriptionId); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Config/MicrosoftGraphWebJobsBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config 5 | { 6 | using System; 7 | using Microsoft.Azure.WebJobs.Extensions.AuthTokens; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | internal static class MicrosoftGraphWebJobsBuilderExtensions 11 | { 12 | public static IWebJobsBuilder AddMicrosoftGraph(this IWebJobsBuilder builder) 13 | { 14 | if (builder == null) 15 | { 16 | throw new ArgumentNullException(nameof(builder)); 17 | } 18 | 19 | builder.AddExtension() 20 | .BindOptions() 21 | .BindOptions() 22 | .Services 23 | .AddAuthTokenServices() 24 | .AddSingleton, TokenConverter>() 25 | .AddSingleton() 26 | .AddSingleton(); 27 | return builder; 28 | } 29 | 30 | public static IWebJobsBuilder AddMicrosoftGraphForTests(this IWebJobsBuilder builder) 31 | { 32 | if (builder == null) 33 | { 34 | throw new ArgumentNullException(nameof(builder)); 35 | } 36 | 37 | builder.AddExtension(); 38 | return builder; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Config/MicrosoftGraphWebJobsStartup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph; 5 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config; 6 | using Microsoft.Azure.WebJobs.Hosting; 7 | 8 | [assembly: WebJobsStartup(typeof(MicrosoftGraphWebJobsStartup))] 9 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph 10 | { 11 | public class MicrosoftGraphWebJobsStartup : IWebJobsStartup 12 | { 13 | public void Configure(IWebJobsBuilder builder) 14 | { 15 | builder.AddMicrosoftGraph(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Config/SubscriptionEntry.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config 5 | { 6 | using Microsoft.Graph; 7 | 8 | internal class SubscriptionEntry 9 | { 10 | /// 11 | /// Gets or sets subscription ID returned by MS Graph after creation 12 | /// 13 | public Subscription Subscription { get; set; } 14 | 15 | /// 16 | /// Gets or sets the user id for the subscription 17 | /// 18 | public string UserId { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Config/WebhookSubscriptionStore.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph 5 | { 6 | using System; 7 | using System.Collections.Concurrent; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config; 13 | using Microsoft.Extensions.Options; 14 | using Microsoft.Graph; 15 | using Newtonsoft.Json; 16 | using File = System.IO.File; 17 | 18 | /// 19 | /// Store mapping from webhook subscription IDs to a token. 20 | /// 21 | internal class WebhookSubscriptionStore : IGraphSubscriptionStore 22 | { 23 | private readonly GraphOptions _options; 24 | 25 | private FileLock _fileLock; 26 | 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// Find webhook token cache path 30 | /// If it doesn't exist, create directory 31 | /// 32 | /// Value of app setting used to det. webhook token cache path 33 | public WebhookSubscriptionStore(IOptions options) 34 | { 35 | _options = options.Value; 36 | _fileLock = new FileLock(); 37 | _fileLock.PerformWriteIO(_options.TokenMapLocation, () => CreateRootDirectory()); 38 | } 39 | 40 | private void CreateRootDirectory() 41 | { 42 | if (!Directory.Exists(_options.TokenMapLocation)) 43 | { 44 | Directory.CreateDirectory(_options.TokenMapLocation); 45 | } 46 | } 47 | 48 | private string GetSubscriptionPath(string subscriptionId) 49 | { 50 | return Path.Combine(_options.TokenMapLocation, subscriptionId); 51 | } 52 | 53 | private string GetSubscriptionPath(Subscription subscription) 54 | { 55 | return GetSubscriptionPath(subscription.Id); 56 | } 57 | 58 | public async Task SaveSubscriptionEntryAsync(Subscription subscription, string userId) 59 | { 60 | var entry = new SubscriptionEntry 61 | { 62 | Subscription = subscription, 63 | UserId = userId, 64 | }; 65 | var subPath = this.GetSubscriptionPath(subscription); 66 | var jsonString = JsonConvert.SerializeObject(entry); 67 | await _fileLock.PerformWriteIOAsync(subPath, () => File.WriteAllText(subPath, jsonString)); 68 | } 69 | 70 | public async Task GetAllSubscriptionsAsync() 71 | { 72 | var subscriptionPaths = await _fileLock.PerformReadIOAsync>(_options.TokenMapLocation, Directory.EnumerateFiles); 73 | var entryTasks = subscriptionPaths.Select(path => this.GetAsyncFromPath(path)); 74 | return await Task.WhenAll(entryTasks); 75 | } 76 | 77 | private async Task GetAsyncFromPath(string path) 78 | { 79 | var contents = await _fileLock.PerformReadIOAsync(path, File.ReadAllText); 80 | var entry = JsonConvert.DeserializeObject(contents); 81 | return entry; 82 | } 83 | 84 | public async Task GetSubscriptionEntryAsync(string subId) 85 | { 86 | string path = this.GetSubscriptionPath(subId); 87 | return await this.GetAsyncFromPath(path); 88 | } 89 | 90 | /// 91 | /// Delete a single subscription entry 92 | /// 93 | /// Subscription entry to be deleted 94 | public async Task DeleteAsync(string subscriptionId) 95 | { 96 | var path = this.GetSubscriptionPath(subscriptionId); 97 | await _fileLock.PerformWriteIOAsync(path, () => DeleteFileIfExists(path)); 98 | } 99 | 100 | private void DeleteFileIfExists(string path) 101 | { 102 | if (File.Exists(path)) 103 | { 104 | File.Delete(path); 105 | } 106 | } 107 | 108 | private class FileLock 109 | { 110 | private readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); 111 | 112 | public void PerformWriteIO(string path, Action ioAction) 113 | { 114 | lock (_locks.GetOrAdd(path, new object())) 115 | { 116 | try 117 | { 118 | ioAction(); 119 | } 120 | finally 121 | { 122 | object removedLock; 123 | _locks.TryRemove(path, out removedLock); 124 | } 125 | } 126 | } 127 | 128 | public async Task PerformWriteIOAsync(string path, Action ioAction) 129 | { 130 | PerformWriteIO(path, ioAction); 131 | } 132 | 133 | public async Task PerformReadIOAsync(string path, Func ioAction) 134 | { 135 | lock (_locks.GetOrAdd(path, new object())) 136 | { 137 | T result = default(T); 138 | try 139 | { 140 | result = ioAction(path); 141 | } 142 | finally 143 | { 144 | object removedLock; 145 | _locks.TryRemove(path, out removedLock); 146 | } 147 | 148 | return result; 149 | } 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/MicrosoftGraphBinding.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Microsoft.Azure.WebJobs.Extensions.O365 6 | Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph 7 | 8 | Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph 9 | This extension adds bindings for Microsoft Graph 10 | 1 11 | 0 12 | 0 13 | beta6 14 | $(MajorVersion).0.0.0 15 | $(MajorVersion).$(MinorVersion).$(PatchVersion)-$(BetaVersion)$(VersionSuffix) 16 | N/A 17 | $(Version) Commit hash: $(CommitHash) 18 | 19 | Microsoft 20 | Microsoft 21 | © .NET Foundation. All rights reserved. 22 | https://go.microsoft.com/fwlink/?linkid=2028464 23 | 24 | git 25 | https://github.com/Azure/azure-functions-microsoftgraph-extension 26 | true 27 | https://github.com/Azure/azure-functions-microsoftgraph-extension 28 | true 29 | ../../sign.snk 30 | 31 | 32 | 33 | Full 34 | true 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | All 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Services/ExcelClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services 5 | { 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.Graph; 9 | using Newtonsoft.Json.Linq; 10 | 11 | /// 12 | /// Helper for calling onto Excel (MS) Graph 13 | /// 14 | internal static class ExcelClient 15 | { 16 | public static async Task GetTableWorkbookAsync(this IGraphServiceClient client, string path, string tableName, CancellationToken token) 17 | { 18 | return await client 19 | .GetWorkbookTableRequest(path, tableName) 20 | .Request() 21 | .GetAsync(token); 22 | } 23 | 24 | public static async Task GetTableWorkbookRangeAsync(this IGraphServiceClient client, string path, string tableName, CancellationToken token) 25 | { 26 | return await client 27 | .GetWorkbookTableRequest(path, tableName) 28 | .Range() 29 | .Request() 30 | .GetAsync(token); 31 | } 32 | 33 | public static async Task GetWorksheetWorkbookAsync(this IGraphServiceClient client, string path, string worksheetName, CancellationToken token) 34 | { 35 | return await client 36 | .GetWorkbookWorksheetRequest(path, worksheetName) 37 | .UsedRange() 38 | .Request() 39 | .GetAsync(token); 40 | } 41 | 42 | public static async Task GetWorkSheetWorkbookInRangeAsync(this IGraphServiceClient client, string path, string worksheetName, string range, CancellationToken token) 43 | { 44 | return await client 45 | .GetWorkbookWorksheetRequest(path, worksheetName) 46 | .Range(range) 47 | .Request() 48 | .GetAsync(token); 49 | } 50 | 51 | public static async Task GetTableHeaderRowAsync(this IGraphServiceClient client, string path, string tableName, CancellationToken token) 52 | { 53 | var headerRowRange = await client 54 | .GetWorkbookTableRequest(path, tableName) 55 | .HeaderRowRange() 56 | .Request() 57 | .GetAsync(token); 58 | return headerRowRange.Values.ToObject()[0]; //header row array is embedded as the only element in its own array 59 | } 60 | 61 | public static async Task PostTableRowAsync(this IGraphServiceClient client, string path, string tableName, JToken row, CancellationToken token) 62 | { 63 | return await client 64 | .GetWorkbookTableRequest(path, tableName) 65 | .Rows 66 | .Add(null, row) 67 | .Request() 68 | .PostAsync(token); 69 | } 70 | 71 | public static async Task PatchWorksheetAsync(this IGraphServiceClient client, string path, string worksheetName, string range, WorkbookRange newWorkbook, CancellationToken token) 72 | { 73 | return await client 74 | .GetWorkbookWorksheetRequest(path, worksheetName) 75 | .Range(range) 76 | .Request() 77 | .PatchAsync(newWorkbook, token); 78 | } 79 | 80 | 81 | private static IWorkbookTableRequestBuilder GetWorkbookTableRequest(this IGraphServiceClient client, string path, string tableName) 82 | { 83 | return client 84 | .Me 85 | .Drive 86 | .Root 87 | .ItemWithPath(path) 88 | .Workbook 89 | .Tables[tableName]; 90 | } 91 | 92 | private static IWorkbookWorksheetRequestBuilder GetWorkbookWorksheetRequest(this IGraphServiceClient client, string path, string worksheetName) 93 | { 94 | return client 95 | .Me 96 | .Drive 97 | .Root 98 | .ItemWithPath(path) 99 | .Workbook 100 | .Worksheets[worksheetName]; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Services/OneDriveClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services 5 | { 6 | using System.IO; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Microsoft.Graph; 10 | using Newtonsoft.Json; 11 | 12 | /// 13 | /// Helper class for calling onto (MS) OneDrive Graph 14 | /// 15 | internal static class OneDriveClient 16 | { 17 | /// 18 | /// Retrieve contents of OneDrive file 19 | /// 20 | /// Authenticated Graph Service Client used to retrieve file 21 | /// Attribute with necessary data (e.g. path) 22 | /// Stream of file content 23 | public static async Task GetOneDriveContentStreamAsync(this IGraphServiceClient client, string path, CancellationToken token) 24 | { 25 | // Retrieve stream of OneDrive item 26 | return await client 27 | .Me 28 | .Drive 29 | .Root 30 | .ItemWithPath(path) 31 | .Content 32 | .Request() 33 | .GetAsync(token); 34 | } 35 | 36 | public static async Task GetOneDriveItemAsync(this IGraphServiceClient client, string path, CancellationToken token) 37 | { 38 | // Retrieve OneDrive item 39 | return await client 40 | .Me 41 | .Drive 42 | .Root 43 | .ItemWithPath(path) 44 | .Request() 45 | .GetAsync(token); 46 | } 47 | 48 | public static async Task GetOneDriveContentStreamFromShareAsync(this IGraphServiceClient client, string shareToken, CancellationToken token) 49 | { 50 | return await client 51 | .Shares[shareToken] 52 | .Root 53 | .Content 54 | .Request() 55 | .GetAsync(token); 56 | } 57 | 58 | /// 59 | /// Uploads new OneDrive Item OR updates existing OneDrive Item 60 | /// 61 | /// 62 | /// 63 | /// Stream of input to be uploaded 64 | /// Drive item representing newly added/updated item 65 | public static async Task UploadOneDriveItemAsync(this IGraphServiceClient client, string path, Stream fileStream, CancellationToken token) 66 | { 67 | fileStream.Position = 0; 68 | DriveItem result = await client 69 | .Me 70 | .Drive 71 | .Root 72 | .ItemWithPath(path) 73 | .Content 74 | .Request() 75 | .PutAsync(fileStream, token); 76 | return result; 77 | } 78 | 79 | class GetRootModel 80 | { 81 | [JsonProperty("@microsoft.graph.downloadUrl")] 82 | public string DownloadUrl { get; set; } 83 | 84 | public string name { get; set; } 85 | 86 | public int size { get; set; } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Services/OneDriveService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using System.IO; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config; 8 | using Microsoft.Graph; 9 | 10 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services 11 | { 12 | internal class OneDriveService 13 | { 14 | private GraphServiceClientManager _clientProvider; 15 | 16 | public OneDriveService(GraphServiceClientManager clientProvider) 17 | { 18 | _clientProvider = clientProvider; 19 | } 20 | 21 | /// 22 | /// Retrieve contents of OneDrive file 23 | /// 24 | /// 25 | /// 26 | /// 27 | /// 28 | public async Task GetOneDriveContentsAsByteArrayAsync(OneDriveAttribute attr, CancellationToken token) 29 | { 30 | var response = await GetOneDriveContentsAsStreamAsync(attr, token); 31 | 32 | using (MemoryStream ms = new MemoryStream()) 33 | { 34 | await response.CopyToAsync(ms); 35 | return ms.ToArray(); 36 | } 37 | } 38 | 39 | public Stream ConvertStream(Stream stream, OneDriveAttribute attribute, IGraphServiceClient client) 40 | { 41 | if (attribute.Access != FileAccess.Write) 42 | { 43 | return stream; 44 | } 45 | return new OneDriveWriteStream(client, attribute.Path); 46 | } 47 | 48 | public async Task GetOneDriveContentsAsStreamAsync(OneDriveAttribute attr, CancellationToken token) 49 | { 50 | IGraphServiceClient client = await _clientProvider.GetMSGraphClientFromTokenAttributeAsync(attr, token); 51 | Stream response; 52 | // How to download from OneDrive: https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/item_downloadcontent 53 | // GET https://graph.microsoft.com/v1.0/me/drive/root:/test1/hi.txt:/content HTTP/1.1 54 | bool isShare = attr.Path.StartsWith("https://"); 55 | if (isShare) 56 | { 57 | // TODO: Move this to GraphServiceClient 58 | 59 | // Download via a Share URL 60 | var shareToken = UrlToSharingToken(attr.Path); 61 | response = await client.GetOneDriveContentStreamFromShareAsync(shareToken, token); 62 | } 63 | else 64 | { 65 | try 66 | { 67 | // Retrieve stream of OneDrive item 68 | response = await client.GetOneDriveContentStreamAsync(attr.Path, token); 69 | } catch 70 | { 71 | //File does not exist, so create new memory stream 72 | response = new MemoryStream(); 73 | } 74 | 75 | 76 | } 77 | 78 | return ConvertStream(response, attr, client); 79 | } 80 | 81 | public async Task GetOneDriveItemAsync(OneDriveAttribute attr, CancellationToken token) 82 | { 83 | IGraphServiceClient client = await _clientProvider.GetMSGraphClientFromTokenAttributeAsync(attr, token); 84 | return await client.GetOneDriveItemAsync(attr.Path, token); 85 | } 86 | 87 | public async Task UploadOneDriveContentsAsync(OneDriveAttribute attr, Stream fileStream, CancellationToken token) 88 | { 89 | IGraphServiceClient client = await _clientProvider.GetMSGraphClientFromTokenAttributeAsync(attr, token); 90 | return await client.UploadOneDriveItemAsync(attr.Path, fileStream, token); 91 | } 92 | 93 | // https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/shares_get#transform-a-sharing-url 94 | private static string UrlToSharingToken(string inputUrl) 95 | { 96 | var base64Value = System.Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(inputUrl)); 97 | return "u!" + base64Value.TrimEnd('=').Replace('/', '_').Replace('+', '-'); 98 | } 99 | 100 | 101 | public static Stream CreateStream(byte[] byteArray) 102 | { 103 | return new MemoryStream(byteArray); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Services/OutlookClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services 5 | { 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.Graph; 9 | 10 | internal static class OutlookClient 11 | { 12 | /// 13 | /// Send an email with a dynamically set body 14 | /// 15 | /// GraphServiceClient used to send request 16 | /// Async task for posted message 17 | public static async Task SendMessageAsync(this IGraphServiceClient client, Message msg, CancellationToken token) 18 | { 19 | // Send message & save to sent items folder 20 | await client.Me.SendMail(msg, true).Request().PostAsync(token); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/Services/OutlookService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Graph; 7 | 8 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services 9 | { 10 | internal class OutlookService 11 | { 12 | private GraphServiceClientManager _clientManager; 13 | 14 | public OutlookService(GraphServiceClientManager clientManager) 15 | { 16 | _clientManager = clientManager; 17 | } 18 | 19 | public async Task SendMessageAsync(OutlookAttribute attr, Message msg, CancellationToken token) 20 | { 21 | IGraphServiceClient client = await _clientManager.GetMSGraphClientFromTokenAttributeAsync(attr, token); 22 | await client.SendMessageAsync(msg, token); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/MicrosoftGraphBinding/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | // ACTION REQUIRED: This file was automatically added to your project, but it 3 | // will not take effect until additional steps are taken to enable it. See the 4 | // following page for additional information: 5 | // 6 | // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md 7 | 8 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 9 | "settings": { 10 | "documentationRules": { 11 | "companyName": "Microsoft" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/TokenBinding/AadClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens 5 | { 6 | using System; 7 | using System.Threading.Tasks; 8 | using Microsoft.Azure.Services.AppAuthentication; 9 | using Microsoft.Extensions.Options; 10 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 11 | 12 | /// 13 | /// Helper class for calling onto ADAL 14 | /// 15 | internal class AadClient : IAadClient 16 | { 17 | private AuthenticationContext _authContext; 18 | private ClientCredential _clientCredentials; 19 | private TokenOptions _options; 20 | 21 | private AuthenticationContext AuthContext { 22 | get 23 | { 24 | if (_authContext == null) 25 | { 26 | // NOTE: We had to turn off authority validation here, otherwise we would 27 | // get the error "AADSTS50049: Unknown or invalid instance" for some tenants. 28 | _authContext = new AuthenticationContext(_options.TenantUrl, false); 29 | } 30 | return _authContext; 31 | } 32 | } 33 | 34 | private ClientCredential ClientCredentials 35 | { 36 | get 37 | { 38 | if (_clientCredentials == null) 39 | { 40 | if (_options.ClientId == null || _options.ClientSecret == null) 41 | { 42 | throw new InvalidOperationException($"The Application Settings {Constants.ClientIdName} and {Constants.ClientSecretName} must be set to perform this operation."); 43 | } 44 | _clientCredentials = new ClientCredential(_options.ClientId, _options.ClientSecret); 45 | } 46 | 47 | return _clientCredentials; 48 | } 49 | } 50 | 51 | public AadClient(IOptions options) 52 | { 53 | _options = options.Value; 54 | } 55 | 56 | /// 57 | /// Use client credentials to retrieve auth token 58 | /// Typically used to retrieve a token for a different audience 59 | /// 60 | /// User's token for a given resource 61 | /// Resource the token is for (e.g. https://graph.microsoft.com) 62 | /// Access token for correct audience 63 | public async Task GetTokenOnBehalfOfUserAsync( 64 | string userToken, 65 | string resource) 66 | { 67 | if (string.IsNullOrEmpty(userToken)) 68 | { 69 | throw new ArgumentException("A usertoken is required to retrieve a token for a user."); 70 | } 71 | 72 | if (string.IsNullOrEmpty(resource)) 73 | { 74 | throw new ArgumentException("A resource is required to retrieve a token for a user."); 75 | } 76 | 77 | UserAssertion userAssertion = new UserAssertion(userToken); 78 | AuthenticationResult ar = await AuthContext.AcquireTokenAsync(resource, ClientCredentials, userAssertion); 79 | return ar.AccessToken; 80 | } 81 | 82 | public async Task GetTokenFromClientCredentials(string resource) 83 | { 84 | if (string.IsNullOrEmpty(resource)) 85 | { 86 | throw new ArgumentException("A resource is required to retrieve a token from client credentials."); 87 | } 88 | 89 | AuthenticationResult authResult = await AuthContext.AcquireTokenAsync(resource, ClientCredentials); 90 | return authResult.AccessToken; 91 | } 92 | 93 | public async Task GetTokenFromAppIdentity(string resource, string connectionString) 94 | { 95 | if (string.IsNullOrEmpty(resource)) 96 | { 97 | throw new ArgumentException("A resource is required to retrieve a token via the application's managed identity."); 98 | } 99 | 100 | var azureServiceTokenProvider = new AzureServiceTokenProvider(connectionString); 101 | 102 | var token = await azureServiceTokenProvider.GetAccessTokenAsync(resource); 103 | 104 | return token; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/TokenBinding/AuthTokenExtensionConfigProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | using System.Runtime.CompilerServices; 4 | 5 | [assembly: InternalsVisibleTo("Microsoft.Azure.WebJobs.Extensions.Token.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100cd1dabd5a893b40e75dc901fe7293db4a3caf9cd4d3e3ed6178d49cd476969abe74a9e0b7f4a0bb15edca48758155d35a4f05e6e852fff1b319d103b39ba04acbadd278c2753627c95e1f6f6582425374b92f51cca3deb0d2aab9de3ecda7753900a31f70a236f163006beefffe282888f85e3c76d1205ec7dfef7fa472a17b1")] 6 | [assembly: InternalsVisibleTo("Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100cd1dabd5a893b40e75dc901fe7293db4a3caf9cd4d3e3ed6178d49cd476969abe74a9e0b7f4a0bb15edca48758155d35a4f05e6e852fff1b319d103b39ba04acbadd278c2753627c95e1f6f6582425374b92f51cca3deb0d2aab9de3ecda7753900a31f70a236f163006beefffe282888f85e3c76d1205ec7dfef7fa472a17b1")] 7 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100cd1dabd5a893b40e75dc901fe7293db4a3caf9cd4d3e3ed6178d49cd476969abe74a9e0b7f4a0bb15edca48758155d35a4f05e6e852fff1b319d103b39ba04acbadd278c2753627c95e1f6f6582425374b92f51cca3deb0d2aab9de3ecda7753900a31f70a236f163006beefffe282888f85e3c76d1205ec7dfef7fa472a17b1")] 8 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens 9 | { 10 | using Microsoft.Azure.WebJobs; 11 | using Microsoft.Azure.WebJobs.Host.Config; 12 | using Microsoft.Extensions.Options; 13 | 14 | /// 15 | /// WebJobs SDK Extension for Token binding. 16 | /// 17 | internal class AuthTokenExtensionConfigProvider : IExtensionConfigProvider 18 | { 19 | private TokenConverter _converter; 20 | private TokenOptions _options; 21 | 22 | public AuthTokenExtensionConfigProvider(IOptions options, IAadClient aadClient, IEasyAuthClient easyAuthClient, INameResolver appSettings) 23 | { 24 | _options = options.Value; 25 | _options.SetAppSettings(appSettings); 26 | _converter = new TokenConverter(options, easyAuthClient, aadClient); 27 | } 28 | 29 | //TODO: https://github.com/Azure/azure-functions-microsoftgraph-extension/issues/48 30 | internal static string CreateBindingCategory(string bindingName) 31 | { 32 | return $"Host.Bindings.{bindingName}"; 33 | } 34 | 35 | /// 36 | /// Initialize the binding extension 37 | /// 38 | /// Context for extension 39 | public void Initialize(ExtensionConfigContext context) 40 | { 41 | var tokenRule = context.AddBindingRule(); 42 | tokenRule.BindToInput(_converter); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/TokenBinding/AuthTokenWebJobsBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens 5 | { 6 | 7 | using System; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | public static class AuthTokenWebJobsBuilderExtensions 11 | { 12 | public static IWebJobsBuilder AddAuthToken(this IWebJobsBuilder builder) 13 | { 14 | if (builder == null) 15 | { 16 | throw new ArgumentNullException(nameof(builder)); 17 | } 18 | 19 | builder.AddExtension() 20 | .BindOptions() 21 | .Services 22 | .AddSingleton() 23 | .AddSingleton(); 24 | return builder; 25 | } 26 | 27 | public static IWebJobsBuilder AddAuthTokenForTests(this IWebJobsBuilder builder) 28 | { 29 | if (builder == null) 30 | { 31 | throw new ArgumentNullException(nameof(builder)); 32 | } 33 | 34 | builder.AddExtension(); 35 | return builder; 36 | } 37 | 38 | public static IServiceCollection AddAuthTokenServices(this IServiceCollection services) 39 | { 40 | services.AddSingleton() 41 | .AddSingleton(); 42 | return services; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/TokenBinding/AuthTokenWebJobsStartup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Azure.WebJobs.Extensions.AuthTokens; 5 | using Microsoft.Azure.WebJobs.Hosting; 6 | 7 | [assembly: WebJobsStartup(typeof(AuthTokenWebJobsStartup))] 8 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens 9 | { 10 | public class AuthTokenWebJobsStartup : IWebJobsStartup 11 | { 12 | public void Configure(IWebJobsBuilder builder) 13 | { 14 | builder.AddAuthToken(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/TokenBinding/Constants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens 5 | { 6 | internal static class Constants 7 | { 8 | public const string ClientIdName = "WEBSITE_AUTH_CLIENT_ID"; 9 | 10 | public const string ClientSecretName = "WEBSITE_AUTH_CLIENT_SECRET"; 11 | 12 | public const string WebsiteHostname = "WEBSITE_HOSTNAME"; 13 | 14 | public const string WebsiteAuthSigningKey = "WEBSITE_AUTH_SIGNING_KEY"; 15 | 16 | public const string WebsiteAuthOpenIdIssuer = "WEBSITE_AUTH_OPENID_ISSUER"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/TokenBinding/EasyAuthTokenClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens 5 | { 6 | using System; 7 | using System.IdentityModel.Tokens.Jwt; 8 | using System.Net; 9 | using System.Net.Http; 10 | using System.Threading.Tasks; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.Extensions.Options; 13 | using Newtonsoft.Json; 14 | 15 | /// 16 | /// The client responsible for handling all EasyAuth token-related tasks. 17 | /// 18 | internal class EasyAuthTokenClient : IEasyAuthClient 19 | { 20 | private static readonly HttpClient _httpClient = new HttpClient(); 21 | private readonly ILogger _log; 22 | private readonly TokenOptions _options; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// The hostname of the webapp 28 | /// The website authorization signing key 29 | public EasyAuthTokenClient(IOptions options, ILoggerFactory loggerFactory) 30 | { 31 | _log = loggerFactory.CreateLogger(AuthTokenExtensionConfigProvider.CreateBindingCategory("AuthToken")); 32 | _options = options.Value; 33 | } 34 | 35 | public async Task GetTokenStoreEntry(JwtSecurityToken jwt, TokenBaseAttribute attribute) 36 | { 37 | // Send the token to the local /.auth/me endpoint and return the JSON 38 | string meUrl = $"https://{_options.HostName}/.auth/me?provider={attribute.IdentityProvider}"; 39 | 40 | using (var request = new HttpRequestMessage(HttpMethod.Get, meUrl)) 41 | { 42 | request.Headers.Add("x-zumo-auth", jwt.RawData); 43 | _log.LogTrace($"Fetching user token data from {meUrl}"); 44 | using (HttpResponseMessage response = await _httpClient.SendAsync(request)) 45 | { 46 | _log.LogTrace($"Response from '${meUrl}: {response.StatusCode}"); 47 | if (!response.IsSuccessStatusCode) 48 | { 49 | string errorResponse = await response.Content.ReadAsStringAsync(); 50 | throw new InvalidOperationException($"Request to {meUrl} failed. Status Code: {response.StatusCode}; Body: {errorResponse}"); 51 | } 52 | var responseString = await response.Content.ReadAsStringAsync(); 53 | return JsonConvert.DeserializeObject(responseString); 54 | } 55 | } 56 | } 57 | 58 | public async Task RefreshToken(JwtSecurityToken jwt, TokenBaseAttribute attribute) 59 | { 60 | if (string.IsNullOrEmpty(attribute.Resource)) 61 | { 62 | throw new ArgumentException("A resource is required to renew an access token."); 63 | } 64 | 65 | if (string.IsNullOrEmpty(attribute.UserId)) 66 | { 67 | throw new ArgumentException("A userId is required to renew an access token."); 68 | } 69 | 70 | if (string.IsNullOrEmpty(attribute.IdentityProvider)) 71 | { 72 | throw new ArgumentException("A provider is necessary to renew an access token."); 73 | } 74 | 75 | string refreshUrl = $"https://{_options.HostName}/.auth/refresh?resource=" + WebUtility.UrlEncode(attribute.Resource); 76 | 77 | using (var refreshRequest = new HttpRequestMessage(HttpMethod.Get, refreshUrl)) 78 | { 79 | refreshRequest.Headers.Add("x-zumo-auth", jwt.RawData); 80 | _log.LogTrace($"Refreshing ${attribute.IdentityProvider} access token for user ${attribute.UserId} at ${refreshUrl}"); 81 | using (HttpResponseMessage response = await _httpClient.SendAsync(refreshRequest)) 82 | { 83 | _log.LogTrace($"Response from ${refreshUrl}: {response.StatusCode}"); 84 | if (!response.IsSuccessStatusCode) 85 | { 86 | string errorResponse = await response.Content.ReadAsStringAsync(); 87 | throw new InvalidOperationException($"Failed to refresh {attribute.UserId} {attribute.IdentityProvider} error={response.StatusCode} {errorResponse}"); 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/TokenBinding/EasyAuthTokenManager.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens 5 | { 6 | using System; 7 | using System.IdentityModel.Tokens.Jwt; 8 | using System.Security.Claims; 9 | using System.Threading.Tasks; 10 | using Microsoft.Extensions.Options; 11 | using Microsoft.IdentityModel.Tokens; 12 | 13 | /// 14 | /// Class representing an application's [EasyAuth] Token Store 15 | /// see https://cgillum.tech/2016/03/07/app-service-token-store/ 16 | /// 17 | internal class EasyAuthTokenManager 18 | { 19 | internal readonly JwtSecurityTokenHandler JwtHandler = new JwtSecurityTokenHandler(); 20 | private const int GraphTokenBufferInMinutes = 5; 21 | private const int JwtTokenDurationInMinutes = 15; 22 | 23 | private readonly TokenOptions _options; 24 | private readonly IEasyAuthClient _client; 25 | 26 | /// 27 | /// Initializes a new instance of the class. 28 | /// 29 | /// The hostname of the keystore. 30 | /// The website authorization signing key 31 | public EasyAuthTokenManager(IEasyAuthClient client, IOptions options) 32 | { 33 | _client = client; 34 | _options = options.Value; 35 | } 36 | 37 | /// 38 | /// Retrieve Easy Auth token based on provider & principal ID 39 | /// 40 | /// The metadata for the token to grab 41 | /// Task with Token Store entry of the token 42 | public async Task GetEasyAuthAccessTokenAsync(TokenBaseAttribute attribute) 43 | { 44 | var jwt = CreateTokenForEasyAuthAccess(attribute); 45 | EasyAuthTokenStoreEntry tokenStoreEntry = await _client.GetTokenStoreEntry(jwt, attribute); 46 | 47 | bool isTokenValid = IsTokenValid(tokenStoreEntry.AccessToken); 48 | bool isTokenExpired = tokenStoreEntry.ExpiresOn <= DateTime.UtcNow.AddMinutes(GraphTokenBufferInMinutes); 49 | bool isRefreshable = IsRefreshableProvider(attribute.IdentityProvider); 50 | 51 | if (isRefreshable && (isTokenExpired || !isTokenValid)) 52 | { 53 | await _client.RefreshToken(jwt, attribute); 54 | 55 | // Now that the refresh has occured, grab the new token 56 | tokenStoreEntry = await _client.GetTokenStoreEntry(jwt, attribute); 57 | } 58 | 59 | return tokenStoreEntry.AccessToken; 60 | } 61 | 62 | private bool IsTokenValid(string token) 63 | { 64 | return JwtHandler.CanReadToken(token); 65 | } 66 | 67 | private static bool IsRefreshableProvider(string provider) 68 | { 69 | //TODO: For now, since we are focusing on AAD, only include it in the refresh path. 70 | return provider.Equals("AAD", StringComparison.OrdinalIgnoreCase); 71 | } 72 | 73 | private JwtSecurityToken CreateTokenForEasyAuthAccess(TokenBaseAttribute attribute) 74 | { 75 | if (string.IsNullOrEmpty(attribute.UserId)) 76 | { 77 | throw new ArgumentException("A userId is required to obtain an access token."); 78 | } 79 | 80 | if (string.IsNullOrEmpty(attribute.IdentityProvider)) 81 | { 82 | throw new ArgumentException("A provider is necessary to obtain an access token."); 83 | } 84 | 85 | var identityClaims = new ClaimsIdentity(attribute.UserId); 86 | identityClaims.AddClaim(new Claim(ClaimTypes.NameIdentifier, attribute.UserId)); 87 | identityClaims.AddClaim(new Claim("idp", attribute.IdentityProvider)); 88 | 89 | var baseUrl = $"https://{_options.HostName}/"; 90 | var descr = new SecurityTokenDescriptor 91 | { 92 | Audience = baseUrl, 93 | Issuer = baseUrl, 94 | Subject = identityClaims, 95 | Expires = DateTime.UtcNow.AddMinutes(JwtTokenDurationInMinutes), 96 | SigningCredentials = new HmacSigningCredentials(_options.SigningKey), 97 | }; 98 | 99 | return (JwtSecurityToken)JwtHandler.CreateToken(descr); 100 | } 101 | 102 | 103 | public class HmacSigningCredentials : SigningCredentials 104 | { 105 | public HmacSigningCredentials(string base64EncodedKey) 106 | : this(ParseKeyString(base64EncodedKey)) 107 | { } 108 | 109 | public HmacSigningCredentials(byte[] key) 110 | : base(new SymmetricSecurityKey(key), CreateSignatureAlgorithm(key)) 111 | { 112 | } 113 | 114 | /// 115 | /// Converts a base64 OR hex-encoded string into a byte array. 116 | /// 117 | protected static byte[] ParseKeyString(string keyString) 118 | { 119 | if (string.IsNullOrEmpty(keyString)) 120 | { 121 | return new byte[0]; 122 | } 123 | else if (IsHexString(keyString)) 124 | { 125 | return HexStringToByteArray(keyString); 126 | } 127 | else 128 | { 129 | return Convert.FromBase64String(keyString); 130 | } 131 | } 132 | 133 | protected static bool IsHexString(string input) 134 | { 135 | if (string.IsNullOrEmpty(input)) 136 | { 137 | throw new ArgumentNullException(nameof(input)); 138 | } 139 | 140 | for (int i = 0; i < input.Length; i++) 141 | { 142 | char c = input[i]; 143 | bool isHexChar = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); 144 | if (!isHexChar) 145 | { 146 | return false; 147 | } 148 | } 149 | 150 | return true; 151 | } 152 | 153 | protected static byte[] HexStringToByteArray(string hexString) 154 | { 155 | byte[] bytes = new byte[hexString.Length / 2]; 156 | for (int i = 0; i < hexString.Length; i += 2) 157 | { 158 | bytes[i / 2] = Convert.ToByte(hexString.Substring(i, 2), 16); 159 | } 160 | 161 | return bytes; 162 | } 163 | 164 | protected static string CreateSignatureAlgorithm(byte[] key) 165 | { 166 | if (key.Length <= 32) 167 | { 168 | return Algorithms.HmacSha256Signature; 169 | } 170 | else if (key.Length <= 48) 171 | { 172 | return Algorithms.HmacSha384Signature; 173 | } 174 | else 175 | { 176 | return Algorithms.HmacSha512Signature; 177 | } 178 | } 179 | 180 | /// 181 | /// Key value pairs (algorithm name, w3.org link) 182 | /// 183 | protected static class Algorithms 184 | { 185 | public const string HmacSha256Signature = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256"; 186 | public const string HmacSha384Signature = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha384"; 187 | public const string HmacSha512Signature = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha512"; 188 | } 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/TokenBinding/EasyAuthTokenStoreEntry.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens 4 | { 5 | using System; 6 | using System.Runtime.Serialization; 7 | 8 | /// 9 | /// Class representing a single entry in an application's [EasyAuth] Token Store 10 | /// Names of the fields match the names of the fields returned by the /.auth/me endpoint 11 | /// 12 | [DataContract] 13 | public class EasyAuthTokenStoreEntry 14 | { 15 | [DataMember(Name = "access_token", EmitDefaultValue = false)] 16 | public string AccessToken { get; set; } 17 | 18 | [DataMember(Name = "id_token", EmitDefaultValue = false)] 19 | public string IdToken { get; set; } 20 | 21 | [DataMember(Name = "refresh_token", EmitDefaultValue = false)] 22 | public string RefreshToken { get; set; } 23 | 24 | [DataMember(Name = "provider_name")] 25 | public string ProviderName { get; set; } 26 | 27 | [DataMember(Name = "user_id")] 28 | public string UserId { get; set; } 29 | 30 | [DataMember(Name = "expires_on", EmitDefaultValue = false)] 31 | public DateTime ExpiresOn { get; set; } 32 | 33 | [DataContract] 34 | public class Claim 35 | { 36 | [DataMember(Name = "typ")] 37 | public string Type { get; set; } 38 | 39 | [DataMember(Name = "val")] 40 | public string Value { get; set; } 41 | } 42 | 43 | [DataMember(Name = "user_claims", EmitDefaultValue = false)] 44 | public Claim[] UserClaims { get; set; } 45 | } 46 | } -------------------------------------------------------------------------------- /src/TokenBinding/IAadClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens 5 | { 6 | using System.Threading.Tasks; 7 | 8 | public interface IAadClient 9 | { 10 | Task GetTokenOnBehalfOfUserAsync(string userToken, string resource); 11 | 12 | Task GetTokenFromClientCredentials(string resource); 13 | 14 | Task GetTokenFromAppIdentity(string resource, string connectionString); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/TokenBinding/IEasyAuthClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens 5 | { 6 | using System.IdentityModel.Tokens.Jwt; 7 | using System.Threading.Tasks; 8 | 9 | public interface IEasyAuthClient 10 | { 11 | Task GetTokenStoreEntry(JwtSecurityToken jwt, TokenBaseAttribute attribute); 12 | 13 | Task RefreshToken(JwtSecurityToken jwt, TokenBaseAttribute attribute); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/TokenBinding/TokenAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs 5 | { 6 | using Microsoft.Azure.WebJobs.Description; 7 | 8 | [Binding] 9 | public sealed class TokenAttribute : TokenBaseAttribute 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/TokenBinding/TokenBaseAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs 5 | { 6 | using System; 7 | using Microsoft.Azure.WebJobs.Description; 8 | 9 | public abstract class TokenBaseAttribute : Attribute 10 | { 11 | private TokenIdentityMode _identity; 12 | 13 | /// 14 | /// Gets or sets a resource for a token exchange. Optional 15 | /// 16 | public string Resource { get; set; } 17 | 18 | /// 19 | /// Gets or sets an identity provider for the token exchange. Optional 20 | /// 21 | public string IdentityProvider { get; set; } 22 | 23 | /// 24 | /// Gets or sets token to grab on-behalf-of. Required if Identity="userFromToken". 25 | /// 26 | [AutoResolve] 27 | public string UserToken { get; set; } 28 | 29 | /// 30 | /// Gets or sets user id to grab token for. Required if Identity="userFromId". 31 | /// 32 | [AutoResolve] 33 | public string UserId { get; set; } 34 | 35 | /// 36 | /// Gets or sets connection string to use for an application's managed identity. Optional 37 | /// 38 | [AutoResolve] 39 | public string IdentityConnectionString { get; set; } 40 | 41 | /// 42 | /// Gets or sets how to determine identity. Required. 43 | /// 44 | public TokenIdentityMode Identity 45 | { 46 | get 47 | { 48 | return _identity; 49 | } 50 | 51 | set 52 | { 53 | if (value == TokenIdentityMode.UserFromRequest) 54 | { 55 | _identity = TokenIdentityMode.UserFromToken; 56 | this.UserToken = "{headers.X-MS-TOKEN-AAD-ID-TOKEN}"; 57 | } 58 | else 59 | { 60 | _identity = value; 61 | } 62 | } 63 | } 64 | 65 | public void CheckValidity() 66 | { 67 | switch (this.Identity) 68 | { 69 | case TokenIdentityMode.ClientCredentials: 70 | break; 71 | case TokenIdentityMode.UserFromId: 72 | if (string.IsNullOrWhiteSpace(this.UserId)) 73 | { 74 | throw new FormatException("A token attribute with identity=userFromId requires a userId"); 75 | } 76 | 77 | break; 78 | case TokenIdentityMode.UserFromToken: 79 | if (string.IsNullOrWhiteSpace(this.UserToken)) 80 | { 81 | throw new FormatException("A token attribute with identity=userFromToken requires a userToken"); 82 | } 83 | 84 | break; 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/TokenBinding/TokenBinding.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Microsoft.Azure.WebJobs.Extensions.Tokens 6 | Microsoft.Azure.WebJobs.Extensions.AuthTokens 7 | 8 | 9 | Microsoft.Azure.WebJobs.Extensions.AuthTokens 10 | This extension adds bindings for AuthTokens 11 | 1 12 | 0 13 | 0 14 | beta6 15 | $(MajorVersion).0.0.0 16 | $(MajorVersion).$(MinorVersion).$(PatchVersion)-$(BetaVersion)$(VersionSuffix) 17 | N/A 18 | $(Version) Commit hash: $(CommitHash) 19 | 20 | Microsoft 21 | Microsoft 22 | © .NET Foundation. All rights reserved. 23 | https://go.microsoft.com/fwlink/?linkid=2028464 24 | 25 | git 26 | https://github.com/Azure/azure-functions-microsoftgraph-extension 27 | https://github.com/Azure/azure-functions-microsoftgraph-extension 28 | true 29 | true 30 | ../../sign.snk 31 | 32 | 33 | 34 | Full 35 | true 36 | 37 | 38 | 39 | 40 | 41 | 42 | All 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/TokenBinding/TokenConverter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens 5 | { 6 | using System; 7 | using System.IdentityModel.Tokens.Jwt; 8 | using System.Linq; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Microsoft.Extensions.Options; 12 | 13 | public class TokenConverter : IAsyncConverter, IAsyncConverter 14 | { 15 | private IOptions _options; 16 | private IAadClient _aadManager; 17 | private IEasyAuthClient _easyAuthClient; 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// TokenExtensionConfig containing necessary context & methods 23 | public TokenConverter(IOptions options, IEasyAuthClient easyAuthClient, IAadClient aadClient) 24 | { 25 | _options = options; 26 | _easyAuthClient = easyAuthClient; 27 | _aadManager = aadClient; 28 | } 29 | 30 | public async Task ConvertAsync(TokenBaseAttribute attribute, CancellationToken cancellationToken) 31 | { 32 | attribute.CheckValidity(); 33 | switch (attribute.Identity) 34 | { 35 | case TokenIdentityMode.UserFromId: 36 | // If the attribute has no identity provider, assume AAD 37 | attribute.IdentityProvider = attribute.IdentityProvider ?? "AAD"; 38 | var easyAuthTokenManager = new EasyAuthTokenManager(_easyAuthClient, _options); 39 | return await easyAuthTokenManager.GetEasyAuthAccessTokenAsync(attribute); 40 | case TokenIdentityMode.UserFromToken: 41 | return await GetAuthTokenFromUserToken(attribute.UserToken, attribute.Resource); 42 | case TokenIdentityMode.ClientCredentials: 43 | return await _aadManager.GetTokenFromClientCredentials(attribute.Resource); 44 | case TokenIdentityMode.AppIdentity: 45 | return await _aadManager.GetTokenFromAppIdentity(attribute.Resource, attribute.IdentityConnectionString); 46 | } 47 | 48 | throw new InvalidOperationException("Unable to authorize without Principal ID or ID Token."); 49 | } 50 | 51 | public async Task ConvertAsync(TokenAttribute attribute, CancellationToken cancellationToken) 52 | { 53 | return await ConvertAsync(attribute as TokenBaseAttribute, cancellationToken); 54 | } 55 | 56 | private async Task GetAuthTokenFromUserToken(string userToken, string resource) 57 | { 58 | if (string.IsNullOrWhiteSpace(resource)) 59 | { 60 | throw new ArgumentException("A resource is required to get an auth token on behalf of a user."); 61 | } 62 | 63 | // If the incoming token already has the correct audience (resource), then skip the exchange (it will fail with AADSTS50013!) 64 | var currentAudience = GetAudience(userToken); 65 | if (currentAudience != resource) 66 | { 67 | string token = await _aadManager.GetTokenOnBehalfOfUserAsync( 68 | userToken, 69 | resource); 70 | return token; 71 | } 72 | 73 | // No exchange requested, return token directly. 74 | return userToken; 75 | } 76 | 77 | private string GetAudience(string rawToken) 78 | { 79 | var jwt = new JwtSecurityToken(rawToken); 80 | var audience = jwt.Audiences.FirstOrDefault(); 81 | return audience; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/TokenBinding/TokenIdentityMode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs 5 | { 6 | /// 7 | /// How the binding should grab the access token 8 | /// 9 | public enum TokenIdentityMode 10 | { 11 | /// Obtains the access token on behalf of the user whose token is in the userToken field of metadata. 12 | UserFromToken, 13 | /// Same as with the user token taken from the X-MS-TOKEN-AAD-ID-TOKEN header. Only works for HttpTrigger 14 | UserFromRequest, //This cannot be the default value, as it requires binding expressions that could cause errors. 15 | /// Obtains the access token for the user with the id found in the userId field of metadata. 16 | UserFromId, 17 | /// Obtains the access token for the client credentials found in the application settings. 18 | ClientCredentials, 19 | /// Obtains an access token for the application using the application's identity. 20 | AppIdentity 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/TokenBinding/TokenOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens 5 | { 6 | public class TokenOptions 7 | { 8 | /// 9 | /// The website hostname 10 | /// 11 | public string HostName { get; set; } 12 | 13 | /// 14 | /// Application setting key for the client ID 15 | /// 16 | public string ClientId { get; set; } 17 | 18 | /// 19 | /// The client secret used with AAD 20 | /// 21 | public string ClientSecret { get; set; } 22 | 23 | /// 24 | /// Base tenant URL for AAD 25 | /// 26 | public string TenantUrl { get; set; } 27 | 28 | /// 29 | /// The signing key used for EasyAuth tokens 30 | /// 31 | public string SigningKey { get; set; } 32 | 33 | /// 34 | /// The default base url to gr. 35 | /// 36 | public string DefaultEnvironmentBaseUrl { get; set; } = "https://login.windows.net/"; 37 | 38 | /// 39 | /// The default tenant to grab the token for 40 | /// 41 | public string DefaultTenantId { get; set; } = "common"; 42 | 43 | public virtual void SetAppSettings(INameResolver appSettings) 44 | { 45 | if (HostName == null) 46 | { 47 | HostName = appSettings.Resolve(Constants.WebsiteHostname); 48 | } 49 | if (ClientId == null) 50 | { 51 | ClientId = appSettings.Resolve(Constants.ClientIdName); 52 | } 53 | if (ClientSecret == null) 54 | { 55 | ClientSecret = appSettings.Resolve(Constants.ClientSecretName); 56 | } 57 | if (TenantUrl == null) 58 | { 59 | TenantUrl = appSettings.Resolve(Constants.WebsiteAuthOpenIdIssuer); 60 | } 61 | if (SigningKey == null) 62 | { 63 | SigningKey = appSettings.Resolve(Constants.WebsiteAuthSigningKey); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/MicrosoftGraph.Tests/ExcelMockUtilities.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests 5 | { 6 | using System; 7 | using System.Linq.Expressions; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.Graph; 11 | using Moq; 12 | using Newtonsoft.Json.Linq; 13 | 14 | public static class ExcelMockUtilities 15 | { 16 | public static void MockGetTableWorkbookAsync(this Mock mock, WorkbookTable returnValue) 17 | { 18 | mock.Setup(client => client 19 | .Me 20 | .Drive 21 | .Root 22 | .ItemWithPath(It.IsAny()) 23 | .Workbook 24 | .Tables[It.IsAny()] 25 | .Request() 26 | .GetAsync(It.IsAny())).Returns(Task.FromResult(returnValue)); 27 | } 28 | 29 | public static void MockGetTableWorkbookRangeAsync(this Mock mock, WorkbookRange returnValue) 30 | { 31 | mock.Setup(client => client 32 | .Me 33 | .Drive 34 | .Root 35 | .ItemWithPath(It.IsAny()) 36 | .Workbook 37 | .Tables[It.IsAny()] 38 | .Range() 39 | .Request(null) 40 | .GetAsync(It.IsAny())).Returns(Task.FromResult(returnValue)); 41 | } 42 | 43 | public static void MockGetWorksheetWorkbookAsync(this Mock mock, WorkbookRange returnValue) 44 | { 45 | mock.Setup(client => client 46 | .Me 47 | .Drive 48 | .Root 49 | .ItemWithPath(It.IsAny()) 50 | .Workbook 51 | .Worksheets[It.IsAny()] 52 | .UsedRange() 53 | .Request(null) 54 | .GetAsync(It.IsAny())).Returns(Task.FromResult(returnValue)); 55 | } 56 | 57 | public static void MockGetWorkSheetWorkbookInRangeAsync(this Mock mock, WorkbookRange returnValue) 58 | { 59 | mock.Setup(client => client 60 | .Me 61 | .Drive 62 | .Root 63 | .ItemWithPath(It.IsAny()) 64 | .Workbook 65 | .Worksheets[It.IsAny()] 66 | .Range(It.IsAny()) 67 | .Request(null) 68 | .GetAsync(It.IsAny())).Returns(Task.FromResult(returnValue)); 69 | } 70 | 71 | public static void MockGetTableHeaderRowAsync(this Mock mock, WorkbookRange returnValue) 72 | { 73 | mock.Setup(client => client 74 | .Me 75 | .Drive 76 | .Root 77 | .ItemWithPath(It.IsAny()) 78 | .Workbook 79 | .Tables[It.IsAny()] 80 | .HeaderRowRange() 81 | .Request(null) 82 | .GetAsync(It.IsAny())).Returns(Task.FromResult(returnValue)); 83 | } 84 | 85 | public static void MockPostTableRowAsyc(this Mock mock, WorkbookTableRow returnValue) 86 | { 87 | mock.Setup(client => client 88 | .Me 89 | .Drive 90 | .Root 91 | .ItemWithPath(It.IsAny()) 92 | .Workbook 93 | .Tables[It.IsAny()] 94 | .Rows 95 | .Add(null, It.IsAny()) 96 | .Request(null) 97 | .PostAsync(It.IsAny())).Returns(Task.FromResult(returnValue)); 98 | } 99 | 100 | public static void VerifyPostTableRowAsync(this Mock mock, string path, string tableName, Expression> rowCondition) 101 | { 102 | //first verify PostAsync() called 103 | mock.Verify(client => client 104 | .Me 105 | .Drive 106 | .Root 107 | .ItemWithPath(path) 108 | .Workbook 109 | .Tables[tableName] 110 | .Rows 111 | .Add(null, It.IsAny()) 112 | .Request(null) 113 | .PostAsync(It.IsAny())); 114 | 115 | //Now verify row condition is true 116 | mock.Verify(client => client 117 | .Me 118 | .Drive 119 | .Root 120 | .ItemWithPath(path) 121 | .Workbook 122 | .Tables[tableName] 123 | .Rows 124 | .Add(null, It.Is(rowCondition))); 125 | } 126 | 127 | public static void MockPatchWorksheetAsync(this Mock mock, WorkbookRange returnValue) 128 | { 129 | mock.Setup(client => client 130 | .Me 131 | .Drive 132 | .Root 133 | .ItemWithPath(It.IsAny()) 134 | .Workbook 135 | .Worksheets[It.IsAny()] 136 | .Range(It.IsAny()) 137 | .Request(null) 138 | .PatchAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(returnValue)); 139 | } 140 | 141 | public static void VerifyPatchWorksheetAsync(this Mock mock, string path, string worksheetName, string range, Expression> newWorkbookCondition) 142 | { 143 | mock.Verify(client => client 144 | .Me 145 | .Drive 146 | .Root 147 | .ItemWithPath(path) 148 | .Workbook 149 | .Worksheets[worksheetName] 150 | .Range(range) 151 | .Request(null) 152 | .PatchAsync(It.Is(newWorkbookCondition), It.IsAny())); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /test/MicrosoftGraph.Tests/MemorySubscriptionStore.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests 5 | { 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config; 10 | using Microsoft.Graph; 11 | 12 | class MemorySubscriptionStore : IGraphSubscriptionStore 13 | { 14 | //The key is the subscription id of the subscription entry 15 | private IDictionary _map = new Dictionary(); 16 | 17 | public async Task DeleteAsync(string subscriptionId) 18 | { 19 | if (_map.ContainsKey(subscriptionId)) 20 | { 21 | _map.Remove(subscriptionId); 22 | } 23 | } 24 | 25 | public async Task GetAllSubscriptionsAsync() 26 | { 27 | return _map.Values.ToArray(); 28 | } 29 | 30 | public async Task GetSubscriptionEntryAsync(string subscriptionId) 31 | { 32 | SubscriptionEntry value = null; 33 | _map.TryGetValue(subscriptionId, out value); 34 | return value; 35 | } 36 | 37 | public async Task SaveSubscriptionEntryAsync(Subscription subscription, string userId) 38 | { 39 | _map[subscription.Id] = new SubscriptionEntry() 40 | { 41 | Subscription = subscription, 42 | UserId = userId, 43 | }; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/MicrosoftGraph.Tests/MicrosoftGraph.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.0 5 | Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests 6 | Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests 7 | true 8 | ../../sign.snk 9 | 10 | 11 | 12 | full 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/MicrosoftGraph.Tests/MockGraphServiceClientProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests 5 | { 6 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config; 7 | using Microsoft.Graph; 8 | 9 | class MockGraphServiceClientProvider : IGraphServiceClientProvider 10 | { 11 | private IGraphServiceClient _client; 12 | 13 | public MockGraphServiceClientProvider(IGraphServiceClient client) 14 | { 15 | _client = client; 16 | } 17 | 18 | public IGraphServiceClient CreateNewGraphServiceClient(string token) 19 | { 20 | return _client; 21 | } 22 | 23 | 24 | public void UpdateGraphServiceClientAuthToken(IGraphServiceClient client, string token) 25 | { 26 | //NO-OP 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/MicrosoftGraph.Tests/MockTokenConverter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests 5 | { 6 | using System; 7 | using System.IdentityModel.Tokens.Jwt; 8 | using System.Security.Claims; 9 | using System.Text; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.IdentityModel.Tokens; 13 | 14 | internal class MockTokenConverter : IAsyncConverter 15 | { 16 | private readonly string _secretKey = "dummy_secret_key"; 17 | private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); 18 | 19 | public Task ConvertAsync(TokenBaseAttribute input, CancellationToken cancellationToken) 20 | { 21 | ClaimsIdentity identity = new ClaimsIdentity(); 22 | identity.AddClaim(new Claim(ClaimTypes.Name, "Sample")); 23 | identity.AddClaim(new Claim("idp", "aad")); 24 | identity.AddClaim(new Claim("oid", Guid.NewGuid().ToString())); 25 | identity.AddClaim(new Claim("scp", "read")); 26 | 27 | var descr = new SecurityTokenDescriptor 28 | { 29 | Audience = "https://sample.com", 30 | Issuer = "https://microsoft.graph.com", 31 | Subject = identity, 32 | SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey)), SecurityAlgorithms.HmacSha256), 33 | Expires = DateTime.UtcNow.AddHours(1) 34 | }; 35 | string accessToken = _tokenHandler.CreateJwtSecurityToken(descr).RawData; 36 | return Task.FromResult(accessToken); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/MicrosoftGraph.Tests/OneDriveMockUtilities.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests 5 | { 6 | using System; 7 | using System.IO; 8 | using System.Linq.Expressions; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Microsoft.Graph; 12 | using Moq; 13 | 14 | internal static class OneDriveMockUtilities 15 | { 16 | public static void MockGetOneDriveContentStreamAsync(this Mock mock, Stream returnValue) 17 | { 18 | mock.Setup(client => client.Me 19 | .Drive 20 | .Root 21 | .ItemWithPath(It.IsAny()) 22 | .Content 23 | .Request(null) 24 | .GetAsync(It.IsAny(), System.Net.Http.HttpCompletionOption.ResponseContentRead)) 25 | .Returns(Task.FromResult(returnValue)); 26 | } 27 | 28 | public static void MockGetOneDriveContentStreamFromShareAsync(this Mock mock, Stream returnValue) 29 | { 30 | mock.Setup(client => client 31 | .Shares[It.IsAny()] 32 | .Root 33 | .Content 34 | .Request(null) 35 | .GetAsync(It.IsAny(), System.Net.Http.HttpCompletionOption.ResponseContentRead)) 36 | .Returns(Task.FromResult(returnValue)); 37 | } 38 | 39 | public static void VerifyGetOneDriveContentStreamFromShareAsync(this Mock mock, string shareToken) 40 | { 41 | //First verify GetAsync() called 42 | mock.Verify(client => client 43 | .Shares[shareToken] 44 | .Root 45 | .Content 46 | .Request(null) 47 | .GetAsync(It.IsAny(), System.Net.Http.HttpCompletionOption.ResponseContentRead)); 48 | 49 | //Then verify sharetoken correct 50 | mock.Verify(client => client 51 | .Shares[shareToken]); 52 | } 53 | 54 | public static void MockGetOneDriveItemAsync(this Mock mock, DriveItem returnValue) 55 | { 56 | mock.Setup(client => client 57 | .Me 58 | .Drive 59 | .Root 60 | .ItemWithPath(It.IsAny()) 61 | .Request() 62 | .GetAsync(It.IsAny())) 63 | .Returns(Task.FromResult(returnValue)); 64 | } 65 | 66 | public static void MockUploadOneDriveItemAsync(this Mock mock, DriveItem returnValue) 67 | { 68 | mock.Setup(client => client 69 | .Me 70 | .Drive 71 | .Root 72 | .ItemWithPath(It.IsAny()) 73 | .Content 74 | .Request(null) 75 | .PutAsync(It.IsAny(), It.IsAny(), System.Net.Http.HttpCompletionOption.ResponseContentRead)) 76 | .Returns(Task.FromResult(returnValue)); 77 | } 78 | 79 | public static void VerifyUploadOneDriveItemAsync(this Mock mock, string path, Expression> streamCondition) 80 | { 81 | mock.Verify(client => client 82 | .Me 83 | .Drive 84 | .Root 85 | .ItemWithPath(path) 86 | .Content 87 | .Request(null) 88 | .PutAsync(It.Is(streamCondition), It.IsAny(), System.Net.Http.HttpCompletionOption.ResponseContentRead)); 89 | } 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/MicrosoftGraph.Tests/OutlookMockUtilities.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests 5 | { 6 | using System; 7 | using System.Linq.Expressions; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.Graph; 11 | using Moq; 12 | 13 | internal static class OutlookMockUtilities 14 | { 15 | public static void MockSendMessage(this Mock mock) 16 | { 17 | mock.Setup(client => client 18 | .Me 19 | .SendMail(It.IsAny(), true) 20 | .Request(null) 21 | .PostAsync(It.IsAny())).Returns(Task.CompletedTask); 22 | } 23 | 24 | public static void VerifySendMessage(this Mock mock, Expression> messageCondition) 25 | { 26 | // First verify PostAsync() called 27 | mock.Verify(client => client 28 | .Me 29 | .SendMail(It.IsAny(), true) 30 | .Request(null) 31 | .PostAsync(It.IsAny())); 32 | 33 | // Now verify that message condition holds for sent message 34 | mock.Verify(client => client 35 | .Me 36 | .SendMail(It.Is(messageCondition), true)); 37 | } 38 | 39 | public static void VerifyDidNotSendMessage(this Mock mock) 40 | { 41 | mock.Verify(client => client 42 | .Me 43 | .SendMail(It.IsAny(), true) 44 | .Request(null) 45 | .PostAsync(It.IsAny()), Times.Never()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/MicrosoftGraph.Tests/OutlookTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | using Microsoft.Graph; 11 | using Moq; 12 | using Newtonsoft.Json.Linq; 13 | using Xunit; 14 | 15 | public class OutlookTests 16 | { 17 | private static string name = "Sample User"; 18 | private static string address = "sampleuser@microsoft.com"; 19 | private static string subject = "hello"; 20 | private static string body = "world"; 21 | private static BodyType contentType = BodyType.Text; 22 | 23 | [Fact] 24 | public static async Task JObjectOutput_SendsProperMessage() 25 | { 26 | var clientMock = SendMessageMock(); 27 | 28 | await CommonUtilities.ExecuteFunction("OutlookFunctions.SendJObject", clientMock); 29 | 30 | clientMock.VerifySendMessage(msg => MessageEquals(msg, GetMessage())); 31 | } 32 | 33 | [Fact] 34 | public static async Task MessageOutput_SendsProperMessage() 35 | { 36 | var clientMock = SendMessageMock(); 37 | 38 | await CommonUtilities.ExecuteFunction("OutlookFunctions.SendMessage", clientMock); 39 | 40 | clientMock.VerifySendMessage(msg => MessageEquals(msg, GetMessage())); 41 | } 42 | 43 | [Fact] 44 | public static async Task PocoOutput_SendsProperMessage() 45 | { 46 | var clientMock = SendMessageMock(); 47 | 48 | await CommonUtilities.ExecuteFunction("OutlookFunctions.SendPoco", clientMock); 49 | 50 | clientMock.VerifySendMessage(msg => MessageEquals(msg, GetMessage())); 51 | } 52 | 53 | [Fact] 54 | public static async Task PocoOutputWithNoRecipients_ThrowsException() 55 | { 56 | var clientMock = SendMessageMock(); 57 | 58 | await Assert.ThrowsAnyAsync(async () => await CommonUtilities.ExecuteFunction("OutlookFunctions.NoRecipients", clientMock)); 59 | clientMock.VerifyDidNotSendMessage(); 60 | } 61 | 62 | private static Mock SendMessageMock() 63 | { 64 | var clientMock = new Mock(); 65 | clientMock.MockSendMessage(); 66 | return clientMock; 67 | } 68 | 69 | private static bool MessageEquals(Message message1, Message message2) 70 | { 71 | return message1.Body.Content.Equals(message2.Body.Content) && 72 | message1.Body.ContentType.Equals(message2.Body.ContentType) && 73 | message1.Subject.Equals(message2.Subject) && 74 | RecipientsEqual(message1.ToRecipients, message2.ToRecipients); 75 | } 76 | 77 | private static bool RecipientsEqual(IEnumerable recipients1, IEnumerable recipients2) 78 | { 79 | return recipients1.Select(msg => msg.EmailAddress.Name).SequenceEqual(recipients2.Select(msg => msg.EmailAddress.Name)) && 80 | recipients1.Select(msg => msg.EmailAddress.Address).SequenceEqual(recipients2.Select(msg => msg.EmailAddress.Address)); 81 | } 82 | 83 | private static Message GetMessage() 84 | { 85 | return new Message() 86 | { 87 | Body = new ItemBody() 88 | { 89 | Content = body, 90 | ContentType = contentType, 91 | }, 92 | Subject = subject, 93 | ToRecipients = new List() 94 | { 95 | new Recipient() 96 | { 97 | EmailAddress = new EmailAddress() 98 | { 99 | Name = name, 100 | Address = address, 101 | } 102 | } 103 | } 104 | }; 105 | } 106 | 107 | private static JObject GetMessageAsJObject() 108 | { 109 | var message = new JObject(); 110 | message["subject"] = subject; 111 | message["body"] = body; 112 | message["recipient"] = new JObject(); 113 | message["recipient"]["address"] = address; 114 | message["recipient"]["name"] = name; 115 | return message; 116 | } 117 | 118 | private static MessagePoco GetMessageAsPoco() 119 | { 120 | return new MessagePoco() 121 | { 122 | Body = body, 123 | Subject = subject, 124 | Recipients = new List() 125 | { 126 | new RecipientPoco() 127 | { 128 | Address = address, 129 | Name = name, 130 | } 131 | } 132 | }; 133 | } 134 | 135 | public class OutlookFunctions 136 | { 137 | public static void SendJObject([Outlook] out JObject message) 138 | { 139 | message = GetMessageAsJObject(); 140 | } 141 | 142 | public static void SendMessage([Outlook] out Message message) 143 | { 144 | message = GetMessage(); 145 | } 146 | 147 | public static void SendPoco([Outlook] out MessagePoco message) 148 | { 149 | message = GetMessageAsPoco(); 150 | } 151 | 152 | public static void NoRecipients([Outlook] out MessagePoco message) 153 | { 154 | message = GetMessageAsPoco(); 155 | message.Recipients = new List(); 156 | } 157 | } 158 | 159 | public class MessagePoco 160 | { 161 | public string Subject { get; set; } 162 | public string Body { get; set; } 163 | public List Recipients { get; set; } 164 | } 165 | 166 | public class RecipientPoco 167 | { 168 | public string Address { get; set; } 169 | public string Name { get; set; } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /test/MicrosoftGraph.Tests/common/CommonUtilities.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config; 10 | using Microsoft.Azure.WebJobs.Extensions.Token.Tests; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Hosting; 13 | using Microsoft.Graph; 14 | using Moq; 15 | 16 | internal static class CommonUtilities 17 | { 18 | public static async Task ExecuteFunction(string methodName, Mock client, IGraphSubscriptionStore subscriptionStore = null, INameResolver appSettings = null, object argument = null) 19 | { 20 | var outputContainer = new OutputContainer(); 21 | var arguments = new Dictionary() 22 | { 23 | { "outputContainer", outputContainer }, 24 | { "triggerData", argument } 25 | }; 26 | 27 | IHost host = new HostBuilder() 28 | .ConfigureServices(services => 29 | { 30 | appSettings = appSettings ?? new Mock().Object; 31 | subscriptionStore = subscriptionStore ?? new MemorySubscriptionStore(); 32 | services.AddSingleton(new FakeTypeLocator()); 33 | services.AddSingleton>(new MockTokenConverter()); 34 | services.AddSingleton(new MockGraphServiceClientProvider(client.Object)); 35 | services.AddSingleton(subscriptionStore); 36 | services.AddSingleton(appSettings); 37 | }) 38 | .ConfigureWebJobs(builder => 39 | { 40 | builder.AddMicrosoftGraphForTests(); 41 | builder.UseHostId(Guid.NewGuid().ToString("n")); 42 | }) 43 | .Build(); 44 | 45 | JobHost webJobsHost = host.Services.GetService() as JobHost; 46 | await webJobsHost.CallAsync(methodName, arguments); 47 | return outputContainer; 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /test/TokenExtension.Tests/TokenExtension.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.0 5 | Microsoft.Azure.WebJobs.Extensions.Token.Tests 6 | Microsoft.Azure.WebJobs.Extensions.Token.Tests 7 | true 8 | ../../sign.snk 9 | 10 | 11 | 12 | full 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/TokenExtension.Tests/common/FakeLocator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.Token.Tests 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using Microsoft.Azure.WebJobs; 9 | 10 | public class FakeTypeLocator : ITypeLocator 11 | { 12 | public IReadOnlyList Types => new Type[] { typeof(T) }; 13 | 14 | public IReadOnlyList GetTypes() 15 | { 16 | return Types; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /test/TokenExtension.Tests/common/OutputContainer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.Token.Tests 5 | { 6 | public class OutputContainer 7 | { 8 | public object Output { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/TokenExtension.Tests/common/TestHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Azure.WebJobs.Extensions.Token.Tests 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | using Microsoft.Azure.WebJobs.Extensions.AuthTokens; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | using Moq; 13 | 14 | internal class TestHelpers 15 | { 16 | public static async Task RunTestAsync(string methodName, INameResolver appSettings = null, IEasyAuthClient easyAuthClient = null, IAadClient aadClient = null, object argument = null) 17 | { 18 | var outputContainer = new OutputContainer(); 19 | var arguments = new Dictionary() 20 | { 21 | { "outputContainer", outputContainer }, 22 | { "triggerData", argument } 23 | }; 24 | 25 | IHost host = new HostBuilder() 26 | .ConfigureServices(services => 27 | { 28 | easyAuthClient = easyAuthClient ?? new Mock().Object; 29 | aadClient = aadClient ?? new Mock().Object; 30 | appSettings = appSettings ?? new Mock().Object; 31 | services.AddSingleton(new FakeTypeLocator()); 32 | services.AddSingleton(easyAuthClient); 33 | services.AddSingleton(aadClient); 34 | services.AddSingleton(appSettings); 35 | }) 36 | .ConfigureWebJobs(builder => 37 | { 38 | builder.AddAuthTokenForTests(); 39 | builder.UseHostId(Guid.NewGuid().ToString("n")); 40 | }) 41 | .Build(); 42 | 43 | JobHost webJobsHost = host.Services.GetService() as JobHost; 44 | await webJobsHost.CallAsync(methodName, arguments); 45 | return outputContainer; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /update/README.md: -------------------------------------------------------------------------------- 1 | # Upgrade instructions 2 | 1. Stop your function application 3 | 2. Go to Platform features > Advanced tools (Kudu) 4 | 3. On Kudu, go to the Debug Console 5 | 4. Navigate to the directory D:/home/site/wwwroot 6 | 5. Put the file extensions.csproj into this directory 7 | 6. Run the command: dotnet build -o bin extensions.csproj 8 | 7. Restart your function application. 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /update/extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | --------------------------------------------------------------------------------