Initial commit

This commit is contained in:
2025-03-07 19:22:02 +01:00
commit 4a98255d83
55743 changed files with 5280367 additions and 0 deletions
@@ -0,0 +1,96 @@
# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.1.0/yaml.schema.json
# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file
# Visit https://aka.ms/teamsfx-actions for details on actions
version: 1.1.0
provision:
{{#if activePlugins.fx-resource-aad-app-for-teams}}
- uses: aadApp/create # Creates a new Microsoft Entra app to authenticate users if the environment variable that stores clientId is empty
with:
name: {{aadAppName}} # Note: when you run aadApp/update, the Microsoft Entra app name will be updated based on the definition in manifest. If you don't want to change the name, make sure the name in Microsoft Entra manifest is the same with the name defined here.
generateClientSecret: true # If the value is false, the action will not generate client secret for you
signInAudience: "AzureADMyOrg" # Authenticate users with a Microsoft work or school account in your organization's Microsoft Entra tenant (for example, single tenant).
writeToEnvironmentFile: # Write the information of created resources into environment file for the specified environment variable(s).
clientId: AAD_APP_CLIENT_ID
clientSecret: SECRET_AAD_APP_CLIENT_SECRET # Environment variable that starts with `SECRET_` will be stored to the .env.{envName}.user environment file
objectId: AAD_APP_OBJECT_ID
tenantId: AAD_APP_TENANT_ID
authority: AAD_APP_OAUTH_AUTHORITY
authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST
{{/if}}
- uses: teamsApp/create # Creates a Teams app
with:
name: {{teamsAppName}} # Teams app name
writeToEnvironmentFile:
# Write the information of created resources into environment file for the specified environment variable(s).
teamsAppId: TEAMS_APP_ID
{{#if activePlugins.fx-resource-bot}}
- uses: botAadApp/create # Creates a new Microsoft Entra app for Bot Registration.
with:
name: {{appName}}
writeToEnvironmentFile:
botId: BOT_ID
botPassword: SECRET_BOT_PASSWORD
# Create or update the bot registration on dev.botframework.com
- uses: botFramework/create
with:
botId: $\{{BOT_ID}}
name: {{appName}}
messagingEndpoint: $\{{BOT_ENDPOINT}}/api/messages
description: ""
- uses: file/createOrUpdateJsonFile
with:
target: ./appsettings.Development.json
content:
BOT_ID: $\{{BOT_ID}}
BOT_PASSWORD: $\{{SECRET_BOT_PASSWORD}}
{{#if activePlugins.fx-resource-aad-app-for-teams}}
TeamsFx:
Authentication:
ClientId: $\{{AAD_APP_CLIENT_ID}}
ClientSecret: $\{{SECRET_AAD_APP_CLIENT_SECRET}}
OAuthAuthority: $\{{AAD_APP_OAUTH_AUTHORITY}}
{{/if}}
{{/if}}
{{#if activePlugins.fx-resource-frontend-hosting}}
- uses: script # Set env for local launch
with:
run:
echo "::set-teamsfx-env {{placeholderMappings.tabDomain}}=localhost:44302";
echo "::set-teamsfx-env {{placeholderMappings.tabEndpoint}}=https://localhost:44302";
echo "::set-teamsfx-env {{placeholderMappings.tabIndexPath}}=#";
{{#if activePlugins.fx-resource-aad-app-for-teams}}
- uses: file/createOrUpdateJsonFile
with:
target: ./appsettings.Development.json
content:
TeamsFx:
Authentication:
ClientId: $\{{AAD_APP_CLIENT_ID}}
ClientSecret: $\{{SECRET_AAD_APP_CLIENT_SECRET}}
OAuthAuthority: $\{{AAD_APP_OAUTH_AUTHORITY}}
{{/if}}
{{/if}}
{{#if activePlugins.fx-resource-bot}}
- uses: script # Set env for local launch
name: Set {{placeholderMappings.botDomain}} for local launch
with:
run: echo "::set-teamsfx-env {{placeholderMappings.botDomain}}=$\{{BOT_DOMAIN}}"
{{/if}}
{{#if activePlugins.fx-resource-aad-app-for-teams}}
- uses: aadApp/update # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in manifest file to determine which Microsoft Entra app to update.
with:
manifestPath: ./aad.manifest.json # Relative path to this file. Environment variables in manifest will be replaced before apply to Microsoft Entra app
outputFilePath : ./build/aad.manifest.$\{{TEAMSFX_ENV}}.json
{{/if}}
- uses: teamsApp/validateManifest # Validate using manifest schema
with:
manifestPath: ./appPackage/manifest.json # Path to manifest template
- uses: teamsApp/zipAppPackage # Build Teams app package with latest env value
with:
manifestPath: ./appPackage/manifest.json # Path to manifest template
outputZipPath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
outputJsonPath: ./build/appPackage/manifest.$\{{TEAMSFX_ENV}}.json
- uses: teamsApp/update # Apply the Teams app manifest to an existing Teams app in Teams Developer Portal. Will use the app id in manifest file to determine which Teams app to update.
with:
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip # Relative path to this file. This is the path for built zip file.
@@ -0,0 +1,113 @@
# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.1.0/yaml.schema.json
# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file
# Visit https://aka.ms/teamsfx-actions for details on actions
version: 1.1.0
projectId: {{projectId}}
environmentFolderPath: ./ {{~environmentFolder}}
provision:
{{#if activePlugins.fx-resource-aad-app-for-teams}}
- uses: aadApp/create # Creates a new Microsoft Entra app to authenticate users if the environment variable that stores clientId is empty
with:
name: {{aadAppName}} # Note: when you run aadApp/update, the Microsoft Entra app name will be updated based on the definition in manifest. If you don't want to change the name, make sure the name in Microsoft Entra manifest is the same with the name defined here.
generateClientSecret: true # If the value is false, the action will not generate client secret for you
signInAudience: "AzureADMyOrg" # Authenticate users with a Microsoft work or school account in your organization's Microsoft Entra tenant (for example, single tenant).
writeToEnvironmentFile: # Write the information of created resources into environment file for the specified environment variable(s).
clientId: AAD_APP_CLIENT_ID
clientSecret: SECRET_AAD_APP_CLIENT_SECRET # Environment variable that starts with `SECRET_` will be stored to the .env.{envName}.user environment file
objectId: AAD_APP_OBJECT_ID
tenantId: AAD_APP_TENANT_ID
authority: AAD_APP_OAUTH_AUTHORITY
authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST
{{/if}}
- uses: teamsApp/create # Creates a Teams app
with:
name: {{teamsAppName}} # Teams app name
writeToEnvironmentFile:
# Write the information of created resources into environment file for the specified environment variable(s).
teamsAppId: TEAMS_APP_ID
{{#if activePlugins.fx-resource-bot}}
- uses: botAadApp/create # Creates a new Microsoft Entra app for Bot Registration.
with:
name: {{appName}}bt$\{{RESOURCE_SUFFIX}}
writeToEnvironmentFile:
botId: BOT_ID
botPassword: SECRET_BOT_PASSWORD
{{/if}}
- uses: arm/deploy # Deploy given ARM templates parallelly.
with:
subscriptionId: $\{{AZURE_SUBSCRIPTION_ID}} # The AZURE_SUBSCRIPTION_ID is a built-in environment variable. TeamsFx will ask you select one subscription if its value is empty. You're free to reference other environment varialbe here, but TeamsFx will not ask you to select subscription if it's empty in this case.
resourceGroupName: $\{{AZURE_RESOURCE_GROUP_NAME}} # The AZURE_RESOURCE_GROUP_NAME is a built-in environment variable. TeamsFx will ask you to select or create one resource group if its value is empty. You're free to reference other environment varialbe here, but TeamsFx will not ask you to select or create resource grouop if it's empty in this case.
templates:
- path: ./Templates/azure/main.bicep # Relative path to this file
parameters: ./Templates/azure/azure.parameters.$\{{TEAMSFX_ENV}}.json # Relative path to this file. Placeholders will be replaced with corresponding environment variable before ARM deployment.
deploymentName: teams_toolkit_deployment # Required when deploy ARM template
bicepCliVersion: v0.4.613 # Teams Toolkit will download this bicep CLI version from github for you, will use bicep CLI in PATH if you remove this config.
{{#if activePlugins.fx-resource-frontend-hosting}}
- uses: script # Add additional item to .env file
with:
run: echo "::set-teamsfx-env {{placeholderMappings.[state.fx-resource-frontend-hosting.indexPath]}}=/" # Used in appPackage/manifest.json file.
{{/if}}
{{#if activePlugins.fx-resource-aad-app-for-teams}}
- uses: aadApp/update # Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in manifest file to determine which Microsoft Entra app to update.
with:
manifestPath: ./aad.manifest.json # Relative path to this file. Environment variables in manifest will be replaced before apply to Microsoft Entra app
outputFilePath : ./build/aad.manifest.$\{{TEAMSFX_ENV}}.json
{{/if}}
- uses: teamsApp/validateManifest # Validate using manifest schema
with:
manifestPath: ./appPackage/manifest.json # Path to manifest template
- uses: teamsApp/zipAppPackage # Build Teams app package with latest env value
with:
manifestPath: ./appPackage/manifest.json # Path to manifest template
outputZipPath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
outputJsonPath: ./build/appPackage/manifest.$\{{TEAMSFX_ENV}}.json
- uses: teamsApp/update # Apply the Teams app manifest to an existing Teams app in Teams Developer Portal. Will use the app id in manifest file to determine which Teams app to update.
with:
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip # Relative path to this file. This is the path for built zip file.
deploy:
- uses: cli/runDotnetCommand
with:
args: publish --configuration Release --runtime win-x86 --self-contained
{{#if activePlugins.fx-resource-bot}}
{{#if isFunctionBot}}
# Deploy your application to Azure Functions using the zip deploy feature.
# For additional details, see at https://aka.ms/zip-deploy-to-azure-functions
- uses: azureFunctions/zipDeploy
with:
# deploy base folder
artifactFolder: bin/Release/net6.0/win-x86/publish
# The resource id of the cloud resource to be deployed to.
# This key will be generated by arm/deploy action automatically.
# You can replace it with your existing Azure Resource id
# or add it to your environment variable file.
resourceId: $\{{ {{~placeholderMappings.[state.fx-resource-bot.functionAppResourceId]~}} }}
{{else}}
# Deploy your application to Azure App Service using the zip deploy feature.
# For additional details, refer to https://aka.ms/zip-deploy-to-app-services.
- uses: azureAppService/zipDeploy
with:
# deploy base folder
artifactFolder: bin/Release/net6.0/win-x86/publish
# The resource id of the cloud resource to be deployed to.
# This key will be generated by arm/deploy action automatically.
# You can replace it with your existing Azure Resource id
# or add it to your environment variable file.
resourceId: $\{{ {{~placeholderMappings.[state.fx-resource-bot.resourceId]~}} }}
{{/if}}
{{else if activePlugins.fx-resource-frontend-hosting}}
# Deploy your application to Azure App Service using the zip deploy feature.
# For additional details, refer to https://aka.ms/zip-deploy-to-app-services.
- uses: azureAppService/zipDeploy
with:
# deploy base folder
artifactFolder: bin/Release/net6.0/win-x86/publish
# The resource id of the cloud resource to be deployed to.
# This key will be generated by arm/deploy action automatically.
# You can replace it with your existing Azure Resource id
# or add it to your environment variable file.
resourceId: $\{{ {{~placeholderMappings.[state.fx-resource-frontend-hosting.resourceId]~}} }}
{{/if}}
@@ -0,0 +1,349 @@
# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.0.0/yaml.schema.json
# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file
# Visit https://aka.ms/teamsfx-actions for details on actions
version: 1.0.0
{{#config.provision}}
provision:
{{#registerApp}}
{{#if aad}}
# Creates a new Microsoft Entra app to authenticate users if
# the environment variable that stores clientId is empty
- uses: aadApp/create
with:
# Note: when you run aadApp/update, the Microsoft Entra app name will be updated
# based on the definition in manifest. If you don't want to change the
# name, make sure the name in Microsoft Entra manifest is the same with the name
# defined here.
name: $\{{CONFIG__MANIFEST__APPNAME__SHORT}}-aad
# If the value is false, the action will not generate client secret for you
generateClientSecret: true
# Authenticate users with a Microsoft work or school account in your
# organization's Microsoft Entra tenant (for example, single tenant).
signInAudience: "AzureADMyOrg"
# Write the information of created resources into environment file for the
# specified environment variable(s).
writeToEnvironmentFile:
clientId: AAD_APP_CLIENT_ID
# Environment variable that starts with `SECRET_` will be stored to the
# .env.{envName}.user environment file
clientSecret: SECRET_AAD_APP_CLIENT_SECRET
objectId: AAD_APP_OBJECT_ID
tenantId: AAD_APP_TENANT_ID
authority: AAD_APP_OAUTH_AUTHORITY
authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST
{{/if}}
{{#if teamsApp}}
# Creates a Teams app
- uses: teamsApp/create
with:
# Teams app name
name: $\{{CONFIG__MANIFEST__APPNAME__SHORT}}
# Write the information of created resources into environment file for
# the specified environment variable(s).
writeToEnvironmentFile:
teamsAppId: TEAMS_APP_ID
{{/if}}
{{/registerApp}}
{{#bot}}
# Create or reuse an existing Microsoft Entra application for bot.
- uses: botAadApp/create
with:
# The Microsoft Entra application's display name
name: $\{{CONFIG__MANIFEST__APPNAME__SHORT}}-bot
writeToEnvironmentFile:
# The Microsoft Entra application's client id created for bot.
botId: BOT_ID
# The Microsoft Entra application's client secret created for bot.
botPassword: SECRET_BOT_PASSWORD
# Create or update the bot registration on dev.botframework.com
- uses: botFramework/create
with:
botId: $\{{BOT_ID}}
name: $\{{CONFIG__MANIFEST__APPNAME__SHORT}}-bot
messagingEndpoint: {{messagingEndpoint}}
description: ""
channels:
- name: msteams
{{#if isM365}}
- name: m365extensions
{{/if}}
{{/bot}}
{{#configureApp}}
{{#tab}}
- uses: script # Set env for local launch
with:
run:
echo "::set-teamsfx-env {{../../../placeholderMappings.tabDomain}}={{domain}}";
echo "::set-teamsfx-env {{../../../placeholderMappings.tabEndpoint}}={{endpoint}}";
echo "::set-teamsfx-env {{../../../placeholderMappings.tabIndexPath}}=/index.html#";
{{/tab}}
{{#if aad}}
# Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in
# manifest file to determine which Microsoft Entra app to update.
- uses: aadApp/update
with:
# Relative path to this file. Environment variables in manifest will
# be replaced before apply to Microsoft Entra app
manifestPath: ./aad.manifest.json
outputFilePath : ./build/aad.manifest.$\{{TEAMSFX_ENV}}.json
{{/if}}
{{#teamsApp}}
{{#if appPackagePath}}
- uses: teamsApp/update
with:
appPackagePath: {{appPackagePath}}
{{else}}
# Validate using manifest schema
- uses: teamsApp/validateManifest
with:
# Path to manifest template
manifestPath: ./appPackage/manifest.json
# Build Teams app package with latest env value
- uses: teamsApp/zipAppPackage
with:
# Path to manifest template
manifestPath: ./appPackage/manifest.json
outputZipPath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
outputJsonPath: ./build/appPackage/manifest.$\{{TEAMSFX_ENV}}.json
# Validate app package using validation rules
- uses: teamsApp/validateAppPackage
with:
# Relative path to this file. This is the path for built zip file.
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
# Apply the Teams app manifest to an existing Teams app in
# Teams Developer Portal.
# Will use the app id in manifest file to determine which Teams app to update.
- uses: teamsApp/update
with:
# Relative path to this file. This is the path for built zip file.
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
{{/if}}
{{/teamsApp}}
{{#if isM365}}
# Extend your Teams app to Outlook and the Microsoft 365 app
- uses: teamsApp/extendToM365
with:
# Relative path to the build app package.
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
# Write the information of created resources into environment file for
# the specified environment variable(s).
writeToEnvironmentFile:
titleId: M365_TITLE_ID
appId: M365_APP_ID
{{/if}}
{{/configureApp}}
{{/config.provision}}
{{#config.deploy}}
deploy:
{{#tools}}
# Install development tool(s)
- uses: devTool/install
with:
{{#devCert}}
devCert:
trust: {{trust}}
{{/devCert}}
{{#func}}
func:
version: {{version}}
symlinkDir: {{symlinkDir}}
{{/func}}
{{#dotnet}}
dotnet: true
{{/dotnet}}
# Write the information of installed development tool(s) into environment
# file for the specified environment variable(s).
writeToEnvironmentFile:
{{#devCert}}
sslCertFile: SSL_CRT_FILE
sslKeyFile: SSL_KEY_FILE
{{/devCert}}
{{#func}}
funcPath: FUNC_PATH
{{/func}}
{{#dotnet}}
dotnetPath: DOTNET_PATH
{{/dotnet}}
{{/tools}}
{{#dotnetCommand}}
- uses: cli/runDotnetCommand
with:
args: {{args}}
workingDirectory: {{workingDirectory}}
{{#if execPath}}
execPath: {{execPath}}
{{/if}}
{{/dotnetCommand}}
{{#tab}}
# Generate runtime environment variables
- uses: file/createOrUpdateEnvironmentFile
with:
target: ./tabs/.env.teamsfx.local
envs:
BROWSER: none
HTTPS: true
PORT: {{port}}
SSL_CRT_FILE: $\{{SSL_CRT_FILE}}
SSL_KEY_FILE: $\{{SSL_KEY_FILE}}
{{/tab}}
{{#if bot}}
# Generate runtime environment variables
- uses: file/createOrUpdateEnvironmentFile
with:
target: ./bot/.env.teamsfx.local
envs:
BOT_ID: $\{{BOT_ID}}
BOT_PASSWORD: $\{{SECRET_BOT_PASSWORD}}
{{/if}}
{{#ssoTab}}
# Generate runtime environment variables
- uses: file/createOrUpdateEnvironmentFile
with:
target: ./tabs/.env.teamsfx.local
envs:
REACT_APP_START_LOGIN_PAGE_URL: $\{{ {{~../../placeholderMappings.tabEndpoint~}} }}/auth-start.html
REACT_APP_CLIENT_ID: $\{{AAD_APP_CLIENT_ID}}
{{#if functionName}}
REACT_APP_FUNC_ENDPOINT: http://localhost:7071
REACT_APP_FUNC_NAME: {{functionName}}
{{/if}}
{{/ssoTab}}
{{#if ssoBot}}
# Generate runtime environment variables
- uses: file/createOrUpdateEnvironmentFile
with:
target: ./bot/.env.teamsfx.local
envs:
M365_CLIENT_ID: $\{{AAD_APP_CLIENT_ID}}
M365_CLIENT_SECRET: $\{{SECRET_AAD_APP_CLIENT_SECRET}}
M365_TENANT_ID: $\{{AAD_APP_TENANT_ID}}
M365_AUTHORITY_HOST: $\{{AAD_APP_OAUTH_AUTHORITY_HOST}}
INITIATE_LOGIN_ENDPOINT: $\{{ {{~../placeholderMappings.botEndpoint~}} }}/auth-start.html
{{#if ssoTab}}
M365_APPLICATION_ID_URI: api://$\{{ {{~../placeholderMappings.tabEndpoint~}} }}/botid-$\{{BOT_ID}}
{{else}}
M365_APPLICATION_ID_URI: api://botid-$\{{BOT_ID}}
{{/if}}
{{#if ssoFunction}}
API_ENDPOINT: http://localhost:7071
{{/if}}
{{/if}}
{{#if ssoFunction}}
# Generate runtime environment variables
- uses: file/createOrUpdateEnvironmentFile
with:
target: ./api/.env.teamsfx.local
envs:
M365_CLIENT_ID: $\{{AAD_APP_CLIENT_ID}}
M365_CLIENT_SECRET: $\{{SECRET_AAD_APP_CLIENT_SECRET}}
M365_TENANT_ID: $\{{AAD_APP_TENANT_ID}}
M365_AUTHORITY_HOST: $\{{AAD_APP_OAUTH_AUTHORITY_HOST}}
ALLOWED_APP_IDS: 1fec8e78-bce4-4aaf-ab1b-5451cc387264;5e3ce6c0-2b1f-4285-8d4b-75ee78787346;0ec893e0-5785-4de6-99da-4ed124e5296c;4345a7b9-9a63-4910-a426-35363201d503;4765445b-32c6-49b0-83e6-1d93765276ca;d3590ed6-52b3-4102-aeff-aad2292ab01c;00000002-0000-0ff1-ce00-000000000000;bc59ab01-8403-45c6-8796-ac3ef710b3e3;27922004-5251-4030-b22d-91ecd9a37ea4
{{/if}}
{{#npmCommands}}
# Run npm command
- uses: cli/runNpmCommand
with:
args: {{args}}
{{#if workingDirectory}}
workingDirectory: {{workingDirectory}}
{{/if}}
{{/npmCommands}}
{{#frontendStart}}
# Generate runtime environment variables
- uses: file/createOrUpdateEnvironmentFile
with:
target: ./tabs/.localConfigs
envs:
BROWSER: none
HTTPS: true
PORT: 53000
SSL_CRT_FILE: $\{{SSL_CRT_FILE}}
SSL_KEY_FILE: $\{{SSL_KEY_FILE}}
{{#if sso}}
REACT_APP_CLIENT_ID: $\{{AAD_APP_CLIENT_ID}}
REACT_APP_START_LOGIN_PAGE_URL: $\{{ {{~../../placeholderMappings.tabEndpoint~}} }}/auth-start.html
REACT_APP_TEAMSFX_ENDPOINT: http://localhost:55000
{{/if}}
{{#if functionName}}
REACT_APP_FUNC_ENDPOINT: http://localhost:7071
REACT_APP_FUNC_NAME: {{functionName}}
{{/if}}
{{/frontendStart}}
{{#authStart}}
# Generate runtime appsettings to JSON file
- uses: file/createOrUpdateJsonFile
with:
target: {{appsettingsPath}}
appsettings:
CLIENT_ID: $\{{AAD_APP_CLIENT_ID}}
CLIENT_SECRET: $\{{SECRET_AAD_APP_CLIENT_SECRET}}
IDENTIFIER_URI: api://$\{{ {{~../../placeholderMappings.tabDomain~}} }}/$\{{AAD_APP_CLIENT_ID}}
AAD_METADATA_ADDRESS: $\{{AAD_APP_OAUTH_AUTHORITY}}/v2.0/.well-known/openid-configuration
OAUTH_AUTHORITY: $\{{AAD_APP_OAUTH_AUTHORITY}}
TAB_APP_ENDPOINT: $\{{ {{~../../placeholderMappings.tabEndpoint~}} }}
ALLOWED_APP_IDS: 1fec8e78-bce4-4aaf-ab1b-5451cc387264;5e3ce6c0-2b1f-4285-8d4b-75ee78787346;0ec893e0-5785-4de6-99da-4ed124e5296c;4345a7b9-9a63-4910-a426-35363201d503;4765445b-32c6-49b0-83e6-1d93765276ca;d3590ed6-52b3-4102-aeff-aad2292ab01c;00000002-0000-0ff1-ce00-000000000000;bc59ab01-8403-45c6-8796-ac3ef710b3e3;27922004-5251-4030-b22d-91ecd9a37ea4
URLS: http://localhost:55000
{{/authStart}}
{{#botStart}}
# Generate runtime environment variables
- uses: file/createOrUpdateEnvironmentFile
with:
target: ./bot/.localConfigs
envs:
BOT_ID: $\{{BOT_ID}}
BOT_PASSWORD: $\{{SECRET_BOT_PASSWORD}}
{{#if sso}}
M365_CLIENT_ID: $\{{AAD_APP_CLIENT_ID}}
M365_CLIENT_SECRET: $\{{SECRET_AAD_APP_CLIENT_SECRET}}
M365_TENANT_ID: $\{{AAD_APP_TENANT_ID}}
M365_AUTHORITY_HOST: $\{{AAD_APP_OAUTH_AUTHORITY_HOST}}
INITIATE_LOGIN_ENDPOINT: $\{{ {{~../../placeholderMappings.botEndpoint~}} }}/auth-start.html
{{#if tab}}
M365_APPLICATION_ID_URI: api://$\{{ {{~../../placeholderMappings.tabDomain~}} }}/botid-$\{{BOT_ID}}
{{else}}
M365_APPLICATION_ID_URI: api://botid-$\{{BOT_ID}}
{{/if}}
{{/if}}
{{#if function}}
API_ENDPOINT: http://localhost:7071
{{/if}}
{{/botStart}}
{{#if backendStart}}
# Generate runtime environment variables
- uses: file/createOrUpdateEnvironmentFile
with:
target: ./api/.localConfigs
envs:
FUNCTIONS_WORKER_RUNTIME: node
M365_CLIENT_ID: $\{{AAD_APP_CLIENT_ID}}
M365_CLIENT_SECRET: $\{{SECRET_AAD_APP_CLIENT_SECRET}}
M365_TENANT_ID: $\{{AAD_APP_TENANT_ID}}
M365_AUTHORITY_HOST: $\{{AAD_APP_OAUTH_AUTHORITY_HOST}}
ALLOWED_APP_IDS: 1fec8e78-bce4-4aaf-ab1b-5451cc387264;5e3ce6c0-2b1f-4285-8d4b-75ee78787346;0ec893e0-5785-4de6-99da-4ed124e5296c;4345a7b9-9a63-4910-a426-35363201d503;4765445b-32c6-49b0-83e6-1d93765276ca;d3590ed6-52b3-4102-aeff-aad2292ab01c;00000002-0000-0ff1-ce00-000000000000;bc59ab01-8403-45c6-8796-ac3ef710b3e3;27922004-5251-4030-b22d-91ecd9a37ea4
{{/if}}
{{/config.deploy}}
@@ -0,0 +1,308 @@
# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.0.0/yaml.schema.json
# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file
# Visit https://aka.ms/teamsfx-actions for details on actions
version: 1.0.0
projectId: {{projectId}}
environmentFolderPath: ./ {{~environmentFolder}}
# Triggered when 'teamsfx provision' is executed
provision:
{{#if activePlugins.fx-resource-aad-app-for-teams}}
# Creates a new Microsoft Entra app to authenticate users if
# the environment variable that stores clientId is empty
- uses: aadApp/create
with:
# Note: when you run aadApp/update, the Microsoft Entra app name will be updated
# based on the definition in manifest. If you don't want to change the
# name, make sure the name in Microsoft Entra manifest is the same with the name
# defined here.
name: {{aadAppName}}
# If the value is false, the action will not generate client secret for you
generateClientSecret: true
# Authenticate users with a Microsoft work or school account in your
# organization's Microsoft Entra tenant (for example, single tenant).
signInAudience: "AzureADMyOrg"
# Write the information of created resources into environment file for the
# specified environment variable(s).
writeToEnvironmentFile:
clientId: AAD_APP_CLIENT_ID
# Environment variable that starts with `SECRET_` will be stored to the
# .env.{envName}.user environment file
clientSecret: SECRET_AAD_APP_CLIENT_SECRET
objectId: AAD_APP_OBJECT_ID
tenantId: AAD_APP_TENANT_ID
authority: AAD_APP_OAUTH_AUTHORITY
authorityHost: AAD_APP_OAUTH_AUTHORITY_HOST
{{/if}}
# Creates a Teams app
- uses: teamsApp/create
with:
# Teams app name
name: {{teamsAppName}}
# Write the information of created resources into environment file for
# the specified environment variable(s).
writeToEnvironmentFile:
teamsAppId: TEAMS_APP_ID
{{#if activePlugins.fx-resource-bot}}
# Create or reuse an existing Microsoft Entra application for bot.
- uses: botAadApp/create
with:
# The Microsoft Entra application's display name
name: {{appName}}bt$\{{RESOURCE_SUFFIX}}
writeToEnvironmentFile:
# The Microsoft Entra application's client id created for bot.
botId: BOT_ID
# The Microsoft Entra application's client secret created for bot.
botPassword: SECRET_BOT_PASSWORD
{{/if}}
- uses: arm/deploy # Deploy given ARM templates parallelly.
with:
# AZURE_SUBSCRIPTION_ID is a built-in environment variable,
# if its value is empty, TeamsFx will prompt you to select a subscription.
# Referencing other environment variables with empty values
# will skip the subscription selection prompt.
subscriptionId: $\{{AZURE_SUBSCRIPTION_ID}}
# AZURE_RESOURCE_GROUP_NAME is a built-in environment variable,
# if its value is empty, TeamsFx will prompt you to select or create one
# resource group.
# Referencing other environment variables with empty values
# will skip the resource group selection prompt.
resourceGroupName: $\{{AZURE_RESOURCE_GROUP_NAME}}
templates:
- path: ./templates/azure/main.bicep # Relative path to this file
# Relative path to this yaml file.
# Placeholders will be replaced with corresponding environment
# variable before ARM deployment.
parameters: ./templates/azure/azure.parameters.$\{{TEAMSFX_ENV}}.json
# Required when deploying ARM template
deploymentName: teams_toolkit_deployment
# Teams Toolkit will download this bicep CLI version from github for you,
# will use bicep CLI in PATH if you remove this config.
bicepCliVersion: v0.4.613
{{#if activePlugins.fx-resource-frontend-hosting}}
- uses: azureStorage/enableStaticWebsite
with:
storageResourceId: $\{{ {{~placeholderMappings.[state.fx-resource-frontend-hosting.storageResourceId]~}} }}
indexPage: index.html
errorPage: error.html
{{/if}}
{{#if activePlugins.fx-resource-aad-app-for-teams}}
# Apply the Microsoft Entra manifest to an existing Microsoft Entra app. Will use the object id in
# manifest file to determine which Microsoft Entra app to update.
- uses: aadApp/update
with:
# Relative path to this file. Environment variables in manifest will
# be replaced before apply to Microsoft Entra app
manifestPath: ./aad.manifest.json
outputFilePath : ./build/aad.manifest.$\{{TEAMSFX_ENV}}.json
{{/if}}
# Validate using manifest schema
- uses: teamsApp/validateManifest
with:
# Path to manifest template
manifestPath: ./appPackage/manifest.json
# Build Teams app package with latest env value
- uses: teamsApp/zipAppPackage
with:
# Path to manifest template
manifestPath: ./appPackage/manifest.json
outputZipPath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
outputJsonPath: ./build/appPackage/manifest.$\{{TEAMSFX_ENV}}.json
# Validate app package using validation rules
- uses: teamsApp/validateAppPackage
with:
# Relative path to this file. This is the path for built zip file.
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
# Apply the Teams app manifest to an existing Teams app in
# Teams Developer Portal.
# Will use the app id in manifest file to determine which Teams app to update.
- uses: teamsApp/update
with:
# Relative path to this file. This is the path for built zip file.
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
{{#if isM365}}
# Extend your Teams app to Outlook and the Microsoft 365 app
- uses: teamsApp/extendToM365
with:
# Relative path to the build app package.
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
# Write the information of created resources into environment file for
# the specified environment variable(s).
writeToEnvironmentFile:
titleId: M365_TITLE_ID
appId: M365_APP_ID
{{/if}}
# Triggered when 'teamsfx deploy' is executed
deploy:
{{#if activePlugins.fx-resource-frontend-hosting}}
# Run npm command
- uses: cli/runNpmCommand
name: install tab dependencies
with:
workingDirectory: tabs
args: install
# Run npm command
- uses: cli/runNpmCommand
name: build tab app
{{#if activePlugins.fx-resource-aad-app-for-teams}}
env:
REACT_APP_CLIENT_ID: $\{{AAD_APP_CLIENT_ID}}
REACT_APP_START_LOGIN_PAGE_URL: $\{{ {{~placeholderMappings.[state.fx-resource-frontend-hosting.endpoint]~}} }}/auth-start.html
{{#if activePlugins.fx-resource-function}}
REACT_APP_FUNC_ENDPOINT: $\{{ {{~placeholderMappings.[state.fx-resource-function.functionEndpoint]~}} }}
REACT_APP_FUNC_NAME: {{defaultFunctionName}}
{{/if}}
{{/if}}
with:
workingDirectory: tabs
args: run build --if-present
# Deploy bits to Azure Storage Static Website
- uses: azureStorage/deploy
with:
workingDirectory: tabs
# Deploy base folder. This folder includes manifest files for Microsoft Entra app and Teams app that should be ignored using the ignoreFile.
artifactFolder: build
# The resource id of the cloud resource to be deployed to.
# This key will be generated by arm/deploy action automatically.
# You can replace it with your existing Azure Resource id
# or add it to your environment variable file.
resourceId: $\{{ {{~placeholderMappings.[state.fx-resource-frontend-hosting.storageResourceId]~}} }}
{{/if}}
{{#if activePlugins.fx-resource-bot}}
{{#if isFunctionBot}}
- uses: cli/runNpmCommand
name: install bot dependencies
with:
workingDirectory: bot
args: install
{{#if isTypescript}}
- uses: cli/runNpmCommand
name: build bot app
with:
workingDirectory: bot
args: run build --if-present
{{/if}}
# Deploy your application to Azure Functions using the zip deploy feature.
# For additional details, see at https://aka.ms/zip-deploy-to-azure-functions
- uses: azureFunctions/zipDeploy
with:
workingDirectory: bot
# Deploy base folder
artifactFolder: .
# Ignore file location, leave blank will ignore nothing
ignoreFile: .funcignore
# The resource id of the cloud resource to be deployed to.
# This key will be generated by arm/deploy action automatically.
# You can replace it with your existing Azure Resource id
# or add it to your environment variable file.
resourceId: $\{{ {{~botResourceId~}} }}
{{else}}
# Run npm command
- uses: cli/runNpmCommand
name: install bot dependencies
with:
workingDirectory: bot
args: install
{{#if isTypescript}}
# Run npm command
- uses: cli/runNpmCommand
name: build bot app
with:
workingDirectory: bot
args: run build --if-present
{{/if}}
# Deploy your application to Azure App Service using the zip deploy feature.
# For additional details, refer to https://aka.ms/zip-deploy-to-app-services.
- uses: azureAppService/zipDeploy
with:
workingDirectory: bot
# Deploy base folder.
artifactFolder: .
# The resource id of the cloud resource to be deployed to.
# This key will be generated by arm/deploy action automatically.
# You can replace it with your existing Azure Resource id
# or add it to your environment variable file.
resourceId: $\{{ {{~botResourceId~}} }}
{{/if}}
{{/if}}
{{#if activePlugins.fx-resource-function}}
# Install development tool(s)
- uses: devTool/install
with:
dotnet: true
# Write the information of installed development tool(s) into environment
# file for the specified environment variable(s).
writeToEnvironmentFile:
dotnetPath: DOTNET_PATH
- uses: cli/runNpmCommand
name: install api dependencies
with:
workingDirectory: api
args: install
- uses: cli/runDotnetCommand
with:
workingDirectory: api
args: build extensions.csproj -o bin --ignore-failed-sources
execPath: $\{{ {{~dotnetPath~}} }} # Use dotnet installed by devTool/install action
{{#if isTypescript}}
- uses: cli/runNpmCommand
name: build api app
with:
workingDirectory: api
args: run build --if-present
{{/if}}
# Deploy your application to Azure Functions using the zip deploy feature.
# For additional details, see at https://aka.ms/zip-deploy-to-azure-functions
- uses: azureFunctions/zipDeploy
with:
workingDirectory: api
# Deploy base folder
artifactFolder: .
# Ignore file location, leave blank will ignore nothing
ignoreFile: .funcignore
# The resource id of the cloud resource to be deployed to.
# This key will be generated by arm/deploy action automatically.
# You can replace it with your existing Azure Resource id
# or add it to your environment variable file.
resourceId: $\{{ {{~placeholderMappings.[state.fx-resource-function.functionAppResourceId]~}} }}
{{/if}}
# Triggered when 'teamsfx publish' is executed
publish:
# Validate using manifest schema
- uses: teamsApp/validateManifest
with:
# Path to manifest template
manifestPath: ./appPackage/manifest.json
# Build Teams app package with latest env value
- uses: teamsApp/zipAppPackage
with:
# Path to manifest template
manifestPath: ./appPackage/manifest.json
outputZipPath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
outputJsonPath: ./build/appPackage/manifest.$\{{TEAMSFX_ENV}}.json
# Validate app package using validation rules
- uses: teamsApp/validateAppPackage
with:
# Relative path to this file. This is the path for built zip file.
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
# Apply the Teams app manifest to an existing Teams app in
# Teams Developer Portal.
# Will use the app id in manifest file to determine which Teams app to update.
- uses: teamsApp/update
with:
# Relative path to this file. This is the path for built zip file.
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
# Publish the app to
# Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps)
# for review and approval
- uses: teamsApp/publishAppPackage
with:
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
# Write the information of created resources into environment file for
# the specified environment variable(s).
writeToEnvironmentFile:
publishedAppId: TEAMS_APP_PUBLISHED_APP_ID
@@ -0,0 +1,99 @@
# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.0.0/yaml.schema.json
# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file
# Visit https://aka.ms/teamsfx-actions for details on actions
version: 1.0.0
projectId: {{projectId}}
environmentFolderPath: ./ {{~environmentFolder}}
# Triggered when 'teamsfx provision' is executed
provision:
# Creates a Teams app
- uses: teamsApp/create
with:
# Teams app name
name: {{teamsAppName}}
# Write the information of created resources into environment file for
# the specified environment variable(s).
writeToEnvironmentFile:
teamsAppId: TEAMS_APP_ID
# Validate using manifest schema
- uses: teamsApp/validateManifest
with:
# Path to manifest template
manifestPath: ./appPackage/manifest.json
# Build Teams app package with latest env value
- uses: teamsApp/zipAppPackage
with:
# Path to manifest template
manifestPath: ./appPackage/manifest.json
outputZipPath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
outputJsonPath: ./build/appPackage/manifest.$\{{TEAMSFX_ENV}}.json
# Validate app package using validation rules
- uses: teamsApp/validateAppPackage
with:
# Relative path to this file. This is the path for built zip file.
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
# Apply the Teams app manifest to an existing Teams app in
# Teams Developer Portal.
# Will use the app id in manifest file to determine which Teams app to update.
- uses: teamsApp/update
with:
# Relative path to this file. This is the path for built zip file.
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
# Triggered when 'teamsfx deploy' is executed
deploy:
- uses: cli/runNpmCommand
with:
args: install
workingDirectory: SPFx
- uses: cli/runNpxCommand
with:
workingDirectory: SPFx
args: gulp bundle --ship --no-color
- uses: cli/runNpxCommand
with:
workingDirectory: SPFx
args: gulp package-solution --ship --no-color
- uses: spfx/deploy
with:
createAppCatalogIfNotExist: false
packageSolutionPath: ./SPFx/config/package-solution.json
# Triggered when 'teamsfx publish' is executed
publish:
# Validate using manifest schema
- uses: teamsApp/validateManifest
with:
# Path to manifest template
manifestPath: ./appPackage/manifest.json
# Build Teams app package with latest env value
- uses: teamsApp/zipAppPackage
with:
# Path to manifest template
manifestPath: ./appPackage/manifest.json
outputZipPath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
outputJsonPath: ./build/appPackage/manifest.$\{{TEAMSFX_ENV}}.json
- uses: teamsApp/copyAppPackageToSPFx
with:
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
spfxFolder: ./SPFx
# Apply the Teams app manifest to an existing Teams app in
# Teams Developer Portal.
# Will use the app id in manifest file to determine which Teams app to update.
- uses: teamsApp/update
with:
# Relative path to this file. This is the path for built zip file.
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
# Publish the app to
# Teams Admin Center (https://admin.teams.microsoft.com/policies/manage-apps)
# for review and approval
- uses: teamsApp/publishAppPackage
with:
appPackagePath: ./build/appPackage/appPackage.$\{{TEAMSFX_ENV}}.zip
# Write the information of created resources into environment file for
# the specified environment variable(s).
writeToEnvironmentFile:
publishedAppId: TEAMS_APP_PUBLISHED_APP_ID
@@ -0,0 +1,52 @@
# Review project upgrade and changes
Your project was automatically upgraded to work with this version of Teams Toolkit. The upgrade is one-way, and you will not be able to use the version of Teams Toolkit in which it was originally created unless you [roll back the changes](#how-to-roll-back-after-the-upgrade).
> If you encountered a problem after the automatic upgrade, review the [known issues](https://aka.ms/teams-toolkit-5.0-upgrade#known-issues). If you are still having trouble, contact ttkfeedback@microsoft.com or [file an issue on GitHub](https://github.com/officedev/teamsfx/issues) with details about the problem.
## Important changes to your development flow
The new features in this release make developing your apps simpler and more flexible, but also include changes to how the toolkit creates projects, automates configuration, and deploys resources. Visit [the upgrade guide](https://aka.ms/teamsfx-v5.0-guide) for more info about all of the changes.
Some of the changes you may immediately notice are:
* Configuration of the lifecycle management using Provision, Deploy, and Publish are now fully customizable and expressed in `teamsapp.local.yml` and `teamsapp.yml`. [More info](https://aka.ms/teamsfx-v5.0-guide#project-files)
* Configuration and values that were saved in `.fx/config` and `.fx/state` are now handled with environment files and saved to `/env` by default. [More info](https://aka.ms/teams-toolkit-5.0-upgrade#environment-management)
* The changes to use environment files give greater flexibility of configuring which resources are used to provision, but may require some manual steps when creating new environments. [More info](https://aka.ms/teams-toolkit-5.0-upgrade#environment-management)
If you are using Visual Studio version 17.7 or later and developing a bot app locally:
* You can use the dev tunnel as your tunneling service. [More Info](https://aka.ms/vs-dev-tunnel-guidance)
* In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel.
If you're using APIM:
* You need to provide values to `APIM__PUBLISHEREMAIL` and the `APIM__PUBLISHERNAME` environment variables. [More info](https://aka.ms/teams-toolkit-5.0-upgrade#provision-apim-service)
You can [view these changes on GitHub](https://aka.ms/teams-toolkit-5.0-upgrade#feature-changes-that-impact-your-development-flow).
## Changes to your project file structure
1. Created `teamsapp.yml` and `teamsapp.local.yml` in your project's root directory.
2. Moved environment files in `.fx` to `.env.{env}` in `env` folder.
3. If your project contains file `.fx/states/{env}.userdata`, the content will be moved to `.env.{env}.user` in `env` folder
4. Moved `templates/appPackage` to `appPackage`, renamed `manifest.template.json` to `manifest.json` and placeholders in it will be updated using the latest default conventions.
5. If your project contains `templates/appPackage/aad.template.json`, it will be moved and renamed to `aad.manifest.json` and the templated variables names are updated to the latest default conventions.
6. If your project contains file `.vscode/tasks.json` and `.vscode/launch.json`, they will be updated.
7. Updated `.gitignore` to ignore new environment user files.
8. Removed `.fx` folder.
You can [view these changes on GitHub](https://aka.ms/teams-toolkit-5.0-upgrade#file-changes).
## Known issues
1. If your project only contains a bot, you might get an error about `STATE__FX_RESOURCE_FRONTEND_HOSTING__ENDPOINT` missing when running Provision or using Start Debugging. Find this placeholder variable in `appPackage/manifest.json` and replace it with a valid URL to resolve this issue. [More Info](https://aka.ms/teams-toolkit-5.0-upgrade#state__fx_resource_frontend_hosting__endpoint-missing-error-in-some-projects)
2. If your project is created with Visual Studio version < 17.4, you might get an error like `InvalidParameter: Following parameter is missing or invalid for aadApp/create action: name` when running commands. [Try these steps](#how-to-roll-back-after-the-upgrade) to roll back the changes, install VS 17.4, and run the upgrade again.
3. If your tab app is created with Teams Toolkit 3.2.0 or an earlier version, you may get an error like `simpleAuthEndpoint in configuration is invalid` when remote debugging your app. [Try these steps](https://aka.ms/teams-toolkit-5.0-upgrade#simpleauthendpoint-in-configuration-is-invalid) to learn how to mitigate this error.
4. If your project was successfully provisioned before, but after upgrading it cannot be provisioned or published using the `teamsApp/validateAppPackage` action, try using the [validation report in Teams Developer Portal](https://dev.teams.microsoft.com/validation) to check the manifest for errors. [More Info](https://aka.ms/teams-toolkit-5.0-upgrade#teamsappvalidateapppackage-failed-error)
## How to roll back after the upgrade
Follow these steps if you want to restore your project configuration after the upgrade is successful or need to use a previous version of Teams Toolkit:
1. Copy everything in the `.backup` folder that was generated during the upgrade to your project root folder.
2. Delete the new files created during the upgrade. The [Changes to your project](#changes-to-your-project) section contains info on everything that was created.
You can [view these steps on GitHub](https://aka.ms/teams-toolkit-5.0-upgrade#how-to-roll-back).
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,65 @@
@page "/bot-auth-end.html"
<html>
<head>
<title>Login End Page</title>
<meta charset="utf-8" />
</head>
<body>
<script src="https://res.cdn.office.net/teams-js/2.17.0/js/MicrosoftTeams.min.js"
integrity="sha384-xp55t/129OsN192JZYLP0rGhzjCF9aYtjY0LVtXvolkDrBe4Jchylp56NrUYJ4S2"
crossorigin="anonymous"></script>
<div id="divError"></div>
<script type="text/javascript">
microsoftTeams.app.initialize().then(() => {
let hashParams = getHashParameters();
if (hashParams["error"]) {
// Authentication failed
handleAuthError(hashParams["error"], hashParams);
} else if (hashParams["code"]) {
// Get the stored state parameter and compare with incoming state
let expectedState = localStorage.getItem("state");
if (expectedState !== hashParams["state"]) {
// State does not match, report error
handleAuthError("StateDoesNotMatch", hashParams);
} else {
microsoftTeams.authentication.notifySuccess();
}
} else {
// Unexpected condition: hash does not contain error or access_token parameter
handleAuthError("UnexpectedFailure", hashParams);
}
});
// Parse hash parameters into key-value pairs
function getHashParameters() {
let hashParams = {};
location.hash
.substr(1)
.split("&")
.forEach(function (item) {
let s = item.split("="),
k = s[0],
v = s[1] && decodeURIComponent(s[1]);
hashParams[k] = v;
});
return hashParams;
}
// Show error information
function handleAuthError(errorType, errorMessage) {
const err = JSON.stringify({
error: errorType,
message: JSON.stringify(errorMessage),
});
let para = document.createElement("p");
let node = document.createTextNode(err);
para.appendChild(node);
let element = document.getElementById("divError");
element.appendChild(para);
}
</script>
</body>
</html>
@@ -0,0 +1,168 @@
@page "/bot-auth-start"
<!--This file is used during the Teams Bot authentication flow to assist with retrieval of the access token.-->
<!--If you're not familiar with this, do not alter or remove this file from your project.-->
<html>
<head>
<title>Login Start Page</title>
<meta charset="utf-8" />
</head>
<body>
<script type="text/javascript">
popUpSignInWindow();
async function popUpSignInWindow() {
// Generate random state string and store it, so we can verify it in the callback
let state = _guid();
localStorage.setItem("state", state);
localStorage.removeItem("codeVerifier");
var currentURL = new URL(window.location);
var clientId = currentURL.searchParams.get("clientId");
var tenantId = currentURL.searchParams.get("tenantId");
var loginHint = currentURL.searchParams.get("loginHint");
if (!loginHint) {
loginHint = "";
}
var scope = currentURL.searchParams.get("scope");
var originalCode = _guid();
var codeChallenge = await pkceChallengeFromVerifier(originalCode);
localStorage.setItem("codeVerifier", originalCode);
let queryParams = {
client_id: clientId,
response_type: "code",
response_mode: "fragment",
scope: scope,
redirect_uri: window.location.origin + "/bot-auth-end.html",
nonce: _guid(),
login_hint: loginHint,
state: state,
code_challenge: codeChallenge,
code_challenge_method: "S256",
};
let authorizeEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${toQueryString(
queryParams
)}`;
window.location.assign(authorizeEndpoint);
}
// Build query string from map of query parameter
function toQueryString(queryParams) {
let encodedQueryParams = [];
for (let key in queryParams) {
encodedQueryParams.push(key + "=" + encodeURIComponent(queryParams[key]));
}
return encodedQueryParams.join("&");
}
// Converts decimal to hex equivalent
// (From ADAL.js: https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/dev/lib/adal.js)
function _decimalToHex(number) {
var hex = number.toString(16);
while (hex.length < 2) {
hex = "0" + hex;
}
return hex;
}
// Generates RFC4122 version 4 guid (128 bits)
// (From ADAL.js: https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/dev/lib/adal.js)
function _guid() {
// RFC4122: The version 4 UUID is meant for generating UUIDs from truly-random or
// pseudo-random numbers.
// The algorithm is as follows:
// Set the two most significant bits (bits 6 and 7) of the
// clock_seq_hi_and_reserved to zero and one, respectively.
// Set the four most significant bits (bits 12 through 15) of the
// time_hi_and_version field to the 4-bit version number from
// Section 4.1.3. Version4
// Set all the other bits to randomly (or pseudo-randomly) chosen
// values.
// UUID = time-low "-" time-mid "-"time-high-and-version "-"clock-seq-reserved and low(2hexOctet)"-" node
// time-low = 4hexOctet
// time-mid = 2hexOctet
// time-high-and-version = 2hexOctet
// clock-seq-and-reserved = hexOctet:
// clock-seq-low = hexOctet
// node = 6hexOctet
// Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
// y could be 1000, 1001, 1010, 1011 since most significant two bits needs to be 10
// y values are 8, 9, A, B
var cryptoObj = window.crypto || window.msCrypto; // for IE 11
if (cryptoObj && cryptoObj.getRandomValues) {
var buffer = new Uint8Array(16);
cryptoObj.getRandomValues(buffer);
//buffer[6] and buffer[7] represents the time_hi_and_version field. We will set the four most significant bits (4 through 7) of buffer[6] to represent decimal number 4 (UUID version number).
buffer[6] |= 0x40; //buffer[6] | 01000000 will set the 6 bit to 1.
buffer[6] &= 0x4f; //buffer[6] & 01001111 will set the 4, 5, and 7 bit to 0 such that bits 4-7 == 0100 = "4".
//buffer[8] represents the clock_seq_hi_and_reserved field. We will set the two most significant bits (6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively.
buffer[8] |= 0x80; //buffer[8] | 10000000 will set the 7 bit to 1.
buffer[8] &= 0xbf; //buffer[8] & 10111111 will set the 6 bit to 0.
return (
_decimalToHex(buffer[0]) +
_decimalToHex(buffer[1]) +
_decimalToHex(buffer[2]) +
_decimalToHex(buffer[3]) +
"-" +
_decimalToHex(buffer[4]) +
_decimalToHex(buffer[5]) +
"-" +
_decimalToHex(buffer[6]) +
_decimalToHex(buffer[7]) +
"-" +
_decimalToHex(buffer[8]) +
_decimalToHex(buffer[9]) +
"-" +
_decimalToHex(buffer[10]) +
_decimalToHex(buffer[11]) +
_decimalToHex(buffer[12]) +
_decimalToHex(buffer[13]) +
_decimalToHex(buffer[14]) +
_decimalToHex(buffer[15])
);
} else {
var guidHolder = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
var hex = "0123456789abcdef";
var r = 0;
var guidResponse = "";
for (var i = 0; i < 36; i++) {
if (guidHolder[i] !== "-" && guidHolder[i] !== "4") {
// each x and y needs to be random
r = (Math.random() * 16) | 0;
}
if (guidHolder[i] === "x") {
guidResponse += hex[r];
} else if (guidHolder[i] === "y") {
// clock-seq-and-reserved first hex is filtered and remaining hex values are random
r &= 0x3; // bit and with 0011 to set pos 2 to zero ?0??
r |= 0x8; // set pos 3 to 1 as 1???
guidResponse += hex[r];
} else {
guidResponse += guidHolder[i];
}
}
return guidResponse;
}
}
// Calculate the SHA256 hash of the input text.
// Returns a promise that resolves to an ArrayBuffer
function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest("SHA-256", data);
}
// Base64-urlencodes the input string
function base64urlencode(str) {
// Convert the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
// Then convert the base64 encoded to base64url encoded
// (replace + with -, replace / with _, trim trailing =)
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
// Return the base64-urlencoded sha256 hash for the PKCE challenge
async function pkceChallengeFromVerifier(v) {
hashed = await sha256(v);
return base64urlencode(hashed);
}
</script>
</body>
</html>
@@ -0,0 +1,230 @@
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Options;
using Microsoft.TeamsFx.Bot;
using Microsoft.TeamsFx.Configuration;
namespace {{YOUR_NAMESPACE}}.SSO;
public class DialogConstants
{
internal static readonly string COMMAND_ROUTE_DIALOG = "CommandRouteDialog";
internal static readonly string TEAMS_SSO_PROMPT = "TeamsFxSsoPrompt";
}
public class StoreItem
{
public string eTag;
}
public class SsoDialog : ComponentDialog
{
ILogger<SsoDialog> _logger;
BotAuthenticationOptions _botAuthOptions;
Dictionary<string, string> _commandMapping = new Dictionary<string, string>();
IStorage _dedupStorage = new MemoryStorage();
List<string> _dedupStorageKeys = new List<string>() {};
public SsoDialog(IOptions<BotAuthenticationOptions> botAuthenticationOptions, ILogger<SsoDialog> logger)
{
_logger = logger;
InitialDialogId = DialogConstants.COMMAND_ROUTE_DIALOG;
try
{
_logger.LogTrace("Validate bot authentication configuration");
_botAuthOptions = botAuthenticationOptions.Value;
}
catch (OptionsValidationException e)
{
throw new Exception($"Bot authentication config is missing or not correct with error: {e.Message}");
}
var settings = new TeamsBotSsoPromptSettings(_botAuthOptions, new string[] { "User.Read" });
AddDialog(new TeamsBotSsoPrompt(DialogConstants.TEAMS_SSO_PROMPT, settings));
WaterfallDialog commandRouteDialog = new WaterfallDialog(
DialogConstants.COMMAND_ROUTE_DIALOG,
new WaterfallStep[]
{
CommandRouteStepAsync
});
AddDialog(commandRouteDialog);
_logger.LogInformation("Construct Main Dialog");
}
public async Task RunAsync(ITurnContext context, IStatePropertyAccessor<DialogState> accessor)
{
DialogSet dialogSet = new DialogSet(accessor);
dialogSet.Add(this);
DialogContext dialogContext = await dialogSet.CreateContextAsync(context);
DialogTurnResult results = await dialogContext.ContinueDialogAsync();
if (results != null && results.Status == DialogTurnStatus.Empty)
{
await dialogContext.BeginDialogAsync(Id);
}
}
public void addCommand(
string commandId,
string commandText,
Func<ITurnContext, string, BotAuthenticationOptions, Task> operation)
{
if (_commandMapping.ContainsValue(commandId))
{
return;
}
_commandMapping.Add(commandText, commandId);
WaterfallDialog dialog = new WaterfallDialog(
commandId,
new WaterfallStep[]
{
PromptStepAsync,
DedupStepAsync,
async (WaterfallStepContext stepContext, CancellationToken cancellationToken) => {
TeamsBotSsoPromptTokenResponse tokenResponce = (TeamsBotSsoPromptTokenResponse)stepContext.Result;
var turnContext = stepContext.Context;
try
{
if (tokenResponce != null)
{
await operation(turnContext, tokenResponce.Token, _botAuthOptions);
} else
{
await turnContext.SendActivityAsync("Failed to retrieve user token from conversation context.");
}
return await stepContext.EndDialogAsync();
} catch (Exception error)
{
await turnContext.SendActivityAsync("Failed to retrieve user token from conversation context.");
await turnContext.SendActivityAsync(error.Message);
return await stepContext.EndDialogAsync();
}
}
});
AddDialog(dialog);
}
protected override async Task OnEndDialogAsync(ITurnContext context, DialogInstance instance, DialogReason reason, CancellationToken cancellationToken = default(CancellationToken))
{
var conversationId = context.Activity.Conversation.Id;
var currentDedupKeys = _dedupStorageKeys.Where((key) => key.IndexOf(conversationId) > 0).ToArray();
await _dedupStorage.DeleteAsync(currentDedupKeys);
_dedupStorageKeys = _dedupStorageKeys.Where((key) => key.IndexOf(conversationId) < 0).ToList<string>();
}
private async Task<DialogTurnResult> PromptStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
_logger.LogInformation("Step: Prompt to get SSO token");
try
{
return await stepContext.BeginDialogAsync(DialogConstants.TEAMS_SSO_PROMPT, null, cancellationToken);
} catch (Exception error)
{
await stepContext.Context.SendActivityAsync("Failed to run SSO prompt");
await stepContext.Context.SendActivityAsync(error.Message);
return await stepContext.EndDialogAsync();
}
}
private async Task<DialogTurnResult> CommandRouteStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
_logger.LogInformation("Step: Route to pre-added commands");
var turnContext = stepContext.Context;
string text = turnContext.Activity.RemoveRecipientMention();
if (text != null)
{
text = text.ToLower().Trim();
}
string commandId = MatchCommands(text);
if (commandId != null)
{
return await stepContext.BeginDialogAsync(commandId);
}
await stepContext.Context.SendActivityAsync(String.Format("Cannot find command: {0}", text));
return await stepContext.EndDialogAsync();
}
private async Task<DialogTurnResult> DedupStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
try
{
var tokenResponse = stepContext.Result;
if (tokenResponse != null && (await ShouldDedup(stepContext.Context)))
{
return EndOfTurn;
}
return await stepContext.NextAsync(tokenResponse);
}
catch (Exception error)
{
await stepContext.Context.SendActivityAsync("Failed to run dedup step");
await stepContext.Context.SendActivityAsync(error.Message);
return await stepContext.EndDialogAsync();
}
}
private async Task<bool> ShouldDedup(ITurnContext context)
{
var storeItem = new StoreItem()
{
eTag = (context.Activity.Value as dynamic).id,
};
var key = GetStorageKey(context);
var storeItems = new Dictionary<string, object>()
{
{key, storeItem}
};
var res = await _dedupStorage.ReadAsync(new string[] { key });
if (res.Count != 0)
{
return true;
}
await _dedupStorage.WriteAsync(storeItems);
_dedupStorageKeys.Add(key);
return false;
}
private string GetStorageKey(ITurnContext context)
{
if (context == null || context.Activity == null || context.Activity.Conversation == null)
{
throw new Exception("Invalid context, can not get storage key!");
}
var activity = context.Activity;
var channelId = activity.ChannelId;
var conversationId = activity.Conversation.Id;
if (activity.Type != ActivityTypes.Invoke || activity.Name != SignInConstants.TokenExchangeOperationName)
{
throw new Exception("TokenExchangeState can only be used with Invokes of signin/tokenExchange.");
}
var value = activity.Value;
if (value == null || (value as dynamic).id == null)
{
throw new Exception("Invalid signin/tokenExchange. Missing activity.value.id.");
}
return $"{channelId}/{conversationId}/{(value as dynamic).id}";
}
private string MatchCommands(string text)
{
if (_commandMapping.ContainsKey(text))
{
return _commandMapping[text];
}
return null;
}
}
@@ -0,0 +1,37 @@
using Microsoft.Bot.Builder;
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.TeamsFx.Configuration;
namespace {{YOUR_NAMESPACE}}.SSO;
public class TokenProvider : IAccessTokenProvider
{
private string token { get; set; }
public TokenProvider(String token)
{
this.token = token;
}
public Task<string> GetAuthorizationTokenAsync(Uri uri, Dictionary<string, object> additionalAuthenticationContext = default,
CancellationToken cancellationToken = default)
{
// get the token and return it
return Task.FromResult(this.token);
}
public AllowedHostsValidator AllowedHostsValidator { get; }
}
public static class SsoOperations
{
public static async Task ShowUserInfo(ITurnContext stepContext, string token, BotAuthenticationOptions botAuthOptions)
{
await stepContext.SendActivityAsync("Retrieving user information from Microsoft Graph ...");
var tokenCredential = new BaseBearerTokenAuthenticationProvider(new TokenProvider(token));
var graphClient = new GraphServiceClient(tokenCredential);
var profile = await graphClient.Me.GetAsync();
await stepContext.SendActivityAsync($"You're logged in as {profile.DisplayName}");
}
}
@@ -0,0 +1,50 @@
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Teams;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Builder.Dialogs;
namespace {{YOUR_NAMESPACE}}.SSO;
public class TeamsSsoBot<T> : TeamsActivityHandler where T : Dialog
{
private readonly ILogger<TeamsSsoBot<T>> _logger;
private readonly BotState _conversationState;
private readonly Dialog _dialog;
private readonly IStatePropertyAccessor<DialogState> _dialogState;
public TeamsSsoBot(ConversationState conversationState, T dialog, ILogger<TeamsSsoBot<T>> logger)
{
Console.WriteLine("sso bot init");
_conversationState = conversationState;
_dialog = dialog;
_logger = logger;
_dialogState = _conversationState.CreateProperty<DialogState>("DialogState");
((SsoDialog)_dialog).addCommand("showUserInfo", "show", SsoOperations.ShowUserInfo);
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
Console.WriteLine("Receive message activity");
_logger.LogInformation("Receive message activity");
await ((SsoDialog)_dialog).RunAsync(turnContext, _dialogState);
}
protected override async Task OnTeamsSigninVerifyStateAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
_logger.LogInformation("Receive invoke activity of teams sign in verify state");
await _dialog.RunAsync(turnContext, _conversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}
protected override async Task OnSignInInvokeAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
_logger.LogInformation("Receive invoke activity of sign in");
await _dialog.RunAsync(turnContext, _conversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken).ConfigureAwait(false);
}
}
@@ -0,0 +1,16 @@
Enable Single Sign-on for Teams Applications
-------------------------
On click of Visual Studio menu Project -> Teams Toolkit -> Add Authentication Code, couple of files for Single Sign-on are generated in "TeamsFx-Auth" folder, including a manifest template file for Microsoft Entra application and authentication redirect pages.
Teams Toolkit helps you generate the authentication files, then you will need to link the files to your Teams application by updating authentication configurations to make sure the Single Sign-on works for your application. Please be noted that for different Teams application type like Tab or Bot, the detailed steps are slightly different.
Basically, you will need to take care of these configurations:
* In the Microsoft Entra manifest file, you need to specify URIs such as the URI to identify the Microsoft Entra authentication app and the redirect URI for returning token.
* In the Teams manifest file, add the SSO application to link it with Teams application.
* Add SSO application information in Teams Toolkit configuration files in order to make sure the authentication app can be registered on backend service and started by Teams Toolkit when you debugging or previewing Teams application.
Refer to the step-by-step guide for Teams Tab Application at https://aka.ms/teams-toolkit-vs-add-SSO-tab.
Refer to the step-by-step guide for Teams Bot Applications at https://aka.ms/teams-toolkit-vs-add-SSO-bot. This guide use Command and Response bot as an example to show case how to enable SSO.
@@ -0,0 +1,98 @@
@using System.IO
@using Azure.Core
@using Microsoft.Graph
@using Microsoft.Graph.Models
@inject TeamsFx teamsfx
@inject TeamsUserCredential teamsUserCredential
<div>
<h2>Get the user's profile</h2>
@if (NeedConsent)
{
<p>Click below to authorize this app to read your profile photo using Microsoft Graph.</p>
<FluentButton Appearance="Appearance.Accent" Disabled="@IsLoading" @onclick="ConsentAndShow">Authorize</FluentButton>
}
else if (!string.IsNullOrEmpty(@ErrorMessage))
{
<div class="error">@ErrorMessage</div>
}
else if (Profile != null)
{
<div>Hello @Profile.DisplayName</div>
}
</div>
@code {
[Parameter]
public string ErrorMessage { get; set; }
public bool IsLoading { get; set; }
public bool NeedConsent { get; set; }
public User Profile { get; set; }
private readonly string _scope = "User.Read";
protected override async Task OnInitializedAsync()
{
IsLoading = true;
if (await HasPermission(_scope))
{
await ShowProfile();
}
}
private async Task ShowProfile()
{
IsLoading = true;
var graph = GetGraphServiceClient();
Profile = await graph.Me.GetAsync();
IsLoading = false;
ErrorMessage = string.Empty;
}
private async Task ConsentAndShow()
{
try
{
await teamsUserCredential.LoginAsync(_scope);
NeedConsent = false;
await ShowProfile();
}
catch (ExceptionWithCode e)
{
ErrorMessage = e.Message;
}
}
private async Task<bool> HasPermission(string scope)
{
IsLoading = true;
try
{
await teamsUserCredential.GetTokenAsync(new TokenRequestContext(new string[] { _scope }), new System.Threading.CancellationToken());
return true;
}
catch (ExceptionWithCode e)
{
if (e.Code == ExceptionCode.UiRequiredError)
{
NeedConsent = true;
}
else
{
ErrorMessage = e.Message;
}
}
IsLoading = false;
return false;
}
private GraphServiceClient GetGraphServiceClient()
{
var client = new GraphServiceClient(teamsUserCredential, new string[] { _scope });
return client;
}
}
@@ -0,0 +1,102 @@
{
"id": "${{AAD_APP_OBJECT_ID}}",
"appId": "${{AAD_APP_CLIENT_ID}}",
"name": "YOUR_AAD_APP_NAME",
"accessTokenAcceptedVersion": 2,
"signInAudience": "AzureADMyOrg",
"optionalClaims": {
"idToken": [],
"accessToken": [
{
"name": "idtyp",
"source": null,
"essential": false,
"additionalProperties": []
}
],
"saml2Token": []
},
"requiredResourceAccess": [
{
"resourceAppId": "Microsoft Graph",
"resourceAccess": [
{
"id": "User.Read",
"type": "Scope"
}
]
}
],
"oauth2Permissions": [
{
"adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.",
"adminConsentDisplayName": "Teams can access app's web APIs",
"id": "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}",
"isEnabled": true,
"type": "User",
"userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have",
"userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf",
"value": "access_as_user"
}
],
"preAuthorizedApplications": [
{
"appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "00000002-0000-0ff1-ce00-000000000000",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "0ec893e0-5785-4de6-99da-4ed124e5296c",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "4765445b-32c6-49b0-83e6-1d93765276ca",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "4345a7b9-9a63-4910-a426-35363201d503",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "27922004-5251-4030-b22d-91ecd9a37ea4",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
}
],
"identifierUris": [
],
"replyUrlsWithType": [
]
}
@@ -0,0 +1,434 @@
# Enable single sign-on for bot applications
> Note: This document includes single sign-on instructions applicable for both bot and message extension. Make sure to add the corresponding Teams capability first and then follow the documentation.
Microsoft Teams provides a mechanism by which an application can obtain the signed-in Teams user token to access Microsoft Graph (and other APIs). Teams Toolkit facilitates this interaction by abstracting some of the Microsoft Entra ID flows and integrations behind some simple, high level APIs. This enables you to add single sign-on (SSO) features easily to your Teams application.
For a bot application, user can invoke the Microsoft Entra consent flow to obtain sso token to call Graph and other APIs.
<h2>Contents </h2>
- [Changes to your project](#1)
- [Update code to Use SSO for Bot](#2)
- [Set up the Microsoft Entra redirects](#2.1)
- [Update your business logic](#2.2)
- [(Optional) Add a new sso command to the bot](#2.3)
- [Update code to Use SSO for Message Extension](#3)
- [Debug your application](#4)
- [Customize Microsoft Entra applications](#5)
- [Trouble Shooting](#6)
<h2 id='1'>Changes to your project</h2>
When you added the SSO feature to your application, Teams Toolkit updated your project to support SSO:
After you successfully added SSO into your project, Teams Toolkit will create and modify some files that helps you implement SSO feature.
| Action | File | Description |
| ------ | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Modify | `azureWebAppBotConfig.bicep` under `templates/azure/teamsFx` and `azure.parameters.dev.json` under `.fx/configs` | Insert environment variables used for bot web app to enable SSO feature |
| Modify | `manifest.template.json` under `templates/appPackage` | An `webApplicationInfo` object will be added into your Teams app manifest template. This field is required by Teams when enabling SSO. |
| Modify | `projectSettings.json` under `.fx/configs` | Add bot sso capability, which will be used internally by Teams Toolkit. |
| Create | `aad.template.json` under `templates/appPackage` | The Microsoft Entra application manifest that is used to register the application with Microsoft Entra. |
| Create | `auth/bot` | Reference code, redirect pages and a `README.md` file. These files are provided for reference. See below for more information. |
<h2 id='2'>Update your code to Use SSO for Bot</h2>
As described above, the Teams Toolkit generated some configuration to set up your application for SSO, but you need to update your application business logic to take advantage of the SSO feature as appropriate.
> Note: The following part is for `command and response bot`. For `basic bot`, please refer to the [bot-sso sample](https://aka.ms/bot-sso-sample).
<h3 id='2.1'>Set up the Microsoft Entra redirects</h3>
1. Move the `auth/bot/public` folder to `bot/src`. This folder contains HTML pages that the bot application hosts. When single sign-on flows are initiated with Microsoft Entra, Microsoft Entra will redirect the user to these pages.
1. Modify your `bot/src/index` to add the appropriate `express` routes to these pages.
```ts
const path = require("path");
const send = require("send");
expressApp.get(["/auth-start.html", "/auth-end.html"], async (req, res) => {
send(
req,
path.join(
__dirname,
"public",
req.url.includes("auth-start.html") ? "auth-start.html" : "auth-end.html"
)
).pipe(res);
});
```
<h3 id='2.2'>Update your business logic</h3>
The sample business logic provides a sso command handler `ProfileSsoCommandHandler` that use a Microsoft Entra token to call Microsoft Graph. This token is obtained by using the logged-in Teams user token. The flow is brought together in a dialog that will display a consent dialog if required.
To make this work in your application:
1. Move `profileSsoCommandHandler` file under `auth/bot/sso` folder to `bot/src`. ProfileSsoCommandHandler class is a sso command handler to get user info with SSO token. You can follow this method and create your own sso command handler.
1. Open `package.json` file, make sure that teamsfx SDK version >= 2.2.0
1. Execute the following commands under `bot` folder: `npm install isomorphic-fetch --save`
1. (For ts only) Execute the following commands under `bot` folder: `npm install copyfiles --save-dev` and replace following line in package.json:
```json
"build": "tsc --build && shx cp -r ./src/adaptiveCards ./lib/src",
```
with:
```json
"build": "tsc --build && shx cp -r ./src/adaptiveCards ./lib/src && copyfiles src/public/*.html lib/",
```
By doing this, the HTML pages used for auth redirect will be copied when building this bot project.
1. After adding the following files, you need to update `bot/src/index` file.
Please replace the following code to make sso consent flow works:
```ts
server.post("/api/messages", async (req, res) => {
await commandBot.requestHandler(req, res);
});
```
with:
```ts
server.post("/api/messages", async (req, res) => {
await commandBot.requestHandler(req, res).catch((err) => {
// Error message including "412" means it is waiting for user's consent, which is a normal process of SSO, sholdn't throw this error.
if (!err.message.includes("412")) {
throw err;
}
});
});
```
1. Replace the options for `ConversationBot` instance in `bot/src/internal/initialize` to add the sso config and sso command handler:
```ts
export const commandBot = new ConversationBot({
...
command: {
enabled: true,
commands: [new HelloWorldCommandHandler()],
},
});
```
with:
```ts
import { ProfileSsoCommandHandler } from "../profileSsoCommandHandler";
export const commandBot = new ConversationBot({
...
// To learn more about ssoConfig, please refer teamsfx sdk document: https://docs.microsoft.com/microsoftteams/platform/toolkit/teamsfx-sdk
ssoConfig: {
aad :{
scopes:["User.Read"],
},
},
command: {
enabled: true,
commands: [new HelloWorldCommandHandler() ],
ssoCommands: [new ProfileSsoCommandHandler()],
},
});
```
1. Register your command in the Teams app manifest. Open `templates/appPackage/manifest.template.json`, and add following lines under `commands` in `commandLists` of your bot:
```json
{
"title": "profile",
"description": "Show user profile using Single Sign On feature"
}
```
<h3 id='2.3'>(Optional) Add a new sso command to the bot</h3>
After successfully add SSO in your project, you can also add a new sso command.
1. Create a new file (e.g. `photoSsoCommandHandler.ts` or `photoSsoCommandHandler.js`) under `bot/src/` and add your own business logic to call Graph API:
```TypeScript
// for TypeScript:
import { Activity, TurnContext, ActivityTypes } from "botbuilder";
import "isomorphic-fetch";
import {
CommandMessage,
TriggerPatterns,
TeamsFxBotSsoCommandHandler,
TeamsBotSsoPromptTokenResponse,
OnBehalfOfUserCredential,
OnBehalfOfCredentialAuthConfig,
} from "@microsoft/teamsfx";
import { Client } from "@microsoft/microsoft-graph-client";
import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
const oboAuthConfig: OnBehalfOfCredentialAuthConfig = {
authorityHost: process.env.M365_AUTHORITY_HOST,
clientId: process.env.M365_CLIENT_ID,
tenantId: process.env.M365_TENANT_ID,
clientSecret: process.env.M365_CLIENT_SECRET,
};
export class PhotoSsoCommandHandler implements TeamsFxBotSsoCommandHandler {
triggerPatterns: TriggerPatterns = "photo";
async handleCommandReceived(
context: TurnContext,
message: CommandMessage,
tokenResponse: TeamsBotSsoPromptTokenResponse,
): Promise<string | Partial<Activity> | void> {
await context.sendActivity("Retrieving user information from Microsoft Graph ...");
// Init OnBehalfOfUserCredential instance with SSO token
const oboCredential = new OnBehalfOfUserCredential(tokenResponse.ssoToken, oboAuthConfig);
// Create an instance of the TokenCredentialAuthenticationProvider by passing the tokenCredential instance and options to the constructor
const authProvider = new TokenCredentialAuthenticationProvider(oboCredential, {
scopes: ["User.Read"],
});
// Initialize Graph client instance with authProvider
const graphClient = Client.initWithMiddleware({
authProvider: authProvider,
});
let photoUrl = "";
try {
const photo = await graphClient.api("/me/photo/$value").get();
const arrayBuffer = await photo.arrayBuffer();
const buffer=Buffer.from(arrayBuffer, 'binary');
photoUrl = "data:image/png;base64," + buffer.toString("base64");
} catch {
// Could not fetch photo from user's profile, return empty string as placeholder.
}
if (photoUrl) {
const photoMessage: Partial<Activity> = {
type: ActivityTypes.Message,
text: 'This is your photo:',
attachments: [
{
name: 'photo.png',
contentType: 'image/png',
contentUrl: photoUrl
}
]
};
return photoMessage;
} else {
return "Could not retrieve your photo from Microsoft Graph. Please make sure you have uploaded your photo.";
}
}
}
```
```javascript
// for JavaScript:
const { ActivityTypes } = require("botbuilder");
require("isomorphic-fetch");
const { OnBehalfOfUserCredential } = require("@microsoft/teamsfx");
const { Client } = require("@microsoft/microsoft-graph-client");
const {
TokenCredentialAuthenticationProvider,
} = require("@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials");
const oboAuthConfig = {
authorityHost: process.env.M365_AUTHORITY_HOST,
clientId: process.env.M365_CLIENT_ID,
tenantId: process.env.M365_TENANT_ID,
clientSecret: process.env.M365_CLIENT_SECRET,
};
class PhotoSsoCommandHandler {
triggerPatterns = "photo";
async handleCommandReceived(context, message, tokenResponse) {
await context.sendActivity("Retrieving user information from Microsoft Graph ...");
// Init OnBehalfOfUserCredential instance with SSO token
const oboCredential = new OnBehalfOfUserCredential(tokenResponse.ssoToken, oboAuthConfig);
// Create an instance of the TokenCredentialAuthenticationProvider by passing the tokenCredential instance and options to the constructor
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: ["User.Read"],
});
// Initialize Graph client instance with authProvider
const graphClient = Client.initWithMiddleware({
authProvider: authProvider,
});
let photoUrl = "";
try {
const photo = await graphClient.api("/me/photo/$value").get();
const arrayBuffer = await photo.arrayBuffer();
const buffer = Buffer.from(arrayBuffer, "binary");
photoUrl = "data:image/png;base64," + buffer.toString("base64");
} catch {
// Could not fetch photo from user's profile, return empty string as placeholder.
}
if (photoUrl) {
const photoMessage = {
type: ActivityTypes.Message,
text: "This is your photo:",
attachments: [
{
name: "photo.png",
contentType: "image/png",
contentUrl: photoUrl,
},
],
};
return photoMessage;
} else {
return "Could not retrieve your photo from Microsoft Graph. Please make sure you have uploaded your photo.";
}
}
}
module.exports = {
PhotoSsoCommandHandler,
};
```
1. Put `PhotoSsoCommandHandler` instance to `ssoCommands` array in `bot/src/internal/initialize.ts` as below:
```ts
// for TypeScript:
import { PhotoSsoCommandHandler } from "../photoSsoCommandHandler";
export const commandBot = new ConversationBot({
...
command: {
...
ssoCommands: [new ProfileSsoCommandHandler(), new PhotoSsoCommandHandler()],
},
});
```
```javascript
// for JavaScript:
...
const { PhotoSsoCommandHandler } = require("../photoSsoCommandHandler");
const commandBot = new ConversationBot({
...
command: {
...
ssoCommands: [new ProfileSsoCommandHandler(), new PhotoSsoCommandHandler()]
},
});
...
```
1. Register your command in the Teams app manifest. Open 'templates/appPackage/manifest.template.json', and add following lines under `commands` in `commandLists` of your bot:
```json
{
"title": "photo",
"description": "Show user photo using Single Sign On feature"
}
```
<h2 id='3'>Update your business logic for Message Extension</h2>
The sample business logic provides a handler `TeamsBot` extends TeamsActivityHandler and override `handleTeamsMessagingExtensionQuery`.
You can update the query logic in the `handleMessageExtensionQueryWithSSO` with token which is obtained by using the logged-in Teams user token.
To make this work in your application:
1. Move the `auth/bot/public` folder to `bot`. This folder contains HTML pages that the bot application hosts. When single sign-on flows are initiated with Microsoft Entra, Microsoft Entra will redirect the user to these pages.
1. Modify your `bot/index` to add the appropriate `express` routes to these pages.
```ts
const path = require("path");
const send = require("send");
// Listen for incoming requests.
expressApp.post("/api/messages", async (req, res) => {
await adapter
.process(req, res, async (context) => {
await bot.run(context);
})
.catch((err) => {
// Error message including "412" means it is waiting for user's consent, which is a normal process of SSO, sholdn't throw this error.
if (!err.message.includes("412")) {
throw err;
}
});
});
expressApp.get(["/auth-start.html", "/auth-end.html"], async (req, res) => {
send(
req,
path.join(
__dirname,
"public",
req.url.includes("auth-start.html") ? "auth-start.html" : "auth-end.html"
)
).pipe(res);
});
```
1. Override `handleTeamsMessagingExtensionQuery` interface under `bot/teamsBot`. You can follow the sample code in the `handleMessageExtensionQueryWithSSO` to do your own query logic.
1. Open `bot/package.json`, ensure that `@microsoft/teamsfx` version >= 2.2.0
1. Install `isomorphic-fetch` npm packages in your bot project.
1. (For ts only) Install `copyfiles` npm packages in your bot project, add or update the `build` script in `bot/package.json` as following
```json
"build": "tsc --build && copyfiles ./public/*.html lib/",
```
By doing this, the HTML pages used for auth redirect will be copied when building this bot project.
1. Update `templates/appPackage/aad.template.json` your scopes which used in `handleMessageExtensionQueryWithSSO`.
```json
"requiredResourceAccess": [
{
"resourceAppId": "Microsoft Graph",
"resourceAccess": [
{
"id": "User.Read",
"type": "Scope"
}
]
}
]
```
<h2 id='4'>Debug your application </h2>
You can debug your application by pressing F5.
Teams Toolkit will use the Microsoft Entra manifest file to register a Microsoft Entra application registered for SSO.
To learn more about Teams Toolkit local debug functionalities, refer to this [document](https://docs.microsoft.com/microsoftteams/platform/toolkit/debug-local).
<h2 id='5'>Customize Microsoft Entra applications</h2>
The Microsoft Entra [manifest](https://docs.microsoft.com/azure/active-directory/develop/reference-app-manifest) allows you to customize various aspects of your application registration. You can update the manifest as needed.
Follow this [document](https://aka.ms/teamsfx-aad-manifest#customize-aad-manifest-template) if you need to include additional API permissions to access your desired APIs.
Follow this [document](https://aka.ms/teamsfx-aad-manifest#How-to-view-the-AAD-app-on-the-Azure-portal) to view your Microsoft Entra application in Azure Portal.
<h2 id='6'>Trouble Shooting </h2>
<h3>Login page does not pop up after clicking `continue`</h3>
First check whether your auth-start page is available by directly go to "{your-bot-endpoint}/auth-start.html" in your browser. You can find your-bot-endpoint in `.fx/states/state.{env}.json`.
- If the auth-start page can be opened in your browser, please try sign out current account in Teams app page and sign in again and run the command again.
- If encounter with ngrok page below when local debug, please follow the steps to solve this issue.
1. Stop debugging in Visual Studio Code.
1. Sign up an ngrok account in https://dashboard.ngrok.com/signup.
1. Copy your personal ngrok authtoken from https://dashboard.ngrok.com/get-started/your-authtoken.
1. Run `npx ngrok authtoken <your-personal-ngrok-authtoken>` in Visual Studio Code terminal.
1. Start debugging the project again by hitting the F5 key in Visual Studio Code.
![ngrok auth page](https://user-images.githubusercontent.com/63089166/190566043-6957edc9-c5b8-409d-b532-979ee0ef6ce5.png)
@@ -0,0 +1,188 @@
Enable single sign-on for Teams bot applications
-------------------------
For Teams bot application, SSO manifests as an Adaptive Card which the user can interact with to invoke the Microsoft Entra consent flow.
Files generated/updated in your project
-------------------------
1. New file - `aad.template.json` is created in folder `templates/appPackage`
- The Azure Active Directory application manifest that is used to register the application with Microsoft Entra.
2. Update file - 'templates/appPackage/manifest.template.json'
- An `webApplicationInfo` object will be added into your Teams app manifest template. This field is required by Teams when enabling SSO. |
3. New file - `Auth/bot`
- Sample code, redirect pages and a `README.txt` file. These files are provided for reference. See below for more information. |
4. Update file - 'appsettings.json' and 'appsettings.Development.json'
- Configs that will be used by TeamsFx SDK will be added into your app settings. Please update add the 'TeamsFx' object if you have other appsettings files.
Actions required - update your code to add SSO authentication
-------------------------
Note: This part is for `command and response bot`.
1. Please upgrade your SDK and make sure your SDK version:
TeamsFx: >= 1.1.0
Microsoft.Bot.Builder >= 4.17.1
2. Create "Pages" folder and move files in `Auth/bot/Pages` folder to `Pages`
`Auth/bot/Pages` folder contains HTML pages that hosted by bot application. When single sign-on flows are initiated with Microsoft Entra ID, Microsoft Entra ID will redirect the user to these pages.
3. Create "SSO" folder and move files in 'Auth/bot/SSO' folder to 'SSO'
This folder contains two files as reference for SSO implementation:
2.1 SsoDialog.cs: This creates a ComponentDialog that used for SSO.
2.2 TeamsSsoBot.cs: This create a TeamsActivityHandler with `SsoDialog` and add 'showUserInfo' as a command that can be triggered.
2.3 SsoOperations.cs: This implements class with a function to get user info with SSO token. You can follow this method and create your own method that requires SSO token.
Note: Remember to replace '{Your_NameSpace}' with your project namespace.
4. Update 'Program.cs'
4.1 Find code: 'builder.Services.AddSingleton<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();'
and add the following code below:
'''
builder.Services.AddRazorPages();
// Create the Bot Framework Adapter with error handling enabled.
builder.Services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();
builder.Services.AddSingleton<IStorage, MemoryStorage>();
// Create the Conversation state. (Used by the Dialog system itself.)
builder.Services.AddSingleton<ConversationState>();
// The Dialog that will be run by the bot.
builder.Services.AddSingleton<SsoDialog>();
// Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
builder.Services.AddTransient<IBot, TeamsSsoBot<SsoDialog>>();
builder.Services.AddOptions<AuthenticationOptions>().Bind(builder.Configuration.GetSection("TeamsFx").GetSection(AuthenticationOptions.Authentication)).ValidateDataAnnotations();
builder.Services.AddOptions<BotAuthenticationOptions>().Configure<IOptions<AuthenticationOptions>>((botAuthOption, authOptions) => {
AuthenticationOptions authOptionsValue = authOptions.Value;
botAuthOption.ClientId = authOptionsValue.ClientId;
botAuthOption.ClientSecret = authOptionsValue.ClientSecret;
botAuthOption.OAuthAuthority = authOptionsValue.OAuthAuthority;
botAuthOption.ApplicationIdUri = authOptionsValue.ApplicationIdUri;
botAuthOption.InitiateLoginEndpoint = authOptionsValue.Bot.InitiateLoginEndpoint;
}).ValidateDataAnnotations();
'''
4.2 Find the following lines:
'''
builder.Services.AddSingleton<HelloWorldCommandHandler>();
builder.Services.AddSingleton(sp =>
{
var options = new ConversationOptions()
{
Adapter = sp.GetService<CloudAdapter>(),
Command = new CommandOptions()
{
Commands = new List<ITeamsCommandHandler> { sp.GetService<HelloWorldCommandHandler>() }
}
};
return new ConversationBot(options);
});
'''
and replace with:
'''
builder.Services.AddSingleton(sp =>
{
var options = new ConversationOptions()
{
Adapter = sp.GetService<CloudAdapter>(),
Command = new CommandOptions()
{
Commands = new List<ITeamsCommandHandler> { }
}
};
return new ConversationBot(options);
});
'''
4.3 Find and delete the following code:
'''
// Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
builder.Services.AddTransient<IBot, TeamsBot>();
'''
4.4 Find the following code:
'''
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
'''
and replace with:
'''
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorPages();
});
'''
5. Register your command in the Teams app manifest. Open 'Templates/appPackage/manifest.template.json', and add following lines under `commands` in `commandLists` of your bot:
'''
{
"title": "show",
"description": "Show user profile using Single Sign On feature"
}
'''
(Optional) Add a new command to the bot
-------------------------
After successfully add SSO in your project, you can also add a new command.
1. Create a new method in class SsoOperations in 'SSO/SsoOperations' and add your own business logic to call Graph API:
'''
public static async Task GetUserImageInfo(ITurnContext stepContext, string token, BotAuthenticationOptions botAuthOptions)
{
await stepContext.SendActivityAsync("Retrieving user information from Microsoft Graph ...");
var authProvider = new DelegateAuthenticationProvider((request) =>
{
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
return Task.CompletedTask;
});
var graphClient = new GraphServiceClient(authProvider);
// You can add following code to get your photo size:
// var photo = await graphClient.Me.Photo.Request().GetAsync();
// await stepContext.SendActivityAsync($"Size of your photo is: {photo.Width} * {photo.Height}");
}
'''
2. Register a new command using 'addCommand' in 'TeamsSsoBot':
Find the following line:
'''
((SsoDialog)_dialog).addCommand("showUserInfo", "show", SsoOperations.ShowUserInfo);
'''
and add following lines after the above line to register a new command 'photo' and hook up with method 'GetUserImageInfo' added above:
'''
((SsoDialog)_dialog).addCommand("getUserImageInfo", "photo", SsoOperations.GetUserImageInfo);
'''
3. Register your command in the Teams app manifest. Open 'Templates/appPackage/manifest.template.json', and add following lines under `commands` in `commandLists` of your bot:
'''
{
"title": "photo",
"description": "Show user photo size using Single Sign On feature"
}
'''
Debug your application
-------------------------
You can debug your application by:
1. Right-click your project and select Teams Toolkit > Prepare Teams app dependencies
2. If prompted, sign in with an M365 account for the Teams organization you want
to install the app to
3. Press F5, or select the Debug > Start Debugging menu in Visual Studio
4. In the launched browser, select the Add button to load the app in Teams
Teams Toolkit will use the Microsoft Entra manifest file to register a Microsoft Entra application registered for SSO.
To learn more about Teams Toolkit local debug functionalities, refer to https://docs.microsoft.com/microsoftteams/platform/toolkit/debug-local.
Customize Microsoft Entra applications
-------------------------
The Microsoft Entra manifest allows you to customize various aspects of your application registration. You can update the manifest as needed.
Related Doc: https://docs.microsoft.com/azure/active-directory/develop/reference-app-manifest
Follow https://aka.ms/teamsfx-aad-manifest#how-to-customize-the-aad-manifest-template if you need to include additional API permissions to access your desired APIs.
Follow https://aka.ms/teamsfx-aad-manifest#How-to-view-the-AAD-app-on-the-Azure-portal to view your Microsoft Entra application in Azure Portal.
@@ -0,0 +1,65 @@
@page "/bot-auth-end.html"
<html>
<head>
<title>Login End Page</title>
<meta charset="utf-8" />
</head>
<body>
<script src="https://res.cdn.office.net/teams-js/2.17.0/js/MicrosoftTeams.min.js"
integrity="sha384-xp55t/129OsN192JZYLP0rGhzjCF9aYtjY0LVtXvolkDrBe4Jchylp56NrUYJ4S2"
crossorigin="anonymous"></script>
<div id="divError"></div>
<script type="text/javascript">
microsoftTeams.app.initialize().then(() => {
let hashParams = getHashParameters();
if (hashParams["error"]) {
// Authentication failed
handleAuthError(hashParams["error"], hashParams);
} else if (hashParams["code"]) {
// Get the stored state parameter and compare with incoming state
let expectedState = localStorage.getItem("state");
if (expectedState !== hashParams["state"]) {
// State does not match, report error
handleAuthError("StateDoesNotMatch", hashParams);
} else {
microsoftTeams.authentication.notifySuccess();
}
} else {
// Unexpected condition: hash does not contain error or access_token parameter
handleAuthError("UnexpectedFailure", hashParams);
}
});
// Parse hash parameters into key-value pairs
function getHashParameters() {
let hashParams = {};
location.hash
.substr(1)
.split("&")
.forEach(function (item) {
let s = item.split("="),
k = s[0],
v = s[1] && decodeURIComponent(s[1]);
hashParams[k] = v;
});
return hashParams;
}
// Show error information
function handleAuthError(errorType, errorMessage) {
const err = JSON.stringify({
error: errorType,
message: JSON.stringify(errorMessage),
});
let para = document.createElement("p");
let node = document.createTextNode(err);
para.appendChild(node);
let element = document.getElementById("divError");
element.appendChild(para);
}
</script>
</body>
</html>
@@ -0,0 +1,168 @@
@page "/bot-auth-start"
<!--This file is used during the Teams Bot authentication flow to assist with retrieval of the access token.-->
<!--If you're not familiar with this, do not alter or remove this file from your project.-->
<html>
<head>
<title>Login Start Page</title>
<meta charset="utf-8" />
</head>
<body>
<script type="text/javascript">
popUpSignInWindow();
async function popUpSignInWindow() {
// Generate random state string and store it, so we can verify it in the callback
let state = _guid();
localStorage.setItem("state", state);
localStorage.removeItem("codeVerifier");
var currentURL = new URL(window.location);
var clientId = currentURL.searchParams.get("clientId");
var tenantId = currentURL.searchParams.get("tenantId");
var loginHint = currentURL.searchParams.get("loginHint");
if (!loginHint) {
loginHint = "";
}
var scope = currentURL.searchParams.get("scope");
var originalCode = _guid();
var codeChallenge = await pkceChallengeFromVerifier(originalCode);
localStorage.setItem("codeVerifier", originalCode);
let queryParams = {
client_id: clientId,
response_type: "code",
response_mode: "fragment",
scope: scope,
redirect_uri: window.location.origin + "/bot-auth-end.html",
nonce: _guid(),
login_hint: loginHint,
state: state,
code_challenge: codeChallenge,
code_challenge_method: "S256",
};
let authorizeEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${toQueryString(
queryParams
)}`;
window.location.assign(authorizeEndpoint);
}
// Build query string from map of query parameter
function toQueryString(queryParams) {
let encodedQueryParams = [];
for (let key in queryParams) {
encodedQueryParams.push(key + "=" + encodeURIComponent(queryParams[key]));
}
return encodedQueryParams.join("&");
}
// Converts decimal to hex equivalent
// (From ADAL.js: https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/dev/lib/adal.js)
function _decimalToHex(number) {
var hex = number.toString(16);
while (hex.length < 2) {
hex = "0" + hex;
}
return hex;
}
// Generates RFC4122 version 4 guid (128 bits)
// (From ADAL.js: https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/dev/lib/adal.js)
function _guid() {
// RFC4122: The version 4 UUID is meant for generating UUIDs from truly-random or
// pseudo-random numbers.
// The algorithm is as follows:
// Set the two most significant bits (bits 6 and 7) of the
// clock_seq_hi_and_reserved to zero and one, respectively.
// Set the four most significant bits (bits 12 through 15) of the
// time_hi_and_version field to the 4-bit version number from
// Section 4.1.3. Version4
// Set all the other bits to randomly (or pseudo-randomly) chosen
// values.
// UUID = time-low "-" time-mid "-"time-high-and-version "-"clock-seq-reserved and low(2hexOctet)"-" node
// time-low = 4hexOctet
// time-mid = 2hexOctet
// time-high-and-version = 2hexOctet
// clock-seq-and-reserved = hexOctet:
// clock-seq-low = hexOctet
// node = 6hexOctet
// Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
// y could be 1000, 1001, 1010, 1011 since most significant two bits needs to be 10
// y values are 8, 9, A, B
var cryptoObj = window.crypto || window.msCrypto; // for IE 11
if (cryptoObj && cryptoObj.getRandomValues) {
var buffer = new Uint8Array(16);
cryptoObj.getRandomValues(buffer);
//buffer[6] and buffer[7] represents the time_hi_and_version field. We will set the four most significant bits (4 through 7) of buffer[6] to represent decimal number 4 (UUID version number).
buffer[6] |= 0x40; //buffer[6] | 01000000 will set the 6 bit to 1.
buffer[6] &= 0x4f; //buffer[6] & 01001111 will set the 4, 5, and 7 bit to 0 such that bits 4-7 == 0100 = "4".
//buffer[8] represents the clock_seq_hi_and_reserved field. We will set the two most significant bits (6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively.
buffer[8] |= 0x80; //buffer[8] | 10000000 will set the 7 bit to 1.
buffer[8] &= 0xbf; //buffer[8] & 10111111 will set the 6 bit to 0.
return (
_decimalToHex(buffer[0]) +
_decimalToHex(buffer[1]) +
_decimalToHex(buffer[2]) +
_decimalToHex(buffer[3]) +
"-" +
_decimalToHex(buffer[4]) +
_decimalToHex(buffer[5]) +
"-" +
_decimalToHex(buffer[6]) +
_decimalToHex(buffer[7]) +
"-" +
_decimalToHex(buffer[8]) +
_decimalToHex(buffer[9]) +
"-" +
_decimalToHex(buffer[10]) +
_decimalToHex(buffer[11]) +
_decimalToHex(buffer[12]) +
_decimalToHex(buffer[13]) +
_decimalToHex(buffer[14]) +
_decimalToHex(buffer[15])
);
} else {
var guidHolder = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
var hex = "0123456789abcdef";
var r = 0;
var guidResponse = "";
for (var i = 0; i < 36; i++) {
if (guidHolder[i] !== "-" && guidHolder[i] !== "4") {
// each x and y needs to be random
r = (Math.random() * 16) | 0;
}
if (guidHolder[i] === "x") {
guidResponse += hex[r];
} else if (guidHolder[i] === "y") {
// clock-seq-and-reserved first hex is filtered and remaining hex values are random
r &= 0x3; // bit and with 0011 to set pos 2 to zero ?0??
r |= 0x8; // set pos 3 to 1 as 1???
guidResponse += hex[r];
} else {
guidResponse += guidHolder[i];
}
}
return guidResponse;
}
}
// Calculate the SHA256 hash of the input text.
// Returns a promise that resolves to an ArrayBuffer
function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest("SHA-256", data);
}
// Base64-urlencodes the input string
function base64urlencode(str) {
// Convert the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
// Then convert the base64 encoded to base64url encoded
// (replace + with -, replace / with _, trim trailing =)
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
// Return the base64-urlencoded sha256 hash for the PKCE challenge
async function pkceChallengeFromVerifier(v) {
hashed = await sha256(v);
return base64urlencode(hashed);
}
</script>
</body>
</html>
@@ -0,0 +1,230 @@
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Options;
using Microsoft.TeamsFx.Bot;
using Microsoft.TeamsFx.Configuration;
namespace {Your_NameSpace}.SSO;
public class DialogConstants
{
internal static readonly string COMMAND_ROUTE_DIALOG = "CommandRouteDialog";
internal static readonly string TEAMS_SSO_PROMPT = "TeamsFxSsoPrompt";
}
public class StoreItem
{
public string eTag;
}
public class SsoDialog : ComponentDialog
{
ILogger<SsoDialog> _logger;
BotAuthenticationOptions _botAuthOptions;
Dictionary<string, string> _commandMapping = new Dictionary<string, string>();
IStorage _dedupStorage = new MemoryStorage();
List<string> _dedupStorageKeys = new List<string>() {};
public SsoDialog(IOptions<BotAuthenticationOptions> botAuthenticationOptions, ILogger<SsoDialog> logger)
{
_logger = logger;
InitialDialogId = DialogConstants.COMMAND_ROUTE_DIALOG;
try
{
_logger.LogTrace("Validate bot authentication configuration");
_botAuthOptions = botAuthenticationOptions.Value;
}
catch (OptionsValidationException e)
{
throw new Exception($"Bot authentication config is missing or not correct with error: {e.Message}");
}
var settings = new TeamsBotSsoPromptSettings(_botAuthOptions, new string[] { "User.Read" });
AddDialog(new TeamsBotSsoPrompt(DialogConstants.TEAMS_SSO_PROMPT, settings));
WaterfallDialog commandRouteDialog = new WaterfallDialog(
DialogConstants.COMMAND_ROUTE_DIALOG,
new WaterfallStep[]
{
CommandRouteStepAsync
});
AddDialog(commandRouteDialog);
_logger.LogInformation("Construct Main Dialog");
}
public async Task RunAsync(ITurnContext context, IStatePropertyAccessor<DialogState> accessor)
{
DialogSet dialogSet = new DialogSet(accessor);
dialogSet.Add(this);
DialogContext dialogContext = await dialogSet.CreateContextAsync(context);
DialogTurnResult results = await dialogContext.ContinueDialogAsync();
if (results != null && results.Status == DialogTurnStatus.Empty)
{
await dialogContext.BeginDialogAsync(Id);
}
}
public void addCommand(
string commandId,
string commandText,
Func<ITurnContext, string, BotAuthenticationOptions, Task> operation)
{
if (_commandMapping.ContainsValue(commandId))
{
return;
}
_commandMapping.Add(commandText, commandId);
WaterfallDialog dialog = new WaterfallDialog(
commandId,
new WaterfallStep[]
{
PromptStepAsync,
DedupStepAsync,
async (WaterfallStepContext stepContext, CancellationToken cancellationToken) => {
TeamsBotSsoPromptTokenResponse tokenResponce = (TeamsBotSsoPromptTokenResponse)stepContext.Result;
var turnContext = stepContext.Context;
try
{
if (tokenResponce != null)
{
await operation(turnContext, tokenResponce.Token, _botAuthOptions);
} else
{
await turnContext.SendActivityAsync("Failed to retrieve user token from conversation context.");
}
return await stepContext.EndDialogAsync();
} catch (Exception error)
{
await turnContext.SendActivityAsync("Failed to retrieve user token from conversation context.");
await turnContext.SendActivityAsync(error.Message);
return await stepContext.EndDialogAsync();
}
}
});
AddDialog(dialog);
}
protected override async Task OnEndDialogAsync(ITurnContext context, DialogInstance instance, DialogReason reason, CancellationToken cancellationToken = default(CancellationToken))
{
var conversationId = context.Activity.Conversation.Id;
var currentDedupKeys = _dedupStorageKeys.Where((key) => key.IndexOf(conversationId) > 0).ToArray();
await _dedupStorage.DeleteAsync(currentDedupKeys);
_dedupStorageKeys = _dedupStorageKeys.Where((key) => key.IndexOf(conversationId) < 0).ToList<string>();
}
private async Task<DialogTurnResult> PromptStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
_logger.LogInformation("Step: Prompt to get SSO token");
try
{
return await stepContext.BeginDialogAsync(DialogConstants.TEAMS_SSO_PROMPT, null, cancellationToken);
} catch (Exception error)
{
await stepContext.Context.SendActivityAsync("Failed to run SSO prompt");
await stepContext.Context.SendActivityAsync(error.Message);
return await stepContext.EndDialogAsync();
}
}
private async Task<DialogTurnResult> CommandRouteStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
_logger.LogInformation("Step: Route to pre-added commands");
var turnContext = stepContext.Context;
string text = turnContext.Activity.RemoveRecipientMention();
if (text != null)
{
text = text.ToLower().Trim();
}
string commandId = MatchCommands(text);
if (commandId != null)
{
return await stepContext.BeginDialogAsync(commandId);
}
await stepContext.Context.SendActivityAsync(String.Format("Cannot find command: {0}", text));
return await stepContext.EndDialogAsync();
}
private async Task<DialogTurnResult> DedupStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
try
{
var tokenResponse = stepContext.Result;
if (tokenResponse != null && (await ShouldDedup(stepContext.Context)))
{
return EndOfTurn;
}
return await stepContext.NextAsync(tokenResponse);
}
catch (Exception error)
{
await stepContext.Context.SendActivityAsync("Failed to run dedup step");
await stepContext.Context.SendActivityAsync(error.Message);
return await stepContext.EndDialogAsync();
}
}
private async Task<bool> ShouldDedup(ITurnContext context)
{
var storeItem = new StoreItem()
{
eTag = (context.Activity.Value as dynamic).id,
};
var key = GetStorageKey(context);
var storeItems = new Dictionary<string, object>()
{
{key, storeItem}
};
var res = await _dedupStorage.ReadAsync(new string[] { key });
if (res.Count != 0)
{
return true;
}
await _dedupStorage.WriteAsync(storeItems);
_dedupStorageKeys.Add(key);
return false;
}
private string GetStorageKey(ITurnContext context)
{
if (context == null || context.Activity == null || context.Activity.Conversation == null)
{
throw new Exception("Invalid context, can not get storage key!");
}
var activity = context.Activity;
var channelId = activity.ChannelId;
var conversationId = activity.Conversation.Id;
if (activity.Type != ActivityTypes.Invoke || activity.Name != SignInConstants.TokenExchangeOperationName)
{
throw new Exception("TokenExchangeState can only be used with Invokes of signin/tokenExchange.");
}
var value = activity.Value;
if (value == null || (value as dynamic).id == null)
{
throw new Exception("Invalid signin/tokenExchange. Missing activity.value.id.");
}
return $"{channelId}/{conversationId}/{(value as dynamic).id}";
}
private string MatchCommands(string text)
{
if (_commandMapping.ContainsKey(text))
{
return _commandMapping[text];
}
return null;
}
}
@@ -0,0 +1,22 @@
using Microsoft.Bot.Builder;
using Microsoft.Graph;
using Microsoft.TeamsFx.Configuration;
namespace {Your_NameSpace}.SSO;
public static class SsoOperations
{
public static async Task ShowUserInfo(ITurnContext stepContext, string token, BotAuthenticationOptions botAuthOptions)
{
await stepContext.SendActivityAsync("Retrieving user information from Microsoft Graph ...");
var authProvider = new DelegateAuthenticationProvider((request) =>
{
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
return Task.CompletedTask;
});
var graphClient = new GraphServiceClient(authProvider);
var profile = await graphClient.Me.Request().GetAsync();
await stepContext.SendActivityAsync($"You're logged in as {profile.DisplayName}");
}
}
@@ -0,0 +1,48 @@
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Teams;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Builder.Dialogs;
namespace {Your_NameSpace}.SSO;
public class TeamsSsoBot<T> : TeamsActivityHandler where T : Dialog
{
private readonly ILogger<TeamsSsoBot<T>> _logger;
private readonly BotState _conversationState;
private readonly Dialog _dialog;
private readonly IStatePropertyAccessor<DialogState> _dialogState;
public TeamsSsoBot(ConversationState conversationState, T dialog, ILogger<TeamsSsoBot<T>> logger)
{
_conversationState = conversationState;
_dialog = dialog;
_logger = logger;
_dialogState = _conversationState.CreateProperty<DialogState>("DialogState");
((SsoDialog)_dialog).addCommand("showUserInfo", "show", SsoOperations.ShowUserInfo);
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
_logger.LogInformation("Receive message activity");
await ((SsoDialog)_dialog).RunAsync(turnContext, _dialogState);
}
protected override async Task OnTeamsSigninVerifyStateAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
_logger.LogInformation("Receive invoke activity of teams sign in verify state");
await _dialog.RunAsync(turnContext, _conversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}
protected override async Task OnSignInInvokeAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
_logger.LogInformation("Receive invoke activity of sign in");
await _dialog.RunAsync(turnContext, _conversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken).ConfigureAwait(false);
}
}
@@ -0,0 +1,69 @@
<!--This file is auto generated by Teams Toolkit to provide you instructions and reference code to implement single sign on. -->
<!--This file is used during the Teams Bot authentication flow to assist with retrieval of the access token.-->
<!--If you're not familiar with this, do not alter or remove this file from your project.-->
<html>
<head>
<title>Login End Page</title>
<meta charset="utf-8" />
</head>
<body>
<script
src="https://res.cdn.office.net/teams-js/2.17.0/js/MicrosoftTeams.min.js"
integrity="sha384-xp55t/129OsN192JZYLP0rGhzjCF9aYtjY0LVtXvolkDrBe4Jchylp56NrUYJ4S2"
crossorigin="anonymous"
></script>
<div id="divError"></div>
<script type="text/javascript">
microsoftTeams.app.initialize().then(() => {
let hashParams = getHashParameters();
if (hashParams.get("error")) {
// Authentication failed
handleAuthError(hashParams.get("error"), hashParams);
} else if (hashParams.get("code")) {
// Get the stored state parameter and compare with incoming state
let expectedState = localStorage.getItem("state");
if (expectedState !== hashParams.get("state")) {
// State does not match, report error
handleAuthError("StateDoesNotMatch", hashParams);
} else {
microsoftTeams.authentication.notifySuccess();
}
} else {
// Unexpected condition: hash does not contain error or access_token parameter
handleAuthError("UnexpectedFailure", hashParams);
}
});
// Parse hash parameters into key-value pairs
function getHashParameters() {
let hashParams = new Map();
location.hash
.substr(1)
.split("&")
.forEach(function (item) {
let s = item.split("="),
k = s[0],
v = s[1] && decodeURIComponent(s[1]);
hashParams.set(k, v);
});
return hashParams;
}
// Show error information
function handleAuthError(errorType, errorMessage) {
const err = JSON.stringify({
error: encodeURIComponent(errorType),
message: encodeURIComponent(JSON.stringify(errorMessage)),
});
let para = document.createElement("p");
let node = document.createTextNode(err);
para.appendChild(node);
let element = document.getElementById("divError");
element.appendChild(para);
}
</script>
</body>
</html>
@@ -0,0 +1,178 @@
<!--This file is auto generated by Teams Toolkit to provide you instructions and reference code to implement single sign on. -->
<!--This file is used during the Teams Bot authentication flow to assist with retrieval of the access token.-->
<!--If you're not familiar with this, do not alter or remove this file from your project.-->
<html>
<head>
<title>Login Start Page</title>
<meta charset="utf-8" />
</head>
<body>
<script type="text/javascript">
popUpSignInWindow();
async function popUpSignInWindow() {
// Generate random state string and store it, so we can verify it in the callback
let state = _guid();
localStorage.setItem("state", state);
localStorage.removeItem("codeVerifier");
var currentURL = new URL(window.location);
var clientId = currentURL.searchParams.get("clientId");
var tenantId = currentURL.searchParams.get("tenantId");
var loginHint = currentURL.searchParams.get("loginHint");
if (!loginHint) {
loginHint = "";
}
var scope = currentURL.searchParams.get("scope");
var originalCode = _guid();
var codeChallenge = await pkceChallengeFromVerifier(originalCode);
localStorage.setItem("codeVerifier", originalCode);
let queryParams = {
client_id: clientId,
response_type: "code",
response_mode: "fragment",
scope: scope,
redirect_uri: window.location.origin + "/auth-end.html",
nonce: _guid(),
login_hint: loginHint,
state: state,
code_challenge: codeChallenge,
code_challenge_method: "S256",
};
let authorizeEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${toQueryString(
queryParams
)}`;
window.location.assign(authorizeEndpoint);
}
// Build query string from map of query parameter
function toQueryString(queryParams) {
let encodedQueryParams = [];
for (let key in queryParams) {
encodedQueryParams.push(key + "=" + encodeURIComponent(queryParams[key]));
}
return encodedQueryParams.join("&");
}
// Converts decimal to hex equivalent
// (From ADAL.js: https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/dev/lib/adal.js)
function _decimalToHex(number) {
var hex = number.toString(16);
while (hex.length < 2) {
hex = "0" + hex;
}
return hex;
}
// Generates RFC4122 version 4 guid (128 bits)
// (From ADAL.js: https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/dev/lib/adal.js)
function _guid() {
// RFC4122: The version 4 UUID is meant for generating UUIDs from truly-random or
// pseudo-random numbers.
// The algorithm is as follows:
// Set the two most significant bits (bits 6 and 7) of the
// clock_seq_hi_and_reserved to zero and one, respectively.
// Set the four most significant bits (bits 12 through 15) of the
// time_hi_and_version field to the 4-bit version number from
// Section 4.1.3. Version4
// Set all the other bits to randomly (or pseudo-randomly) chosen
// values.
// UUID = time-low "-" time-mid "-"time-high-and-version "-"clock-seq-reserved and low(2hexOctet)"-" node
// time-low = 4hexOctet
// time-mid = 2hexOctet
// time-high-and-version = 2hexOctet
// clock-seq-and-reserved = hexOctet:
// clock-seq-low = hexOctet
// node = 6hexOctet
// Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
// y could be 1000, 1001, 1010, 1011 since most significant two bits needs to be 10
// y values are 8, 9, A, B
var cryptoObj = window.crypto || window.msCrypto; // for IE 11
if (cryptoObj && cryptoObj.getRandomValues) {
var buffer = new Uint8Array(16);
cryptoObj.getRandomValues(buffer);
//buffer[6] and buffer[7] represents the time_hi_and_version field. We will set the four most significant bits (4 through 7) of buffer[6] to represent decimal number 4 (UUID version number).
buffer[6] |= 0x40; //buffer[6] | 01000000 will set the 6 bit to 1.
buffer[6] &= 0x4f; //buffer[6] & 01001111 will set the 4, 5, and 7 bit to 0 such that bits 4-7 == 0100 = "4".
//buffer[8] represents the clock_seq_hi_and_reserved field. We will set the two most significant bits (6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively.
buffer[8] |= 0x80; //buffer[8] | 10000000 will set the 7 bit to 1.
buffer[8] &= 0xbf; //buffer[8] & 10111111 will set the 6 bit to 0.
return (
_decimalToHex(buffer[0]) +
_decimalToHex(buffer[1]) +
_decimalToHex(buffer[2]) +
_decimalToHex(buffer[3]) +
"-" +
_decimalToHex(buffer[4]) +
_decimalToHex(buffer[5]) +
"-" +
_decimalToHex(buffer[6]) +
_decimalToHex(buffer[7]) +
"-" +
_decimalToHex(buffer[8]) +
_decimalToHex(buffer[9]) +
"-" +
_decimalToHex(buffer[10]) +
_decimalToHex(buffer[11]) +
_decimalToHex(buffer[12]) +
_decimalToHex(buffer[13]) +
_decimalToHex(buffer[14]) +
_decimalToHex(buffer[15])
);
} else {
var guidHolder = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
var hex = "0123456789abcdef";
var r = 0;
var guidResponse = "";
for (var i = 0; i < 36; i++) {
if (guidHolder[i] !== "-" && guidHolder[i] !== "4") {
// each x and y needs to be random
r = (Math.random() * 16) | 0;
}
if (guidHolder[i] === "x") {
guidResponse += hex[r];
} else if (guidHolder[i] === "y") {
// clock-seq-and-reserved first hex is filtered and remaining hex values are random
r &= 0x3; // bit and with 0011 to set pos 2 to zero ?0??
r |= 0x8; // set pos 3 to 1 as 1???
guidResponse += hex[r];
} else {
guidResponse += guidHolder[i];
}
}
return guidResponse;
}
}
// Calculate the SHA256 hash of the input text.
// Returns a promise that resolves to an ArrayBuffer
function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest("SHA-256", data);
}
// Base64-urlencodes the input string
function base64urlencode(str) {
// Convert the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
// Then convert the base64 encoded to base64url encoded
// (replace + with -, replace / with _, trim trailing =)
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
// Return the base64-urlencoded sha256 hash for the PKCE challenge
async function pkceChallengeFromVerifier(v) {
hashed = await sha256(v);
return base64urlencode(hashed);
}
</script>
</body>
</html>
@@ -0,0 +1 @@
const{TeamsActivityHandler,CardFactory}=require("botbuilder"),{handleMessageExtensionQueryWithSSO,OnBehalfOfUserCredential}=require("@microsoft/teamsfx"),{Client}=require("@microsoft/microsoft-graph-client"),{TokenCredentialAuthenticationProvider}=require("@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials");require("isomorphic-fetch");const oboAuthConfig={authorityHost:process.env.M365_AUTHORITY_HOST,clientId:process.env.M365_CLIENT_ID,tenantId:process.env.M365_TENANT_ID,clientSecret:process.env.M365_CLIENT_SECRET},initialLoginEndpoint=process.env.INITIATE_LOGIN_ENDPOINT;class TeamsBot extends TeamsActivityHandler{constructor(){super()}async handleTeamsMessagingExtensionQuery(context,query){return await handleMessageExtensionQueryWithSSO(context,oboAuthConfig,initialLoginEndpoint,"User.Read",(async token=>{const credential=new OnBehalfOfUserCredential(token.ssoToken,oboAuthConfig),authProvider=new TokenCredentialAuthenticationProvider(credential,{scopes:["User.Read"]}),graphClient=Client.initWithMiddleware({authProvider}),profile=await graphClient.api("/me").get();return{composeExtension:{type:"result",attachmentLayout:"list",attachments:[CardFactory.thumbnailCard(profile.displayName,profile.mail)]}}}))}async handleTeamsMessagingExtensionSelectItem(context,obj){return{composeExtension:{type:"result",attachmentLayout:"list",attachments:[CardFactory.heroCard(obj.name,obj.description)]}}}}module.exports.TeamsBot=TeamsBot;
@@ -0,0 +1 @@
const{OnBehalfOfUserCredential}=require("@microsoft/teamsfx"),{Client}=require("@microsoft/microsoft-graph-client"),{TokenCredentialAuthenticationProvider}=require("@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials");require("isomorphic-fetch");const oboAuthConfig={authorityHost:process.env.M365_AUTHORITY_HOST,clientId:process.env.M365_CLIENT_ID,tenantId:process.env.M365_TENANT_ID,clientSecret:process.env.M365_CLIENT_SECRET};class ProfileSsoCommandHandler{triggerPatterns="profile";async handleCommandReceived(context,message,tokenResponse){await context.sendActivity("Retrieving user information from Microsoft Graph ...");const oboCredential=new OnBehalfOfUserCredential(tokenResponse.ssoToken,oboAuthConfig),authProvider=new TokenCredentialAuthenticationProvider(oboCredential,{scopes:["User.Read"]}),graphClient=Client.initWithMiddleware({authProvider}),me=await graphClient.api("/me").get();return me?`Your command is '${message.text}' and you're logged in as ${me.displayName} (${me.userPrincipalName}).`:"Could not retrieve profile information from Microsoft Graph."}}module.exports={ProfileSsoCommandHandler};
@@ -0,0 +1,68 @@
<!--This file is auto generated by Teams Toolkit to provide you instructions and reference code to implement single sign on. -->
<!--This file is used during the Teams Bot authentication flow to assist with retrieval of the access token.-->
<!--If you're not familiar with this, do not alter or remove this file from your project.-->
<html>
<head>
<title>Login End Page</title>
<meta charset="utf-8" />
</head>
<body>
<script
src="https://res.cdn.office.net/teams-js/2.17.0/js/MicrosoftTeams.min.js"
integrity="sha384-xp55t/129OsN192JZYLP0rGhzjCF9aYtjY0LVtXvolkDrBe4Jchylp56NrUYJ4S2"
crossorigin="anonymous"
></script>
<div id="divError"></div>
<script type="text/javascript">
microsoftTeams.app.initialize().then(() => {
let hashParams = getHashParameters();
if (hashParams.get("error")) {
// Authentication failed
handleAuthError(hashParams.get("error"), hashParams);
} else if (hashParams.get("code")) {
// Get the stored state parameter and compare with incoming state
let expectedState = localStorage.getItem("state");
if (expectedState !== hashParams.get("state")) {
// State does not match, report error
handleAuthError("StateDoesNotMatch", hashParams);
} else {
microsoftTeams.authentication.notifySuccess();
}
} else {
// Unexpected condition: hash does not contain error or access_token parameter
handleAuthError("UnexpectedFailure", hashParams);
}
});
// Parse hash parameters into key-value pairs
function getHashParameters() {
let hashParams = new Map();
location.hash
.substr(1)
.split("&")
.forEach(function (item) {
let s = item.split("="),
k = s[0],
v = s[1] && decodeURIComponent(s[1]);
hashParams.set(k, v);
});
return hashParams;
}
// Show error information
function handleAuthError(errorType, errorMessage) {
const err = JSON.stringify({
error: encodeURIComponent(errorType),
message: encodeURIComponent(JSON.stringify(errorMessage)),
});
let para = document.createElement("p");
let node = document.createTextNode(err);
para.appendChild(node);
let element = document.getElementById("divError");
element.appendChild(para);
}
</script>
</body>
</html>
@@ -0,0 +1,178 @@
<!--This file is auto generated by Teams Toolkit to provide you instructions and reference code to implement single sign on. -->
<!--This file is used during the Teams Bot authentication flow to assist with retrieval of the access token.-->
<!--If you're not familiar with this, do not alter or remove this file from your project.-->
<html>
<head>
<title>Login Start Page</title>
<meta charset="utf-8" />
</head>
<body>
<script type="text/javascript">
popUpSignInWindow();
async function popUpSignInWindow() {
// Generate random state string and store it, so we can verify it in the callback
let state = _guid();
localStorage.setItem("state", state);
localStorage.removeItem("codeVerifier");
var currentURL = new URL(window.location);
var clientId = currentURL.searchParams.get("clientId");
var tenantId = currentURL.searchParams.get("tenantId");
var loginHint = currentURL.searchParams.get("loginHint");
if (!loginHint) {
loginHint = "";
}
var scope = currentURL.searchParams.get("scope");
var originalCode = _guid();
var codeChallenge = await pkceChallengeFromVerifier(originalCode);
localStorage.setItem("codeVerifier", originalCode);
let queryParams = {
client_id: clientId,
response_type: "code",
response_mode: "fragment",
scope: scope,
redirect_uri: window.location.origin + "/auth-end.html",
nonce: _guid(),
login_hint: loginHint,
state: state,
code_challenge: codeChallenge,
code_challenge_method: "S256",
};
let authorizeEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${toQueryString(
queryParams
)}`;
window.location.assign(authorizeEndpoint);
}
// Build query string from map of query parameter
function toQueryString(queryParams) {
let encodedQueryParams = [];
for (let key in queryParams) {
encodedQueryParams.push(key + "=" + encodeURIComponent(queryParams[key]));
}
return encodedQueryParams.join("&");
}
// Converts decimal to hex equivalent
// (From ADAL.js: https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/dev/lib/adal.js)
function _decimalToHex(number) {
var hex = number.toString(16);
while (hex.length < 2) {
hex = "0" + hex;
}
return hex;
}
// Generates RFC4122 version 4 guid (128 bits)
// (From ADAL.js: https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/dev/lib/adal.js)
function _guid() {
// RFC4122: The version 4 UUID is meant for generating UUIDs from truly-random or
// pseudo-random numbers.
// The algorithm is as follows:
// Set the two most significant bits (bits 6 and 7) of the
// clock_seq_hi_and_reserved to zero and one, respectively.
// Set the four most significant bits (bits 12 through 15) of the
// time_hi_and_version field to the 4-bit version number from
// Section 4.1.3. Version4
// Set all the other bits to randomly (or pseudo-randomly) chosen
// values.
// UUID = time-low "-" time-mid "-"time-high-and-version "-"clock-seq-reserved and low(2hexOctet)"-" node
// time-low = 4hexOctet
// time-mid = 2hexOctet
// time-high-and-version = 2hexOctet
// clock-seq-and-reserved = hexOctet:
// clock-seq-low = hexOctet
// node = 6hexOctet
// Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
// y could be 1000, 1001, 1010, 1011 since most significant two bits needs to be 10
// y values are 8, 9, A, B
var cryptoObj = window.crypto || window.msCrypto; // for IE 11
if (cryptoObj && cryptoObj.getRandomValues) {
var buffer = new Uint8Array(16);
cryptoObj.getRandomValues(buffer);
//buffer[6] and buffer[7] represents the time_hi_and_version field. We will set the four most significant bits (4 through 7) of buffer[6] to represent decimal number 4 (UUID version number).
buffer[6] |= 0x40; //buffer[6] | 01000000 will set the 6 bit to 1.
buffer[6] &= 0x4f; //buffer[6] & 01001111 will set the 4, 5, and 7 bit to 0 such that bits 4-7 == 0100 = "4".
//buffer[8] represents the clock_seq_hi_and_reserved field. We will set the two most significant bits (6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively.
buffer[8] |= 0x80; //buffer[8] | 10000000 will set the 7 bit to 1.
buffer[8] &= 0xbf; //buffer[8] & 10111111 will set the 6 bit to 0.
return (
_decimalToHex(buffer[0]) +
_decimalToHex(buffer[1]) +
_decimalToHex(buffer[2]) +
_decimalToHex(buffer[3]) +
"-" +
_decimalToHex(buffer[4]) +
_decimalToHex(buffer[5]) +
"-" +
_decimalToHex(buffer[6]) +
_decimalToHex(buffer[7]) +
"-" +
_decimalToHex(buffer[8]) +
_decimalToHex(buffer[9]) +
"-" +
_decimalToHex(buffer[10]) +
_decimalToHex(buffer[11]) +
_decimalToHex(buffer[12]) +
_decimalToHex(buffer[13]) +
_decimalToHex(buffer[14]) +
_decimalToHex(buffer[15])
);
} else {
var guidHolder = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
var hex = "0123456789abcdef";
var r = 0;
var guidResponse = "";
for (var i = 0; i < 36; i++) {
if (guidHolder[i] !== "-" && guidHolder[i] !== "4") {
// each x and y needs to be random
r = (Math.random() * 16) | 0;
}
if (guidHolder[i] === "x") {
guidResponse += hex[r];
} else if (guidHolder[i] === "y") {
// clock-seq-and-reserved first hex is filtered and remaining hex values are random
r &= 0x3; // bit and with 0011 to set pos 2 to zero ?0??
r |= 0x8; // set pos 3 to 1 as 1???
guidResponse += hex[r];
} else {
guidResponse += guidHolder[i];
}
}
return guidResponse;
}
}
// Calculate the SHA256 hash of the input text.
// Returns a promise that resolves to an ArrayBuffer
function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest("SHA-256", data);
}
// Base64-urlencodes the input string
function base64urlencode(str) {
// Convert the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
// Then convert the base64 encoded to base64url encoded
// (replace + with -, replace / with _, trim trailing =)
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
// Return the base64-urlencoded sha256 hash for the PKCE challenge
async function pkceChallengeFromVerifier(v) {
hashed = await sha256(v);
return base64urlencode(hashed);
}
</script>
</body>
</html>
@@ -0,0 +1,84 @@
import { TeamsActivityHandler, CardFactory, TurnContext } from "botbuilder";
import {
MessageExtensionTokenResponse,
handleMessageExtensionQueryWithSSO,
OnBehalfOfCredentialAuthConfig,
OnBehalfOfUserCredential,
} from "@microsoft/teamsfx";
import { Client } from "@microsoft/microsoft-graph-client";
import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
import "isomorphic-fetch";
const oboAuthConfig: OnBehalfOfCredentialAuthConfig = {
authorityHost: process.env.M365_AUTHORITY_HOST,
clientId: process.env.M365_CLIENT_ID,
tenantId: process.env.M365_TENANT_ID,
clientSecret: process.env.M365_CLIENT_SECRET,
};
const initialLoginEndpoint = process.env.INITIATE_LOGIN_ENDPOINT;
export class TeamsBot extends TeamsActivityHandler {
constructor() {
super();
}
public async handleTeamsMessagingExtensionQuery(context: TurnContext, query: any): Promise<any> {
// eslint-disable-next-line no-secrets/no-secrets
/**
* User Code Here.
* If query without token, no need to implement handleMessageExtensionQueryWithSSO;
* Otherwise, just follow the sample code below to modify the user code.
*/
return await handleMessageExtensionQueryWithSSO(
context,
oboAuthConfig,
initialLoginEndpoint,
"User.Read",
async (token: MessageExtensionTokenResponse) => {
// User Code
// Init OnBehalfOfUserCredential instance with SSO token
const credential = new OnBehalfOfUserCredential(token.ssoToken, oboAuthConfig);
// Create an instance of the TokenCredentialAuthenticationProvider by passing the tokenCredential instance and options to the constructor
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: ["User.Read"],
});
// Initialize Graph client instance with authProvider
const graphClient = Client.initWithMiddleware({
authProvider: authProvider,
});
// Call graph api use `graph` instance to get user profile information.
const profile = await graphClient.api("/me").get();
// Organize thumbnailCard to display User's profile info.
const thumbnailCard = CardFactory.thumbnailCard(profile.displayName, profile.mail);
// Message Extension return the user profile info to user.
return {
composeExtension: {
type: "result",
attachmentLayout: "list",
attachments: [thumbnailCard],
},
};
}
);
}
public async handleTeamsMessagingExtensionSelectItem(
context: TurnContext,
obj: any
): Promise<any> {
return {
composeExtension: {
type: "result",
attachmentLayout: "list",
attachments: [CardFactory.heroCard(obj.name, obj.description)],
},
};
}
}
@@ -0,0 +1,54 @@
import { Activity, TurnContext } from "botbuilder";
import {
CommandMessage,
TriggerPatterns,
TeamsFxBotSsoCommandHandler,
TeamsBotSsoPromptTokenResponse,
OnBehalfOfUserCredential,
OnBehalfOfCredentialAuthConfig,
} from "@microsoft/teamsfx";
import { Client } from "@microsoft/microsoft-graph-client";
import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
import "isomorphic-fetch";
const oboAuthConfig: OnBehalfOfCredentialAuthConfig = {
authorityHost: process.env.M365_AUTHORITY_HOST,
clientId: process.env.M365_CLIENT_ID,
tenantId: process.env.M365_TENANT_ID,
clientSecret: process.env.M365_CLIENT_SECRET,
};
export class ProfileSsoCommandHandler implements TeamsFxBotSsoCommandHandler {
triggerPatterns: TriggerPatterns = "profile";
async handleCommandReceived(
context: TurnContext,
message: CommandMessage,
tokenResponse: TeamsBotSsoPromptTokenResponse
): Promise<string | Partial<Activity> | void> {
await context.sendActivity("Retrieving user information from Microsoft Graph ...");
// Init OnBehalfOfUserCredential instance with SSO token
const oboCredential = new OnBehalfOfUserCredential(tokenResponse.ssoToken, oboAuthConfig);
// Create an instance of the TokenCredentialAuthenticationProvider by passing the tokenCredential instance and options to the constructor
const authProvider = new TokenCredentialAuthenticationProvider(oboCredential, {
scopes: ["User.Read"],
});
// Initialize Graph client instance with authProvider
const graphClient = Client.initWithMiddleware({
authProvider: authProvider,
});
// Call graph api use `graph` instance to get user profile information
const me = await graphClient.api("/me").get();
if (me) {
// Bot will send the user profile info to user
return `Your command is '${message.text}' and you're logged in as ${me.displayName} (${me.userPrincipalName}).`;
} else {
return "Could not retrieve profile information from Microsoft Graph.";
}
}
}
@@ -0,0 +1,50 @@
# Enable single sign-on for tab applications
Microsoft Teams provides a mechanism by which an application can obtain the signed-in Teams user token to access Microsoft Graph (and other APIs). Teams Toolkit facilitates this interaction by abstracting some of the Microsoft Entra flows and integrations behind some simple, high level APIs. This enables you to add single sign-on (SSO) features easily to your Teams application.
# Changes to your project
When you added the SSO feature to your application, Teams Toolkit updated your project to support SSO:
After you successfully added SSO into your project, Teams Toolkit will create and modify some files that helps you implement SSO feature.
| Action | File | Description |
| - | - | - |
| Create| `aad.template.json` under `templates/appPackage` | The Microsoft Entra application manifest that is used to register the application with Microsoft Entra. |
| Modify | `manifest.template.json` under `templates/appPackage` | An `webApplicationInfo` object will be added into your Teams app manifest template. This field is required by Teams when enabling SSO. |
| Create | `auth/tab` | Reference code, redirect pages and a `README.md` file. These files are provided for reference. See below for more information. |
# Update your code to add SSO
As described above, the Teams Toolkit generated some configuration to set up your application for SSO, but you need to update your application business logic to take advantage of the SSO feature as appropriate.
1. Move `auth-start.html` and `auth-end.html` in `auth/tab/public` folder to `tabs/public/`.
These two HTML files are used for auth redirects.
1. Move `sso` folder under `auth/tab` to `tabs/src/sso/`.
`InitTeamsFx`: This file implements a function that initialize TeamsFx SDK and will open `GetUserProfile` component after SDK is initialized.
`GetUserProfile`: This file implements a function that calls Microsoft Graph API to get user info.
2. Add the following lines to `tabs/src/components/sample/Welcome.*` to import `InitTeamsFx`:
```
import { InitTeamsFx } from "../../sso/InitTeamsFx";
```
3. Replace the following line: `<AddSSO />` with `<InitTeamsFx />` to replace the `AddSSO` component with `InitTeamsFx` component.
# Debug your application
You can debug your application by pressing F5.
Teams Toolkit will use the Microsoft Entra manifest file to register a Microsoft Entra application registered for SSO.
To learn more about Teams Toolkit local debug functionalities, refer to this [document](https://docs.microsoft.com/microsoftteams/platform/toolkit/debug-local).
# Customize Microsoft Entra applications
The Microsoft Entra [manifest](https://docs.microsoft.com/azure/active-directory/develop/reference-app-manifest) allows you to customize various aspects of your application registration. You can update the manifest as needed.
Follow this [document](https://aka.ms/teamsfx-aad-manifest#how-to-customize-the-aad-manifest-template) if you need to include additional API permissions to access your desired APIs.
Follow this [document](https://aka.ms/teamsfx-aad-manifest#How-to-view-the-AAD-app-on-the-Azure-portal) to view your Microsoft Entra application in Azure Portal.
@@ -0,0 +1,45 @@
Enable single sign-on for Teams tab applications
-------------------------
Files generated/updated in your project
-------------------------
1. New file - 'aad.template.json' is created in folder 'templates/appPackage'
- The Microsoft Entra application manifest that is used to register the application with Microsoft Entra.
2. Update file - 'templates/appPackage/manifest.template.json'
- An 'webApplicationInfo' object will be added into your Teams app manifest template. This field is required by Teams when enabling SSO.
3. New file - 'Auth/tab'
- Sample code and a 'README.txt' file. These files are provided for reference. See below for more information.
4. Update file - 'appsettings.json' and 'appsettings.Development.json'
- Configs that will be used by TeamsFx SDK will be added into your app settings. Please update add the 'TeamsFx' object if you have other appsettings files.
Actions required - update your code to add SSO authentication
-------------------------
You need to update your application code to take advantage of the SSO authentication.
1. Move 'GetUserProfile.razor' file from 'Auth/tab' folder to 'Components/' folder.
- 'GetUserProfile': This file implements a function that uses TeamsFx SDK to call Microsoft Graph API to get user info.
2. Rplace the 'AddSSO' component with 'GetUserProfile' component. To do this, just replace the following line: '<AddSSO />' with '<GetUserProfile />' in 'Components/Welcome.razor' file.
Debug your application
-------------------------
1. Right-click your project and select Teams Toolkit > Prepare Teams app dependencies
2. If prompted, sign in with an M365 account for the Teams organization you want
to install the app to
3. Press F5, or select the Debug > Start Debugging menu in Visual Studio
4. In the launched browser, select the Add button to load the app in Teams
Teams Toolkit will use the Microsoft Entra manifest file to register a Microsoft Entra application registered for SSO.
To learn more about Teams Toolkit local debug functionalities, refer to https://docs.microsoft.com/microsoftteams/platform/toolkit/debug-local.
Customize Microsoft Entra applications
-------------------------
The Microsoft Entra manifest allows you to customize various aspects of your application registration. You can update the manifest as needed.
Related Doc: https://docs.microsoft.com/azure/active-directory/develop/reference-app-manifest
Follow https://aka.ms/teamsfx-aad-manifest#how-to-customize-the-aad-manifest-template if you need to include additional API permissions to access your desired APIs.
Follow https://aka.ms/teamsfx-aad-manifest#How-to-view-the-AAD-app-on-the-Azure-portal to view your Microsoft Entra application in Azure Portal.
@@ -0,0 +1,97 @@
@using System.IO
@using Azure.Core
@using Microsoft.Graph
@inject TeamsFx teamsfx
@inject TeamsUserCredential teamsUserCredential
<div>
<h2>Get the user's profile</h2>
@if (NeedConsent)
{
<p>Click below to authorize this app to read your profile photo using Microsoft Graph.</p>
<FluentButton Appearance="Appearance.Accent" Disabled="@IsLoading" @onclick="ConsentAndShow">Authorize</FluentButton>
}
else if (!string.IsNullOrEmpty(@ErrorMessage))
{
<div class="error">@ErrorMessage</div>
}
else if (Profile != null)
{
<div>Hello @Profile.DisplayName</div>
}
</div>
@code {
[Parameter]
public string ErrorMessage { get; set; }
public bool IsLoading { get; set; }
public bool NeedConsent { get; set; }
public User Profile { get; set; }
private readonly string _scope = "User.Read";
protected override async Task OnInitializedAsync()
{
IsLoading = true;
if (await HasPermission(_scope))
{
await ShowProfile();
}
}
private async Task ShowProfile()
{
IsLoading = true;
var graph = GetGraphServiceClient();
Profile = await graph.Me.Request().GetAsync();
IsLoading = false;
ErrorMessage = string.Empty;
}
private async Task ConsentAndShow()
{
try
{
await teamsUserCredential.LoginAsync(_scope);
NeedConsent = false;
await ShowProfile();
}
catch (ExceptionWithCode e)
{
ErrorMessage = e.Message;
}
}
private async Task<bool> HasPermission(string scope)
{
IsLoading = true;
try
{
await teamsUserCredential.GetTokenAsync(new TokenRequestContext(new string[] { _scope }), new System.Threading.CancellationToken());
return true;
}
catch (ExceptionWithCode e)
{
if (e.Code == ExceptionCode.UiRequiredError)
{
NeedConsent = true;
}
else
{
ErrorMessage = e.Message;
}
}
IsLoading = false;
return false;
}
private GraphServiceClient GetGraphServiceClient()
{
var client = new GraphServiceClient(teamsUserCredential, new string[] { _scope });
return client;
}
}
@@ -0,0 +1,59 @@
<!--This file is used during the Teams authentication flow to assist with retrieval of the access token.-->
<!--If you're not familiar with this, do not alter or remove this file from your project.-->
<html>
<head>
<title>Login End Page</title>
<meta charset="utf-8" />
</head>
<body>
<script
src="https://res.cdn.office.net/teams-js/2.17.0/js/MicrosoftTeams.min.js"
integrity="sha384-xp55t/129OsN192JZYLP0rGhzjCF9aYtjY0LVtXvolkDrBe4Jchylp56NrUYJ4S2"
crossorigin="anonymous"
></script>
<script
type="text/javascript"
src="https://alcdn.msauth.net/browser/2.21.0/js/msal-browser.min.js"
integrity="sha384-s/NxjjAgw1QgpDhOlVjTceLl4axrp5nqpUbCPOEQy1PqbFit9On6uw2XmEF1eq0s"
crossorigin="anonymous"
></script>
<script type="text/javascript">
var currentURL = new URL(window.location);
var clientId = currentURL.searchParams.get("clientId");
microsoftTeams.app.initialize().then(() => {
microsoftTeams.app.getContext().then(async (context) => {
const msalConfig = {
auth: {
clientId: clientId,
authority: `https://login.microsoftonline.com/${context.user.tenant.id}`,
navigateToLoginRequestUrl: false,
},
cache: {
cacheLocation: "sessionStorage",
},
};
const msalInstance = new window.msal.PublicClientApplication(msalConfig);
msalInstance
.handleRedirectPromise()
.then((tokenResponse) => {
if (tokenResponse !== null) {
microsoftTeams.authentication.notifySuccess(
JSON.stringify({
sessionStorage: sessionStorage,
})
);
} else {
microsoftTeams.authentication.notifyFailure("Get empty response.");
}
})
.catch((error) => {
microsoftTeams.authentication.notifyFailure(JSON.stringify(error));
});
});
});
</script>
</body>
</html>
@@ -0,0 +1,53 @@
<!--This file is used during the Teams authentication flow to assist with retrieval of the access token.-->
<!--If you're not familiar with this, do not alter or remove this file from your project.-->
<html>
<head>
<title>Login Start Page</title>
<meta charset="utf-8" />
</head>
<body>
<script
src="https://res.cdn.office.net/teams-js/2.17.0/js/MicrosoftTeams.min.js"
integrity="sha384-xp55t/129OsN192JZYLP0rGhzjCF9aYtjY0LVtXvolkDrBe4Jchylp56NrUYJ4S2"
crossorigin="anonymous"
></script>
<script
type="text/javascript"
src="https://alcdn.msauth.net/browser/2.21.0/js/msal-browser.min.js"
integrity="sha384-s/NxjjAgw1QgpDhOlVjTceLl4axrp5nqpUbCPOEQy1PqbFit9On6uw2XmEF1eq0s"
crossorigin="anonymous"
></script>
<script type="text/javascript">
microsoftTeams.app.initialize().then(() => {
microsoftTeams.app.getContext().then(async (context) => {
// Generate random state string and store it, so we can verify it in the callback
var currentURL = new URL(window.location);
var clientId = currentURL.searchParams.get("clientId");
var scope = currentURL.searchParams.get("scope");
var loginHint = currentURL.searchParams.get("loginHint");
const msalConfig = {
auth: {
clientId: clientId,
authority: `https://login.microsoftonline.com/${context.user.tenant.id}`,
navigateToLoginRequestUrl: false,
},
cache: {
cacheLocation: "sessionStorage",
},
};
const msalInstance = new msal.PublicClientApplication(msalConfig);
const scopesArray = scope.split(" ");
const scopesRequest = {
scopes: scopesArray,
redirectUri: window.location.origin + `/auth-end.html?clientId=${clientId}`,
loginHint: loginHint,
};
await msalInstance.loginRedirect(scopesRequest);
});
});
</script>
</body>
</html>
@@ -0,0 +1,46 @@
// This file is auto generated by Teams Toolkit to provide you instructions and reference code to implement single sign on.
// This file will use TeamsFx SDK to call Graph API to get user profile.
// Refer to this link to learn more: https://www.npmjs.com/package/@microsoft/teamsfx-react#calling-the-microsoft-graph-api.
import { Button } from "@fluentui/react-northstar";
import { useGraph } from "@microsoft/teamsfx-react";
export function GetUserProfile(props) {
const { teamsfx } = {
teamsfx: undefined,
...props,
};
// For usage of useGraph(), please refer to: https://www.npmjs.com/package/@microsoft/teamsfx-react#usegraph.
const { loading, error, data, reload } = useGraph(
async (graph, teamsfx, scope) => {
// Call graph api directly to get user profile information
const profile = await graph.api("/me").get();
// You can also add following code to get your photo:
// let photoUrl = "";
// try {
// const photo = await graph.api("/me/photo/$value").get();
// photoUrl = URL.createObjectURL(photo);
// } catch {
// // Could not fetch photo from user's profile, return empty string as placeholder.
// }
return { profile };
},
// Add scope for your Microsoft Entra app. For example: Mail.Read, etc.
// Use teamsfx instance from `InitTeamsFx`
{ scope: ["User.Read"], teamsfx: teamsfx }
);
return (
<div>
<h2>GetUserProfile</h2>
<p>Click below to authorize button to grant permission to using Microsoft Graph.</p>
<Button primary content="Authorize" disabled={loading} onClick={reload} />
{!loading && error && (
<div className="error">Failed to read your profile. Please try again later.</div>
)}
{!loading && data && <div>Hello {data.profile.displayName}</div>}
</div>
);
}
@@ -0,0 +1,23 @@
// This file is auto generated by Teams Toolkit to provide you instructions and reference code to implement single sign on.
// This file will initialize TeamsFx SDK and show `GetUserProfile` component after initialization.
import { useTeamsFx } from "@microsoft/teamsfx-react";
import { GetUserProfile } from "./GetUserProfile";
export function InitTeamsFx() {
// For usage of useTeamsFx(), please refer to: https://www.npmjs.com/package/@microsoft/teamsfx-react#useTeamsfx.
// You need to wait until `loading == false` to use TeamsFx SDK.
const { loading, error, teamsfx } = useTeamsFx({
initiateLoginEndpoint: process.env.REACT_APP_START_LOGIN_PAGE_URL,
clientId: process.env.REACT_APP_CLIENT_ID,
});
return (
<div>
{!loading && error && (
<div className="error">Failed init TeamsFx. Please try again later.</div>
)}
{!loading && teamsfx && <GetUserProfile teamsfx={teamsfx} />}
</div>
);
}
@@ -0,0 +1,59 @@
<!--This file is used during the Teams authentication flow to assist with retrieval of the access token.-->
<!--If you're not familiar with this, do not alter or remove this file from your project.-->
<html>
<head>
<title>Login End Page</title>
<meta charset="utf-8" />
</head>
<body>
<script
src="https://res.cdn.office.net/teams-js/2.17.0/js/MicrosoftTeams.min.js"
integrity="sha384-xp55t/129OsN192JZYLP0rGhzjCF9aYtjY0LVtXvolkDrBe4Jchylp56NrUYJ4S2"
crossorigin="anonymous"
></script>
<script
type="text/javascript"
src="https://alcdn.msauth.net/browser/2.21.0/js/msal-browser.min.js"
integrity="sha384-s/NxjjAgw1QgpDhOlVjTceLl4axrp5nqpUbCPOEQy1PqbFit9On6uw2XmEF1eq0s"
crossorigin="anonymous"
></script>
<script type="text/javascript">
var currentURL = new URL(window.location);
var clientId = currentURL.searchParams.get("clientId");
microsoftTeams.app.initialize().then(() => {
microsoftTeams.app.getContext().then(async (context) => {
const msalConfig = {
auth: {
clientId: clientId,
authority: `https://login.microsoftonline.com/${context.user.tenant.id}`,
navigateToLoginRequestUrl: false,
},
cache: {
cacheLocation: "sessionStorage",
},
};
const msalInstance = new window.msal.PublicClientApplication(msalConfig);
msalInstance
.handleRedirectPromise()
.then((tokenResponse) => {
if (tokenResponse !== null) {
microsoftTeams.authentication.notifySuccess(
JSON.stringify({
sessionStorage: sessionStorage,
})
);
} else {
microsoftTeams.authentication.notifyFailure("Get empty response.");
}
})
.catch((error) => {
microsoftTeams.authentication.notifyFailure(JSON.stringify(error));
});
});
});
</script>
</body>
</html>
@@ -0,0 +1,53 @@
<!--This file is used during the Teams authentication flow to assist with retrieval of the access token.-->
<!--If you're not familiar with this, do not alter or remove this file from your project.-->
<html>
<head>
<title>Login Start Page</title>
<meta charset="utf-8" />
</head>
<body>
<script
src="https://res.cdn.office.net/teams-js/2.17.0/js/MicrosoftTeams.min.js"
integrity="sha384-xp55t/129OsN192JZYLP0rGhzjCF9aYtjY0LVtXvolkDrBe4Jchylp56NrUYJ4S2"
crossorigin="anonymous"
></script>
<script
type="text/javascript"
src="https://alcdn.msauth.net/browser/2.21.0/js/msal-browser.min.js"
integrity="sha384-s/NxjjAgw1QgpDhOlVjTceLl4axrp5nqpUbCPOEQy1PqbFit9On6uw2XmEF1eq0s"
crossorigin="anonymous"
></script>
<script type="text/javascript">
microsoftTeams.app.initialize().then(() => {
microsoftTeams.app.getContext().then(async (context) => {
// Generate random state string and store it, so we can verify it in the callback
var currentURL = new URL(window.location);
var clientId = currentURL.searchParams.get("clientId");
var scope = currentURL.searchParams.get("scope");
var loginHint = currentURL.searchParams.get("loginHint");
const msalConfig = {
auth: {
clientId: clientId,
authority: `https://login.microsoftonline.com/${context.user.tenant.id}`,
navigateToLoginRequestUrl: false,
},
cache: {
cacheLocation: "sessionStorage",
},
};
const msalInstance = new msal.PublicClientApplication(msalConfig);
const scopesArray = scope.split(" ");
const scopesRequest = {
scopes: scopesArray,
redirectUri: window.location.origin + `/auth-end.html?clientId=${clientId}`,
loginHint: loginHint,
};
await msalInstance.loginRedirect(scopesRequest);
});
});
</script>
</body>
</html>
@@ -0,0 +1,47 @@
// This file is auto generated by Teams Toolkit to provide you instructions and reference code to implement single sign on.
// This file will use TeamsFx SDK to call Graph API to get user profile.
// Refer to this link to learn more: https://www.npmjs.com/package/@microsoft/teamsfx-react#calling-the-microsoft-graph-api.
import { Button } from "@fluentui/react-northstar";
import { TeamsFx } from "@microsoft/teamsfx";
import { useGraph } from "@microsoft/teamsfx-react";
export function GetUserProfile(props: { teamsfx?: TeamsFx }) {
const { teamsfx } = {
teamsfx: undefined,
...props,
};
// For usage of useGraph(), please refer to: https://www.npmjs.com/package/@microsoft/teamsfx-react#usegraph.
const { loading, error, data, reload } = useGraph(
async (graph, teamsfx, scope) => {
// Call graph api use `graph` instance to get user profile information
const profile = await graph.api("/me").get();
// You can also add following code to get your photo:
// let photoUrl = "";
// try {
// const photo = await graph.api("/me/photo/$value").get();
// photoUrl = URL.createObjectURL(photo);
// } catch {
// // Could not fetch photo from user's profile, return empty string as placeholder.
// }
return { profile };
},
// Add scope for your Microsoft Entra app. For example: Mail.Read, etc.
// Use teamsfx instance from `InitTeamsFx`
{ scope: ["User.Read"], teamsfx: teamsfx }
);
return (
<div>
<h2>GetUserProfile</h2>
<p>Click below to authorize button to grant permission to using Microsoft Graph.</p>
<Button primary content="Authorize" disabled={loading} onClick={reload} />
{!loading && error && (
<div className="error">Failed to read your profile. Please try again later.</div>
)}
{!loading && data && <div>Hello {data.profile.displayName}</div>}
</div>
);
}
@@ -0,0 +1,23 @@
// This file is auto generated by Teams Toolkit to provide you instructions and reference code to implement single sign on.
// This file will initialize TeamsFx SDK and show `GetUserProfile` component after initialization.
import { useTeamsFx } from "@microsoft/teamsfx-react";
import { GetUserProfile } from "./GetUserProfile";
export function InitTeamsFx() {
// For usage of useTeamsFx(), please refer to: https://www.npmjs.com/package/@microsoft/teamsfx-react#useTeamsfx.
// You need to wait until `loading == false` to use TeamsFx SDK.
const { loading, error, teamsfx } = useTeamsFx({
initiateLoginEndpoint: process.env.REACT_APP_START_LOGIN_PAGE_URL!,
clientId: process.env.REACT_APP_CLIENT_ID!,
});
return (
<div>
{!loading && error && (
<div className="error">Failed init TeamsFx. Please try again later.</div>
)}
{!loading && teamsfx && <GetUserProfile teamsfx={teamsfx} />}
</div>
);
}
@@ -0,0 +1,102 @@
{
"id": "{{state.fx-resource-aad-app-for-teams.objectId}}",
"appId": "{{state.fx-resource-aad-app-for-teams.clientId}}",
"name": "{{config.manifest.appName.short}}-aad",
"accessTokenAcceptedVersion": 2,
"signInAudience": "AzureADMyOrg",
"optionalClaims": {
"idToken": [],
"accessToken": [
{
"name": "idtyp",
"source": null,
"essential": false,
"additionalProperties": []
}
],
"saml2Token": []
},
"requiredResourceAccess": [
{
"resourceAppId": "Microsoft Graph",
"resourceAccess": [
{
"id": "User.Read",
"type": "Scope"
}
]
}
],
"oauth2Permissions": [
{
"adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.",
"adminConsentDisplayName": "Teams can access app's web APIs",
"id": "{{state.fx-resource-aad-app-for-teams.oauth2PermissionScopeId}}",
"isEnabled": true,
"type": "User",
"userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have",
"userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf",
"value": "access_as_user"
}
],
"preAuthorizedApplications": [
{
"appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264",
"permissionIds": [
"{{state.fx-resource-aad-app-for-teams.oauth2PermissionScopeId}}"
]
},
{
"appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346",
"permissionIds": [
"{{state.fx-resource-aad-app-for-teams.oauth2PermissionScopeId}}"
]
},
{
"appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c",
"permissionIds": [
"{{state.fx-resource-aad-app-for-teams.oauth2PermissionScopeId}}"
]
},
{
"appId": "00000002-0000-0ff1-ce00-000000000000",
"permissionIds": [
"{{state.fx-resource-aad-app-for-teams.oauth2PermissionScopeId}}"
]
},
{
"appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3",
"permissionIds": [
"{{state.fx-resource-aad-app-for-teams.oauth2PermissionScopeId}}"
]
},
{
"appId": "0ec893e0-5785-4de6-99da-4ed124e5296c",
"permissionIds": [
"{{state.fx-resource-aad-app-for-teams.oauth2PermissionScopeId}}"
]
},
{
"appId": "4765445b-32c6-49b0-83e6-1d93765276ca",
"permissionIds": [
"{{state.fx-resource-aad-app-for-teams.oauth2PermissionScopeId}}"
]
},
{
"appId": "4345a7b9-9a63-4910-a426-35363201d503",
"permissionIds": [
"{{state.fx-resource-aad-app-for-teams.oauth2PermissionScopeId}}"
]
},
{
"appId": "27922004-5251-4030-b22d-91ecd9a37ea4",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
}
],
"identifierUris": [
"{{state.fx-resource-aad-app-for-teams.applicationIdUris}}"
],
"replyUrlsWithType": []
}
@@ -0,0 +1,107 @@
{
"id": "${{AAD_APP_OBJECT_ID}}",
"appId": "${{AAD_APP_CLIENT_ID}}",
"name": "YOUR_AAD_APP_NAME",
"accessTokenAcceptedVersion": 2,
"signInAudience": "AzureADMyOrg",
"optionalClaims": {
"idToken": [],
"accessToken": [
{
"name": "idtyp",
"source": null,
"essential": false,
"additionalProperties": []
}
],
"saml2Token": []
},
"requiredResourceAccess": [
{
"resourceAppId": "Microsoft Graph",
"resourceAccess": [
{
"id": "User.Read",
"type": "Scope"
}
]
}
],
"oauth2Permissions": [
{
"adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.",
"adminConsentDisplayName": "Teams can access app's web APIs",
"id": "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}",
"isEnabled": true,
"type": "User",
"userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have",
"userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf",
"value": "access_as_user"
}
],
"preAuthorizedApplications": [
{
"appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "00000002-0000-0ff1-ce00-000000000000",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "0ec893e0-5785-4de6-99da-4ed124e5296c",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "4765445b-32c6-49b0-83e6-1d93765276ca",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "4345a7b9-9a63-4910-a426-35363201d503",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "27922004-5251-4030-b22d-91ecd9a37ea4",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
}
],
"identifierUris": [
"api://botid-${{BOT_ID}}"
],
"replyUrlsWithType": [
{
"url": "https://${{BOT_DOMAIN}}/auth-end.html",
"type": "Web"
}
]
}
@@ -0,0 +1,115 @@
{
"id": "${{AAD_APP_OBJECT_ID}}",
"appId": "${{AAD_APP_CLIENT_ID}}",
"name": "YOUR_AAD_APP_NAME",
"accessTokenAcceptedVersion": 2,
"signInAudience": "AzureADMyOrg",
"optionalClaims": {
"idToken": [],
"accessToken": [
{
"name": "idtyp",
"source": null,
"essential": false,
"additionalProperties": []
}
],
"saml2Token": []
},
"requiredResourceAccess": [
{
"resourceAppId": "Microsoft Graph",
"resourceAccess": [
{
"id": "User.Read",
"type": "Scope"
}
]
}
],
"oauth2Permissions": [
{
"adminConsentDescription": "Allows Teams to call the app's web APIs as the current user.",
"adminConsentDisplayName": "Teams can access app's web APIs",
"id": "${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}",
"isEnabled": true,
"type": "User",
"userConsentDescription": "Enable Teams to call this app's web APIs with the same rights that you have",
"userConsentDisplayName": "Teams can access app's web APIs and make requests on your behalf",
"value": "access_as_user"
}
],
"preAuthorizedApplications": [
{
"appId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "5e3ce6c0-2b1f-4285-8d4b-75ee78787346",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "d3590ed6-52b3-4102-aeff-aad2292ab01c",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "00000002-0000-0ff1-ce00-000000000000",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "bc59ab01-8403-45c6-8796-ac3ef710b3e3",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "0ec893e0-5785-4de6-99da-4ed124e5296c",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "4765445b-32c6-49b0-83e6-1d93765276ca",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "4345a7b9-9a63-4910-a426-35363201d503",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
},
{
"appId": "27922004-5251-4030-b22d-91ecd9a37ea4",
"permissionIds": [
"${{AAD_APP_ACCESS_AS_USER_PERMISSION_ID}}"
]
}
],
"identifierUris": [
"api://${{TAB_DOMAIN}}/${{AAD_APP_CLIENT_ID}}"
],
"replyUrlsWithType": [
{
"url": "${{TAB_ENDPOINT}}/auth-end.html",
"type": "Web"
},
{
"url": "${{TAB_ENDPOINT}}/auth-end.html?clientId=${{AAD_APP_CLIENT_ID}}",
"type": "Spa"
},
{
"url": "${{TAB_ENDPOINT}}/blank-auth-end.html",
"type": "Spa"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B