How To: Decode LogonHours Attribute

In this post we look at the LogonHours attribute, which is used to restrict when a user is allowed to logon, and how to decode this attribute.

The LogonHours attribute has a octet data type that is used to store a 21 byte value which defines when a user is allowed to logon, outside of these hours the user will receive the following error message when they try to logon:

This may be seen as one of the following errors:

Error 1327: Account restrictions are preventing this user from signing in. For example: blank passwords aren't allowed, sign-in times are limited, or a policy restriction has been enforced

Error 1328: Your account has time restrictions that keep you from signing in right now.

The LogonHours attribute is used to define when a user is permitted to log on, it uses the 21 byte data structure to represent the day’s of the week.  It uses three bytes to represent each day of the week. The three bytes represent the hours of the day, the diagram below shows the mapping of the bytes to days and hours.

The user's permitted logon hours are displayed in the properties of the user in Active Directory User and Computers under the Account tab. 

One of the challenges with decoding the LogonHours attribute is that the data is saved based on UTC, as shown in the mapping above, however, Active Directory Users and Computers will display the details based on the local time zone of the computer running ADUC, and will adjust the times based on the time zone offset.   Below we can see that the left hand picture shows the Logon Hours on a computer with the time zone set to UTC, while the right shows the same details but the computer has a time zone set to Melbourne (UTC+10).

The time zone of the Domain Controller, which authenticates the user will be used to determine, if they can log on, or not.

This is the value of the attribute based on the permitted logon hours of Monday to Friday 6am to 7pm on a machine with time zone set to UTC, as shown in the left picture above.

DN> CN=Teena Lee,OU=Domain Users,DC=w2k12,DC=local
> logonHours: 00 00 00 C0 FF 03 C0 FF 03 C0 FF 03 C0 FF 03 C0 FF 03 00 00 00

We can see that this aligns with the mapping above, with the Sunday and Saturday bytes set to zeros. Next, this is the value set for the same time window on a machine with the time zone set to Melbourne (UTC+10)

DN> CN=Teena Lee,OU=Domain Users,DC=w2k12,DC=local
> logonHours: 00 00 F0 FF 01 F0 FF 01 F0 FF 01 F0 FF 01 F0 FF 01 00 00 00 00

The Sunday bytes now have values set, as the time was adjusted by -10 hours before it was saved. Next, this is the value set for the same time window on a machine with the time zone set to Pacific Time (UTC-10)

DN> CN=Teena Lee,OU=Domain Users,DC=w2k12,DC=local
> logonHours (BIN): 00 00 00 00 C0 FF 07 C0 FF 07 C0 FF 07 C0 FF 07 C0 FF 07 00 00 

With this one, the hours data is now written into the Saturday bytes due to the UTC-10 offset.

The LogonHours functionality is limited to a single time zone, and can potentially cause logon issues, if a user travels, or authenticates to a Domain Controller which has a different time zone set.

The AD Properties dialog in NetTools has a Restrictions tab which displays the Logon Hours, by default it will use the local time zone to display this information, however, there is an option to allow you to manually adjust the time zone to see the impact the user's ability to logon.

Below is the code used to display the LogonHours in NetTools, the function is called for each square in the grid, the ACol and ARow defining the square that is being queried, the function will colour the square blue, if the LogonHour is set.  The function also automatically adjusts the LogonHours based on the local or user selected time zone.

void dgHoursDrawCell(TObject *Sender, int ACol, int ARow, TRect &Rect, TGridDrawState State)
{
int Index, Col,Row, Mask;
int Val, Bias;

   // use Col and Row to reflect tz offset
   Col = ACol;
   Row = ARow;

   // change start of week to Monday
   if (Row==6){
      Row = 0;
   } else {
      Row++;
   }

   if (chkLocalTime->Checked){
      Bias = tz.Bias/60;  // get local time zone, tz populated when form is loaded
   } else {
      try {
          Bias = StrToInt(cmbTZOffset->Text);  // get user selection
      }
      catch(...){
          Bias = 0;
      }
   }

   Col += Bias;  // add time zone offset

   if (Col > 23) {  // rap pointer to start of next day
      Row++;
      Col -= 24;
   }

   if (Col < 0) {  // rap pointer to end of the previous day
      Row--;
      Col += 24;
   }

   if (Row > 6) Row = 0; // rap pointer to valid data
   if (Row < 0) Row = 6;

   if (Col >=0 && Col <=7) Index=0;  // select the correct hours offset bytes
   if (Col >=8 && Col <=15) Index=1;
   if (Col >=16 && Col <=23) Index=2;

   Index += (3 * Row);  // get correct byte
   Mask = 0x1 << (Col % 8);  // create bit mask for hour based on col number

   Val = HourBuffer[Index] & Mask;  // apply mask to check if set

   if (Val){  // Val is non zero set square to blue
       dgHours->Canvas->Brush->Color = clBlue;
   } else {
       dgHours->Canvas->Brush->Color = clWhite;
   }

   dgHours->Canvas->FillRect(Rect);  // draw the square

}