Currently I dig a little into the area of Intune. And as soon as you consider to deploy AADJ clients only but you have still on-premise File Server data, you will come across logon scripts.

So I searched for a solution and saw that Intune unfortunately does not have something built-in for that so far. But there are some solutions around. I want to mention the solution from Nicola Suter and Matt White.

I took some inspirations but customized these solutions quite a bit.

The idea behind that solution is, that you deploy one task for each Use Case / Trigger (let’s say Logon or Logoff). In that task you call and invoke a script which is hosted online, not on the client machine. This let’s you the flexibility to make changes centrally at any time and don’t have to bother you about how these scripts get to the clients again.

That initial script behind may call other scripts, let’s say one for drive mapping, another one for different procedures. You even can make some helper modules which can then be consumed by those scripts.

How-to create a logon script with Intune?

First I explain the steps in a short way, after we look at each step in detail.

Procedure for creating a logon script with drive mapping logic in Intune. Click on the links to directly jump down to that part.

  1. Create Logging Helper Module (WriteLog.psm1)

    The first thing I did, was creating a logging module. This logs for each script (drive mapping, Task Scheduler) a separate log file. You can always extend that logging, for example if you wish to log to some central database, so for analyzing you won’t have to fetch the client logs.

  2. Create Drive Mapping logic (DriveMapLogic.psm1)

    This script contains some functions like for querying Active Directory groups of a user, check if the local domain is reachable and a drive mapping function.

  3. Create Drive Mapping script (DriveMappingConfig.ps1)

    The idea is, that with this separate script which calls the logic, we can focus on the parameters, the drives we need. So this file is usually that one to modify if we have to modify something on our drive mappings.

  4. Create Logon Script (LogonUser.ps1)

    We do not call the Drive mapping script from our client, we put a Logon script between, which calls the Drive mapping script. The reason behind that approach: You can easily add other logon procedures you want to run to that script (referencing other scripts) and don’t have to create a task for each.

  5. Create Intune Script for Task creation (LogonTaskUser.ps1)

    If you have your logic together, you need to create a script which you can deploy through Intune. This script just creates a task which calls the Logon script which is online hosted.

  6. Deploy the script LogonTaskUser.ps1 through Intune

    Now as you have all scripts together you can deploy the Scheduled Task Script in Intune and assign it to the desired groups.

Overview

As you can see, the whole script logic I have made available over Azure Blob Storage.

Detailed How-to for drive mapping in Intune

Preparation: Create Storage Account

In Azure Portal create a new storage account, type BlobStorage.

After creation, in the Overview section click on Container and create a new container. Here it’s important to allow anonymous read access.

After the container is created you can upload PowerShell files there. When you click on an uploaded file, you see the URL of that file which you will need in some scripts below.

Important Note

In the steps below we use Invoke-RestMethod several times. When dealing with Invoke-RestMethod and Invoke-Expression it may fail because of some characters like  – this is because the file requested is not UTF-8 encoded.

To resolve this I saw some hackish solutions which looked like this:

(Invoke-RestMethod '$url').Replace('ï','').Replace('»','').Replace('¿','')

This may resolve the issue in some cases. But if you start nesting your Invoke-RestMethod calls as I did in this example this does not help. But luckily the solutions is an easy one:

Switch the encoding of your PowerShell file from UTF-8-BOM (or maybe even a different encoding) to UTF-8. You can check what encoding the file has with Notepad++, with that tool you can also convert it easily to UTF-8.

1. Create Logging Helper Module (WriteLog.psm1)

The logging helper module creates some logging information for us, when the other scripts run. This is useful and important to detect errors. Actually in that script I log only to a logfile. But you can think about extending it:

  • Add Event Log entries
  • Put that into a database which your Service Desk is able to query
  • Notifications on some events
<#
        .SYNOPSIS
            This script contains the logging function.

        .DESCRIPTION

        .NOTES
            Author: Philippe Tschumi - https://techblog.ptschumi.ch
            Last Edit: 2020-05-03
            Version 1.0 - initial Release
#>

function Write-Log {
    [CmdletBinding()]
    param(
         [Parameter(Mandatory=$True)]
         [ValidateNotNullOrEmpty()]
         [string]$Source,

         [Parameter()]
         [ValidateNotNullOrEmpty()]
         [int]$EventId = 0,
                        
         [Parameter(Mandatory=$True)]
         [ValidateNotNullOrEmpty()]
         [string]$Message,
 
         [Parameter(Mandatory=$True)]
         [ValidateNotNullOrEmpty()]
         [ValidateSet('Information','Warning','Error')]
         [string]$EntryType = 'Information'
     )

    $LogFolder = "$($env:ProgramData)\IntuneLogging"
    $LogFile = "$($LogFolder )\$($Source).log"
    if(-not (Test-Path $LogFolder)) {
        New-Item -Path $LogFolder -ItemType directory
    }
    if(-not (Test-Path $LogFile)) {
        Add-Content -Path $LogFile -Value "Date;Level;Message" -ErrorAction SilentlyContinue
    }
    Add-Content -Path $LogFile -Value "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss");$($EntryType);$($Message)" -ErrorAction SilentlyContinue

 }

2. Create Drive Mapping logic (DriveMapLogic.psm1)

The connection check

The connection check function accepts a timeout parameter which is by default 30 seconds. It checks if $env:USERDNSDOMAIN can be reached. If not the script will exit otherwise it’s allowed to continue.

function ConnectionCheck {
    <#
        .SYNOPSIS
            This functions checks if a connection to the corporate network is available.

        .DESCRIPTION

        .PARAMETER Timeout
            Timeout value in seconds.

        .EXAMPLE
            ConnectonCheck
            This checks the connection every 5 seconds until the default timeout of 30 seconds is over.

        .EXAMPLE
            ConnectonCheck -Timeout 45
            Connection check with custom timeout.
    #>
    param(
         [Parameter()]
         [ValidateNotNullOrEmpty()]
         [int]$Timeout=30
     )

    $Connected = $false
    do {
        if (Resolve-DnsName $env:USERDNSDOMAIN -ErrorAction SilentlyContinue) {
            $connected=$true
            if($EnableLog)  { Write-Log -Source DriveMapping -Message "DNS Domain $($env:USERDNSDOMAIN) successfully reached." -EntryType Information }
        }
        else {
            if($EnableLog)  { Write-Log -Source DriveMapping -Message "Cannot resolve DNS Domain Name $($env:USERDNSDOMAIN). It seems there is currently no connection to that network." -EntryType Warning }
            Start-Sleep -Seconds 5
            $Timeout -= 5
            if ($Timeout -le 0){
                if($EnableLog)  { Write-Log -Source DriveMapping -Message "Timeout exceeded to resolve DNS Domain Name ($($env:USERDNSDOMAIN)). Script exited." -EntryType Error }
                exit
            }
        }
    } 
    while( -not $Connected)
}
ConnectionCheck -Timeout $Timeout

AD Group Membership Check

This part allows us to make a group filtering for the drive mapping. We query the on-Premise Active Directory to fetch all groups the user is member of. This is possible with the System.DirectoryServices.AccountManagement namespace. Here we use again $env:USERDNSDOMAIN to determine where to connect. $env:USERNAME returns us the name of the current user. For this to work, the client must be able to reach a Domain Controller.

function Get-ADGroupMembership {
    <#
        .SYNOPSIS
            This functions makes a Query to fetch all groups the current user is a member of.

        .DESCRIPTION

        .EXAMPLE
            Get-ADGroupMembership
            Return Value: Array of Groups

        .OUTPUTS
            System.Array. Array of Group names in String format.
    #>

    Add-Type -AssemblyName System.DirectoryServices.AccountManagement
    $pc = [System.DirectoryServices.AccountManagement.PrincipalContext]::new([System.DirectoryServices.AccountManagement.ContextType]::Domain,$env:USERDNSDOMAIN)
    $up = [System.DirectoryServices.AccountManagement.AuthenticablePrincipal]::FindByIdentity($pc,$env:USERNAME)

    $ActiveDirectoryGroupsOfUser = @()
    foreach($group in $up.GetGroups($pc)) {
        $ActiveDirectoryGroupsOfUser += $group.SamAccountName
    }
    if($EnableLog)  { Write-Log -Source DriveMapping -Message "Group Information queried: $($ActiveDirectoryGroupsOfUser -join ", ")" -EntryType Information }
    return $ActiveDirectoryGroupsOfUser
}

Check if the user is allowed to map the drive

Here we compare a list of groups which are allowed to map a specific drive, a deny list of groups and the Users Group membership to determine if the drive should be mapped or not and returns that information.

function CheckDriveMapEligibility {
    <#
        .SYNOPSIS
            This functions checks if the user is allowed to map a specific drive.

        .DESCRIPTION

        .PARAMETER Drive
            Array of drive mapping information.
        
         .PARAMETER ADGroupsOfUser
            Array Groups the User is member of.

        .EXAMPLE
            CheckDriveMapEligibility -Drive $drive -ADGroupsOfUser $ADGroupsOfUser
            
        .OUTPUTS
            System.Array. Array of Eligibility information.
    #>

    param(
         [Parameter(Mandatory=$True)]
         [ValidateNotNullOrEmpty()]
         [array]$Drive,

         [Parameter(Mandatory=$True)]
         [ValidateNotNullOrEmpty()]
         [array]$ADGroupsOfUser
    )

    $allowMatch = ""
    $denyMatch = ""
    $denied = $false
    $allowed = $false

    foreach($deny in $Drive.DeniedGroups) {
        if($ADGroupsOfUser.contains($deny)) { 
            $denied = $true
            $denyMatch = $deny
            break
        }
    }
    if(-not $denied) {
        if(($Drive.AllowedGroups | Measure-Object).Count -eq 0) {
            $allowed = $true
        }
        else {
            foreach($allow in $Drive.AllowedGroups) {
                if($ADGroupsOfUser.contains($allow)) { 
                    $allowed = $true
                    $allowMatch = $allow
                    break
                }
            }
        }
    }
    return @{
        "denied" = $denied
        "allowed" = $allowed 
        "denyMatch" = $deny
        "allowMatch" = $allow
    }
}

Check if drive is already mapped

If we have already a drive mapping which conflicts with drive mapping script, we remove that mapping. If the combination of drive letter and UNC path already exists, we return true, otherwise false.

function CheckForDriveExistence {
    <#
        .SYNOPSIS
            This function checks if a drive or path is already mapped differently.

        .DESCRIPTION

        .PARAMETER Drive
            Array of drive mapping information.

        .EXAMPLE
            CheckForDriveExistence -Drive $drive
            
        .OUTPUTS
            bool. Drive exists yes or no.
    #>

    param(
         [Parameter(Mandatory=$True)]
         [ValidateNotNullOrEmpty()]
         [array]$Drive
    )

    # Clean up conflicting mappings
     if(Get-PSDrive | Where-Object {($_.DisplayRoot -eq $Drive.UNCPath -and $_.Name -ne $Drive.DriveLetter) -or ($_.Name -eq $Drive.DriveLetter -and $_.DisplayRoot -ne $Drive.UNCPath) }) {
        $ExitCode = (Start-Process -FilePath "net" -ArgumentList "use $($Drive.DriveLetter): /delete" -Wait -PassThru -WindowStyle Hidden).ExitCode
        Write-Log -Source DriveMapping -Message "Removed conflicting Drive $($Drive.DriveLetter). UNC Path $($Drive.UNCPath). Exit Code: $($ExitCode)" -EntryType Warning
    }
   
    if(Get-PSDrive | Where-Object {($_.DisplayRoot -eq $Drive.UNCPath -and $_.Name -eq $Drive.DriveLetter)}) { return $true } 
    else { return $false }
}

Drive Mapping function

This function is responsible for the drive mapping. It checks with helper function if a mapping is allowed or not. It also knows the replace mode, so if variable $ReplaceMode in the configuration is set to true, a drive will be removed and re-added when the script runs.

function MapDrive {
    <#
        .SYNOPSIS
            This function maps a network drive.
 
        .DESCRIPTION
 
        .PARAMETER Drive
            Array of drive mapping information.
 
         .PARAMETER ADGroupsOfUser
            Array Groups the User is member of.
 
        .PARAMETER ReplaceMode
            Whether existing drives should be replaced (delete and newly created) or not. This Parameter is optional and false by default.
 
        .EXAMPLE
            MapDrive -Drive $Drive -ADGroupsOfUser $ADGroupsOfUser -ReplaceMode $ReplaceMode
 
    #>
 
    param(
         [Parameter(Mandatory=$True)]
         [ValidateNotNullOrEmpty()]
         [array]$Drive,
 
         [Parameter(Mandatory=$True)]
         [ValidateNotNullOrEmpty()]
         [array]$ADGroupsOfUser,
 
         [Parameter()]
         [ValidateNotNullOrEmpty()]
         [bool]$ReplaceMode=$false
    )
 
    $Eligibility = CheckDriveMapEligibility $Drive $ADGroupsOfUser
    $DriveLetterExisting = CheckForDriveExistence $Drive
 
    if(-not $Eligibility.allowed) {
        if($DriveLetterExisting) {
            $ExitCode = (Start-Process -FilePath "net" -ArgumentList "use $($Drive.DriveLetter): /delete" -Wait -PassThru -WindowStyle Hidden).ExitCode
            if($EnableLog)  { Write-Log -Source DriveMapping -Message "Drive $($Drive.DriveLetter) removed. User is not permitted to access UNC Path $($Drive.UNCPath). Exit Code: $($ExitCode)" -EntryType Warning }
        }
        if($EnableLog)  { Write-Log -Source DriveMapping -Message "User was not allowed to map Drive $($Drive.DriveLetter) on UNC Path $($Drive.UNCPath). Explicit deny: $($Eligibility.denied). Deny match: $($Eligibility.denyMatch). Allow status: $($Eligibility.allowed))" -EntryType Information }
    }
    else {
        if(($DriveLetterExisting -and ($ReplaceMode -or (Get-SmbMapping -LocalPath "$($Drive.DriveLetter):").Status -eq "Disconnected"))) {
            $ExitCode = (Start-Process -FilePath "net" -ArgumentList "use $($Drive.DriveLetter): /delete" -Wait -PassThru -WindowStyle Hidden).ExitCode
            if($ReplaceMode) {
                $ReasonString = "Script is running in Replace Mode"
            }
            else {
                $ReasonString = "Drive was disconnected"
            }
            Write-Log -Source DriveMapping -Message "Drive $($Drive.DriveLetter) removed because $($ReasonString). UNC Path: $($Drive.UNCPath). Exit Code: $($ExitCode)" -EntryType Information
            $DriveLetterExisting = $false
        }
        if(-not $DriveLetterExisting) {
            try {
                New-PSDrive -PSProvider FileSystem -Name $Drive.DriveLetter -Root $Drive.UNCPath -Persist -Scope Global -ErrorAction Stop | Out-Null
                Set-ItemProperty -Path "HKCU:\Network\$($Drive.DriveLetter)" -Name ConnectionType -Value 1 -Force -ErrorAction SilentlyContinue
                (New-Object -ComObject Shell.Application).NameSpace("$($Drive.DriveLetter):").Self.Name=$Drive.Label            
                if($EnableLog)  { Write-Log -Source DriveMapping -Message "Drive $($Drive.DriveLetter) mapped successfully on UNC Path $($Drive.UNCPath). Allow match: $($Eligibility.allowMatch)" -EntryType Information }
            }
            catch {
                if($EnableLog)  { Write-Log -Source DriveMapping -Message "Error in mapping Drive $($Drive.DriveLetter) on UNC Path $($Drive.UNCPath). Error: $($_.Exception.Message)" -EntryType Error }
 
            }
        }
        else{
            (New-Object -ComObject Shell.Application).NameSpace("$($Drive.DriveLetter):").Self.Name=$Drive.Label
            if($EnableLog)  { Write-Log -Source DriveMapping -Message "Drive $($Drive.DriveLetter) already exists. Updated." -EntryType Information }
        }
    }
 
}

3. Create Drive Mapping Script (DriveMappingConfig.ps1)

Here in this step I directly put the complete script of that part. It’s self explaining. You have here some variables to configure at the beginning, as the URL path to the modules before, if you want logging or not, the timeout and the replace mode.

<#
        .SYNOPSIS
            Intune Drive Mapping PowerShell Script to connect on-Premise Network Drives.

        .DESCRIPTION

        .NOTES
            Author: Philippe Tschumi - https://techblog.ptschumi.ch
            Last Edit: 2020-05-03
            Version 1.0 - initial Release
#>


####################################################################################################################
# CUSTOM PARAMETERS                                                                                                #
# Modify as you needed                                                                                             #
####################################################################################################################


# Enable or disable logging
$EnableLog = $true

# URLs to the other modules.
$LogModule = "https://yourstorageaccount.blob.core.windows.net/yourcontainer/WriteLog.psm1"
$DriveMapLogic = "https://yourstorageaccount.blob.core.windows.net/yourcontainer/DriveMapLogic.psm1"

# If drive should be replaced (delete and recreate) if it is existing.
$ReplaceMode = $false

# if not connected to domain, after which Timeout script should terminate?
$Timeout = 30

# Add the drives and UNC Paths here. If you don't have groups and it should map for everyone, keep the arrays empty. Deny overrules Allow.
$DriveMappings = @(
    @{
        "AllowedGroups" = @("GS-HumanResources")
        "DeniedGroups" = @()
        "DriveLetter" = "H"
        "Label" = "Human Resources"
        "UNCPath" = "\\fileserver.domain.local\hr"
     },
    @{
        "AllowedGroups" = @("GS-Finance","GS-Executive")
        "DeniedGroups" = @("GS-FinanceBasic")
        "DriveLetter" = "X"
        "Label" = "Finance"
        "UNCPath" = "\\fileserver.domain.local\finance$"
     }
     @{
        "AllowedGroups" = @()
        "DeniedGroups" = @()
        "DriveLetter" = "P"
        "Label" = "$($env:USERNAME)"
        "UNCPath" = "\\fileserver.domain.local\$($env:USERNAME)"
     }
)
# END OF CUSTOM SECTION #############################################################################################


if($EnableLog)  { 
    Invoke-RestMethod $LogModule | Invoke-Expression
    Write-Log -Source DriveMapping -Message "********** Drive mapping started: Time: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss") **********" -EntryType Information
}

Invoke-RestMethod $DriveMapLogic | Invoke-Expression

# Query AD Groups of User
$ADGroupsOfUser = Get-ADGroupMembership

foreach($Drive in $DriveMappings) {
    MapDrive -Drive $Drive -ADGroupsOfUser $ADGroupsOfUser -ReplaceMode $ReplaceMode
}

if($EnableLog)  { Write-Log -Source DriveMapping -Message "********** Drive mapping finished. Time: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss") **********" -EntryType Information }

4. Create Logon Script (LogonUser.ps1)

This is the logon script which the Scheduled Task will run. It just calls the DriveMappingConfig.ps1 which is responsible for the drive mapping. This script you can extend with other logon logic.

#Drive Mapping
$DriveMapConfig = "https://yourstorageaccount.blob.core.windows.net/yourcontainer/DriveMappingConfig.ps1"
Invoke-RestMethod $DriveMapConfig | Invoke-Expression

5. Create Intune Script for Task Creation (LogonTaskUser.ps1)

With this script we create a Scheduled Task which runs in User Context. That task basically runs PowerShell, calls an URL and Invokes the code. We set here some parameters like a little delay trigger. This is the Script we use in Intune.

<#
        .SYNOPSIS
            This script contains User Logon Task which starts online logon script.

        .DESCRIPTION
            This script should be deployed through Intune to register a Logon Task.

        .NOTES
            Author: Philippe Tschumi - https://techblog.ptschumi.ch
            Last Edit: 2020-10-18
            Version 1.1 - added Session Unlock trigger
#>

# Path to Logging Module
$LogModule = "https://yourstorageaccount.blob.core.windows.net/scripts/WriteLog.psm1"
Invoke-RestMethod $LogModule | Invoke-Expression

Write-Log -Source TaskScheduler -Message "********** LogonScriptUser for $($env:USERNAME) started: Time: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss") **********" -EntryType Information

$ScheduledTaskArguments = '-WindowStyle Hidden -Command "& Invoke-RestMethod https://yourstorageaccount.blob.core.windows.net/scripts/LogonUser.ps1 | Invoke-Expression"'
$TaskName = "LogonScript_$($env:USERNAME)"
$TaskPath = "\Intune\"

$TaskExists = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
# If Task exist compare
if (($TaskExists)-and (Get-ScheduledTask -TaskName $TaskName).Actions[0].Arguments -eq $ScheduledTaskArguments) {
    Write-Log -Source TaskScheduler -Message "Task exists, no update needed." -EntryType Information
}
# update if different
elseif ($TaskExists) {
        $TaskExists.Actions[0].Arguments = $ScheduledTaskArguments
        $TaskExists | Set-ScheduledTask
        Write-Log -Source TaskScheduler -Message "Task updated." -EntryType Information
}
# create if not existing
else {
    $ScheduledTaskAction = New-ScheduledTaskAction -Execute "$($PSHOME)\powershell.exe" -Argument $ScheduledTaskArguments
    $ScheduledTaskTrigger1 = New-ScheduledTaskTrigger -AtLogon -User "$($env:USERDOMAIN)\$($env:USERNAME)"
    $ScheduledTaskTrigger1.Delay = "PT5S"
    $ScheduledTaskTrigger1.ExecutionTimeLimit = "PT10M"

    $stateChangeTrigger = Get-CimClass -Namespace ROOT\Microsoft\Windows\TaskScheduler -ClassName MSFT_TaskSessionStateChangeTrigger
    $ScheduledTaskTrigger2 = New-CimInstance -CimClass $stateChangeTrigger -ClientOnly
    $ScheduledTaskTrigger2.UserId = "$($env:USERDOMAIN)\$($env:USERNAME)"
    $ScheduledTaskTrigger2.StateChange = 8 # TASK_SESSION_STATE_CHANGE_TYPE.TASK_SESSION_UNLOCK
    $ScheduledTaskTrigger2.Delay = "PT5S"
    $ScheduledTaskTrigger2.ExecutionTimeLimit = "PT10M"

    $ScheduledTaskTriggers = @(
        $ScheduledTaskTrigger1,
        $ScheduledTaskTrigger2
    )
   

    $ScheduledTaskSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -Compatibility Win8
    $ScheduledTaskPrincipal = New-ScheduledTaskPrincipal -UserId "$($env:USERDOMAIN)\$($env:USERNAME)"
    $ScheduledTask = New-ScheduledTask -Action $ScheduledTaskAction -Settings $ScheduledTaskSettings -Trigger $ScheduledTaskTriggers -Principal $ScheduledTaskPrincipal
    Register-ScheduledTask -InputObject $ScheduledTask -TaskName $TaskName -TaskPath $TaskPath 
    Start-ScheduledTask $TaskName -TaskPath $TaskPath
    Write-Log -Source TaskScheduler -Message "LogonScriptUser registered." -EntryType Information
}

Write-Log -Source TaskScheduler -Message "********** LogonScriptUser for $($env:USERNAME) finished: Time: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss") **********" -EntryType Information
exit(0);

6. Deploy the script LogonTaskUser.ps1 in Intune

In the last section we finally switch to Intune to deploy everything. For that we have to select Devices and then Scripts within Intune. Here we click Add for adding the script LogonTaskUser.ps1, after choosing a name we can now upload the file.

Important: You should modify the switch for Run this script using the logged on credentials to Yes. In other scenarios (for other scripts than User Logon Scripts) you may want to keep it run as system.