Friday, June 18, 2021

PrincipalContext::ValidateCredentials doesn't always work like you think it should (false positives!)

We've recently stumbled upon a nasty issue with PrincipalContext::ValidateCredentials, the one you could use to authenticate users against given Active Directory LDAP server. The library method is often referred to and there are tons of references to use it just like
var principalContext = new PrincipalContext(ContextType.Domain, domain);
principalContext.ValidateCredentials(username, password);
And this is where the problem is.
One of our Clients pointed us to an edge case where somehow expired passwords were accepted. We confirmed that passwords are expired for sure (some passwords were expired for over a year!). They had another service that uses the LDAP authentication and the two services were uneven: the other service was rejecting user credentials, our was happily accepting users. The other service doesn't use .NET's built in method of course.
What we have found is a complaint of someone who posted in 2016 their own issue where for some reason expired passwords were accepted over LDAPS. The post says
I am using PrincipalContext.ValidateCredentials(string, string) to validate user credentials. I am encountering behaviour that I am unable to explain and have, so far, been unable to clarify through usual searches and forum visits. Here is the behaviour, the credentials can be assumed to be correct on each call. The non-SSL behaviour is what I would expect. The SSL behaviour is not as expected in my understanding of the method.
Not using SSL/TLS

User is enabled and NOT expired - method returns TRUE
User is disabled and NOT expired - method returns FALSE
User is enabled and EXPIRED - method returns FALSE
User is disabled and EXPIRED - Method returns FALSE
Using SSL/TLS

User is enabled and NOT expired - method returns TRUE
User is disabled and NOT expired - method returns FALSE
User is enabled and EXPIRED - method returns TRUE
User is disabled and EXPIRED - Method returns FALSE
To summarise - when both machines in the negotiation (app server and domain controller) have the required certs installed, they will use SSL (port 636). In these circumstances the method returns TRUE for Expired accounts but when not using SSL (port 389) (for example the app server does not have the cert installed) the method is returning FALSE for Expired accounts. I would expect the return value to be the same for expired accounts in both scenarios, but it's possible I'm not considering something.
This looked like the case, yes, we've turned on SSL over LDAP for this particular installation. Yes, we also see that expired passwords are accepted.
Unfortunately, the post author is asked to create an entry on Connect and as we already know, Connect has been closed. No solution then.
But what ValidateCredentials has to do with LDAPS? Well, we've found another clue. Someone at StackOverflow describes how ValidateCredentials works internally:
Here's how ValidateCredentials(string, string) works: First, it tries to authenticate with the Negotiate, Signing, and Sealing context options. If this fails, it tries again with SimpleBind and SecureSocketLayer.
Ha! There goes the neighbourhood. When ValidateCredentials fails on Kerberos (it fails because the password is expired), it falls back to SimpleBind (basic auth) over SSL (which succeeds!).
Turning LDAPS off is not an option here but surely this is an option: instead of
var principalContext = new PrincipalContext(ContextType.Domain, domain);
principalContext.ValidateCredentials(username, password);
just make sure you call it
var principalContext = new PrincipalContext(ContextType.Domain, domain, ContextOptions.Negotiate | ContextOptions.Sealing | ContextOptions.Signing);
principalContext.ValidateCredentials(username, password);
This makes sure there's no SimpleBind+SSL fallback as it forces Kerberos only. And guess what? It just works correctly. Expired passwords are no longer accepted.

Tuesday, April 27, 2021

Microsoft Teams loads but renders a blank (white) window

Today I've spent like an hour trying to fix a sudden issue - Teams loads, the loading window shows correctly but then, instead of a normal main window, all I got was a blank white window.
It never happened before and of course I've tried to search for similar problems people had and possibly found solutions. I've tried all of this:
  • closing/reopening Teams
  • shutting down the machine
  • reinstalling Teams
  • removing both ..\App Data\Local\Microsoft\Teams and ..\App Data\Roaming\Microsoft\Teams
  • creating a custom link that points to Teams.exe instead of Update.exe --processStart "Teams.exe" where the default link points to
  • forcing a custom power scheme for the application so that for my dual GPU machine, the correct GPU is used
None of these worked.
What I suspected is that the Teams problem is somehow related to account switching. The default Windows client doesn't support multiple accounts, having two of them (for my two organizations) means I have to sign out and relogin to the other account every time I want to use it (alternatively I could switch to another user profile). This always worked. But today - the white window was clearly related to signing out of a working session and trying to sign into the other account.
What people recommend is to remove all Windows 10 linked accounts (work/school accounts in the Windows Credentials Manager) and this didn't help either. Somehow I noticed, however, that even removing a user profile (..\App Data\Roaming\Microsoft\Teams) caused my Teams to show the login prompt but skipping the password prompt. This leads to an interesting question: where does Teams cache user credentials? Interestingly enough, people also noticed this (Google "Teams Not Asking for Password").
The end of this story is: while I was not able to find a way to clear the cache locally, I just changed my domain's account password in the remote organization's registry and then cleared the Teams profile. This once again triggered the login prompt but this time it also asked for a password. And Teams loaded correctly.
Hope this helps someone in trouble.

Thursday, April 15, 2021

C# Puzzle No.24 (intermediate)

Given a generic interface definition
public interface IDoSomething : IDisposable { }
please explain why this is possible
class DoesSomething<String> : IDoSomething<String>
{
    public void Dispose()
    {
    }
}
and this is not possible (doesn't compile)
class DoesSomething<string> : IDoSomething<string>
{
    public void Dispose()
    {
    }
}
An explanation can be found here.

Thursday, March 25, 2021

Węzeł krajowy dla .NET - zakończenie zasadniczych prac części klienckiej i serwerowej

Moja implementacja biblioteki integracyjnej dla .NET do Węzła Krajowego, OldMusicBox.EIH.Client, o której wspominałem w poprzednich wpisach otrzymała właśnie sporą aktualizację dodającą do już wcześniej istniejącej demonstracji części klienckiej (czyli przykładowej aplikacji która pokazuje jak implementować klienta węzła krajowego), implementację części serwerowej.
Zwracam uwagę na ten element - istnieje demonstracyjnej wersji serwera oznacza bowiem, że prace integracyjne we własnej aplikacji można rozpocząć od prób integracji z tą przykładową implementacją serwera. Mój serwer, podobnie jak Symulator Węzła Krajowego, nie wymaga posiadania kont, nie wymaga w szczególności wpisywania haseł - użytkownik sam wybiera jakimi atrybutami chce się zalogować. Następnie symuluje część serwerową protokołu SAML2 w takiej wersji w jakiej implementuje go Węzeł Krajowy, zachowując dialekt SAML2 (Artifact Binding) oraz sposób szyfrowania asercji.
Jest to szczególnie użytecznie w sytuacji, w której implementacja klienta nie ma związku z załączoną tu częścią kliencką - na przykład wtedy kiedy klient powstaje w Javie, node.js czy PHP. Dla takich aplikacji klienckich pierwszą możliwością przeprowadzenia testu integracyjnego był do tej pory Symulator Węzła Krajowego. Teraz natomiast można użyć załączonej aplikacji i zasymulować część serwerową mając ją pod całkowitą kontrolą - w szczególności można ją w pełni debugować i dzięki temu prześledzić w którym miejscu aplikacja kliencka popełnia błąd.
Dla zainteresowanych - to było całkiem nietrywialne. Implementacja części klienckiej była wzorowana na przykładowym Javowym kodzie części klienckiej, jaka jest dołączana do dokumentacji WK. Ale symulowanie serwera wymagało zrozumienia algorytmu Key Agreement w wersji dla krzywych eliptycznych a potem całego mnóstwa prób i błędów. Istniejąca implementacja wymaga od klienta posiadania klucza prywatnego (bo klucz publiczny serwera jest częścią informacji zwracanej w zaszyfrowanej asercji), a serwer potrzebuje, oprócz swojego certyfikatu i klucza prywatnego, również klucza publicznego klienta.