So, a customer complained that the “password never expires” status is incorrectly represented within our product. Evidence was provided in form of the output of a PowerShell script, something along these lines:
Get-MGuser -UserId <user ID> -Property UserPrincipalName, PasswordPolicies | Select-Object UserPrincipalName, @{N="PasswordNeverExpires";E={$_.PasswordPolicies -contains "DisablePasswordExpiration"}
Part of my job is to assist our support folks with Microsoft 365 queries that they are unable to answer, or have doubts about. So, after some due diligence, the query eventually ended up in a Teams channel I monitor. My initial reaction is to point out the obvious inaccuracy in the code above… only to be parried by a link to the official documentation. Lo an behold the script sample found therein:
Get-MGuser -All -Property UserPrincipalName, PasswordPolicies | Select-Object UserprincipalName,@{N="PasswordNeverExpires";E={$_.PasswordPolicies -contains "DisablePasswordExpiration"}}
Sigh… that’s what we get in the AI era, I suppose. As Microsoft removed the ability for submitting community contributions to the official documentation, the only thing I could do about this is to hit the feedback button and leave a short note about it. Well, and ping our favorite Entra guy, just in case.
Now, why is the above (and some of the other examples in the official documentation) incorrect? Well, few things need to be noted here. First, the PasswordPolicies property is multi-valued by definition, as mentioned in the documentation:
| passwordPolicies | String | Specifies password policies for the user. This value is an enumeration with one possible value being DisableStrongPassword, which allows weaker passwords than the default policy to be specified. DisablePasswordExpiration can also be specified. The two can be specified together; for example: DisablePasswordExpiration, DisableStrongPassword. |
In other words, it is perfectly acceptable to have values such as “DisablePasswordExpiration, DisableStrongPassword“. The other issue is that the value of the PasswordPolicies property is returned as a string, both in the “raw” API response and via the Graph SDK for PowerShell. And strings do not mix well with PowerShell’s -contains operator. This is illustrated via the below screenshot, where the first query gives you incorrect results, due to its use of the -contains operator:
There are multiple ways to obtain the “correct” output, but the easiest one is to use the -match operator instead:
Get-MGuser -All -Property UserPrincipalName, PasswordPolicies | Select-Object UserPrincipalName,@{N="PasswordNeverExpires";E={$_.PasswordPolicies -match "DisablePasswordExpiration"}},PasswordPolicies
So in case you have been blindly following the official documentation, make sure to double-check your code. Hopefully, the documentation will be updated soon, though come to think of it, I probably didn’t phrase my feedback very well when filling in that form. Oh well, it’s not like the form gives you much to work with…
But wait, there is more! I decided to check whether the correct value is reflected in the CSV you can export via the Microsoft 365 admin center. Safe to say, I was not disappointed. It’s actually even worse therein, as all my users are reported as having password never expires set to True. As I have no way of telling how exactly the CSV file is being generated, I’m not sure what is wrong with it, but the same behavior is observed in other tenants, too. Even a freshly created Demo tenant will generate a CSV export with all user’s password apparently set to never expire.
The screenshot above overlays the CSV export from the Microsoft 365 Admin Center, with all irrelevant columns hidden, and the output of the “correct” PowerShell cmdlet (even though the -contains one should also give you the proper results in this case). If I were to take a guess, perhaps the CSV output is based on the password validity setting configured on the domain associated with the user… but that’s just a guess. Either way, the output is not to be trusted.



I used your improved command against our tenant, which dates to about 2014 and has always disallowed PW expiry (currently “Set passwords to never expire” is still checked in Admin Center).
But…the results are bewildering. Almost all of the older accounts come back “True,” but almost all of the newer ones come back False or False, None. If it were all one way or the other, I could chalk it up to MS going about the policy differently, say, in the 2020s versus before, but it can’t be that simple since there are exceptions on both sides (a few older accounts are False; a few newer ones are True).
Whatever the answer, I do know that passwords expire for no one, so I guess this all is academic.
We’re only checking the per-user flag here, there’s also the domain-wide setting:
which can take effect.