Automating with PowerShell: Teams Automapping

Something like 3 years ago I wrote a blog about using PowerShell to configure Onedrive sites using the odopen protocol. This was pretty much the only method to configure Onedrive to automatically map sites and have a zero-touch configuration.

Of course over the years the management side has improved and OneDrive usage has exploded. Unfortunately the onedrive automatic mapping structure isn’t where it should be yet. For example the GPO/intune method for automatic mapping configuration can take up to 8 hours to apply on any client.

During migrations and new deployment this is pretty much unacceptable. To make sure that mapping would be instant I’ve decided to create two scripts; One Azure Function which I’ve called AzMapper, and another client based script.

AzMapper requires you to create an Azure Function. To do that follow this manual and replace the script with the one below. This script is compatible with the Secure Application Model, and as such it can check all of your partner tenants too. Meaning you’ll only need to host a single version.

Replace $ApplicationID and $ApplicationSecret with your own from the Secure App Model.

You’ll also need to give your Secure Application Model a little more permissions, specifically to read the groups:

  • Go to the Azure Portal.
  • Click on Azure Active Directory, now click on “App Registrations”.
  • Find your Secure App Model application. You can search based on the ApplicationID.
  • Go to “API Permissions” and click Add a permission.
  • Choose “Microsoft Graph” and “Application permission”.
  • Search for “Sites” and click on “Sites.Read.All”. Click on add permission.
  • Search for “Team” and click on “TeamMember.Read.All”. Click on add permission.
  • Search for “Team” and click on “Team.ReadBasic.Alll”. Click on add permission.
  • Do the same for “Delegate Permissions”.
  • Finally, click on “Grant Admin Consent for Company Name.


using namespace System.Net
param($Request, $TriggerMetadata)
$ApplicationId = 'ApplicationID'
$ApplicationSecret = 'ApplicationSecret' 
$TenantID = $Request.Query.TenantID
$user = $Request.Query.Username

$body = @{
    'resource'      = ''
    'client_id'     = $ApplicationId
    'client_secret' = $ApplicationSecret
    'grant_type'    = "client_credentials"
    'scope'         = "openid"
$ClientToken = Invoke-RestMethod -Method post -Uri "$($tenantid)/oauth2/token" -Body $body -ErrorAction Stop
$headers = @{ "Authorization" = "Bearer $($ClientToken.access_token)" }
$UserID = (Invoke-RestMethod -Uri "$($user)" -Headers $Headers -Method Get -ContentType "application/json").id

$AllTeamsURI = "$($UserID)/JoinedTeams"
$Teams = (Invoke-RestMethod -Uri $AllTeamsURI -Headers $Headers -Method Get -ContentType "application/json").value
$MemberOf = foreach ($Team in $Teams) {
    $SiteRootUri = "$($"
    $SiteRootReq = Invoke-RestMethod -Uri $SiteRootUri  -Headers $Headers -Method Get -ContentType "application/json"
    $SiteDrivesUri = "$($"
    $SitesDrivesReq = (Invoke-RestMethod -Uri $SiteDrivesUri -Headers $Headers -Method Get -ContentType "application/json").value | where-object { $_.Name -eq "Shared Documents" }
    $DriveInfo = $SitesDrivesReq.ParentReference.siteid -split ','
    if($SiteRootReq.description -like "*no-auto-map*"){ continue }
    if ($null -eq [System.Web.HttpUtility]::UrlEncode($ { continue }
    [pscustomobject] @{
        SiteID    = [System.Web.HttpUtility]::UrlEncode("{$($DriveInfo[1])}")
        WebID     = [System.Web.HttpUtility]::UrlEncode("{$($DriveInfo[2])}")
        ListID    = [System.Web.HttpUtility]::UrlEncode($
        WebURL    = [System.Web.HttpUtility]::UrlEncode($SiteRootReq.webUrl)
        Webtitle  = [System.Web.HttpUtility]::UrlEncode($($Team.displayName)).Replace("+", "%20")
        listtitle = [System.Web.HttpUtility]::UrlEncode($


# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = [HttpStatusCode]::OK
    Body = $MemberOf

If you want sites to be skipped, you can add “no-auto-map” in the description in Teams. this will cause the script to skip that site.

Now we can browse to the AzMapper URL to check if our function is working. To test the AzMapper click on “Get Function URL” in the Azure Portal and copy the URL required. You’ll end up with something like:

an example of a test url would be:

This should return all sites for that specific user, it will contain exactly the information you need to create a odopen:// URL.

Client script

So you can schedule the client script using whatever method you prefer – as a startup script, using your RMM, or just a one-off during migrations. When a site is already configured to be synced it will skip this site. We do assume that OneDrive is already configured and just waiting to sync sites. 😉

$AutoMapURL = ""
$AutomapAPIKey = "TheAPIKeyFromTheAppAlsoKnownAsTheCode"

write-host "Grabbing OneDrive info from registry" -ForegroundColor Green
$TenantID = (get-itemproperty "HKCU:\SOFTWARE\Microsoft\OneDrive\Accounts\Business1").ConfiguredTenantId
$TenantDisplayName = (get-itemproperty "HKCU:\SOFTWARE\Microsoft\OneDrive\Accounts\Business1").Displayname
$username = (get-itemproperty "HKCU:\SOFTWARE\Microsoft\OneDrive\Accounts\Business1").userEmail
$CurrentlySynced = (get-itemproperty "HKCU:\SOFTWARE\Microsoft\OneDrive\Accounts\Business1\Tenants\$($tenantdisplayname)" -ErrorAction SilentlyContinue)
Write-Host "Retrieving all possible teams."  -ForegroundColor Green
$ListOfTeams = Invoke-RestMethod -Method get -uri "$($AutoMapURL)?Tenantid=$($TenantID)&Username=$($Username)&code=$($AutomapAPIKey)" -UseBasicParsing
$Upn = [System.Web.HttpUtility]::Urldecode($username)
foreach ($Team in $ListOfTeams) {
    write-host "Checking if site is not already synced" -ForegroundColor Green
    $sitename = [System.Web.HttpUtility]::Urldecode($Team.Webtitle)
    if ($ -like "*$($sitename) -*") {
        write-host "Site $Sitename is already being synced. Skipping." -ForegroundColor Green 
    else {
        write-host "Mapping Team: $Sitename" -ForegroundColor Green
        Start-Process "odopen://sync/?siteId=$($team.SiteID)&webId=$($team.webid)&listId=$($team.ListID)&userEmail=$upn&webUrl=$($team.Weburl)&webtitle=$($team.Webtitle)"
        start-sleep 5

And that’s it! running this script will map all the sites this specific user has access to, it won’t give weird pop-ups for users that do not have access and this should help you ease all Teams deployments by a lot. As always, Happy PowerShelling!

18 thoughts on “Automating with PowerShell: Teams Automapping

  1. Mike

    Hi Kelvin, been eagerly awaiting this one 🙂
    Im getting an “access denied” message when running the function – it looks like its occurring in the MemberOf for each.
    The invoke-restmethods inside the foreach all come back with the message, however I know my ID and Secret are valid, as it is successfully getting the list of Teams the user is a member of?
    Looks like its failing in our own tenant as well as our customers.

    Really appreciate your articles & your amazing contribution to the community!

    1. Kelvin Tegelaar Post author

      Hi Mike,

      I just realized that I forgot to add you’ll need to add some groups to your Secure Application Model App. I’ll update the post in the coming 5 minutes. 🙂

      1. Mike

        Bosch, easy as that 🙂

        That’s done the trick, many thanks for your super quick response! Amazing work! 🙂

  2. Bryan Schulz


    I am attempting to get this to work in our AZ enviroment.

    I am stuck on the part where I need to give my Secure Application Model a little more permissions.

    I cannot find the function I added in the list to allow the permissions.

    Could you give any tips or tricks to get it to show?

    Thank you

    1. Kelvin Tegelaar Post author

      You don’t need to look for the function, but the Application ID (GUID) you use for the Secure Application Model. You can also search for the display name. Hope that helps! 🙂

      1. Bryan Schulz

        Awesome i have one more issue.

        After that we re-ran the script and got the following error:
        The term ‘enter path or command to execute e.g.: C:\Users\bschulz\Overview Technology Solutions Inc\Coding – General\AZGlue/src/foo.ps1 or Invoke-Pester’ is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
        + CategoryInfo : ObjectNotFound: (enter path or c…r Invoke-Pester:String) [], ParentContainsErrorRecordException
        + FullyQualifiedErrorId : CommandNotFoundException

        Cannot get past step 5 in the README.txt

  3. Darren McCabe

    What a legend, I only recently started using Intune and Autopilot and the whole “8hour wait” thing was murdering me.

    I will admit to not having a clue what I have just done, but I managed to follow through it and get it working first time.

    THank you so much

  4. Darren McCabe

    Hi Kelvin,

    So as an addendum to my previous statement, we are wanting to run this as part of our InTune/Autopilot deployments.

    When I add it as a script to Intune I have no control over when it runs. It appears that it is running before OneDrive has been configured by InTune, therefore it isnt actually working, but InTune marks the deployment as successful.

    Is there anyway to make it wait until OneDrive is done before it runs the script?

  5. Pingback: Automating with PowerShell: Deploying Azure Functions - CyberDrain

  6. David Rigdon

    Hey Kelvin!

    I’m got a slight issue that I’m not sure where to start troubleshooting… The script runs great on the client side, it identifies the. proper teams that the user is apart of, and then even says “Mapping Team: TeamName. But it never shows up in OneDrive. Thoughts?

  7. ictgorilla

    Love to get this working..

    where can i find
    $AutomapAPIKey = “TheAPIKeyFromTheAppAlsoKnownAsTheCode”


  8. Stephen Moody

    I am having trouble getting the no-auto-map to work. For all Teams, the $SiteRootReq.description appears to be pulling $null, even when it does have a value. My secure app model app has Team.ReadBasic.All permissions (both delegated and app). Any idea what I might be missing?


Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.