Most of the Active Directory object properties can be easily accessed by using the Get- or Set- such as Get-ADUser or Set-ADUser, but it is not that easy regarding LogonHours.
Multiple steps and challenges required to build the value, and in this tutorial, you will learn all the tips and tricks needed to set the LogonHours hours via Windows PowerShell.

You can download the script from my GitHub Repo over here

Understanding Logon Hours (LogonHours) Basics.

First, Let’s try to understand how the logon hours work and how it works by using the GUI of the Logon Hours window.
Open any AD user account property —> Account Tab —> click on Logon Hours.
The Blue block means a permitted logon.
If there is any white block, it means a logon denied on that selected time/day slot.

In the example below, the user cannot log in on Sunday or Saturday but can log in from Monday through Friday.

This user is not allowed to log in on Saturday or Sunday

Let’s go deeper and see how these values represented in the Attribute Editor.

Ensuring that the Advanced Features in Active Directory Management Console is checked to see the Attribute Editor.

Start by opening any Active Directory user property —> Attribute Editor tab.
From the Attributes Editor, scroll to LogonHours, double click on the LogonHours. A new window popped up that looks similar to the one below. Change the Value Format to Binary.

Logon Hours in Binary

Here is a basic explanation of what these values mean, don’t worry if you don’t understand it now; an in-depth explanation is on the way.

Logon Hours GUI

Additional information about LogonHours attribute can be found on the Microsoft.com site

Understanding LogonHours Property

In the LogonHours attribute (Binary view), the information represents each hour of the week. The value of each 1 means allowed to log in and the value of 0 means not allowed to log in.

The list below explains more about the Bits, Bytes, and each group represents.

  • One Bit represents one hour.
  • Each Byte (or a group of 8 bits) represents eight hours.
  • Each three Byte represents One Day (24 Hours).
  • 21 Byte represents one week.
  • 168 Bit represents one week in hours, which also means 168 hours per week.

All the results in the property are based on UTC.

Logon Hours GUI

Changing any value from 1 to 0 or 0 to 1 reflected in the Logon Hours GUI.
Let’s give it a try. In the first block, change the first bit and make it 0, so the first byte looks like 01111111.

Click on OK to save and close the LogonHours attribute, and then click on OK to close the user property window. Refresh the view by clicking on the Refresh icon in the Active Directory Users and Computers.

Reopen the same user, navigate to the Account tab and open the Logon Hours.
You find a single white block, which means logon denied on that time slot.

Result After Changing one bit

The result on your side may look different as these values are affected by the timezone, which is covered in a later section.
The LogonHours property that you saw in the Attribute Editor uses UTC, and Logon Hours GUI uses the local timezone. The GUI manages the time shifting based on the timezone to provide you with an accurate view.

Parameters for the PowerShell Script.

To make things easy to use for the user, I wrote a function Set-LogonHours to set the AD user logon hours and respect the timezone bias.
This function accepts the following parameters.

  • Identity: The Username of the target principal (String, Position=0).
  • TimeIn24Format: Array of the hours access with a permitted logon (Array, ValidateRange (0- 23).
  • Sunday: Apply the TimeIn24Format parameter this day (Switch).
  • Monday: Apply the TimeIn24Format parameter to this day (Switch).
  • Tuesday: Apply the TimeIn24Format parameter to this day (Switch).
  • Wednesday: Apply the TimeIn24Format parameter to this day (Switch).
  • Thursday: Apply the TimeIn24Format parameter to this day (Switch).
  • Friday: Apply the TimeIn24Format parameterto this day (Switch).
  • Saturday: Apply the TimeIn24Format parameter to this day (Switch).
  • NonSelectedDaysAre: This option for the non-selected weeks. What the default value should be? is it a permit or denied (ValidateSet =”WorkingDays” or “NonWorkingDays”).

Example How to Use Set-LogonHours

The following are some quick examples of how to execute the script function.

The example below sets the LogonHours for MyTestUser to allow login at 8,9,10,11,12 o’clock on the following days Monday, Tuesday, Wednesday, and Thursday. All the non-selected days are considered as permitted days to logon.

Set-LogonHours -identity "MyTestUser" -TimeIn24Format @(8,9,10,11,12) -Monday -Tuesday -Wednesday -Thursday -NonSelectedDaysare WorkingDays
LogonHours in the GUI

To set the LogonHours for users in a single OU, use the following example.

The Set-LogonHours function accepts values from the pipeline, so you can pipeline the result from the Get-ADUser cmdlet and pass the result to the Set-LogonHours function.

Get-ADUser -SearchBase 'OU=Test,OU=DevUsers,DC=Dev-Test,DC=local' -Filter *| Set-LogonHours -TimeIn24Format @(8,9,10,11,12,13,14,15,16)  -Monday -Tuesday -Wednesday -Thursday -NonSelectedDaysare WorkingDays 
Result for one of the users in the specified OU

LogonHours PowerShell Script Explination.

First, create a Byte array to store the final result.
The array length is 21, which represents a week.
One thing to note is that the LogonHours AD attribute accepts a Byte array, not an integer array.

$FullByte=New-Object "byte[]" 21

Then create a Hashtable that represents each hour of the day.
The value of the hashtable items is set to One based on the user input
The hashtable values are based on the user input, so if the user sets the value of TimeIn24Format to @(8,9,10,11,12,13,14,15), the respected value in the Hashtable should be set to 1.

$FullDay=[ordered]@{}
 0..23 | foreach{$FullDay.Add($_,"0")}
 $TimeIn24Format.ForEach({$FullDay[$_]=1})

Then join all the hashtable values together and store the result in a variable named $Working.

$Working= -join ($FullDay.values)

The user selects the working days via switch parameters, but what about the non-selected days?
What is the default value for the non-selected days? Permit login or denied login?
Whether the user considered all the non-selected days a permitted login or denied login, the first Switch set the default value for the weekdays.
The Second Switch statement will fill the user’s added weekdays parameter value with the allowed logon hours.

 Switch ($PSBoundParameters["NonSelectedDaysare"])
     { 
'NonWorkingDays' {$SundayValue=$MondayValue=$TuesdayValue=$WednesdayValue=$ThursdayValue=$FridayValue=$SaturdayValue="000000000000000000000000"} 
'WorkingDays' {$SundayValue=$MondayValue=$TuesdayValue=$WednesdayValue=$ThursdayValue=$FridayValue=$SaturdayValue="111111111111111111111111"}     }
 Switch ($PSBoundParameters.Keys)
 {
     'Sunday' {$SundayValue=$Working}
     'Monday' {$MondayValue=$Working}
     'Tuesday' {$TuesdayValue=$Working}
     'Wednesday' {$WednesdayValue=$Working}
     'Thursday' {$ThursdayValue=$Working}
     'Friday' {$FridayValue=$Working}
     'Saturday' {$SaturdayValue=$Working}
 }

The next line is to build up the full week string by combining all the values in one row.
This includes the hashtable values and the NonSelectedDaysare, which is required for fixing the time zone offset.
This makes the script can give the same result regardless of the time zone.

$AllTheWeek="{0}{1}{2}{3}{4}{5}{6}" -f $SundayValue,$MondayValue,$TuesdayValue,$WednesdayValue,$ThursdayValue,$FridayValue,$SaturdayValue

Timezone Challenge and Bits Shifting.

The LogonHours value is based on UTC format, and this led to the following possible scenarios:

  • Scenarios 1: The user is located in a minus time zone range like the US and Canada with -8 Hours UTC.
  • Scenarios 2: The user is located in the zero time zone range, such as Dublin, London +00 UTC.
  • Scenarios 3: The User is located in a plus time zone such as Bangkok, Hanoi, Jakarta +7 UTC.

To see and understand the challenge, let’s see it first in action to know how to build the solution.
Set the time zone to any time zone with 0 UTC, such as London, Dublin. 0 UTC.
Then change the Logon Hours for a test user to only allow a login for one hour as follows.
This helps see how this bit is moving after changing the time zone.

Only One hour is selected.

Change the Timezone to -2 or -3, such as (UTC – 03:00) Salvador. Open the same user Logon Hours GUI interface again, and see where the permitted login slot has now shifted.

The bit shifted 3 bits to the left.

The same thing happens if the selected timezone is (UTC +). The permit login slot is shifted to the right.
Timezone bias change should be reflected in the order of the bits, so the result is always correct.

In PowerShell, using the Get-Timezone cmdlet will read the current time zone information.
The Get-Timezone contains a property that shows the Timezone bias (Get-TimeZone).baseutcoffset.hours.
It’s possible to change the order of the bits based on the Timezone bais.

Timezone in UTC -
 If ((Get-TimeZone).baseutcoffset.hours -lt 0){
     $TimeZoneOffset = $AllTheWeek.Substring(0,168+ ((Get-TimeZone).baseutcoffset.hours))
     $TimeZoneOffset1 = $AllTheWeek.SubString(168 + ((Get-TimeZone).baseutcoffset.hours))
     $FixedTimeZoneOffSet="$TimeZoneOffset1$TimeZoneOffset"
 }
 Timezone is UTC +
 If ((Get-TimeZone).baseutcoffset.hours -gt 0){
     $TimeZoneOffset = $AllTheWeek.Substring(0,((Get-TimeZone).baseutcoffset.hours))
     $TimeZoneOffset1 = $AllTheWeek.SubString(((Get-TimeZone).baseutcoffset.hours))
     $FixedTimeZoneOffSet="$TimeZoneOffset1$TimeZoneOffset"
 }
 Timezone is UTC 0
 if ((Get-TimeZone).baseutcoffset.hours -eq 0){
     $FixedTimeZoneOffSet=$AllTheWeek
 }

Building the Final Byte array and setting the values

Now the hours are aligned with the time zone bias, and it’s time to build the array and convert the string to a byte array.
The result is stored in a variable named $FixedTimeZoneOffset, and it should be split into a group of 8 characters for converting it to Byte later.

Another challenge is each 8 bits group needs to have a reverse order, so the 1’s and 0’s order reflects the correct binary number bits order, then update the result in the $FullByte variable, which will be used to update the AD user information by using Set-ADUser cmdlet.

$BinaryResult=$FixedTimeZoneOffSet -split '(\d{8})' | where {$_ -match '(\d{8})'}
     Foreach($singleByte in $BinaryResult){
         $Tempvar=$singleByte.tochararray()
         [array]::Reverse($Tempvar)
         $Tempvar= -join $Tempvar
         $Byte = [Convert]::ToByte($Tempvar, 2)
         $FullByte[$i]=$Byte
         $i++
     }
 Set-ADUser  -Identity $Identity -Replace @{logonhours = $FullByte}

All the PowerShell Script.

Function Set-LogonHours{
 [CmdletBinding()]
 Param(
 [Parameter(Mandatory=$True)]
 [ValidateRange(0,23)]
 $TimeIn24Format,
 [Parameter(Mandatory=$True,
 ValueFromPipeline=$True,
 ValueFromPipelineByPropertyName=$True, 
 Position=0)]$Identity,
 [parameter(mandatory=$False)]
 [ValidateSet("WorkingDays", "NonWorkingDays")]$NonSelectedDaysare="NonWorkingDays",
 [parameter(mandatory=$false)][switch]$Sunday,
 [parameter(mandatory=$false)][switch]$Monday,
 [parameter(mandatory=$false)][switch]$Tuesday,
 [parameter(mandatory=$false)][switch]$Wednesday,
 [parameter(mandatory=$false)][switch]$Thursday,
 [parameter(mandatory=$false)][switch]$Friday,
 [parameter(mandatory=$false)][switch]$Saturday
 )
 Process{
 $FullByte=New-Object "byte[]" 21
 $FullDay=[ordered]@{}
 0..23 | foreach{$FullDay.Add($_,"0")}
 $TimeIn24Format.ForEach({$FullDay[$_]=1})
 $Working= -join ($FullDay.values)
 Switch ($PSBoundParameters["NonSelectedDaysare"])
 {
 'NonWorkingDays' {$SundayValue=$MondayValue=$TuesdayValue=$WednesdayValue=$ThursdayValue=$FridayValue=$SaturdayValue="000000000000000000000000"}
 'WorkingDays' {$SundayValue=$MondayValue=$TuesdayValue=$WednesdayValue=$ThursdayValue=$FridayValue=$SaturdayValue="111111111111111111111111"}
 }
 Switch ($PSBoundParameters.Keys)
 {
 'Sunday' {$SundayValue=$Working}
 'Monday' {$MondayValue=$Working}
 'Tuesday' {$TuesdayValue=$Working}
 'Wednesday' {$WednesdayValue=$Working}
 'Thursday' {$ThursdayValue=$Working}
 'Friday' {$FridayValue=$Working}
 'Saturday' {$SaturdayValue=$Working}
 }
 $AllTheWeek="{0}{1}{2}{3}{4}{5}{6}" -f $SundayValue,$MondayValue,$TuesdayValue,$WednesdayValue,$ThursdayValue,$FridayValue,$SaturdayValue
# Timezone Check
 if ((Get-TimeZone).baseutcoffset.hours -lt 0){
 $TimeZoneOffset = $AllTheWeek.Substring(0,168+ ((Get-TimeZone).baseutcoffset.hours))
 $TimeZoneOffset1 = $AllTheWeek.SubString(168 + ((Get-TimeZone).baseutcoffset.hours))
 $FixedTimeZoneOffSet="$TimeZoneOffset1$TimeZoneOffset"
 }
 if ((Get-TimeZone).baseutcoffset.hours -gt 0){
 $TimeZoneOffset = $AllTheWeek.Substring(0,((Get-TimeZone).baseutcoffset.hours))
 $TimeZoneOffset1 = $AllTheWeek.SubString(((Get-TimeZone).baseutcoffset.hours))
 $FixedTimeZoneOffSet="$TimeZoneOffset1$TimeZoneOffset"
 }
 if ((Get-TimeZone).baseutcoffset.hours -eq 0){
 $FixedTimeZoneOffSet=$AllTheWeek
 }
 $i=0
 $BinaryResult=$FixedTimeZoneOffSet -split '(\d{8})' | Where {$_ -match '(\d{8})'}
 Foreach($singleByte in $BinaryResult){
 $Tempvar=$singleByte.tochararray()
 [array]::Reverse($Tempvar)
 $Tempvar= -join $Tempvar
 $Byte = [Convert]::ToByte($Tempvar, 2)
 $FullByte[$i]=$Byte
 $i++
 }
 Set-ADUser  -Identity $Identity -Replace @{logonhours = $FullByte}                                   
 }
 end{
 Write-Output "All Done :)"
 }
 }

# Change the LogonHours for all the users in the Test OI
 Get-ADUser -SearchBase "OU=Test,DC=test,DC=local" -Filter *| Set-LogonHours -TimeIn24Format @(8,9,10,11,12,13,14,15,16)  -Monday -Tuesday -Wednesday -Thursday -NonSelectedDaysare WorkingDays 
# Change the LogonHours for a single user
 Set-LogonHours -Identity Jack.Ripper -TimeIn24Format @(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,0) -Monday -Tuesday -Wednesday -Thursday -Friday -NonSelectedDaysare NonWorkingDays # Allow Access during weekday

What’s Next

Rate this post