-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
NTLM Hash Attribute in LDAP Outpost? #8768
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I would be happy to contribute code or code up my own solution. I just need some direction. |
Yeah you can do so in the password change flow. Other question though: why not use the Radius provider directly instead of using the LDAP one? |
Can you help me unpack how I might do that in the password change flow? I didn't see any way to inject arbitrary code. The RADIUS provider doesn't support EAP w/ PEAP-MSCHAPv2 as far as I know.. |
In the flow user.attributes["ipaNTHash"] = ntlm_hash(context['flow_plan'].context["prompt_data"]["password"]) I might be wrong about the code itself, I'm not 100% fluent in authentik policy yet, but it should be this. Also, you'll need to get an ntlm_hash function somehow. |
@rissson your help got me to the point that my testing with from Crypto.Hash import MD4
request.user.attributes["ipaNTHash"] = MD4.new(context["prompt_data"]["password"].encode('utf-16-le')).digest().hex()
return True |
Perfect! Would you be willing to contribute to https://docs.goauthentik.io/integrations/? |
@ZUCCzwp you will have to translate that error for us. I haven't upgraded to 2024.2 yet. |
@jcrapuchettes Could you share the freeradius config you used to get it to work with authentik ldap? |
I configured FreeRADIUS via the pfSense GUI, but attached are the eap and ldap config files. |
After upgrading to 2024.4.2, I found (like @ZUCCzwp did) that the Crypto package no longer is included in the shipped docker container. I will post when I find a solution. |
Here is the solution: https://gist.github.com/jcrapuchettes/830c99982e391c858f5b7eb066c02749 |
@jcrapuchettes Thank you for your work, you save me a lot of time, i'm wondering if you can share with me the authentik configuration in order to setting up in the right way my server :). And if it is possible, can you share your virtual server configuration? for example i see in your eap config file, that you have two virtual server, inner-tunnel-peap and inner-tunnel-ttls. in any case thank you |
Hi, Is it possible to update |
@jcrapuchettes I noticed that this policy didn't work if You're reseting password I came up with version that works eather during password change from user level and recovery flow: import hashlib
def generate_ntlm_hash(password):
"""Generate an NTLM hash from the given password."""
password_unicode = password.encode('utf-16le')
return hashlib.new('md4', password_unicode).hexdigest()
def store_ntlm_hash(user, password):
"""Store the NTLM hash in the user's attributes."""
if not user.attributes:
user.attributes = {}
user.attributes["ipaNTHash"] = generate_ntlm_hash(password)
return user.save()
# Determine if the user is authenticated or pending
user = request.user if request.user and not request.user.is_anonymous else context.get("pending_user")
if user:
# Extract the password from the context
password = context.get("prompt_data", {}).get("password") or context.get("password")
if password:
return store_ntlm_hash(user, password)
else:
ak_message("There is no new password value in context")
return False
else:
ak_message("There is no user context")
return False |
@showier-drastic The best workaround so far is to generate recovery links or send recovery emails to the user. |
I didn't want to ask every user to change their passwords in order to populate new hash fields, so I tried to incorporate this into a authentication flow. import os
import base64
import hashlib
import datetime
from authentik.stages.password.stage import authenticate
def extract_ldap_hash(ldap_hash, salt_size=4):
if not ldap_hash:
return None, os.urandom(salt_size)
try:
ssha_password = base64.b64decode(ldap_hash).decode('utf-8')
except Exception:
return None, os.urandom(salt_size)
if not ssha_password.startswith('{SSHA}'):
return None, os.urandom(salt_size)
cleaned_ssha_password = ssha_password[6:]
ssha_dec = base64.b64decode(cleaned_ssha_password)
payload = ssha_dec[:-salt_size]
salt = ssha_dec[-salt_size:]
return payload, salt
def generate_ldap_hash(user, password):
ldap_hash = user.attributes.get("userPassword")
payload, salt = extract_ldap_hash(ldap_hash)
check_hash = hashlib.sha1(password.encode('utf-8') + salt).digest()
return (b'{SSHA}' + base64.b64encode(check_hash + salt)).decode('utf-8')
def generate_ntlm_hash(user, password):
password_unicode = password.encode('utf-16le')
return hashlib.new('md4', password_unicode).hexdigest()
def store_user_hash(user, password):
now = datetime.datetime.now().timestamp()
if not user.attributes:
user.attributes = {}
previous_attributes = {**user.attributes}
user.attributes["userPassword"] = generate_ldap_hash(user, password)
user.attributes["ipaNTHash"] = generate_ntlm_hash(user, password)
user.attributes["sambaNTPassword"] = user.attributes["ipaNTHash"].upper()
return user.save()
user = request.user if request.user and not request.user.is_anonymous else context.get("pending_user")
if user:
# Extract the password from the context
password = context.get("prompt_data", {}).get("password") or context.get("password")
if password:
if authenticate(
request=request.http_request,
backends=[
'authentik.core.auth.InbuiltBackend',
'authentik.core.auth.TokenBackend',
'authentik.sources.ldap.auth.LDAPBackend',
],
username=user.username, password=password
):
store_user_hash(user, password)
return True
else:
ak_message("Invalid password")
return False
else:
ak_message("There is no new password value in context")
return False
else:
ak_message("There is no user context")
return False As you can see in this, I use this policy to populate two fields, for LDAP and RADIUS logins (although I haven't tested it yet). EDIT: I've tweaked the code a bit, since I realized I could directly use the |
I have built upon the fantastic information in this post and would like to share for others. I successfully configured Unifi to accept incoming connection requests from users (VPN or Wifi WPA2 Enterprise) when that request is using MSCHAPv2 for strong authentication. Those authentication requests are sent to FreeRADIUS (which I have running in docker), which then queries Authentik's LDAP outpost. I used Authentik's guide for installing the LDAP outpost. I am not using the TOTP 2FA for this. Only the First, each user must have the
Once that is done, test and confirm the 'ipaNTHash' attribute is written to the user whenever they go through any of those flows. Look through Authentik's event logs for debugging. Next, create an Authentik Group you will use to determine which users should pass this authentication (otherwise, ALL users will be able to access your WPA2 Enterprise Wifi and/or VPN). I created a group called Next, you must install FreeRADIUS. I have not documented that here. If you install it in docker like I did, it is tricky. You must run the freeRADIUS container WITHOUT bind-mounting /etc/freeradius configuration directory. After it's running, you then copy the whole /etc/freeradius/ configuration directory to another folder, which IS bind mounted. From inside the freeRADIUS container, use That said, here is my compose.yaml file for freeRADIUS:
Next, you'll delete all files in FreeRADIUS located at /etc/freeradius/sites-enabled. I created a new file /etc/freeradius/sitesenabled/default with these contents. It checks to confirm the user requesting access belongs to the
Next, you need to configure the ldap, eap and mschap modules in freeRADIUS. To start, I deleted all three from Here is my /etc/freeradius/mod-enabled/eap file (with thanks to @jcrapuchettes ). Note you will need to provide certificates in the area I marked as 'REPLACE-THIS'
Here is my /etc/freeradius/mods-enabled/ldap file (with thanks to @jcrapuchettes). There are several things you'll need to modify based on your Authentik server configuration. Note, certificates are again needed in FreeRADIUS.
And finally, here is my /etc/freeradius/mods-enabled/mschap file. Nothing needs to be modified here.
The final step will be configuring Unifi (or another networking platform you're using) to connect to freeRADIUS. Doing so requires a configuration in freeRADIUS at /etc/freeradius/clients.conf to indicate what devices can connect, and what shared secret they should use to do so. Mine looks like this:
Finally, you'll need to configure Unifi (or your equivalent) to query the FreeRADIUS server for authentication. Ensure it is configured to require strong authentication (MSCHAPv2) from the user. At this point, you can try initiating a connection to Unifi (or your equivalent). Monitor FreeRADIUS output for debugging. |
Hi @Junto026 First of all, thank you for your tutorial! It has been incredibly helpful, and I’ve successfully implemented the expression policy in both the enrollment flow and user settings flow. However, I’m running into an issue when using the password recovery flow. I get the following error: It seems like the password recovery flow does not provide prompt_data, which is available in the other flows. I’m not sure if this is expected behavior or if I’m missing something in my configuration. Would you happen to know why this is happening? Is there a way to work around it or retrieve the necessary data differently in the password recovery flow? I really appreciate any guidance you can provide. Thanks in advance for your help! |
@QuantumCorral I'm not sure. The original version PopcornPanda posted worked for the recovery flow for me. Perhaps share a screenshot of the stages in your password recovery flow, and an exact copy-paste of the expression policy you're using. Assuming your password recovery flow has a prompt stage in it, also share a screenshot of the details of that prompt stage's configuration. Finally, verify the expression policy is binded to the 'user write' stage (the very last stage) in the password recovery flow. Not an earlier stage in the flow. |
@Junto026 I appreciate your input. Here’s what I’ve checked so far:
Despite these checks, I'm still encountering the error. Could you take a look and see if there's anything I'm missing? Any insights would be greatly appreciated! Thanks in advance! :) |
@QuantumCorral Everything looks lined up to me, except I notice in your recovery flow it is possible to go through the flow without doing the identification stage. Can you test your recovery flow, making sure you go through the identification stage? |
Describe your question
I’m going down the path of figuring out WPA Enterprise WiFi Security. As I have been reviewing posts online and github issues, I’ve been trying to figure out why I wouldn’t be able to use the following setup: Authentik -> LDAP Outpost -> FreeRADIUS -> UniFi. After setting it all up, the FreeRADIUS server reported "mschap: FAILED: No NT-Password." I noticed that the LDAP settings in FreeRADIUS included
control:NT-Password := 'ipaNTHash'
. I tried adding an attribute to my user in Authentik calledipaNTHash
and set its value to the NTLM hash of my password. I then tested my setup witheapol_test
and got a SUCCESS! I've also tested connecting through two different computers and it worked!My question: Is there a way for me to add a calculation of the NTLM Hash to user attributes when the password is changed? Could I make use of a property mapping?
Relevant info
Version and Deployment (please complete the following information):
Additional context
WPA Enterprise through PEAP-MSCHAPv2 is pretty standard and having to setup FreeRADIUS and FreeIPA seems like a lot given the competition offers their own RADIUS server that supports PEAP-MSCHAPv2. I know that it is possible to setup FreeRADIUS to communicate to Authentik via OAuth2, but that required EAP-TTLS/PAP. I'm not opposed to that with the exception that we have a number of iOS users and from my research, it will be a pain to set them up.
The text was updated successfully, but these errors were encountered: