diff --git a/Support/Check-AppRegistrations.ps1 b/Support/Check-AppRegistrations.ps1 new file mode 100644 index 000000000..3f05391b0 --- /dev/null +++ b/Support/Check-AppRegistrations.ps1 @@ -0,0 +1,144 @@ +<# +.SYNOPSIS + Script to test if the Applications can get an access token using the stored secrets +.DESCRIPTION + To make sure that the configuration which is stored in the keyvault is correct this script will + 1. Fetch the applicationId's from the Application Registrations + 2. Fetch the application secrets from the KeyVault + 3. Try to fetch an acces token using the combination of the AppId and AppSecret + +.PARAMETER ConfigFile + Path to where the parameters.json is stored which was used to deploy the application. +.PARAMETER TenantId + TenantId (see the Azure Portal to retrieve this GUID, this is also known as the DirectoryId which can be found in the Application Registration as well) +.PARAMETER BaseResourceName + "Base" name of the resources (e.g. how all the resources are named) in the Resource Group, this is the same name as in the parameters.json file + See https://github.com/OfficeDev/microsoft-teams-apps-company-communicator/wiki/Deployment-guide step 5), this name is used in the script to find all the components in the Resource Group +.NOTES + Author: Robin Meure MSFT + ChangeLog: + 1.0.0 - Robin Meure, 2022-Feb-23 - First Release. + + Make sure that the account which is used to connect to the Azure environment has read access on the KeyVault secrets. + Azure Portal -> Company Communicator Resource Group -> KeyVault > Access policies -> Add user to read the secrets + See https://docs.microsoft.com/en-us/azure/key-vault/general/assign-access-policy?tabs=azure-portal for more information. + +#> + +[CmdletBinding(DefaultParametersetName="Variables")] +Param +( + [Parameter( ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true, + ParameterSetName="ConfigFile", + HelpMessage="Load the configfile of the deployment folder.")] + [switch]$ConfigFile, + + [Parameter( ParameterSetName="ConfigFile", + HelpMessage="The path where the parameters.json which is used for the deployment is located.")] + [string]$configFilePath, + + [Parameter( ParameterSetName="Variables", + HelpMessage="The TenantId where the application is deployed.")] + [string]$tenantId, + [Parameter( ParameterSetName="Variables", + HelpMessage="'Base' name of the resources (e.g. how all the resources are named) in the Resource Group.")] + [string]$baseName +) + +Function Get-AccessToken +{ + param( + [Parameter(Mandatory = $true, HelpMessage = "ApplicationId")] + [string] + $appId, + [Parameter(Mandatory = $true, HelpMessage = "ApplicationSecret")] + [string] + $appSecret, + [Parameter(Mandatory = $true, HelpMessage = "Resource to authenticate against (e.g. https://graph.microsoft.com)")] + [string] + $resource, + [Parameter(Mandatory = $true, HelpMessage = "Authority to receive the token from (e.g. 'https://login.microsoftonline.com/tenant/oauth2/v2.0/token'))")] + $authority + ) + + $body = [string]::Format("grant_type=client_credentials&client_id={0}&client_secret={1}&scope=https%3A%2F%2F{2}%2F.default", $appId, $appSecret, $resource) + + Write-Output ("Fetching access token using for Application: {0}." -f $appId) + $token = Invoke-RestMethod -Uri $authority -Method Post -Body $body + + if ($token.access_token -ne $null) + { + Write-Output ("Got an access token") + } + else + { + Write-Output ("Failed to retrieve an access token") + } +} + +# check if we have a config file we're using and need to parse +if ($ConfigFile) +{ + if ([string]::IsNullOrEmpty($configFilePath)) + { + Write-Output ("No config file is set, trying to fetch the configuration from the deployment folder.") + $config = Get-Content '..\deployment\parameters.json' | Out-String | ConvertFrom-Json + Write-Verbose ("Found following configuration: {0}." -f $config) + } + else + { + Write-Output ("FilePath set, trying to fetch the configuration from the specified folder.") + $config = Get-Content -Path $configFilePath | Out-String | ConvertFrom-Json + } + # Fetching the configuration of the deployment + $tenantId = $config.tenantId.Value + $baseName = $config.baseResourceName.Value +} + + + +# Connecting to the Azure Portal, please make sure you're connecting using the same account as for the deployment +$connectionSucceeded = $false +try{ + $connectionSucceeded = Connect-AzAccount -Tenant $tenantId +} +catch{ + $connectionSucceeded = $false +} + +# if we could successfully connect to Azure, then we're going to try to fetch the App Registrations and secrets from the KeyVault +if ($connectionSucceeded) +{ + # Fetching the App registrations with their Id's + $applications = Get-AzADApplication -DisplayNameStartWith $baseName | Select-Object ApplicationId, DisplayName + $graphAppId = $applications | Where-Object { $_.DisplayName -eq $baseName}| Select-Object ApplicationId -ExpandProperty ApplicationId + $userAppId = $applications | Where-Object { $_.DisplayName -eq [string]::Format("{0}-users",$baseName)} | Select-Object ApplicationId -ExpandProperty ApplicationId + $authorAppId = $applications | Where-Object { $_.DisplayName -eq [string]::Format("{0}-authors",$baseName)}| Select-Object ApplicationId -ExpandProperty ApplicationId + + # setting up keyvaultName + $keyVaultName = [string]::Format("{0}{1}", $baseName,"vault") + + # Defining the secretNames to retrieve the actual secrets from the Keyvault + $graphAppSecretName = [string]::Format("{0}{1}", $keyVaultName, "GraphAppPassword") + $userAppSecretName = [string]::Format("{0}{1}", $keyVaultName, "UserAppPassword") + $authorAppSecretName = [string]::Format("{0}{1}", $keyVaultName, "AuthorAppPassword") + + # Fetch the secrets from the KeyVault + $graphAppSecret = Get-AzKeyVaultSecret -VaultName $keyVaultName -Name $graphAppSecretName -AsPlainText + $userAppSecret = Get-AzKeyVaultSecret -VaultName $keyVaultName -Name $userAppSecretName -AsPlainText + $authorAppSecret = Get-AzKeyVaultSecret -VaultName $keyVaultName -Name $authorAppSecretName -AsPlainText + + # First check the Graph App + $graphAuthorityUrl = [string]::Format("https://login.microsoftonline.com/{0}/oauth2/v2.0/token", $tenantId) + $graphResource = "graph.microsoft.com" + + Get-AccessToken -appId $graphAppId -appSecret $graphAppSecret -authority $graphAuthorityUrl -resource $graphResource + + # Checking the User and Author Apps + $botAuthorityUrl = [string]::Format("https://login.microsoftonline.com/{0}/oauth2/v2.0/token", "botframework.com") + $botResource = "api.botframework.com" + + Get-AccessToken -appId $userAppId -appSecret $userAppSecret -authority $botAuthorityUrl -resource $botResource + Get-AccessToken -appId $authorAppId -appSecret $authorAppSecret -authority $botAuthorityUrl -resource $botResource +} \ No newline at end of file diff --git a/Support/Check-GraphApp.ps1 b/Support/Check-GraphApp.ps1 new file mode 100644 index 000000000..8fbb2a33a --- /dev/null +++ b/Support/Check-GraphApp.ps1 @@ -0,0 +1,82 @@ +<# +.SYNOPSIS + Script to test if the created Applications in the Application Registration work +.DESCRIPTION + To make sure that the AppReg itself is working, this script is meant to test the basic AppId + AppSecret combination to see if an access token is being returned. + Also, this can be used to see the App has the proper permissions granted to lookup users and/our groups. +.PARAMETER TenantId + TenantId (see the Azure Portal to retrieve this GUID, this is also known as the DirectoryId which can be found in the Application Registration as well) +.PARAMETER AppId + ApplicationId of the Graph App (main application, not the user nor the author bot App registration) +.PARAMETER AppSecret + Secret of the Application +.PARAMETER FetchDataFromGraph + When supplied, a call is done to the Graph API using the App and the access token to retrieve O365 groups and/or users +.NOTES + Author: Robin Meure MSFT + ChangeLog: + 1.0.0 - Robin Meure, 2022-Feb-23 - First Release. + + This script does not make use of any Graph libraries/SDK's but just 'simple' Invoke-RestMethod cmdlets, + this to eliminate the dependencies and/or elevated powershell sessions to install these dependencies like MSGraph +#> + +[CmdLetBinding()] +param( + [Parameter(Mandatory = $true, HelpMessage = "The TenantId where the application is deployed")] + [string] + $tenantId, + [Parameter(Mandatory = $true, HelpMessage = "The ApplicationId of the application")] + [string] + $AppId, + [Parameter(Mandatory = $true, HelpMessage = "The Application secret of the application.")] + [string] + $AppSecret, + [Parameter(Mandatory = $false, HelpMessage = "When provided, will call into the graph using the access token.")] + [switch] + $FetchDataFromGraph +) + +$graphAuthorityUrl = [string]::Format("https://login.microsoftonline.com/{0}/oauth2/v2.0/token", $tenantId) +$graphResource = "graph.microsoft.com" + +$body = [string]::Format("grant_type=client_credentials&client_id={0}&client_secret={1}&scope=https%3A%2F%2F{2}%2F.default", $AppId, $AppSecret, $graphResource) +$token = Invoke-RestMethod -Uri $graphAuthorityUrl -Method Post -Body $body + +if (!$token) +{ + Write-Warning ("No token received") +} +else { + Write-Output ("Fetched access token using for Application: {0}." -f $appId) + $graphAccessToken = $token.access_token +} + + +if ($FetchDataFromGraph) +{ + # This is fetching O365 groups when drafting a message as option 4 to send the message to. + # https://graph.microsoft.com/v1.0/groups?$filter=groupTypes/any(c:c+eq+'Unified') + + # GroupMember.Read.All check + $fetchGroupsUrl = [string]::Format("https://graph.microsoft.com/v1.0/groups?`$filter=groupTypes/any(c:c+eq+'Unified')") + $fetchGroupsResponse = Invoke-RestMethod -Uri $fetchGroupsUrl -Headers @{Authorization = "Bearer $graphAccessToken"} -ContentType "application/json" -Method Get + if ($fetchGroupsResponse -ne $null) + { + Write-Output ("Fetched o365 groups using graph API for Application: {0}." -f $appId) + $fetchGroupsResponse.value | Select-Object DisplayName, Id + Write-Output ("---------------------------------------------------------") + } + + # This is fetching users from AAD (e.g. used to send a message to all users (option 3 in the message ux)) + # https://graph.microsoft.com/v1.0/users + + # User.Read.All check + $fetchUsersUrl = [string]::Format("https://graph.microsoft.com/v1.0/users") + $fetchUsersResponse = Invoke-RestMethod -Uri $fetchUsersUrl -Headers @{Authorization = "Bearer $graphAccessToken"} -ContentType "application/json" -Method Get + if ($fetchUsersResponse -ne $null) + { + Write-Output ("Fetched AAD users using graph API for Application: {0}." -f $appId) + $fetchUsersResponse.value | Select-Object DisplayName, Id + } +} \ No newline at end of file diff --git a/Support/Start-ChatWithSpecifiedUser.ps1 b/Support/Start-ChatWithSpecifiedUser.ps1 new file mode 100644 index 000000000..8226f8941 --- /dev/null +++ b/Support/Start-ChatWithSpecifiedUser.ps1 @@ -0,0 +1,170 @@ +<# +.SYNOPSIS + Script to test the functionality of messaging users directly via a bot +.DESCRIPTION + Use this script to see if the configuration of the Bot and the Teams App is correctly. + The script makes use of the Graph to get details of conversations between users and the Teams App, + this is needed to start or continue the conversation using the Bot Framework. Without this, we cannot use + the Bot API to send messages. +.PARAMETER UserUPN + Username of the to send the message to in UPN format +.PARAMETER Message + Message to send to the specified user +.PARAMETER Install + If the App is not installed for the specified user, by passing this, the script will try to install the app for the specified user + This is needed to start the conversation, without the installation this script will fail to send a message +.NOTES + Author: Robin Meure MSFT + ChangeLog: + 1.0.0 - Robin Meure, 2022-Feb-23 - First Release. + + For more information on the API's being used please see: + https://docs.microsoft.com/en-us/microsoftteams/platform/graph-api/proactive-bots-and-messages/graph-proactive-bots-and-messages?tabs=dotnet#additional-code-samples + https://docs.microsoft.com/en-us/graph/auth-v2-service#4-get-an-access-token + +#> + +[CmdLetBinding()] +param( + # TimeSpan + [Parameter(Mandatory = $true, HelpMessage = "User who to send messages to from the bot in UPN format (e.g. user@contoso.onmicrosoft.com)")] + [string] + $userUpn, + [Parameter(Mandatory = $true, HelpMessage = "The contents of the message to send to the specified user")] + [string] + $message, + [Parameter(Mandatory = $false, HelpMessage = "When passed, it will try to install the App for the specified user when the current state is that App is not installed.")] + [switch] + $install + +) + +############################################################################################################# +# Variables need to be replaced to start using the script # +############################################################################################################# + +# Global variables +$tenantId = "tenant.onmicrosoft.com" #or in GUID format "00000000-0000-0000-0000-000000000000" +$teamsAppId = "" # AppId of the Teams App Manifest + +# App Registration details to make the Graph API calls +$graphAppId = "" +$graphAppSecret= "" + +# Bot App registration details +$userAppId = "" +$userAppSecret = "" + +############################################################################################################# +# Authentication section # +############################################################################################################# + +# Bot framework variables +$serviceUrl = "https://smba.trafficmanager.net/emea" +$botAuthorityUrl = "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token" +$botResource = "api.botframework.com" + +# Graph API specific variables +$graphAuthorityUrl = [string]::Format("https://login.microsoftonline.com/{0}/oauth2/v2.0/token", $tenantId) +$graphResource = "graph.microsoft.com" + +# Graph auth section (fetching access token) +$graphBody = [string]::Format("grant_type=client_credentials&client_id={0}&client_secret={1}&scope=https%3A%2F%2F{2}%2F.default", $graphAppId, $graphAppSecret, $graphResource) +$graphToken = Invoke-RestMethod -Uri $graphAuthorityUrl -Method Post -Body $graphBody +Write-Output ("Fetching Graph Access token using {0}." -f $graphAppId) +$graphAccessToken = $graphToken.access_token +if ($graphAccessToken -eq $null) +{ + Write-Error -Message "No Graph access token found" + return +} + +# Bot auth section +$userAppBody = [string]::Format("grant_type=client_credentials&client_id={0}&client_secret={1}&scope=https%3A%2F%2F{2}%2F.default", $userAppId, $userAppSecret, $botResource) +$userToken = Invoke-RestMethod -Uri $botAuthorityUrl -Method Post -Body $userAppBody +Write-Output ("Fetching MSBot Access token using {0}." -f $userAppId) +$userAccessToken = $userToken.access_token +if ($userAccessToken -eq $null) +{ + Write-Error -Message "No bot access token found" + return +} + +############################################################################################################# +# Main logic - this is where the real magic happens :) # +############################################################################################################# + +# In order to send a message to an user via a bot, we first need to get the conversation between the bot and the user, +# this can be fetched via the Graph API using the https://docs.microsoft.com/en-us/graph/api/chat-get?view=graph-rest-1.0&tabs=http&preserve-view=true&viewFallbackFrom=graph-rest-v1.0 endpoint +# Using the current implementation of the API being used, our App needs to have Chat.Read.All permission + +# If no chat/conversation history can be found between the App and the user, we need to deploy the App for the user +# So, if the "installation" property is being passed on, we're going to deploy the Teams App to the user to start the conversation + +Write-Output ("Trying to fetch TeamsApp installation instance for the user." -f $userAppId) +$getAppsForUserUrl = [string]::Format("https://graph.microsoft.com/v1.0/users/{0}/chats?`$filter=installedApps/any(a:a/teamsApp/id eq '{1}')", $userUpn, $teamsAppId) +try +{ + $installAppsForUserData = Invoke-RestMethod -Headers @{Authorization = "Bearer $graphAccessToken"} -ContentType "application/json" -Uri $getAppsForUserUrl -Method Get + $userConversationId = $installAppsForUserData.value.id + Write-Output ("Got the conversationId ({0}) needed to send the message." -f $userConversationId) +} +catch [Net.WebException] +{ + Write-Warning -Message "No installation and thus no conversation found" +} + + +if ($userConversationId -eq $null) +{ + if ($install) + { + Write-Warning -Message "Trying to install App" + # We need to have the API permissions as outlined in the following article + # https://docs.microsoft.com/en-us/graph/api/userteamwork-post-installedapps?view=graph-rest-1.0&tabs=http&preserve-view=true&viewFallbackFrom=graph-rest-v1.0 + + $installationUrl = [string]::Format("https://graph.microsoft.com/v1.0/users/{0}/teamwork/installedApps", $userUpn) + $installationBody = "{ + 'teamsApp@odata.bind':'https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/$teamsAppId' + }" + + try + { + # try installing the app + $installationResult = Invoke-RestMethod -Uri $installationUrl -Headers @{Authorization = "Bearer $graphAccessToken"} -ContentType "application/json" -Body $installationBody -Method Post + + # once installed correctly, try to fetch the conversation that the App was installed + $installAppsForUserData = Invoke-RestMethod -Headers @{Authorization = "Bearer $graphAccessToken"} -ContentType "application/json" -Uri $getAppsForUserUrl -Method Get + $userConversationId = $installAppsForUserData.value.id + Write-Output ("Got the conversationId ({0}) needed to send the message." -f $userConversationId) + } + catch [Net.WebException] + { + [System.Net.HttpWebResponse] $resp = [System.Net.HttpWebResponse] $_.Exception.Response + Write-Warning ("Failed to install the Application because of {0}" -f, $resp.StatusDescription) + } + + } +} + + +if ($userConversationId -eq $null) +{ + Write-Output ("Could not send message because of missing conversationId (unique identifier of the App and the user).") + return +} + +# If we have the chat, we can continue the thread and send our message using the BotFramework REST API +Write-Output ("Sending message {1} to user {0}" -f $userUpn, $message) +$userConversationsUrl = [string]::Format("{0}/v3/conversations/{1}/activities", $serviceUrl, $userConversationId) +$postBody = "{ + 'type': 'message', + 'text': '$message' +}" + +$messageResult = Invoke-RestMethod -Uri $userConversationsUrl -Headers @{Authorization = "Bearer $userAccessToken"} -ContentType "application/json" -Body $postBody -Method Post +if ($messageResult) +{ + Write-Output ("Message sent successfully.") +} + diff --git a/Support/readme.md b/Support/readme.md new file mode 100644 index 000000000..544f6a4a9 --- /dev/null +++ b/Support/readme.md @@ -0,0 +1,20 @@ +## Why +This folder is created to aid in the troubleshooting when deploying this application into your tenant/environment. + +# **Start-ChatWithUser.ps1** +This script is designed for the purpose of testing the chat functionality of the bot within the Teams App. Some modifications are needed before you can start using this. In the "Variables need to be replaced to start using the script" section, there are a couple of variables you will need to replace: + +* $tenantId = "tenant.onmicrosoft.com" #or in GUID format "00000000-0000-0000-0000-000000000000" +* $teamsAppId = "00000000-0000-0000-0000-000000000000" # AppId of the Teams App Manifest +* $graphAppId = "00000000-0000-0000-0000-000000000000" +* $graphAppSecret= "secret" +* $userAppId = "00000000-0000-0000-0000-000000000000" +* $userAppSecret = "secret" + +For the secrets, I recommend to an extra secret per App which you can delete after using this script. This way, it won't interfere with the configuration of the application. And as an added bonus, it's more secure because the script will only run with the newly created secrets. + +# **Check-AppRegistrations.ps1** +This script is meant to retrieve the provisioned App Registrations and the secrets which are stored in the KeyVault are working together correctly or not. If an access token is being returned, it means that the combinations of AppId + AppSecrets are working. + +# **Check-GraphApp.ps1** +This a simplified version of the Check-AppRegistrations script. If you just want to see if the AppId + AppSecret is working correctly, please use this one. It is meant to use if the App registration of the Graph App (e.g. the main App) is working correctly (e.g. combination of AppId + AppSecret) and also if the API Permissions are set correct to retrieve Groups and Users \ No newline at end of file