NTLM Authentication Setup Guide¶
Overview¶
This guide shows how to configure Cesivi Server to use NTLM (Windows) authentication, which is compatible with: - Desktop applications (SharePoint Designer, InfoPath) - Intranet scenarios - SharePoint migration projects - Legacy tools that require Windows authentication
Available Backends: - Configuration-based - Define users in appsettings.json (built-in, default) - Active Directory - Authenticate against real AD via LDAP (✅ fully implemented) - Pure LDAP - OpenLDAP, 389 DS, FreeIPA, Apache DS, etc. (✅ fully implemented) - Local Windows - Authenticate against local Windows accounts (available on-demand)
How NTLM Works¶
NTLM uses a three-way challenge-response handshake:
┌──────────┐ ┌──────────────┐
│ Client │ │ SharePoint │
│ │ │ Mock Server │
└─────┬────┘ └──────┬───────┘
│ │
│ 1. NEGOTIATE (Type 1 message) │
│ "I want to authenticate with NTLM" │
│─────────────────────────────────────────────>│
│ │
│ 2. CHALLENGE (Type 2 message) │
│ "Encrypt this random challenge" │
│<─────────────────────────────────────────────│
│ │
│ 3. AUTHENTICATE (Type 3 message) │
│ "Here's the encrypted response" │
│─────────────────────────────────────────────>│
│ │
│ │ 4. Validate
│ │ credentials
│ │
│ 5. Success (200 OK) or Failure (401) │
│<─────────────────────────────────────────────│
Key Points: - Passwords are never sent over the network - Challenge is a random 8-byte value - Response proves knowledge of password - Compatible with Windows, .NET, and many HTTP libraries
Configuration-Based Setup¶
The simplest NTLM mode - define users directly in configuration.
Step 1: Enable NTLM Provider¶
Edit appsettings.json:
{
"Cesivi": {
"Identity": {
"Providers": {
"NTLM": {
"Enabled": true,
"Priority": 100,
"Backend": "Configuration",
"Configuration": {
"Users": []
}
}
}
}
}
}
Step 2: Add Users¶
{
"Cesivi": {
"Identity": {
"Providers": {
"NTLM": {
"Enabled": true,
"Priority": 100,
"Backend": "Configuration",
"Configuration": {
"Users": [
{
"Username": "alice",
"Domain": "CONTOSO",
"Password": "Alice@123",
"Email": "alice@contoso.com",
"DisplayName": "Alice Smith",
"Groups": ["Developers", "Users"]
},
{
"Username": "bob",
"Domain": "CONTOSO",
"Password": "Bob@456",
"Email": "bob@contoso.com",
"DisplayName": "Bob Johnson",
"Groups": ["Managers", "Users"]
},
{
"Username": "admin",
"Domain": "CONTOSO",
"Password": "Admin@789",
"Email": "admin@contoso.com",
"DisplayName": "Administrator",
"Groups": ["Administrators"]
}
]
}
}
}
}
}
}
Step 3: Test Authentication¶
With CSOM¶
using Microsoft.SharePoint.Client;
using System.Net;
var context = new ClientContext("http://localhost:5000");
context.Credentials = new NetworkCredential("alice", "Alice@123", "CONTOSO");
var web = context.Web;
context.Load(web, w => w.Title);
context.ExecuteQuery();
Console.WriteLine($"Web Title: {web.Title}");
With PnP PowerShell¶
# Create credential object
$username = "CONTOSO\alice"
$password = ConvertTo-SecureString "Alice@123" -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($username, $password)
# Connect
Connect-PnPOnline -Url "http://localhost:5000" -Credentials $credential
# Test
Get-PnPWeb | Select Title
With curl (Manual NTLM)¶
NTLM with curl requires the --ntlm flag:
curl --ntlm \
--user "CONTOSO\alice:Alice@123" \
http://localhost:5000/_api/web
User Configuration Reference¶
User Object Schema¶
{
"Username": "string", // Required - login name (without domain)
"Domain": "string", // Required - domain/workgroup name
"Password": "string", // Required - plain text password
"Email": "string", // Optional - user email address
"DisplayName": "string", // Optional - full name for display
"Groups": ["string"] // Optional - group memberships
}
Field Descriptions¶
| Field | Required | Description | Example |
|---|---|---|---|
Username |
Yes | Login name (without domain) | "alice" |
Domain |
Yes | Domain or workgroup name | "CONTOSO" |
Password |
Yes | Password in plain text | "Alice@123" |
Email |
No | Email address | "alice@contoso.com" |
DisplayName |
No | Full name | "Alice Smith" |
Groups |
No | Array of group names | ["Developers", "Users"] |
Security Note¶
⚠️ Passwords in Plain Text
Configuration mode stores passwords in plain text in appsettings.json. For production:
-
Use User Secrets (development):
dotnet user-secrets set "Cesivi:Identity:Providers:NTLM:Configuration:Users:0:Password" "Alice@123" -
Use Environment Variables (production):
export Cesivi__Identity__Providers__NTLM__Configuration__Users__0__Password="Alice@123" -
Use Azure Key Vault or AWS Secrets Manager (cloud):
{ "KeyVault": { "Url": "https://myvault.vault.azure.net/" } }
Active Directory / LDAP Backend¶
Status: ✅ Fully Implemented
This backend authenticates users against real Active Directory via LDAP. It supports user lookup, group membership retrieval, and result caching.
Configuration¶
{
"Cesivi": {
"Identity": {
"Providers": {
"NTLM": {
"Enabled": true,
"Priority": 100,
"Backend": "ActiveDirectory",
"ActiveDirectory": {
"Server": "ldap://dc.contoso.com",
"BaseDN": "DC=contoso,DC=com",
"ServiceAccount": "CN=svc_spm,OU=ServiceAccounts,DC=contoso,DC=com",
"ServiceAccountPassword": "your-password",
"UserSearchFilter": "(sAMAccountName={0})",
"GroupSearchFilter": "(member={0})",
"UseSsl": true,
"SkipCertificateValidation": false,
"ConnectionTimeout": 30,
"CacheTimeoutSeconds": 300
}
}
}
}
}
}
Configuration Options¶
| Option | Default | Description |
|---|---|---|
Server |
ldap://localhost |
LDAP server URI (supports ldap:// and ldaps://) |
BaseDN |
(required) | Base DN for LDAP searches, e.g., DC=contoso,DC=com |
ServiceAccount |
(optional) | Service account DN for binding (or null for anonymous bind) |
ServiceAccountPassword |
(optional) | Password for service account |
UserSearchFilter |
(sAMAccountName={0}) |
LDAP filter for user search ({0} = username) |
GroupSearchFilter |
(member={0}) |
LDAP filter for group search ({0} = user DN) |
UseSsl |
false |
Enable SSL/TLS for LDAP connection |
SkipCertificateValidation |
false |
Skip SSL certificate validation (⚠️ dev only!) |
ConnectionTimeout |
30 |
Connection timeout in seconds |
CacheTimeoutSeconds |
300 |
Cache user info for this many seconds (0 = disabled) |
Features¶
- User lookup via LDAP search - Uses configurable sAMAccountName filter
- Group membership retrieval - Extracts groups from memberOf attribute
- Result caching - Reduces LDAP queries (5-minute default TTL)
- SSL/TLS support - Secure connections to domain controllers
- Connection pooling - Reuses LDAP connections for performance
- LDAP injection prevention - Escapes special characters in search filters
Development Testing¶
For testing AD/LDAP integration without real Active Directory, use dev-ldap:
# Start dev-ldap (zero-configuration LDAP server)
cd C:\Source\_AI\dev-ldap
.\dev-ldap.exe
# Server starts on ldap://localhost:3389
Configuration for dev-ldap:
{
"Cesivi": {
"Identity": {
"Providers": {
"NTLM": {
"Enabled": true,
"Backend": "ActiveDirectory",
"ActiveDirectory": {
"Server": "ldap://localhost:3389",
"BaseDN": "DC=dev,DC=local",
"UseSsl": false
}
}
}
}
}
}
See C:\Source\_AI\dev-ldap\README.md for configuration options.
Pure LDAP Backend (OpenLDAP, 389 DS, etc.)¶
Status: ✅ Fully Supported
The LDAP backend works with any standards-compliant LDAP server, not just Active Directory. Use "Backend": "LDAP" or "Backend": "ActiveDirectory" - both work identically.
Configuration for OpenLDAP¶
{
"Cesivi": {
"Identity": {
"Providers": {
"NTLM": {
"Enabled": true,
"Priority": 100,
"Backend": "LDAP",
"ActiveDirectory": {
"Server": "ldap://openldap.example.com:389",
"BaseDN": "dc=example,dc=com",
"ServiceAccount": "cn=admin,dc=example,dc=com",
"ServiceAccountPassword": "admin-password",
"UserSearchFilter": "(uid={0})",
"GroupSearchFilter": "(memberUid={0})",
"UseSsl": false,
"CacheTimeoutSeconds": 300
}
}
}
}
}
}
Configuration for 389 Directory Server¶
{
"Cesivi": {
"Identity": {
"Providers": {
"NTLM": {
"Enabled": true,
"Backend": "LDAP",
"ActiveDirectory": {
"Server": "ldap://389ds.example.com:389",
"BaseDN": "dc=example,dc=com",
"ServiceAccount": "cn=Directory Manager",
"ServiceAccountPassword": "password",
"UserSearchFilter": "(uid={0})",
"GroupSearchFilter": "(uniqueMember={0})",
"UseSsl": true
}
}
}
}
}
}
Common LDAP Filter Patterns¶
| LDAP Server | User Search Filter | Group Search Filter |
|---|---|---|
| Active Directory | (sAMAccountName={0}) |
(member={0}) |
| OpenLDAP | (uid={0}) |
(memberUid={0}) |
| 389 Directory Server | (uid={0}) |
(uniqueMember={0}) |
| FreeIPA | (uid={0}) |
(member={0}) |
| Apache DS | (uid={0}) |
(uniqueMember={0}) |
Key Differences from Active Directory¶
| Feature | Active Directory | Pure LDAP |
|---|---|---|
| Default user attribute | sAMAccountName |
uid |
| Default group membership | memberOf (on user) |
member or memberUid (on group) |
| DN format | CN=User,OU=Users,DC=... |
uid=user,ou=people,dc=... |
| Anonymous bind | Usually disabled | Often allowed |
Note: The configuration section is named ActiveDirectory for backward compatibility, but works with any LDAP server when you set appropriate search filters.
Local Windows Backend (Optional)¶
Status: ⏸️ Available On-Demand (not yet implemented)
This backend would authenticate users against local Windows accounts using the Win32 LogonUser API. Can be implemented in 2-3 hours if needed.
Planned Configuration¶
{
"Cesivi": {
"Identity": {
"Providers": {
"NTLM": {
"Enabled": true,
"Priority": 100,
"Backend": "LocalWindows",
"LocalWindows": {
"LogonType": "Network",
"AllowedGroups": ["Administrators", "Users"]
}
}
}
}
}
}
Limitations: - Windows-only (throws PlatformNotSupportedException on Linux/Mac) - Requires appropriate Windows privileges - Not recommended for production (use AD backend instead) - Niche use case - most scenarios should use AD/LDAP or Configuration backend
Multiple Backends¶
You cannot use multiple NTLM backends simultaneously. Choose one:
{
"Backend": "Configuration" // Users in appsettings.json (default)
"Backend": "ActiveDirectory" // Active Directory via LDAP ✅ Implemented
"Backend": "AD" // Alias for ActiveDirectory ✅ Implemented
"Backend": "LDAP" // Pure LDAP (OpenLDAP, 389 DS, etc.) ✅ Implemented
"Backend": "LocalWindows" // Local Windows accounts (available on-demand)
}
Troubleshooting¶
"NTLM authentication failed"¶
Symptoms: - 401 Unauthorized response - "Invalid credentials" error
Possible Causes:
- Wrong username/password
- Check credentials match configuration exactly
-
Domain name is case-sensitive
-
NTLM provider not enabled
{ "NTLM": { "Enabled": true } } -
Wrong domain format
- Use just domain name:
"CONTOSO" - Don't use FQDN: ~~
"contoso.com"~~
"NTLM negotiation failed"¶
Symptoms: - Connection hangs during authentication - No Type 2 challenge received
Solutions:
- Check NTLM provider priority
-
Should be higher priority (lower number) than AcceptAll
{ "NTLM": { "Priority": 100 }, "AcceptAll": { "Priority": 1000 } } -
Enable debug logging
{ "Logging": { "LogLevel": { "Cesivi.Common.Identity.Ntlm": "Debug" } } } -
Check challenge expiration
- Challenges expire after 5 minutes
- Client must complete handshake promptly
"User not found"¶
Cause: Username doesn't exist in configuration
Solution: Add user to Users array in configuration
Debug Logging¶
Enable detailed NTLM logging:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Cesivi.Common.Identity.Ntlm": "Debug"
}
}
}
This logs: - NTLM Type 1/2/3 messages - Challenge generation and validation - User lookup results - Authentication success/failure reasons
Testing Without a Client¶
Manual NTLM Testing¶
-
Start Cesivi Server
cd Cesivi.Server dotnet run -
Test with curl
curl --ntlm \ --user "CONTOSO\alice:Alice@123" \ http://localhost:5000/_api/web \ -v -
Verify output
- Look for
401 UnauthorizedwithWWW-Authenticate: NTLMheader - Followed by
401 Unauthorizedwith challenge - Finally
200 OKon success
Expected Flow¶
> GET /_api/web HTTP/1.1
> Host: localhost:5000
< HTTP/1.1 401 Unauthorized
< WWW-Authenticate: NTLM
> GET /_api/web HTTP/1.1
> Authorization: NTLM TlRMTVNTUAABAAAAB4I...
< HTTP/1.1 401 Unauthorized
< WWW-Authenticate: NTLM TlRMTVNTUAACAAA...
> GET /_api/web HTTP/1.1
> Authorization: NTLM TlRMTVNTUAADAAAAGA...
< HTTP/1.1 200 OK
< Content-Type: application/json
Migration from AcceptAll¶
Step 1: Keep AcceptAll as Fallback¶
{
"Cesivi": {
"Identity": {
"Providers": {
"NTLM": {
"Enabled": true,
"Priority": 100
},
"AcceptAll": {
"Enabled": true,
"Priority": 1000
}
}
}
}
}
This allows: - NTLM clients authenticate with credentials - Other clients fall back to AcceptAll
Step 2: Test NTLM Authentication¶
Use valid credentials from configuration to ensure NTLM works.
Step 3: Disable AcceptAll (Production)¶
Once NTLM is working, disable AcceptAll for production:
{
"AcceptAll": {
"Enabled": false
}
}
Security Best Practices¶
Development¶
- ✅ Use AcceptAll as fallback during testing
- ✅ Test with real NTLM clients (CSOM, PnP)
- ✅ Enable debug logging to understand flow
Production¶
- ✅ Use AD/LDAP backend (not configuration-based)
- ✅ Store passwords in secure configuration (Key Vault, Secrets Manager)
- ✅ Disable AcceptAll provider
- ✅ Use HTTPS for all communications
- ✅ Monitor authentication failures
- ✅ Rotate service account credentials regularly
- ✅ Use least-privilege service accounts
- ✅ Enable audit logging
See Also¶
- Identity Providers Overview - Choose authentication method
- OAuth2 Setup Guide - Modern authentication
- API Reference - Authentication endpoints
- Troubleshooting Guide - Common issues