├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── ask-a-question.md │ └── bug_report.md ├── dependabot.yml ├── policies │ └── resourceManagement.yml └── workflows │ ├── auto-merge-dependabot.yml │ ├── codeql-analysis.yml │ └── maven.yml ├── .gitignore ├── .markdownlint.json ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── TROUBLESHOOTING.md ├── docs ├── ad1.png ├── ad2.png ├── ad3.png ├── ad4.png └── ad5.png ├── graphwebhook ├── .gitignore ├── .mvn │ └── wrapper │ │ ├── MavenWrapperDownloader.java │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── graphwebhook │ │ │ ├── CertificateStoreService.java │ │ │ ├── DiscoverUrlAdapter.java │ │ │ ├── GraphClientHelper.java │ │ │ ├── GraphwebhookApplication.java │ │ │ ├── HomeController.java │ │ │ ├── ListenController.java │ │ │ ├── NewChatMessageNotification.java │ │ │ ├── NewMessageNotification.java │ │ │ ├── SecurityConfig.java │ │ │ ├── SocketIOServerRunner.java │ │ │ ├── SpringOAuth2AuthProvider.java │ │ │ ├── SubscriptionRecord.java │ │ │ ├── SubscriptionStoreService.java │ │ │ ├── TokenHelper.java │ │ │ ├── WatchController.java │ │ │ └── notifications │ │ │ ├── ChangeNotification.java │ │ │ ├── ChangeNotificationCollection.java │ │ │ ├── ChangeNotificationEncryptedContent.java │ │ │ └── ResourceData.java │ └── resources │ │ ├── META-INF │ │ └── additional-spring-configuration-metadata.json │ │ ├── application.example.yml │ │ ├── public │ │ ├── images │ │ │ └── g-raph.png │ │ └── styles │ │ │ └── site.css │ │ └── templates │ │ ├── apponly.html │ │ ├── delegated.html │ │ ├── home.html │ │ └── layout.html │ └── test │ └── java │ └── com │ └── example │ └── graphwebhook │ └── GraphwebhookApplicationTests.java └── images ├── aad-portal-app-registrations.png ├── copy-secret-value.png ├── ngrok-https-url.png ├── register-an-app.png ├── remove-configured-permission.png ├── teams-channel-notifications.png └── user-inbox-notifications.png /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @microsoftgraph/msgraph-devx-samples-write -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ask-a-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Ask a question 3 | about: Ask a question about Graph, adding features to this sample, etc. 4 | title: '' 5 | labels: question, needs triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | Thank you for taking an interest in Microsoft Graph development! Please feel free to ask a question here, but keep in mind the following: 11 | 12 | - This is not an official Microsoft support channel, and our ability to respond to questions here is limited. Questions about Graph, or questions about adding a new feature to the sample, will be answered on a best-effort basis. 13 | - Questions about Microsoft Graph should be asked on [Microsoft Q&A](https://docs.microsoft.com/answers/products/graph). 14 | - Issues with Microsoft Graph itself should be handled through [support](https://developer.microsoft.com/graph/support). 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug report, needs triage 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the bug 10 | 11 | A clear and concise description of the bug. 12 | 13 | ### To Reproduce 14 | 15 | Steps to reproduce the behavior: 16 | 17 | 1. Go to '...' 18 | 1. Click on '....' 19 | 1. Scroll down to '....' 20 | 1. See error 21 | 22 | ### Expected behavior 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | ### Screenshots 27 | 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | ### Desktop 31 | 32 | - OS: [e.g. iOS] 33 | - Browser [e.g. chrome, safari] 34 | - Version [e.g. 22] 35 | 36 | ### Dependency versions 37 | 38 | - Authentication library (MSAL, etc.) version: 39 | - Graph library (Graph SDK, REST library, etc.) version: 40 | 41 | ### Additional context 42 | 43 | Add any other context about the problem here. 44 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/graphwebhook" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.github/policies/resourceManagement.yml: -------------------------------------------------------------------------------- 1 | id: 2 | name: GitOps.PullRequestIssueManagement 3 | description: GitOps.PullRequestIssueManagement primitive 4 | owner: 5 | resource: repository 6 | disabled: false 7 | where: 8 | configuration: 9 | resourceManagementConfiguration: 10 | scheduledSearches: 11 | - description: 12 | frequencies: 13 | - hourly: 14 | hour: 6 15 | filters: 16 | - isIssue 17 | - isOpen 18 | - hasLabel: 19 | label: needs author feedback 20 | - hasLabel: 21 | label: no recent activity 22 | - noActivitySince: 23 | days: 3 24 | actions: 25 | - closeIssue 26 | - description: 27 | frequencies: 28 | - hourly: 29 | hour: 6 30 | filters: 31 | - isIssue 32 | - isOpen 33 | - hasLabel: 34 | label: needs author feedback 35 | - noActivitySince: 36 | days: 4 37 | - isNotLabeledWith: 38 | label: no recent activity 39 | actions: 40 | - addLabel: 41 | label: no recent activity 42 | - addReply: 43 | reply: This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for **4 days**. It will be closed if no further activity occurs **within 3 days of this comment**. 44 | - description: 45 | frequencies: 46 | - hourly: 47 | hour: 6 48 | filters: 49 | - isIssue 50 | - isOpen 51 | - hasLabel: 52 | label: duplicate 53 | - noActivitySince: 54 | days: 1 55 | actions: 56 | - addReply: 57 | reply: This issue has been marked as duplicate and has not had any activity for **1 day**. It will be closed for housekeeping purposes. 58 | - closeIssue 59 | - description: 60 | frequencies: 61 | - hourly: 62 | hour: 3 63 | filters: 64 | - isOpen 65 | - isIssue 66 | - hasLabel: 67 | label: graph question 68 | actions: 69 | - removeLabel: 70 | label: 'needs attention :wave:' 71 | - removeLabel: 72 | label: needs author feedback 73 | - removeLabel: 74 | label: 'needs triage :mag:' 75 | - removeLabel: 76 | label: no recent activity 77 | - addLabel: 78 | label: out of scope 79 | - addReply: 80 | reply: >- 81 | It looks like you are asking a question about using Microsoft Graph or one of the Microsoft Graph SDKs that is not directly related to this sample. Unfortunately we are not set up to answer general questions in this repository, so this issue will be closed. 82 | 83 | 84 | Please try asking your question on [Microsoft Q&A](https://docs.microsoft.com/answers/products/graph). 85 | - closeIssue 86 | - description: 87 | frequencies: 88 | - hourly: 89 | hour: 3 90 | filters: 91 | - isOpen 92 | - isIssue 93 | - hasLabel: 94 | label: graph issue 95 | actions: 96 | - removeLabel: 97 | label: 'needs attention :wave:' 98 | - removeLabel: 99 | label: needs author feedback 100 | - removeLabel: 101 | label: 'needs triage :mag:' 102 | - removeLabel: 103 | label: no recent activity 104 | - addLabel: 105 | label: out of scope 106 | - addReply: 107 | reply: >- 108 | It looks like you are reporting an issue with Microsoft Graph or one of the Microsoft Graph SDKs that is not fixable by changing code in this sample. Unfortunately we are not set up to provide product support in this repository, so this issue will be closed. 109 | 110 | 111 | Please visit one of the following links to report your issue. 112 | 113 | 114 | - Issue with Microsoft Graph service: [Microsoft Graph support](https://developer.microsoft.com/graph/support#report-issues-with-the-service), choose one of the options under **Report issues with the service** 115 | 116 | - Issue with a Microsoft Graph SDK: Open an issue in the SDK's GitHub repository. See [microsoftgraph on GitHub](https://github.com/microsoftgraph?q=sdk+in%3Aname&type=public&language=) for a list of SDK repositories. 117 | - closeIssue 118 | eventResponderTasks: 119 | - if: 120 | - payloadType: Issue_Comment 121 | - isAction: 122 | action: Created 123 | - isActivitySender: 124 | issueAuthor: True 125 | - hasLabel: 126 | label: needs author feedback 127 | - isOpen 128 | then: 129 | - addLabel: 130 | label: 'needs attention :wave:' 131 | - removeLabel: 132 | label: needs author feedback 133 | description: 134 | - if: 135 | - payloadType: Issues 136 | - not: 137 | isAction: 138 | action: Closed 139 | - hasLabel: 140 | label: no recent activity 141 | then: 142 | - removeLabel: 143 | label: no recent activity 144 | description: 145 | - if: 146 | - payloadType: Issue_Comment 147 | - hasLabel: 148 | label: no recent activity 149 | then: 150 | - removeLabel: 151 | label: no recent activity 152 | description: 153 | - if: 154 | - payloadType: Pull_Request 155 | then: 156 | - inPrLabel: 157 | label: in pr 158 | description: 159 | onFailure: 160 | onSuccess: 161 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Auto-merge dependabot updates 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | permissions: 8 | pull-requests: write 9 | contents: write 10 | 11 | jobs: 12 | 13 | dependabot-merge: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | if: ${{ github.actor == 'dependabot[bot]' }} 18 | 19 | steps: 20 | - name: Dependabot metadata 21 | id: metadata 22 | uses: dependabot/fetch-metadata@v2.4.0 23 | with: 24 | github-token: "${{ secrets.GITHUB_TOKEN }}" 25 | 26 | - name: Enable auto-merge for Dependabot PRs 27 | # Only if version bump is not a major version change 28 | if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}} 29 | run: gh pr merge --auto --merge "$PR_URL" 30 | env: 31 | PR_URL: ${{github.event.pull_request.html_url}} 32 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 33 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '0 19 * * 5' 10 | 11 | permissions: 12 | contents: read #those permissions are required to run the codeql analysis 13 | actions: read 14 | security-events: write 15 | 16 | jobs: 17 | analyze: 18 | name: Analyze 19 | runs-on: ubuntu-latest 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'java' ] 25 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 26 | # Learn more: 27 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | 33 | - name: Set up JDK 21 34 | uses: actions/setup-java@v4 35 | with: 36 | java-version: 21 37 | distribution: adopt 38 | cache: maven 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v3 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v3 68 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up JDK 21 20 | uses: actions/setup-java@v4 21 | with: 22 | java-version: 21 23 | distribution: adopt 24 | cache: maven 25 | - name: Build with Maven 26 | working-directory: graphwebhook 27 | run: mvn package --file pom.xml 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.sw? 3 | .#* 4 | *# 5 | *~ 6 | .classpath 7 | .project 8 | .settings 9 | bin 10 | build 11 | target 12 | dependency-reduced-pom.xml 13 | *.sublime-* 14 | /scratch 15 | .gradle 16 | README.html 17 | *.iml 18 | .idea 19 | *.jks 20 | application.yml 21 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint/master/schema/markdownlint-config-schema.json", 4 | "no-trailing-punctuation": false, 5 | "MD013": false 6 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vscjava.vscode-java-pack", 4 | "pivotal.vscode-spring-boot", 5 | "vscjava.vscode-spring-initializr" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "java", 5 | "name": "Spring Boot-GraphwebhookApplication", 6 | "request": "launch", 7 | "cwd": "${workspaceFolder}", 8 | "console": "internalConsole", 9 | "mainClass": "com.example.graphwebhook.GraphwebhookApplication", 10 | "projectName": "graphwebhook", 11 | "args": "" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "automatic", 3 | "java.format.settings.url": "https://raw.githubusercontent.com/google/styleguide/gh-pages/eclipse-java-google-style.xml", 4 | "java.format.enabled": true, 5 | "[java]": { 6 | "editor.suggest.snippetsPreventQuickSuggestions": false, 7 | "editor.defaultFormatter": "redhat.java" 8 | }, 9 | "cSpell.ignoreWords": [ 10 | "AADWSC", 11 | "JKSkeystore", 12 | "Jwts", 13 | "MSRC", 14 | "bcpkix", 15 | "codeql", 16 | "g-raph", 17 | "gson", 18 | "jjwt", 19 | "jwks", 20 | "selfsignedjks", 21 | "socketio", 22 | "ultraq" 23 | ], 24 | "java.compile.nullAnalysis.mode": "automatic", 25 | "cSpell.words": [ 26 | "activedirectory", 27 | "apponly", 28 | "autoconfigure", 29 | "Configurer", 30 | "Deserializers", 31 | "genkey", 32 | "graphwebhook", 33 | "HMACSHA", 34 | "keyalg", 35 | "keydiscoveryurl", 36 | "keysize", 37 | "keytool", 38 | "mailfolders", 39 | "OAEP", 40 | "PKCS", 41 | "storename", 42 | "storepass", 43 | "unsubscribeapponly" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | - Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support) 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to 4 | agree to a Contributor License Agreement (CLA) declaring that you have the right to, 5 | and actually do, grant us the rights to use your contribution. For details, visit 6 | [https://cla.microsoft.com](https://cla.microsoft.com). 7 | 8 | When you submit a pull request, a CLA-bot will automatically determine whether you need 9 | to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the 10 | instructions provided by the bot. You will only need to do this once across all repositories using our CLA. 11 | 12 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 13 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 14 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Microsoft Graph 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - java 5 | products: 6 | - ms-graph 7 | - office-exchange-online 8 | - entra-id 9 | - entra 10 | - office-teams 11 | description: Create Microsoft Graph webhook subscriptions for a Java Spring app, so that it can receive notifications of changes for any resource. This sample also supports receiving change notifications with data, validating and decrypting the payload. 12 | extensions: 13 | contentType: samples 14 | technologies: 15 | - Microsoft Graph 16 | services: 17 | - Microsoft Teams 18 | - Azure AD 19 | - Office 365 20 | - Change notifications 21 | createdDate: 3/15/2020 4:12:18 PM 22 | --- 23 | # Microsoft Graph Change Notifications Sample for Java Spring 24 | 25 | ![Java CI with Maven](https://github.com/microsoftgraph/java-spring-webhooks-sample/workflows/Java%20CI%20with%20Maven/badge.svg?branch=main) 26 | 27 | ## Use this sample application to receive Change Notifications with Resource Data 28 | 29 | ### Prerequisites 30 | 31 | - A tenant administrator account on a Microsoft 365 tenant. You can get a development tenant for free by joining the [Microsoft 365 Developer Program](https://developer.microsoft.com/microsoft-365/dev-program). 32 | - [Visual Studio Code](https://code.visualstudio.com/) 33 | - [ngrok](https://ngrok.com/). 34 | - [JDK21](https://adoptium.net/temurin/releases/) 35 | - [Maven](https://maven.apache.org/). 36 | 37 | ### How the sample application works 38 | 39 | The sample has the following features: 40 | 41 | - The user can sign in with their Microsoft 365 work account and subscribe to notifications on their Exchange Online inbox. This demonstrates how to use delegated authentication to subscribe for change notifications on a user's behalf. 42 | - The user can subscribe to notifications for all new Teams channel messages. This demonstrates how to use app-only authentication to subscribe for change notifications, and how to create a subscription that includes resource data. 43 | - Received notifications are displayed in the app. 44 | 45 | ## Setting up the sample 46 | 47 | 1. Register a Microsoft Identity platform application, and give it the right permissions. 48 | 1. Update application.yml with information from the previous step 49 | 50 | ### Register a Microsoft Identity platform application 51 | 52 | #### Choose the tenant where you want to create your app 53 | 54 | 1. Sign in to the [Azure Active Directory admin center](https://aad.portal.azure.com) using either a work or school account. 55 | 1. If your account is present in more than one Azure AD tenant: 56 | 1. Select your profile from the menu on the top right corner of the page, and then **Switch directory**. 57 | 1. Change your session to the Azure AD tenant where you want to create your application. 58 | 59 | #### Register the app 60 | 61 | 1. Select **Azure Active Directory** in the left-hand navigation, then select [App registrations](https://go.microsoft.com/fwlink/?linkid=2083908) under **Manage**. 62 | 63 | ![A screenshot of the App registrations ](images/aad-portal-app-registrations.png) 64 | 65 | 1. Select **New registration**. On the **Register an application** page, set the values as follows. 66 | 67 | - Set **Name** to `Java Spring Graph Notification Webhook Sample`. 68 | - Set **Supported account types** to **Accounts in this organizational directory only**. 69 | - Under **Redirect URI**, set the first drop-down to `Web` and set the value to `http://localhost:8080/login/oauth2/code/`. 70 | 71 | ![A screenshot of the Register an application page](images/register-an-app.png) 72 | 73 | 1. Select **Register** to create the app. On the app's **Overview** page, copy the value of the **Application (client) ID** and **Directory (tenant) ID** and save them for later. 74 | 75 | 1. Select **Certificates & secrets** under **Manage**. Select the **New client secret** button. Enter a value in **Description** and select one of the options for **Expires** and select **Add**. 76 | 77 | 1. Copy the **Value** of the new secret **before** you leave this page. It will never be displayed again. Save the value for later. 78 | 79 | ![A screenshot of a new secret in the Client secrets list](images/copy-secret-value.png) 80 | 81 | 1. Select **API permissions** under **Manage**. 82 | 83 | 1. In the list of pages for the app, select **API permissions**, then select **Add a permission**. 84 | 85 | 1. Make sure that the **Microsoft APIs** tab is selected, then select **Microsoft Graph**. 86 | 87 | 1. Select **Application permissions**, then find and enable the **ChannelMessage.Read.All** permission. Select **Add permissions** to add the enabled permission. 88 | 89 | > **Note:** To create subscriptions for other resources you need to select different permissions as documented [here](https://docs.microsoft.com/graph/api/subscription-post-subscriptions#permissions) 90 | 91 | 1. In the **Configured permissions** list, select the ellipses (`...`) in the **User.Read** row, and select **Remove permission**. The **User.Read** permission will be requested dynamically as part of the user sign-in process. 92 | 93 | ![A screenshot of the Remove permission menu item](images/remove-configured-permission.png) 94 | 95 | 1. Select **Grant admin consent for `name of your organization`** and **Yes**. This grants consent to the permissions of the application registration you just created to the current organization. 96 | 97 | ### Generate the self-signed certificate 98 | 99 | This step is required for the app-only Teams channel message subscription. Because this subscription requests notification with resource data, Microsoft Graph will encrypt the payload. As the payload is encrypted, it requires a certificate to ensure only you will be able to decrypt the payload. To generate a self-signed certificate run the following command from the sample folder. After running the command a file named `JKSkeystore.jks` should appear at the root of the repository. 100 | 101 | ```shell 102 | keytool -genkey -keyalg RSA -alias selfsignedjks -keystore JKSkeystore.jks -validity 365 -keysize 2048 103 | ``` 104 | 105 | **Note:** this command will request a password to protect the keystore, write it down, you'll need it later. 106 | 107 | ### Update application.yml 108 | 109 | 1. Rename the [application.example.yml](graphwebhook/src/main/resources/application.example.yml) file to `application.yml`. Open the file in Visual Studio code or any text editor. 110 | 111 | 1. Update the following values. 112 | 113 | - `tenant-id`: set to the tenant ID from your app registration 114 | - `client-id`: set to the client ID from your app registration 115 | - `client-secret`: set to the client secret from your app registration 116 | - `storepass`: set to the password for your keystore 117 | 118 | ### Set up the ngrok proxy (optional) 119 | 120 | You must expose a public HTTPS endpoint to create a subscription and receive notifications from Microsoft Graph. While testing, you can use ngrok to temporarily allow messages from Microsoft Graph to tunnel to a *localhost* port on your computer. 121 | 122 | You can use the ngrok web interface `http://127.0.0.1:4040` to inspect the HTTP traffic that passes through the tunnel. To download and learn more about using ngrok, see the [ngrok website](https://ngrok.com/). 123 | 124 | 1. Run the following command in your command-line interface (CLI) to start an ngrok session. 125 | 126 | ```Shell 127 | ngrok http 8080 128 | ``` 129 | 130 | 1. Copy the HTTPS URL that's shown in the console. 131 | 132 | ![The forwarding HTTPS URL in the ngrok console](images/ngrok-https-url.png) 133 | 134 | > **IMPORTANT**: Keep the console open while testing. If you close it, the tunnel also closes and you'll need to generate a new URL and update the sample. See [troubleshooting](./TROUBLESHOOTING.md) for more information about using tunnels. 135 | 136 | 1. Update `host` in `application.yml` with the URL. 137 | 138 | ### Start the application 139 | 140 | Open the repository with Visual Studio Code. Press F5. 141 | 142 | Alternatively if you are not using Visual Studio Code, from the root of the repository: 143 | 144 | ```shell 145 | mvn install 146 | mvn package 147 | java -jar target/graphwebhook-0.0.1-SNAPSHOT.jar 148 | ``` 149 | 150 | ### Use the app to create a subscription 151 | 152 | #### Use delegated authentication to subscribe to a user's inbox 153 | 154 | 1. Choose the **Sign in and subscribe** button and sign in with a work or school account. 155 | 156 | 1. Review and consent to the requested permissions. The subscription is created and you are redirected to a page displaying any notification being received. 157 | 158 | 1. Send an email to yourself. A notification appears showing the subject and message ID. 159 | 160 | ![A screenshot of the user inbox notifications page](images/user-inbox-notifications.png) 161 | 162 | #### Use app-only authentication to subscribe to Teams channel messages 163 | 164 | 1. If you previously subscribed to a user's inbox, choose the **Delete subscription** button to return to the home page. 165 | 166 | 1. Choose the **Subscribe** button. The subscription is created and you are redirected to a page displaying any notification being received. 167 | 168 | 1. Post a message to a channel in any team in Microsoft Teams. A notification appears showing the sender's name and the message. 169 | 170 | ![A screenshot of the Teams channel notifications page](images/teams-channel-notifications.png) 171 | 172 | ## Troubleshooting 173 | 174 | See the dedicated [troubleshooting page](./TROUBLESHOOTING.md). 175 | 176 | ## Contributing 177 | 178 | If you'd like to contribute to this sample, see [CONTRIBUTING.MD](./CONTRIBUTING.md). 179 | 180 | 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. 181 | 182 | ## Questions and comments 183 | 184 | We'd love to get your feedback about the Microsoft Graph Webhooks sample for Java Spring. You can send your questions and suggestions to us in the [Issues](https://github.com/microsoftgraph/java-spring-webhooks-sample/issues) section of this repository. 185 | 186 | Questions about Microsoft Graph in general should be posted to [Microsoft Q&A](https://docs.microsoft.com/answers/products/graph). Make sure that your questions or comments are tagged with the relevant Microsoft Graph tag. 187 | 188 | ## Additional resources 189 | 190 | - [Microsoft Graph Webhooks sample for Node.js](https://github.com/microsoftgraph/nodejs-webhooks-rest-sample) 191 | - [Microsoft Graph Webhooks sample for ASP.NET core](https://github.com/microsoftgraph/aspnetcore-webhooks-sample) 192 | - [Working with Webhooks in Microsoft Graph](https://docs.microsoft.com/graph/api/resources/webhooks) 193 | - [Subscription resource](https://docs.microsoft.com/graph/api/resources/subscription) 194 | - [Microsoft Graph documentation](https://docs.microsoft.com/graph) 195 | -------------------------------------------------------------------------------- /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://docs.microsoft.com/previous-versions/tn-archive/cc751383(v=technet.10)), 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://msrc.microsoft.com/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://www.microsoft.com/msrc/pgp-key-msrc). 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://www.microsoft.com/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://microsoft.com/msrc/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://www.microsoft.com/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | This document covers some of the common issues you may encounter when running this sample. 4 | 5 | ## You get a 403 Forbidden response when you attempt to create a subscription 6 | 7 | Make sure that your app registration includes the required permission for Microsoft Graph (as described in the [Register the app](README.md#register-the-app) section). This permission must be set before you try to create a subscription. Otherwise you'll get an error. Then, make sure a tenant administrator has granted consent to the application. 8 | 9 | ## You do not receive notifications 10 | 11 | If you're using ngrok, you can use the web interface [http://127.0.0.1:4040](http://127.0.0.1:4040) to see whether the notification is being received. If you're not using ngrok, monitor the network traffic using the tools your hosting service provides, or try using ngrok. 12 | 13 | If Microsoft Graph is not sending notifications, please open a [Microsoft Q&A](https://docs.microsoft.com/answers/products/graph) issue tagged `MicrosoftGraph`. Include the subscription ID and the time it was created. 14 | 15 | ## You get a "Subscription validation request timed out" response 16 | 17 | This indicates that Microsoft Graph did not receive a validation response within the expected time frame (about 10 seconds). 18 | 19 | - Make sure that you are not paused in the debugger when the validation request is received. 20 | - If you're using ngrok, make sure that you used your project's HTTP port for the tunnel (not HTTPS). 21 | -------------------------------------------------------------------------------- /docs/ad1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/java-spring-webhooks-sample/1482ad3241fdeb6f7185c637e6d898555e6a6ecd/docs/ad1.png -------------------------------------------------------------------------------- /docs/ad2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/java-spring-webhooks-sample/1482ad3241fdeb6f7185c637e6d898555e6a6ecd/docs/ad2.png -------------------------------------------------------------------------------- /docs/ad3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/java-spring-webhooks-sample/1482ad3241fdeb6f7185c637e6d898555e6a6ecd/docs/ad3.png -------------------------------------------------------------------------------- /docs/ad4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/java-spring-webhooks-sample/1482ad3241fdeb6f7185c637e6d898555e6a6ecd/docs/ad4.png -------------------------------------------------------------------------------- /docs/ad5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/java-spring-webhooks-sample/1482ad3241fdeb6f7185c637e6d898555e6a6ecd/docs/ad5.png -------------------------------------------------------------------------------- /graphwebhook/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /graphwebhook/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /graphwebhook/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/java-spring-webhooks-sample/1482ad3241fdeb6f7185c637e6d898555e6a6ecd/graphwebhook/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /graphwebhook/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.2/apache-maven-3.8.2-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /graphwebhook/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /graphwebhook/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /graphwebhook/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.5.0 9 | 10 | 11 | com.example 12 | graphwebhook 13 | 0.0.1-SNAPSHOT 14 | graphwebhook 15 | Demo project for Spring Boot 16 | 17 | 21 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-test 28 | test 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-thymeleaf 39 | 40 | 41 | 42 | nz.net.ultraq.thymeleaf 43 | thymeleaf-layout-dialect 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-oauth2-client 49 | 50 | 51 | 52 | com.azure.spring 53 | spring-cloud-azure-starter-active-directory 54 | 55 | 56 | 57 | com.microsoft.graph 58 | microsoft-graph 59 | 6.40.0 60 | 61 | 62 | 63 | com.corundumstudio.socketio 64 | netty-socketio 65 | 2.0.13 66 | 67 | 68 | 69 | io.jsonwebtoken 70 | jjwt-api 71 | 0.12.6 72 | 73 | 74 | 75 | io.jsonwebtoken 76 | jjwt-impl 77 | 0.12.6 78 | runtime 79 | 80 | 81 | 82 | io.jsonwebtoken 83 | jjwt-jackson 84 | 0.12.6 85 | runtime 86 | 87 | 88 | 89 | com.auth0 90 | jwks-rsa 91 | 0.22.1 92 | 93 | 94 | 95 | org.bouncycastle 96 | bcpkix-jdk14 97 | 1.81 98 | 99 | 100 | 101 | commons-codec 102 | commons-codec 103 | 1.18.0 104 | 105 | 106 | 107 | com.google.code.gson 108 | gson 109 | 2.13.1 110 | 111 | 112 | 113 | 114 | 115 | 116 | org.springframework.boot 117 | spring-boot-maven-plugin 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | com.azure.spring 126 | spring-cloud-azure-dependencies 127 | 5.22.0 128 | pom 129 | import 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/CertificateStoreService.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import java.io.FileInputStream; 7 | import java.io.IOException; 8 | import java.security.KeyStore; 9 | import java.security.KeyStoreException; 10 | import java.security.NoSuchAlgorithmException; 11 | import java.security.Security; 12 | import java.security.cert.CertificateException; 13 | import java.util.Arrays; 14 | import java.util.Objects; 15 | import javax.crypto.Cipher; 16 | import javax.crypto.Mac; 17 | import javax.crypto.spec.IvParameterSpec; 18 | import javax.crypto.spec.SecretKeySpec; 19 | 20 | import org.apache.commons.codec.binary.Base64; 21 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | import org.springframework.beans.factory.annotation.Value; 25 | import org.springframework.lang.NonNull; 26 | import org.springframework.stereotype.Service; 27 | 28 | /** 29 | * Service responsible for certificate operations: - getting the certificate - validating signatures 30 | * - decrypting content 31 | */ 32 | @Service 33 | public class CertificateStoreService { 34 | 35 | @Value("${certificate.storename}") 36 | private String storeName; 37 | 38 | @Value("${certificate.storepass}") 39 | private String storePassword; 40 | 41 | @Value("${certificate.alias}") 42 | private String alias; 43 | 44 | private Logger log = LoggerFactory.getLogger(this.getClass()); 45 | 46 | 47 | public CertificateStoreService() { 48 | // Add the BouncyCastle provider for 49 | // RSA/None/OAEPWithSHA1AndMGF1Padding cipher support 50 | Security.addProvider(new BouncyCastleProvider()); 51 | } 52 | 53 | /** 54 | * @return the KeyStore specified in application.yml 55 | * @throws KeyStoreException 56 | * @throws NoSuchAlgorithmException 57 | * @throws CertificateException 58 | * @throws IOException 59 | */ 60 | private KeyStore getCertificateStore() 61 | throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { 62 | var keystore = KeyStore.getInstance("JKS"); 63 | keystore.load(new FileInputStream(storeName), storePassword.toCharArray()); 64 | return keystore; 65 | } 66 | 67 | 68 | /** 69 | * @return the certificate specified in application.yml encoded in base64 70 | */ 71 | public String getBase64EncodedCertificate() { 72 | try { 73 | var keystore = getCertificateStore(); 74 | var certificate = keystore.getCertificate(alias); 75 | return new String(Base64.encodeBase64String(certificate.getEncoded())); 76 | } catch (final Exception e) { 77 | log.error("Error getting Base64 encoded certificate", e); 78 | return null; 79 | } 80 | } 81 | 82 | 83 | /** 84 | * @return the certificate ID or alias specified in application.yml 85 | */ 86 | public String getCertificateId() { 87 | return alias; 88 | } 89 | 90 | 91 | /** 92 | * @param base64encodedSymmetricKey the base64-encoded symmetric key to be decrypted 93 | * @return the decrypted symmetric key 94 | */ 95 | public byte[] getEncryptionKey(@NonNull final String base64encodedSymmetricKey) { 96 | Objects.requireNonNull(base64encodedSymmetricKey); 97 | try { 98 | var encryptedSymmetricKey = Base64.decodeBase64(base64encodedSymmetricKey); 99 | var keystore = getCertificateStore(); 100 | var asymmetricKey = keystore.getKey(alias, storePassword.toCharArray()); 101 | var cipher = Cipher.getInstance("RSA/None/OAEPWithSHA1AndMGF1Padding"); 102 | cipher.init(Cipher.DECRYPT_MODE, asymmetricKey); 103 | return cipher.doFinal(encryptedSymmetricKey); 104 | } catch (final Exception e) { 105 | log.error("Error getting encryption key", e); 106 | return new byte[0]; 107 | } 108 | } 109 | 110 | 111 | /** 112 | * @param encryptionKey the symmetric key that was used to sign the encrypted data 113 | * @param encryptedData the signed encrypted data to validate 114 | * @param comparisonSignature the expected signature 115 | * @return true if the signature is valid, false if not 116 | */ 117 | public boolean isDataSignatureValid(@NonNull final byte[] encryptionKey, 118 | @NonNull final String encryptedData, @NonNull final String comparisonSignature) { 119 | Objects.requireNonNull(encryptionKey); 120 | Objects.requireNonNull(comparisonSignature); 121 | Objects.requireNonNull(encryptedData); 122 | try { 123 | var decodedEncryptedData = Base64.decodeBase64(encryptedData); 124 | var mac = Mac.getInstance("HMACSHA256"); 125 | var secretKey = new SecretKeySpec(encryptionKey, "HMACSHA256"); 126 | mac.init(secretKey); 127 | var hashedData = mac.doFinal(decodedEncryptedData); 128 | var encodedHashedData = new String(Base64.encodeBase64String(hashedData)); 129 | return comparisonSignature.equals(encodedHashedData); 130 | } catch (final Exception e) { 131 | log.error("Error validating signature", e); 132 | return false; 133 | } 134 | } 135 | 136 | 137 | /** 138 | * @param encryptionKey the encryption key to use to decrypt the data 139 | * @param encryptedData the encrypted data 140 | * @return the decrypted data 141 | */ 142 | public String getDecryptedData(@NonNull final byte[] encryptionKey, 143 | @NonNull final String encryptedData) { 144 | Objects.requireNonNull(encryptedData); 145 | Objects.requireNonNull(encryptionKey); 146 | try { 147 | var secretKey = new SecretKeySpec(encryptionKey, "AES"); 148 | var ivBytes = Arrays.copyOf(encryptionKey, 16); 149 | @SuppressWarnings("java:S3329") 150 | // Sonar warns that a random IV should be used for encryption 151 | // but we are decrypting here. 152 | var ivSpec = new IvParameterSpec(ivBytes); 153 | 154 | var cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); 155 | cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); 156 | return new String(cipher.doFinal(Base64.decodeBase64(encryptedData))); 157 | } catch (final Exception e) { 158 | log.error("Error decrypting data", e); 159 | return null; 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/DiscoverUrlAdapter.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import java.net.MalformedURLException; 7 | import java.net.URI; 8 | import java.net.URISyntaxException; 9 | import java.security.Key; 10 | import java.util.Objects; 11 | import com.auth0.jwk.JwkProvider; 12 | import com.auth0.jwk.UrlJwkProvider; 13 | 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.lang.NonNull; 17 | import io.jsonwebtoken.JweHeader; 18 | import io.jsonwebtoken.JwsHeader; 19 | import io.jsonwebtoken.LocatorAdapter; 20 | /** 21 | * Custom implementation of SigningKeyResolverAdapter that retrieves the signing key from the 22 | * Microsoft identity platform's JWKS endpoint 23 | */ 24 | public class DiscoverUrlAdapter extends LocatorAdapter { 25 | 26 | private final JwkProvider keyStore; 27 | private final Logger log = LoggerFactory.getLogger(this.getClass()); 28 | 29 | public DiscoverUrlAdapter(@NonNull final String keyDiscoveryUrl) 30 | throws URISyntaxException, MalformedURLException { 31 | this.keyStore = 32 | new UrlJwkProvider(new URI(Objects.requireNonNull(keyDiscoveryUrl)).toURL()); 33 | } 34 | 35 | @Override 36 | protected Key locate(JwsHeader header) { 37 | Objects.requireNonNull(header); 38 | try { 39 | var keyId = header.getKeyId(); 40 | var publicKey = keyStore.get(keyId); 41 | return publicKey.getPublicKey(); 42 | } catch (final Exception e) { 43 | log.error(e.getMessage()); 44 | return null; 45 | } 46 | } 47 | 48 | @Override 49 | protected Key locate(JweHeader header) { 50 | return null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/GraphClientHelper.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import com.microsoft.graph.serviceclient.GraphServiceClient; 7 | import org.springframework.lang.NonNull; 8 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; 9 | 10 | public class GraphClientHelper { 11 | 12 | /** 13 | * private constructor to hide the implicit public one 14 | */ 15 | private GraphClientHelper() { 16 | throw new IllegalStateException("Static class"); 17 | } 18 | 19 | 20 | /** 21 | * @param oauthClient the authorized OAuth2 client to authenticate Graph requests with 22 | * @return A Graph client object that uses the provided OAuth2 client for access tokens 23 | */ 24 | public static GraphServiceClient getGraphClient( 25 | @NonNull final OAuth2AuthorizedClient oauthClient) { 26 | final var authProvider = new SpringOAuth2AuthProvider(oauthClient); 27 | 28 | return new GraphServiceClient(authProvider); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/GraphwebhookApplication.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import com.corundumstudio.socketio.Configuration; 7 | import com.corundumstudio.socketio.SocketIOServer; 8 | 9 | import org.springframework.boot.SpringApplication; 10 | import org.springframework.boot.autoconfigure.SpringBootApplication; 11 | import org.springframework.context.annotation.Bean; 12 | 13 | @SpringBootApplication 14 | public class GraphwebhookApplication { 15 | 16 | 17 | /** 18 | * @return A configured SocketIO server instance 19 | */ 20 | @Bean 21 | public SocketIOServer socketIOServer() { 22 | var config = new Configuration(); 23 | config.setHostname("localhost"); 24 | config.setPort(8081); 25 | return new SocketIOServer(config); 26 | } 27 | 28 | 29 | /** 30 | * @param args command line arguments 31 | */ 32 | public static void main(String[] args) { 33 | SpringApplication.run(GraphwebhookApplication.class, args); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/HomeController.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | 9 | @Controller 10 | public class HomeController { 11 | 12 | 13 | /** 14 | * @return the template name to render 15 | */ 16 | @GetMapping("/") 17 | public String home() { 18 | return "home"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/ListenController.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import java.io.IOException; 7 | import java.net.URI; 8 | import java.net.URISyntaxException; 9 | import java.util.List; 10 | import java.util.Objects; 11 | import com.corundumstudio.socketio.AckRequest; 12 | import com.corundumstudio.socketio.SocketIOClient; 13 | import com.corundumstudio.socketio.SocketIONamespace; 14 | import com.corundumstudio.socketio.SocketIOServer; 15 | import com.corundumstudio.socketio.listener.DataListener; 16 | import com.example.graphwebhook.notifications.ChangeNotification; 17 | import com.example.graphwebhook.notifications.ChangeNotificationCollection; 18 | import com.example.graphwebhook.notifications.ChangeNotificationEncryptedContent; 19 | import com.microsoft.graph.models.ChatMessage; 20 | import com.microsoft.graph.models.Message; 21 | import com.microsoft.kiota.HttpMethod; 22 | import com.microsoft.kiota.RequestInformation; 23 | import com.microsoft.kiota.serialization.KiotaJsonSerialization; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | import org.springframework.beans.factory.annotation.Autowired; 27 | import org.springframework.beans.factory.annotation.Value; 28 | import org.springframework.http.MediaType; 29 | import org.springframework.http.ResponseEntity; 30 | import org.springframework.lang.NonNull; 31 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; 32 | import org.springframework.web.bind.annotation.PostMapping; 33 | import org.springframework.web.bind.annotation.RequestBody; 34 | import org.springframework.web.bind.annotation.RequestParam; 35 | import org.springframework.web.bind.annotation.ResponseBody; 36 | import org.springframework.web.bind.annotation.RestController; 37 | 38 | @RestController 39 | public class ListenController { 40 | 41 | private final Logger log = LoggerFactory.getLogger(this.getClass()); 42 | 43 | @Autowired 44 | private SubscriptionStoreService subscriptionStore; 45 | 46 | @Autowired 47 | private CertificateStoreService certificateStore; 48 | 49 | @Autowired 50 | private OAuth2AuthorizedClientService authorizedClientService; 51 | 52 | private SocketIONamespace socketIONamespace; 53 | 54 | @Value("${spring.cloud.azure.active-directory.credential.client-id}") 55 | private String clientId; 56 | 57 | @Value("${spring.cloud.azure.active-directory.profile.tenant-id}") 58 | private String tenantId; 59 | 60 | @Value("${spring.cloud.azure.active-directory.keydiscoveryurl}") 61 | private String keyDiscoveryUrl; 62 | 63 | public ListenController(SocketIOServer socketIOServer) { 64 | // Set up a SocketIO server namespace to broadcast 65 | // incoming notifications to clients (browser) 66 | socketIONamespace = socketIOServer.addNamespace("/emitNotification"); 67 | socketIONamespace.addEventListener("create_room", String.class, new DataListener() { 68 | @Override 69 | public void onData(SocketIOClient client, String roomName, AckRequest ackSender) 70 | throws Exception { 71 | log.info("Client {} creating room for subscription {}", client.getSessionId(), 72 | roomName); 73 | client.joinRoom(roomName); 74 | } 75 | }); 76 | } 77 | 78 | 79 | /** 80 | *

81 | * This method handles the initial 82 | * endpoint 83 | * validation request sent by Microsoft Graph when the subscription is created. 84 | * 85 | * @param validationToken A validation token provided as a query parameter 86 | * @return a 200 OK response with the validationToken in the text/plain body 87 | */ 88 | @PostMapping(value = "/listen", headers = {"content-type=text/plain"}) 89 | @ResponseBody 90 | public ResponseEntity handleValidation( 91 | @RequestParam(value = "validationToken") final String validationToken) { 92 | return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(validationToken); 93 | } 94 | 95 | 96 | /** 97 | * This method receives and processes incoming notifications from Microsoft Graph 98 | * 99 | * @param jsonPayload the JSON body of the request 100 | * @return A 202 Accepted response 101 | */ 102 | @PostMapping("/listen") 103 | public ResponseEntity handleNotification( 104 | @RequestBody @NonNull final String jsonPayload) { 105 | try { 106 | // Deserialize the JSON body into a ChangeNotificationCollection 107 | final ChangeNotificationCollection notifications = 108 | KiotaJsonSerialization.deserialize(jsonPayload, 109 | ChangeNotificationCollection::createFromDiscriminatorValue); 110 | 111 | if (notifications == null) { 112 | return ResponseEntity.accepted().body(""); 113 | } 114 | 115 | // Check for validation tokens 116 | boolean areTokensValid = true; 117 | final List validationTokens = notifications.getValidationTokens(); 118 | if (validationTokens != null && validationTokens.isEmpty()) { 119 | areTokensValid = TokenHelper.areValidationTokensValid(new String[] {clientId}, 120 | new String[] {tenantId}, Objects.requireNonNull(validationTokens), 121 | Objects.requireNonNull(keyDiscoveryUrl)); 122 | } 123 | 124 | if (areTokensValid) { 125 | for (ChangeNotification notification : Objects.requireNonNull(notifications.getValue())) { 126 | // Look up subscription in store 127 | var subscription = subscriptionStore.getSubscription( 128 | Objects.requireNonNull(notification.getSubscriptionId())); 129 | 130 | // Only process if we know about this subscription AND 131 | // the client state in the notification matches 132 | if (subscription != null 133 | && subscription.clientState.equals(notification.getClientState())) { 134 | if (notification.getEncryptedContent() == null) { 135 | // No encrypted content, this is a new message notification 136 | // without resource data 137 | processNewMessageNotification(notification, subscription); 138 | } else { 139 | // With encrypted content, this is a new channel message 140 | // notification with encrypted resource data 141 | processNewChannelMessageNotification(notification, subscription); 142 | } 143 | } 144 | } 145 | } 146 | } catch (IOException e) { 147 | e.printStackTrace(); 148 | } 149 | 150 | return ResponseEntity.accepted().body(""); 151 | } 152 | 153 | 154 | /** 155 | * Processes a new message notification by getting the message from Microsoft Graph 156 | * 157 | * @param notification the new message notification 158 | * @param subscription the matching subscription record 159 | */ 160 | private void processNewMessageNotification(@NonNull final ChangeNotification notification, 161 | @NonNull final SubscriptionRecord subscription) { 162 | // Get the authorized OAuth2 client for the relevant user 163 | // This allows the service to access the user's mailbox with delegated auth 164 | final var oauthClient = 165 | authorizedClientService.loadAuthorizedClient("graph", subscription.userId); 166 | 167 | final var graphClient = 168 | GraphClientHelper.getGraphClient(Objects.requireNonNull(oauthClient)); 169 | 170 | // The notification contains the relative URL to the message 171 | // so use the customRequest method instead of the fluent API 172 | // Once message has been retrieved, send the information via SocketIO 173 | // to subscribed clients 174 | 175 | final RequestInformation request = new RequestInformation(); 176 | request.httpMethod = HttpMethod.GET; 177 | URI messageUri; 178 | try { 179 | messageUri = new URI( 180 | graphClient.getRequestAdapter().getBaseUrl() + "/" + notification.getResource()); 181 | } catch (URISyntaxException e) { 182 | e.printStackTrace(); 183 | return; 184 | } 185 | 186 | request.setUri(messageUri); 187 | final Message message = graphClient.getRequestAdapter().send(request, null, 188 | Message::createFromDiscriminatorValue); 189 | if (message != null) 190 | socketIONamespace.getRoomOperations(subscription.subscriptionId) 191 | .sendEvent("notificationReceived", new NewMessageNotification(message)); 192 | } 193 | 194 | 195 | /** 196 | * Processes a new channel message notification by decrypting the included resource data 197 | * 198 | * @param notification the new channel message notification 199 | * @param subscription the matching subscription record 200 | */ 201 | private void processNewChannelMessageNotification( 202 | @NonNull final ChangeNotification notification, 203 | @NonNull final SubscriptionRecord subscription) { 204 | // Decrypt the encrypted key from the notification 205 | final ChangeNotificationEncryptedContent encryptedContent = 206 | Objects.requireNonNull(notification.getEncryptedContent()); 207 | final var decryptedKey = Objects.requireNonNull(certificateStore 208 | .getEncryptionKey(encryptedContent.getDataKey())); 209 | 210 | // Validate the signature 211 | final String data = encryptedContent.getData(); 212 | if (certificateStore.isDataSignatureValid(decryptedKey, 213 | data, 214 | encryptedContent.getDataSignature())) { 215 | // Decrypt the data using the decrypted key 216 | final var decryptedData = certificateStore.getDecryptedData(decryptedKey, data); 217 | 218 | // Deserialize the decrypted JSON into a ChatMessage 219 | ChatMessage chatMessage; 220 | try { 221 | chatMessage = KiotaJsonSerialization.deserialize(decryptedData, 222 | ChatMessage::createFromDiscriminatorValue); 223 | // Send the information to subscribed clients 224 | socketIONamespace.getRoomOperations(subscription.subscriptionId) 225 | .sendEvent("notificationReceived", new NewChatMessageNotification(chatMessage)); 226 | } catch (IOException e) { 227 | e.printStackTrace(); 228 | } 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/NewChatMessageNotification.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import java.util.Objects; 7 | import com.microsoft.graph.models.ChatMessage; 8 | import org.springframework.lang.NonNull; 9 | 10 | /** 11 | * Represents the information sent via SocketIO to subscribed clients when a new Teams channel 12 | * message notification is received 13 | */ 14 | public class NewChatMessageNotification { 15 | 16 | /** 17 | * The display name of the sender 18 | */ 19 | public final String sender; 20 | 21 | /** 22 | * The content of the message 23 | */ 24 | public final String body; 25 | 26 | public NewChatMessageNotification(@NonNull ChatMessage message) { 27 | sender = Objects.requireNonNull(Objects.requireNonNull(message.getFrom()).getUser()).getDisplayName(); 28 | body = Objects.requireNonNull(message.getBody()).getContent(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/NewMessageNotification.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import java.util.Objects; 7 | import com.microsoft.graph.models.Message; 8 | import org.springframework.lang.NonNull; 9 | 10 | /** 11 | * Represents the information sent via SocketIO to subscribed 12 | * clients when a new message notification is received 13 | */ 14 | public class NewMessageNotification { 15 | 16 | /** 17 | * The subject of the message 18 | */ 19 | public final String subject; 20 | 21 | /** 22 | * The id of the message, can be used to GET the message via Graph 23 | */ 24 | public final String id; 25 | 26 | public NewMessageNotification(@NonNull Message message) { 27 | Objects.requireNonNull(message); 28 | subject = message.getSubject(); 29 | id = message.getId(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 12 | import org.springframework.security.web.SecurityFilterChain; 13 | import com.azure.spring.cloud.autoconfigure.implementation.aad.security.AadWebApplicationHttpSecurityConfigurer; 14 | 15 | @Configuration(proxyBeanMethods = false) 16 | @EnableWebSecurity 17 | @EnableMethodSecurity 18 | public class SecurityConfig { 19 | 20 | @Value("${app.protect.authenticated}") 21 | private String[] protectedRoutes; 22 | 23 | @SuppressWarnings("removal") 24 | @Bean 25 | SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 26 | http.securityContext(context -> context.requireExplicitSave(false)) 27 | .csrf(csrf -> csrf.ignoringRequestMatchers("/listen")) 28 | .authorizeHttpRequests(authorize -> authorize.requestMatchers(protectedRoutes) 29 | .authenticated().anyRequest().permitAll()) 30 | .apply(AadWebApplicationHttpSecurityConfigurer.aadWebApplication()); 31 | 32 | return http.build(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/SocketIOServerRunner.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import com.corundumstudio.socketio.SocketIOServer; 7 | 8 | import org.springframework.boot.CommandLineRunner; 9 | import org.springframework.stereotype.Component; 10 | 11 | /** 12 | * A CommandLineRunner that Spring will run on startup to 13 | * start the SocketIO server 14 | */ 15 | @Component 16 | public class SocketIOServerRunner implements CommandLineRunner { 17 | 18 | private final SocketIOServer socketIOServer; 19 | 20 | public SocketIOServerRunner(SocketIOServer server) { 21 | socketIOServer = server; 22 | } 23 | 24 | @Override 25 | public void run(String... args) throws Exception { 26 | socketIOServer.start(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/SpringOAuth2AuthProvider.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import java.net.URISyntaxException; 7 | import java.util.Map; 8 | import java.util.Objects; 9 | import com.microsoft.kiota.RequestInformation; 10 | import com.microsoft.kiota.authentication.AuthenticationProvider; 11 | import org.springframework.lang.NonNull; 12 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; 13 | 14 | /** 15 | * An implementation of IAuthenticationProvider that uses Spring's OAuth2AuthorizedClient to get 16 | * access tokens 17 | */ 18 | public class SpringOAuth2AuthProvider implements AuthenticationProvider { 19 | 20 | private OAuth2AuthorizedClient oauthClient; 21 | 22 | public SpringOAuth2AuthProvider(@NonNull OAuth2AuthorizedClient oauthClient) { 23 | this.oauthClient = Objects.requireNonNull(oauthClient); 24 | } 25 | 26 | @Override 27 | public void authenticateRequest(RequestInformation request, 28 | Map additionalAuthenticationContext) { 29 | 30 | try { 31 | if (request.getUri().getHost().equalsIgnoreCase("graph.microsoft.com")) { 32 | final String accessToken = oauthClient.getAccessToken().getTokenValue(); 33 | request.headers.add("Authorization", "Bearer " + accessToken); 34 | } 35 | } catch (IllegalStateException | URISyntaxException e) { 36 | e.printStackTrace(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/SubscriptionRecord.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import jakarta.annotation.Nonnull; 7 | import org.springframework.lang.NonNull; 8 | 9 | /** 10 | * Represents a subscription created by the application 11 | */ 12 | public class SubscriptionRecord { 13 | 14 | /** 15 | * The subscription ID returned by Microsoft Graph when the subscription is created 16 | */ 17 | public final @Nonnull String subscriptionId; 18 | 19 | /** 20 | * For delegated auth, this is the user's ID used to create the subscription. For app-only auth, 21 | * this is "APP-ONLY" 22 | */ 23 | public final @Nonnull String userId; 24 | 25 | /** 26 | * The client state value used to create the subscription. All notifications generated by this 27 | * subscription MUST send this value 28 | */ 29 | public final @Nonnull String clientState; 30 | 31 | public SubscriptionRecord(@Nonnull final String subscriptionId, @Nonnull final String userId, 32 | @NonNull final String clientState) { 33 | this.subscriptionId = subscriptionId; 34 | this.userId = userId; 35 | this.clientState = clientState; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/SubscriptionStoreService.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Objects; 11 | import com.microsoft.graph.models.Subscription; 12 | import jakarta.annotation.Nonnull; 13 | import org.springframework.lang.NonNull; 14 | import org.springframework.stereotype.Service; 15 | 16 | /** 17 | * Service responsible for recording all subscriptions created by the application This 18 | * implementation is in-memory, so all records are lost if the app is restarted 19 | */ 20 | @Service 21 | public class SubscriptionStoreService { 22 | 23 | private static Map subscriptions = new HashMap<>(); 24 | 25 | 26 | /** 27 | * Adds a subscription to the store 28 | * 29 | * @param subscription the subscription to add 30 | * @param userId the user's ID 31 | */ 32 | public void addSubscription(@NonNull @Nonnull final Subscription subscription, 33 | @NonNull final String userId) { 34 | var newRecord = new SubscriptionRecord(subscription.getId(), Objects.requireNonNull(userId), 35 | Objects.requireNonNull(subscription.getClientState())); 36 | subscriptions.put(subscription.getId(), newRecord); 37 | } 38 | 39 | 40 | /** 41 | * Get a subscription by ID 42 | * 43 | * @param id the ID of the subscription 44 | * @return the subscription with the matching ID 45 | */ 46 | public SubscriptionRecord getSubscription(@NonNull final String id) { 47 | return subscriptions.get(Objects.requireNonNull(id)); 48 | } 49 | 50 | 51 | /** 52 | * Delete a subscription 53 | * 54 | * @param id the ID of the subscription 55 | */ 56 | public void deleteSubscription(@NonNull final String id) { 57 | subscriptions.remove(Objects.requireNonNull(id)); 58 | } 59 | 60 | 61 | /** 62 | * Get all subscriptions for a given user ID 63 | * 64 | * @param userId The user ID to match 65 | * @return A list of subscriptions with the specified user ID 66 | */ 67 | public List getSubscriptionsForUser(@NonNull final String userId) { 68 | final List userSubscriptions = new ArrayList<>(); 69 | 70 | subscriptions.forEach((id, subscription) -> { 71 | if (subscription.userId.equals(userId)) { 72 | userSubscriptions.add(subscription); 73 | } 74 | }); 75 | 76 | return userSubscriptions; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/TokenHelper.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import java.security.Key; 7 | import java.util.List; 8 | import java.util.Objects; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.lang.NonNull; 12 | import io.jsonwebtoken.Jwts; 13 | import io.jsonwebtoken.Locator; 14 | 15 | /** 16 | * Helper class for validating the JSON web token included in Microsoft Graph change notifications 17 | * with encrypted content 18 | */ 19 | public class TokenHelper { 20 | 21 | private static Locator keyLocator; 22 | private static final Logger log = LoggerFactory.getLogger(TokenHelper.class); 23 | 24 | private TokenHelper() { 25 | throw new IllegalStateException("Static class"); 26 | } 27 | 28 | 29 | /** 30 | * Validate a JSON web token. 31 | * 32 | * @param validAudiences list of valid audiences - in this case, the app's client ID 33 | * @param validTenantIds list of valid tenant IDs 34 | * @param serializedToken the raw token 35 | * @param keyDiscoveryUrl the JWKS endpoint to use to retrieve signing keys 36 | * @return true if the token is valid, false if not 37 | */ 38 | public static boolean isValidationTokenValid(@NonNull final String[] validAudiences, 39 | @NonNull final String[] validTenantIds, @NonNull final String serializedToken, 40 | @NonNull final String keyDiscoveryUrl) { 41 | try { 42 | if (keyLocator == null) { 43 | keyLocator = new DiscoverUrlAdapter(keyDiscoveryUrl); 44 | } 45 | 46 | // Parse the serialized token 47 | // As part of this process, the signature is validated 48 | // This throws if the signature is invalid 49 | var token = Jwts.parser().keyLocator(keyLocator).build() 50 | .parseSignedClaims(Objects.requireNonNull(serializedToken)); 51 | 52 | var body = token.getPayload(); 53 | var audience = body.getAudience(); 54 | var issuer = body.getIssuer(); 55 | 56 | // The audience should match the app's client ID 57 | boolean isAudienceValid = false; 58 | for (final String validAudience : validAudiences) { 59 | isAudienceValid = isAudienceValid || audience.contains(validAudience); 60 | } 61 | 62 | // Microsoft identity tokens will have an issuer like 63 | // that contains the tenant ID 64 | boolean isIssuerValid = false; 65 | for (final String validTenantId : validTenantIds) { 66 | isIssuerValid = isIssuerValid || issuer.endsWith(validTenantId + "/"); 67 | } 68 | 69 | return isAudienceValid && isIssuerValid; 70 | } catch (final Exception e) { 71 | log.error(e.getMessage()); 72 | return false; 73 | } 74 | } 75 | 76 | 77 | /** 78 | * Validates a list of JSON web tokens 79 | * 80 | * @param validAudiences list of valid audiences - in this case, the app's client ID 81 | * @param validTenantIds list of valid tenant IDs 82 | * @param serializedToken the raw token 83 | * @param keyDiscoveryUrl the JWKS endpoint to use to retrieve signing keys 84 | * @return true if all tokens are valid, false if one or more are invalid 85 | */ 86 | public static boolean areValidationTokensValid(@NonNull final String[] validAudiences, 87 | @NonNull final String[] validTenantIds, @NonNull final List serializedTokens, 88 | @NonNull final String keyDiscoveryUrl) { 89 | for (final String serializedToken : serializedTokens) { 90 | if (!isValidationTokenValid(validAudiences, validTenantIds, 91 | Objects.requireNonNull(serializedToken), keyDiscoveryUrl)) { 92 | return false; 93 | } 94 | } 95 | 96 | return true; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/WatchController.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | package com.example.graphwebhook; 5 | 6 | import java.time.OffsetDateTime; 7 | import java.util.UUID; 8 | import java.util.Objects; 9 | 10 | import com.google.gson.GsonBuilder; 11 | import com.google.gson.JsonParser; 12 | import com.microsoft.graph.models.Entity; 13 | import com.microsoft.graph.models.Subscription; 14 | import com.microsoft.kiota.serialization.KiotaJsonSerialization; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.beans.factory.annotation.Value; 19 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; 20 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; 21 | import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; 22 | import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 23 | import org.springframework.stereotype.Controller; 24 | import org.springframework.ui.Model; 25 | import org.springframework.web.bind.annotation.GetMapping; 26 | import org.springframework.web.bind.annotation.RequestParam; 27 | import org.springframework.web.servlet.mvc.support.RedirectAttributes; 28 | 29 | @Controller 30 | public class WatchController { 31 | 32 | private static final String CREATE_SUBSCRIPTION_ERROR = "Error creating subscription"; 33 | private static final String REDIRECT_HOME = "redirect:/"; 34 | private static final String REDIRECT_LOGOUT = "redirect:/logout"; 35 | private static final String APP_ONLY = "APP-ONLY"; 36 | 37 | private final Logger log = LoggerFactory.getLogger(this.getClass()); 38 | 39 | @Autowired 40 | private SubscriptionStoreService subscriptionStore; 41 | 42 | @Autowired 43 | private CertificateStoreService certificateStore; 44 | 45 | @Autowired 46 | private OAuth2AuthorizedClientService authorizedClientService; 47 | 48 | @Value("${notifications.host}") 49 | private String notificationHost; 50 | 51 | /** 52 | * The delegated auth page of the app. This will subscribe for the authenticated user's inbox on 53 | * Exchange Online 54 | * 55 | * @param model the model provided by Spring 56 | * @param authentication authentication information for the request 57 | * @param redirectAttributes redirect attributes provided by Spring 58 | * @param oauthClient a delegated auth OAuth2 client for the authenticated user 59 | * @return the name of the template used to render the response 60 | */ 61 | @GetMapping("/delegated") 62 | public String delegated(Model model, 63 | OAuth2AuthenticationToken authentication, RedirectAttributes redirectAttributes, 64 | @RegisteredOAuth2AuthorizedClient("graph") OAuth2AuthorizedClient oauthClient) { 65 | 66 | try { 67 | final var graphClient = 68 | GraphClientHelper.getGraphClient(Objects.requireNonNull(oauthClient)); 69 | 70 | // Get the authenticated user's info 71 | final var user = graphClient.me().get(config -> { 72 | config.queryParameters.select = new String[] { "displayName", "mail", "userPrincipalName" }; 73 | }); 74 | 75 | // Create the subscription 76 | final var subscriptionRequest = new Subscription(); 77 | subscriptionRequest.setChangeType("created"); 78 | subscriptionRequest.setNotificationUrl(notificationHost + "/listen"); 79 | subscriptionRequest.setResource("me/mailfolders/inbox/messages"); 80 | subscriptionRequest.setClientState(UUID.randomUUID().toString()); 81 | subscriptionRequest.setIncludeResourceData(false); 82 | subscriptionRequest.setExpirationDateTime(OffsetDateTime.now().plusHours(1)); 83 | 84 | final Subscription subscription = graphClient.subscriptions().post(subscriptionRequest); 85 | 86 | log.info("Created subscription {} for user {}", subscription.getId(), user.getDisplayName()); 87 | 88 | // Save the authorized client so we can use it later from the notification 89 | // controller 90 | authorizedClientService.saveAuthorizedClient(oauthClient, authentication); 91 | 92 | // Add information to the model 93 | model.addAttribute("user", user); 94 | model.addAttribute("subscriptionId", subscription.getId()); 95 | 96 | final var subscriptionJson = this.getJsonRepresentation(subscription); 97 | model.addAttribute("subscription", subscriptionJson); 98 | 99 | // Add record in subscription store 100 | subscriptionStore.addSubscription(subscription, 101 | Objects.requireNonNull(authentication.getName())); 102 | 103 | model.addAttribute("success", "Subscription created."); 104 | 105 | return "delegated"; 106 | } catch (Exception e) { 107 | log.error(CREATE_SUBSCRIPTION_ERROR, e); 108 | redirectAttributes.addFlashAttribute("error", CREATE_SUBSCRIPTION_ERROR); 109 | redirectAttributes.addFlashAttribute("debug", e.getMessage()); 110 | return REDIRECT_HOME; 111 | } 112 | } 113 | 114 | 115 | /** 116 | * The app-only auth page of the app. This will subscribe for notifications on all new Teams 117 | * channel messages 118 | * 119 | * @param model the model provided by Spring 120 | * @param redirectAttributes redirect attributes provided by Spring 121 | * @param oauthClient an app-only auth OAuth2 client 122 | * @return the name of the template used to render the response 123 | */ 124 | @GetMapping("/apponly") 125 | public String apponly(Model model, RedirectAttributes redirectAttributes, 126 | @RegisteredOAuth2AuthorizedClient("apponly") OAuth2AuthorizedClient oauthClient) { 127 | 128 | try { 129 | final var graphClient = 130 | GraphClientHelper.getGraphClient(Objects.requireNonNull(oauthClient)); 131 | 132 | // Apps are only allowed one subscription to the /teams/getAllMessages resource 133 | // If we already had one, delete it so we can create a new one 134 | final var existingSubscriptions = subscriptionStore.getSubscriptionsForUser(APP_ONLY); 135 | for (final var sub : existingSubscriptions) { 136 | 137 | graphClient.subscriptions().bySubscriptionId(sub.subscriptionId).delete(); 138 | } 139 | 140 | // Create the subscription 141 | final var subscriptionRequest = new Subscription(); 142 | subscriptionRequest.setChangeType("created"); 143 | subscriptionRequest.setNotificationUrl(notificationHost + "/listen"); 144 | subscriptionRequest.setResource("/teams/getAllMessages"); 145 | subscriptionRequest.setClientState(UUID.randomUUID().toString()); 146 | subscriptionRequest.setIncludeResourceData(true); 147 | subscriptionRequest.setExpirationDateTime(OffsetDateTime.now().plusHours(1)); 148 | subscriptionRequest.setEncryptionCertificate(certificateStore.getBase64EncodedCertificate()); 149 | subscriptionRequest.setEncryptionCertificateId(certificateStore.getCertificateId()); 150 | 151 | final var subscription = graphClient.subscriptions().post(subscriptionRequest); 152 | 153 | log.info("Created subscription {} for all Teams messages", subscription.getId()); 154 | 155 | // Add information to the model 156 | model.addAttribute("subscriptionId", subscription.getId()); 157 | 158 | var subscriptionJson = this.getJsonRepresentation(subscription); 159 | model.addAttribute("subscription", subscriptionJson); 160 | 161 | // Add record in subscription store 162 | subscriptionStore.addSubscription(subscription, APP_ONLY); 163 | 164 | model.addAttribute("success", "Subscription created."); 165 | return "apponly"; 166 | } catch (Exception e) { 167 | log.error(CREATE_SUBSCRIPTION_ERROR, e); 168 | redirectAttributes.addFlashAttribute("error", CREATE_SUBSCRIPTION_ERROR); 169 | redirectAttributes.addFlashAttribute("debug", e.getMessage()); 170 | return REDIRECT_HOME; 171 | } 172 | } 173 | 174 | 175 | /** 176 | * Deletes a subscription and logs the user out 177 | * 178 | * @param subscriptionId the subscription ID to delete 179 | * @param oauthClient a delegated auth OAuth2 client for the authenticated user 180 | * @return a redirect to the logout page 181 | */ 182 | @GetMapping("/unsubscribe") 183 | public String unsubscribe( 184 | @RequestParam(value = "subscriptionId") final @jakarta.annotation.Nonnull String subscriptionId, 185 | @RegisteredOAuth2AuthorizedClient("graph") OAuth2AuthorizedClient oauthClient) { 186 | 187 | final var graphClient = 188 | GraphClientHelper.getGraphClient(Objects.requireNonNull(oauthClient)); 189 | 190 | graphClient.subscriptions().bySubscriptionId(subscriptionId).delete(); 191 | subscriptionStore.deleteSubscription(Objects.requireNonNull(subscriptionId)); 192 | return REDIRECT_LOGOUT; 193 | } 194 | 195 | 196 | /** 197 | * Deletes an app-only subscription 198 | * 199 | * @param subscriptionId the subscription ID to delete 200 | * @param oauthClient an app-only auth OAuth2 client 201 | * @return a redirect to the home page 202 | */ 203 | @GetMapping("/unsubscribeapponly") 204 | public String unsubscribeapponly( 205 | @RequestParam(value = "subscriptionId") final @jakarta.annotation.Nonnull String subscriptionId, 206 | @RegisteredOAuth2AuthorizedClient("apponly") OAuth2AuthorizedClient oauthClient) { 207 | 208 | final var graphClient = 209 | GraphClientHelper.getGraphClient(Objects.requireNonNull(oauthClient)); 210 | 211 | graphClient.subscriptions().bySubscriptionId(subscriptionId).delete(); 212 | // Remove subscription from store 213 | subscriptionStore.deleteSubscription(Objects.requireNonNull(subscriptionId)); 214 | 215 | // Logout user 216 | return REDIRECT_HOME; 217 | } 218 | 219 | 220 | private String getJsonRepresentation(final T graphObject) { 221 | try { 222 | graphObject.getBackingStore().setIsInitializationCompleted(false); 223 | final var jsonRepresentation = KiotaJsonSerialization.serializeAsString(graphObject); 224 | // Use Gson to pretty-print 225 | final var gson = new GsonBuilder().setPrettyPrinting().create(); 226 | final var jsonElement = JsonParser.parseString(jsonRepresentation); 227 | return gson.toJson(jsonElement); 228 | } catch (Exception e) { 229 | e.printStackTrace(); 230 | return ""; 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/notifications/ChangeNotification.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package com.example.graphwebhook.notifications; 5 | 6 | import java.time.OffsetDateTime; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.Objects; 10 | import com.microsoft.graph.models.Entity; 11 | import com.microsoft.kiota.serialization.ParseNode; 12 | 13 | public class ChangeNotification extends Entity { 14 | public ChangeNotification() { 15 | super(); 16 | } 17 | 18 | @jakarta.annotation.Nonnull 19 | public static ChangeNotification createFromDiscriminatorValue( 20 | @jakarta.annotation.Nonnull final ParseNode parseNode) { 21 | Objects.requireNonNull(parseNode); 22 | return new ChangeNotification(); 23 | } 24 | 25 | @jakarta.annotation.Nonnull 26 | public Map> getFieldDeserializers() { 27 | final HashMap> deserializerMap = 28 | new HashMap>( 29 | super.getFieldDeserializers()); 30 | 31 | deserializerMap.put("changeType", (n) -> { 32 | this.setChangeType(n.getStringValue()); 33 | }); 34 | deserializerMap.put("clientState", (n) -> { 35 | this.setClientState(n.getStringValue()); 36 | }); 37 | deserializerMap.put("encryptedContent", (n) -> { 38 | this.setEncryptedContent(n.getObjectValue(ChangeNotificationEncryptedContent::createFromDiscriminatorValue)); 39 | }); 40 | deserializerMap.put("lifecycleEvent", (n) -> { 41 | this.setLifecycleEvent(n.getStringValue()); 42 | }); 43 | deserializerMap.put("resource", (n) -> { 44 | this.setResource(n.getStringValue()); 45 | }); 46 | deserializerMap.put("resourceData", (n) -> { 47 | this.setResourceData(n.getObjectValue(ResourceData::createFromDiscriminatorValue)); 48 | }); 49 | deserializerMap.put("subscriptionExpirationDateTime", (n) -> { 50 | this.setSubscriptionExpirationDateTime(n.getOffsetDateTimeValue()); 51 | }); 52 | deserializerMap.put("subscriptionId", (n) -> { 53 | this.setSubscriptionId(n.getStringValue()); 54 | }); 55 | deserializerMap.put("tenantId", (n) -> { 56 | this.setTenantId(n.getStringValue()); 57 | }); 58 | 59 | return deserializerMap; 60 | } 61 | 62 | public String getChangeType() { 63 | return this.backingStore.get("changeType"); 64 | } 65 | 66 | public String getClientState() { 67 | return this.backingStore.get("clientState"); 68 | } 69 | 70 | public ChangeNotificationEncryptedContent getEncryptedContent() { 71 | return this.backingStore.get("encryptedContent"); 72 | } 73 | 74 | public String getLifecycleEvent() { 75 | return this.backingStore.get("lifecycleEvent"); 76 | } 77 | 78 | public String getResource() { 79 | return this.backingStore.get("resource"); 80 | } 81 | 82 | public ResourceData getResourceData() { 83 | return this.backingStore.get("resourceData"); 84 | } 85 | 86 | public OffsetDateTime getSubscriptionExpirationDateTime() { 87 | return this.backingStore.get("subscriptionExpirationDateTime"); 88 | } 89 | 90 | public String getSubscriptionId() { 91 | return this.backingStore.get("subscriptionId"); 92 | } 93 | 94 | public String getTenantId() { 95 | return this.backingStore.get("tenantId"); 96 | } 97 | 98 | public void setChangeType(final String value) { 99 | this.backingStore.set("changeType", value); 100 | } 101 | 102 | public void setClientState(final String value) { 103 | this.backingStore.set("clientState", value); 104 | } 105 | 106 | public void setEncryptedContent(final ChangeNotificationEncryptedContent value) { 107 | this.backingStore.set("encryptedContent", value); 108 | } 109 | 110 | public void setLifecycleEvent(final String value) { 111 | this.backingStore.set("lifecycleEvent", value); 112 | } 113 | 114 | public void setResource(final String value) { 115 | this.backingStore.set("resource", value); 116 | } 117 | 118 | public void setResourceData(final ResourceData value) { 119 | this.backingStore.set("resourceData", value); 120 | } 121 | 122 | public void setSubscriptionExpirationDateTime(final OffsetDateTime value) { 123 | this.backingStore.set("subscriptionExpirationDateTime", value); 124 | } 125 | 126 | public void setSubscriptionId(final String value) { 127 | this.backingStore.set("subscriptionId", value); 128 | } 129 | 130 | public void setTenantId(final String value) { 131 | this.backingStore.set("tenantId", value); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/notifications/ChangeNotificationCollection.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package com.example.graphwebhook.notifications; 5 | 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Objects; 10 | import com.microsoft.kiota.serialization.AdditionalDataHolder; 11 | import com.microsoft.kiota.serialization.Parsable; 12 | import com.microsoft.kiota.serialization.ParseNode; 13 | import com.microsoft.kiota.serialization.SerializationWriter; 14 | import com.microsoft.kiota.store.BackedModel; 15 | import com.microsoft.kiota.store.BackingStore; 16 | import com.microsoft.kiota.store.BackingStoreFactorySingleton; 17 | 18 | public class ChangeNotificationCollection implements AdditionalDataHolder, BackedModel, Parsable { 19 | @jakarta.annotation.Nonnull 20 | protected BackingStore backingStore; 21 | 22 | public ChangeNotificationCollection() { 23 | this.backingStore = BackingStoreFactorySingleton.instance.createBackingStore(); 24 | this.setAdditionalData(new HashMap<>()); 25 | } 26 | 27 | @jakarta.annotation.Nonnull 28 | public static ChangeNotificationCollection createFromDiscriminatorValue( 29 | @jakarta.annotation.Nonnull final ParseNode parseNode) { 30 | Objects.requireNonNull(parseNode); 31 | return new ChangeNotificationCollection(); 32 | } 33 | 34 | @jakarta.annotation.Nonnull 35 | public Map> getFieldDeserializers() { 36 | final HashMap> deserializerMap = 37 | new HashMap>(); 38 | 39 | deserializerMap.put("validationTokens", (n) -> { 40 | this.setValidationTokens(n.getCollectionOfPrimitiveValues(String.class)); 41 | }); 42 | deserializerMap.put("value", (n) -> { 43 | this.setValue(n.getCollectionOfObjectValues(ChangeNotification::createFromDiscriminatorValue)); 44 | }); 45 | 46 | return deserializerMap; 47 | } 48 | 49 | 50 | public Map getAdditionalData() { 51 | return this.backingStore.get("additionalData"); 52 | } 53 | 54 | public BackingStore getBackingStore() { 55 | return this.backingStore; 56 | } 57 | 58 | public List getValidationTokens() { 59 | return this.backingStore.get("validationTokens"); 60 | } 61 | 62 | public List getValue() { 63 | return this.backingStore.get("value"); 64 | } 65 | 66 | public void setAdditionalData(@jakarta.annotation.Nullable final Map value) { 67 | this.backingStore.set("additionalData", value); 68 | } 69 | 70 | public void setValidationTokens(final List value) { 71 | this.backingStore.set("validationTokens", value); 72 | } 73 | 74 | public void setValue(final List value) { 75 | this.backingStore.set("value", value); 76 | } 77 | 78 | @Override 79 | public void serialize(SerializationWriter writer) { 80 | throw new UnsupportedOperationException("Unimplemented method 'serialize'"); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/notifications/ChangeNotificationEncryptedContent.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package com.example.graphwebhook.notifications; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.Objects; 9 | import com.microsoft.kiota.serialization.AdditionalDataHolder; 10 | import com.microsoft.kiota.serialization.Parsable; 11 | import com.microsoft.kiota.serialization.ParseNode; 12 | import com.microsoft.kiota.serialization.SerializationWriter; 13 | import com.microsoft.kiota.store.BackedModel; 14 | import com.microsoft.kiota.store.BackingStore; 15 | import com.microsoft.kiota.store.BackingStoreFactorySingleton; 16 | 17 | public class ChangeNotificationEncryptedContent implements AdditionalDataHolder, BackedModel, Parsable { 18 | @jakarta.annotation.Nonnull 19 | protected BackingStore backingStore; 20 | 21 | public ChangeNotificationEncryptedContent() { 22 | this.backingStore = BackingStoreFactorySingleton.instance.createBackingStore(); 23 | this.setAdditionalData(new HashMap<>()); 24 | } 25 | 26 | @jakarta.annotation.Nonnull 27 | public static ChangeNotificationEncryptedContent createFromDiscriminatorValue( 28 | @jakarta.annotation.Nonnull final ParseNode parseNode) { 29 | Objects.requireNonNull(parseNode); 30 | return new ChangeNotificationEncryptedContent(); 31 | } 32 | 33 | @jakarta.annotation.Nonnull 34 | public Map> getFieldDeserializers() { 35 | final HashMap> deserializerMap = 36 | new HashMap>(); 37 | 38 | deserializerMap.put("data", (n) -> { 39 | this.setData(n.getStringValue()); 40 | }); 41 | deserializerMap.put("dataKey", (n) -> { 42 | this.setDataKey(n.getStringValue()); 43 | }); 44 | deserializerMap.put("dataSignature", (n) -> { 45 | this.setDataSignature(n.getStringValue()); 46 | }); 47 | deserializerMap.put("encryptionCertificateId", (n) -> { 48 | this.setEncryptionCertificateId(n.getStringValue()); 49 | }); 50 | deserializerMap.put("encryptionCertificateThumbprint", (n) -> { 51 | this.setEncryptionCertificateThumbprint(n.getStringValue()); 52 | }); 53 | 54 | return deserializerMap; 55 | } 56 | 57 | public Map getAdditionalData() { 58 | return this.backingStore.get("additionalData"); 59 | } 60 | 61 | public BackingStore getBackingStore() { 62 | return this.backingStore; 63 | } 64 | 65 | public String getData() { 66 | return this.backingStore.get("data"); 67 | } 68 | 69 | public String getDataKey() { 70 | return this.backingStore.get("dataKey"); 71 | } 72 | 73 | public String getDataSignature() { 74 | return this.backingStore.get("dataSignature"); 75 | } 76 | 77 | public String getEncryptionCertificateId() { 78 | return this.backingStore.get("encryptionCertificateId"); 79 | } 80 | 81 | public String getEncryptionCertificateThumbprint() { 82 | return this.backingStore.get("encryptionCertificateThumbprint"); 83 | } 84 | 85 | public void setAdditionalData(@jakarta.annotation.Nullable final Map value) { 86 | this.backingStore.set("additionalData", value); 87 | } 88 | 89 | public void setData(final String value) { 90 | this.backingStore.set("data", value); 91 | } 92 | 93 | public void setDataKey(final String value) { 94 | this.backingStore.set("dataKey", value); 95 | } 96 | 97 | public void setDataSignature(final String value) { 98 | this.backingStore.set("dataSignature", value); 99 | } 100 | 101 | public void setEncryptionCertificateId(final String value) { 102 | this.backingStore.set("encryptionCertificateId", value); 103 | } 104 | 105 | public void setEncryptionCertificateThumbprint(final String value) { 106 | this.backingStore.set("encryptionCertificateThumbprint", value); 107 | } 108 | 109 | @Override 110 | public void serialize(SerializationWriter writer) { 111 | throw new UnsupportedOperationException("Unimplemented method 'serialize'"); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /graphwebhook/src/main/java/com/example/graphwebhook/notifications/ResourceData.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package com.example.graphwebhook.notifications; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.Objects; 9 | import com.microsoft.kiota.serialization.AdditionalDataHolder; 10 | import com.microsoft.kiota.serialization.Parsable; 11 | import com.microsoft.kiota.serialization.ParseNode; 12 | import com.microsoft.kiota.serialization.SerializationWriter; 13 | import com.microsoft.kiota.store.BackedModel; 14 | import com.microsoft.kiota.store.BackingStore; 15 | import com.microsoft.kiota.store.BackingStoreFactorySingleton; 16 | 17 | public class ResourceData implements AdditionalDataHolder, BackedModel, Parsable { 18 | @jakarta.annotation.Nonnull 19 | protected BackingStore backingStore; 20 | 21 | public ResourceData() { 22 | this.backingStore = BackingStoreFactorySingleton.instance.createBackingStore(); 23 | this.setAdditionalData(new HashMap<>()); 24 | } 25 | 26 | @jakarta.annotation.Nonnull 27 | public static ResourceData createFromDiscriminatorValue( 28 | @jakarta.annotation.Nonnull final ParseNode parseNode) { 29 | Objects.requireNonNull(parseNode); 30 | return new ResourceData(); 31 | } 32 | 33 | @jakarta.annotation.Nonnull 34 | public Map> getFieldDeserializers() { 35 | final HashMap> deserializerMap = 36 | new HashMap>(); 37 | 38 | return deserializerMap; 39 | } 40 | 41 | public Map getAdditionalData() { 42 | return this.backingStore.get("additionalData"); 43 | } 44 | 45 | public BackingStore getBackingStore() { 46 | return this.backingStore; 47 | } 48 | 49 | public void setAdditionalData(@jakarta.annotation.Nullable final Map value) { 50 | this.backingStore.set("additionalData", value); 51 | } 52 | 53 | @Override 54 | public void serialize(SerializationWriter writer) { 55 | throw new UnsupportedOperationException("Unimplemented method 'serialize'"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /graphwebhook/src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "spring.cloud.azure.active-directory.keydiscoveryurl", 5 | "type": "java.lang.String", 6 | "description": "The URL to the JWKS download for Azure" 7 | }, 8 | { 9 | "name": "app.protect.authenticated", 10 | "type": "java.lang.String[]", 11 | "description": "Limit these pages to authenticated users. Use '=/route1' for a single route. Use '=/route1, /route2' for multiple." 12 | }, 13 | { 14 | "name": "notifications.host", 15 | "type": "java.lang.String", 16 | "description": "The hostname of the server that will accept notifications. Use your ngrok proxy for local development." 17 | }, 18 | { 19 | "name": "certificate.storename", 20 | "type": "java.lang.String", 21 | "description": "The filename of the certifcate store that contains the certificate and key used to sign and decrypt" 22 | }, 23 | { 24 | "name": "certificate.storepass", 25 | "type": "java.lang.String", 26 | "description": "The password that protects the certificate store" 27 | }, 28 | { 29 | "name": "certificate.alias", 30 | "type": "java.lang.String", 31 | "description": "The alias of the certificate in the certifcate store" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /graphwebhook/src/main/resources/application.example.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | cloud: 3 | azure: 4 | active-directory: 5 | enabled: true 6 | keydiscoveryurl: https://login.microsoftonline.com/common/discovery/keys 7 | profile: 8 | tenant-id: YOUR_TENANT_ID_HERE 9 | credential: 10 | client-id: YOUR_CLIENT_ID_HERE 11 | client-secret: 'YOUR_CLIENT_SECRET_HERE' 12 | post-logout-redirect-uri: http://localhost:8080 13 | authorization-clients: 14 | graph: 15 | scopes: user.read, mail.read 16 | apponly: 17 | authorization-grant-type: client_credentials 18 | scopes: https://graph.microsoft.com/.default 19 | 20 | app: 21 | protect: 22 | authenticated: /delegated, /apponly 23 | 24 | notifications: 25 | host: YOUR_NGROK_PROXY_URL_HERE 26 | 27 | certificate: 28 | storename: JKSkeystore.jks 29 | storepass: YOUR_KEYSTORE_PASSWORD_HERE 30 | alias: selfsignedjks 31 | -------------------------------------------------------------------------------- /graphwebhook/src/main/resources/public/images/g-raph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/java-spring-webhooks-sample/1482ad3241fdeb6f7185c637e6d898555e6a6ecd/graphwebhook/src/main/resources/public/images/g-raph.png -------------------------------------------------------------------------------- /graphwebhook/src/main/resources/public/styles/site.css: -------------------------------------------------------------------------------- 1 | .wrapped-pre { 2 | word-wrap: break-word; 3 | word-break: break-all; 4 | white-space: pre-wrap; 5 | } 6 | -------------------------------------------------------------------------------- /graphwebhook/src/main/resources/templates/apponly.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | Teams channel message notifications 7 | 8 | 9 | 10 |

11 |

Notifications

12 |

13 | Notifications should appear below when new messages are sent to any Teams 14 | channel in your organization. 15 |

16 |
17 | 21 | Delete subscription 23 |
24 |
25 |
Subscription here
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
SenderMessage
37 | 38 | 39 | 65 |
66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /graphwebhook/src/main/resources/templates/delegated.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | Inbox notifications 7 | 8 | 9 | 10 |
11 |

Notifications

12 |

13 | Notifications should appear below when new messages are delivered to 14 | Test User's inbox. 15 |

16 |
17 |
Test User
18 | 22 | Delete subscription 24 |
25 |
26 |
Subscription here
27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
SubjectID
38 | 39 | 40 | 66 |
67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /graphwebhook/src/main/resources/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | Home 7 | 8 | 9 | 10 |
11 |
12 |

Microsoft Graph Notifications Sample

13 |

Choose one of the options below to create a subscription

14 |
15 |
16 |
17 |
18 |

Delegated authentication

19 |

Choose this option to sign in as a user and receive notifications when items are created in the user's 20 | Exchange Online mailbox

21 | Sign in and subscribe 22 |
23 |
24 |
25 |
26 |

App-only authentication

27 |

Choose this option to have the application receive notifications when messages are sent to any Teams 28 | channel

29 | Subscribe 30 |
31 |
32 |
33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /graphwebhook/src/main/resources/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Microsoft Graph Notifications Sample 6 | 7 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | g-raph 18 | Microsoft Graph Notifications Sample 19 |
20 |
21 | 26 | 30 |
31 |

Body contents

32 |
33 |
34 |
35 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /graphwebhook/src/test/java/com/example/graphwebhook/GraphwebhookApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.graphwebhook; 2 | 3 | //import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | @SuppressWarnings("java:S2187") 8 | class GraphwebhookApplicationTests { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /images/aad-portal-app-registrations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/java-spring-webhooks-sample/1482ad3241fdeb6f7185c637e6d898555e6a6ecd/images/aad-portal-app-registrations.png -------------------------------------------------------------------------------- /images/copy-secret-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/java-spring-webhooks-sample/1482ad3241fdeb6f7185c637e6d898555e6a6ecd/images/copy-secret-value.png -------------------------------------------------------------------------------- /images/ngrok-https-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/java-spring-webhooks-sample/1482ad3241fdeb6f7185c637e6d898555e6a6ecd/images/ngrok-https-url.png -------------------------------------------------------------------------------- /images/register-an-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/java-spring-webhooks-sample/1482ad3241fdeb6f7185c637e6d898555e6a6ecd/images/register-an-app.png -------------------------------------------------------------------------------- /images/remove-configured-permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/java-spring-webhooks-sample/1482ad3241fdeb6f7185c637e6d898555e6a6ecd/images/remove-configured-permission.png -------------------------------------------------------------------------------- /images/teams-channel-notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/java-spring-webhooks-sample/1482ad3241fdeb6f7185c637e6d898555e6a6ecd/images/teams-channel-notifications.png -------------------------------------------------------------------------------- /images/user-inbox-notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftgraph/java-spring-webhooks-sample/1482ad3241fdeb6f7185c637e6d898555e6a6ecd/images/user-inbox-notifications.png --------------------------------------------------------------------------------