DevOps·

Automate configuration of Teams Tab SSO with PowerShell.

Creating a PowerShell script to configure SSO for the tab of a Teams application.

If you have no interest in reading the blog post and just want the final script, you can find it on this GitHub repository.

Context

Several months ago, I supervised a student project aiming at developing a Teams application for my company. The application is mainly composed of a tab where Human Resources people can see information about arrivals and departures in the company. Once the project was finished and the first version of the application was available, I provisioned the application infrastructure on my company Azure tenant using Pulumi which is a nice infrastructure as code platform.

However, configuring Single Sign-On for the tab of the application did not seem possible with Pulumi as it internally uses Terraform Provider for AzureAD which at the time of writing doesn't have all functionalities necessary to configure this. The documentation about SSO for Teams tab currently lists all the steps necessary to configure it from the Azure Portal, however, it mentions nothing about automating it, hence this blog post.

Steps to create the PowerShell script

Usually, I prefer Azure CLI to PowerShell as I find it easier to find commands I need, but Azure CLI doesn't have yet the necessary commands. Most of the code comes from this script located in a repository of the Azure Samples GitHub organization. I took only what was necessary for Teams Tab SSO, adapted it to use Microsoft Graph objects/commands, and added missing commands.

I am not an expert in PowerShell so there might be things to improve in the final script, but I hope the following steps will help you to understand how to configure SSO for your Teams Tab.

Interacting with Azure Active Directory

PowerShell has a module called AzureAd that allow us to interact with Azure Active Directory. The first step is to install this module if not already installed, import it and authenticate to Azure AD to be able to use Active Directory commands once authenticated.

if ($null -eq (Get-Module -ListAvailable -Name "AzureAD")) { 
    Install-Module -Name "AzureAD" -Force
}

Import-Module AzureAD

Connect-AzureAD -TenantId $tenantId

This will prompt us to log in with our AD account. We will see later in the article how we can avoid that if we are using this script in an Azure Pipeline.

Retrieving the application registration

I already created my application registration in AD with Pulumi so I just have to retrieve it before configuring it.

$app = Get-AzureADMSApplication -ObjectId $applicationObjectId

If you don't have an existing application registration you can create one with the New-AzureADMSApplication command.

You may note that there are similar commands Get-AzureADApplication and New-AzureADApplication that exist. Both commands work fine but commands with MS in their name internally use Microsoft Graph which seems to be the modern way to interact with Azure AD.

Creating the service principal

When you register an application in Azure Portal it creates an Application object and a Service Principal in your tenant. But if you create the Application outside the Azure Portal (Azure CLI, PowerShell, Pulumi, ...), you will have to create the Service Principal as well. Just as a reminder the application object should be considered as the global representation of your application for use across all tenants, and the service principal as the local representation for use in a specific tenant.

New-AzureADServicePrincipal -AppId $app.AppId -Tags {WindowsAzureActiveDirectoryIntegratedApp}

Exposing an application as an API

To expose an application as an API, it is necessary to set the identifier URI of the application. We will use a variable $customDomainName to specify the custom domain of the application. Indeed as stated by the documentation, for the moment Teams Tab SSO does not support applications that use the azurewebsites.net domain.

$appId = $app.AppId
Set-AzureADMSApplication -ObjectId $app.Id -IdentifierUris "api://$customDomainName/$appId"

Creating the access_as_user scope

Teams Tab SSO works by making the Teams client (whether it be Teams mobile app, desktop app, or web app) ask for an Azure AD token with the scope access_as_user of the Tab application you developed. So we need to create a scope access_as_user in the application.

# Add all existing scopes first
$scopes = New-Object System.Collections.Generic.List[Microsoft.Open.MsGraph.Model.PermissionScope]
$app.Api.Oauth2PermissionScopes | foreach-object { $scopes.Add($_) }
$scope = CreateScope -value "access_as_user"  `
    -userConsentDisplayName "Teams can access the user’s profile"  `
    -userConsentDescription "Allows Teams to call the app’s web APIs as the current user."  `
    -adminConsentDisplayName "Teams can access your user profile and make requests on your behalf"  `
    -adminConsentDescription "Enable Teams to call this app’s APIs with the same rights that you have"
$scopes.Add($scope)
$app.Api.Oauth2PermissionScopes = $scopes
Set-AzureADMSApplication -ObjectId $app.Id -Api $app.Api

This piece of PowerShell just ensures existing scopes won't be deleted when adding the scope access_as_user. Display names and descriptions of the new scope are the ones recommended in the documentation. This code calls a PowerShell function that simply creates the scope object.

<#.Description
   This function creates a new Azure AD scope (OAuth2Permission) with default and provided values
#>  
function CreateScope(
    [string] $value,
    [string] $userConsentDisplayName,
    [string] $userConsentDescription,
    [string] $adminConsentDisplayName,
    [string] $adminConsentDescription)
{
    $scope = New-Object Microsoft.Open.MsGraph.Model.PermissionScope
    $scope.Id = New-Guid
    $scope.Value = $value
    $scope.UserConsentDisplayName = $userConsentDisplayName
    $scope.UserConsentDescription = $userConsentDescription
    $scope.AdminConsentDisplayName = $adminConsentDisplayName
    $scope.AdminConsentDescription = $adminConsentDescription
    $scope.IsEnabled = $true
    $scope.Type = "User"
    return $scope
}

Preauthorize Teams clients.

As the Teams clients will ask for a token with the previously created scope, they must be authorized to have access to this permission. That is what does the following script:

# Authorize Teams mobile/desktop client and Teams web client to access API
$preAuthorizedApplications = New-Object 'System.Collections.Generic.List[Microsoft.Open.MSGraph.ModePreAuthorizedApplication]'
$teamsRichClienPreauthorization = CreatePreAuthorizedApplication `
    -applicationIdToPreAuthorize '1fec8e78-bce4-4aaf-ab1b-5451cc387264' `
    -scopeId $scope.Id
$teamsWebClienPreauthorization = CreatePreAuthorizedApplication `
    -applicationIdToPreAuthorize '5e3ce6c0-2b1f-4285-8d4b-75ee78787346' `
    -scopeId $scope.Id
$preAuthorizedApplications.Add($teamsRichClienPreauthorization)
$preAuthorizedApplications.Add($teamsWebClienPreauthorization)   
$app = Get-AzureADMSApplication -ObjectId $applicationObjectId
$app.Api.PreAuthorizedApplications = $preAuthorizedApplications
Set-AzureADMSApplication -ObjectId $app.Id -Api $app.Api

This code calls a PowerShell function that simply creates the PreAuthorizedApplication object.

<#.Description
   This function creates a new PreAuthorized application on a specified scope
#>  
function CreatePreAuthorizedApplication(
    [string] $applicationIdToPreAuthorize,
    [string] $scopeId)
{
    $preAuthorizedApplication = New-Object 'Microsoft.Open.MSGraph.Model.PreAuthorizedApplication'
    $preAuthorizedApplication.AppId = $applicationIdToPreAuthorize
    $preAuthorizedApplication.DelegatedPermissionIds = @($scopeId)
    return $preAuthorizedApplication
}

Grant user-level Graph API permissions

The next step consists in specifying the permissions the application will need for the AAD endpoint: email, offline_access, openid, profile (OpenID connect scopes).

# Add API permissions needed
$requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.MsGraph.Model.RequiredResourceAccess]
$requiredPermissions = GetRequiredPermissions `
    -applicationDisplayName 'Microsoft Graph' `
    -requiredDelegatedPermissions "User.Read|email|offline_access|openid|profile"
$requiredResourcesAccess.Add($requiredPermissions)   
Set-AzureADMSApplication -ObjectId $app.Id -RequiredResourceAccess $requiredPermissions

This code calls a PowerShell function GetRequiredPermissions that add the delegated or application permissions specified in parameter. Here we only ask for delegated permissions of Microsoft Graph needed to retrieve an OpenId Connect token but this function is generic and could be used to require scopes or roles of other APIs.

# Example: GetRequiredPermissions "Microsoft Graph"  "Graph.Read|User.Read"
# See also: http://stackoverflow.com/questions/42164581/how-to-configure-a-new-azure-ad-application-through-powershell
function GetRequiredPermissions(
    [string] $applicationDisplayName,
    [string] $requiredDelegatedPermissions,
    [string]$requiredApplicationPermissions,
    $servicePrincipal)
{
    # If we are passed the service principal we use it directly, otherwise we find it from the display name (which might not be unique)
    if ($servicePrincipal)
    {
        $sp = $servicePrincipal
    }
    else
    {
        $sp = Get-AzureADServicePrincipal -Filter "DisplayName eq '$applicationDisplayName'"
    }

    $requiredAccess = New-Object Microsoft.Open.MsGraph.Model.RequiredResourceAccess
    $requiredAccess.ResourceAppId = $sp.AppId 
    $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.MsGraph.Model.ResourceAccess]

    # $sp.Oauth2Permissions | Select Id,AdminConsentDisplayName,Value: To see the list of all the Delegated permissions for the application:
    if ($requiredDelegatedPermissions)
    {
        AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope"
    }
    
    # $sp.AppRoles | Select Id,AdminConsentDisplayName,Value: To see the list of all the Application permissions for the application
    if ($requiredApplicationPermissions)
    {
        AddResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role"
    }
    return $requiredAccess
}

The GetRequiredPermissions function calls a AddResourcePermission function that creates permissions (ResourceAccess objects).

# Adds the requiredAccesses (expressed as a pipe separated string) to the requiredAccess structure
# The exposed permissions are in the $exposedPermissions collection, and the type of permission (Scope | Role) is 
# described in $permissionType
function AddResourcePermission(
    $requiredAccess,
    $exposedPermissions,
    [string]$requiredAccesses,
    [string]$permissionType)
{
        foreach($permission in $requiredAccesses.Trim().Split("|"))
        {
            foreach($exposedPermission in $exposedPermissions)
            {
                if ($exposedPermission.Value -eq $permission)
                {
                    $resourceAccess = New-Object Microsoft.Open.MsGraph.Model.ResourceAccess
                    $resourceAccess.Type = $permissionType # Scope = Delegated permissions | Role = Application permissions
                    $resourceAccess.Id = $exposedPermission.Id # Read directory data
                    $requiredAccess.ResourceAccess.Add($resourceAccess)
                }
            }
        }
}

Using the script in an Azure Pipeline

To execute this script in the Azure pipeline that deploys and configures the rest of the application infrastructure we can use an Azure PowerShell task.

The task of the Azure Pipeline will look like this:

- task: AzurePowerShell@5
  displayName: 'Configure Teams tab SSO'
  inputs:
    azureSubscription: 'My Azure Service Connection'
    ScriptType: 'FilePath'
    ScriptPath: 'infra/AdditionalScripts/ConfigureTeamsTabSSO.ps1'
    ScriptArguments: 
      -applicationObjectId $(AzureAdObjectId) `
      -customDomainName $(CustomDomainName)
    azurePowerShellVersion: 'LatestVersion'

The advantage is that this task will connect to Azure with an Azure Service Connection that has enough rights to execute the Azure AD commands in this script. However, it involves passing to the Connect-AzureAD command the access token of the Service Principal associated with the Azure Service Connection. This can easily be done as I found out in a StackOverflow post.

$context = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile.DefaultContext
$graphToken = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate($context.Account, $context.Environment, $context.Tenant.Id.ToString(), $null, [Microsoft.Azure.Commands.Common.Authentication.ShowDialog]::Never, $null, "https://graph.microsoft.com").AccessToken
$aadToken = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate($context.Account, $context.Environment, $context.Tenant.Id.ToString(), $null, [Microsoft.Azure.Commands.Common.Authentication.ShowDialog]::Never, $null, "https://graph.windows.net").AccessToken
Connect-AzureAD -AadAccessToken $aadToken -MsAccessToken $graphToken -AccountId $context.Account.Id -TenantId $context.tenant.id

Summary

In this post, I wanted to show the different steps to configure Teams Tab SSO in PowerShell. The final script can be found here and is directly used in an Azure pipeline to automate this configuration. Although it does the job, I hope doing such Azure AD configurations will be supported soon in Pulumi as it would have been easier to set it up instead of coming up with a big PowerShell script like this which is not idempotent.


The opinions expressed herein are my own and do not represent those of my employer or any other third-party views in any way.

Copyright © 2024 Alexandre Nédélec. All rights reserved.