Skip to content

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:

  1. Use User Secrets (development):

    dotnet user-secrets set "Cesivi:Identity:Providers:NTLM:Configuration:Users:0:Password" "Alice@123"
    

  2. Use Environment Variables (production):

    export Cesivi__Identity__Providers__NTLM__Configuration__Users__0__Password="Alice@123"
    

  3. 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:

  1. Wrong username/password
  2. Check credentials match configuration exactly
  3. Domain name is case-sensitive

  4. NTLM provider not enabled

    {
      "NTLM": {
        "Enabled": true
      }
    }
    

  5. Wrong domain format

  6. Use just domain name: "CONTOSO"
  7. Don't use FQDN: ~~"contoso.com"~~

"NTLM negotiation failed"

Symptoms: - Connection hangs during authentication - No Type 2 challenge received

Solutions:

  1. Check NTLM provider priority
  2. Should be higher priority (lower number) than AcceptAll

    {
      "NTLM": {
        "Priority": 100
      },
      "AcceptAll": {
        "Priority": 1000
      }
    }
    

  3. Enable debug logging

    {
      "Logging": {
        "LogLevel": {
          "Cesivi.Common.Identity.Ntlm": "Debug"
        }
      }
    }
    

  4. Check challenge expiration

  5. Challenges expire after 5 minutes
  6. 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

  1. Start Cesivi Server

    cd Cesivi.Server
    dotnet run
    

  2. Test with curl

    curl --ntlm \
      --user "CONTOSO\alice:Alice@123" \
      http://localhost:5000/_api/web \
      -v
    

  3. Verify output

  4. Look for 401 Unauthorized with WWW-Authenticate: NTLM header
  5. Followed by 401 Unauthorized with challenge
  6. Finally 200 OK on 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


External Resources