mssql-auth 0.10.0

Authentication strategies for SQL Server connections
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
//! Native Windows SSPI authentication provider.
//!
//! This module uses the actual Windows Security Support Provider Interface (SSPI)
//! via `secur32.dll` to perform authentication. Unlike the sspi-rs based provider
//! in [`super::sspi_auth`], this module delegates to the Windows kernel's credential
//! management, which supports all account types transparently:
//!
//! - **Domain accounts** (e.g., `CONTOSO\user`)
//! - **Local accounts** (e.g., `MACHINE\user`)
//! - **Microsoft Accounts** (e.g., `user@outlook.com` linked to Windows 11)
//!
//! This is the mechanism that .NET's `SqlClient` and ODBC use for
//! `Integrated Security=SSPI` on Windows.
//!
//! ## Why Native SSPI?
//!
//! The sspi-rs crate provides a pure-Rust SSPI implementation, but it cannot
//! acquire credentials from the current Windows logon session without explicit
//! username/password. The Windows SSPI subsystem (`LSASS`) has direct access to
//! cached credentials for all supported account types, including Microsoft Accounts
//! where the NTLM hash is derived from the cloud credential cache.
//!
//! ## Security
//!
//! All credential access is mediated by the Windows SSPI subsystem. This module
//! never handles raw passwords or credential material — it only receives opaque
//! security tokens from the Windows APIs.
//!
//! ## Platform
//!
//! This module is only available on Windows with the `sspi-auth` feature enabled.

use std::sync::Mutex;

use windows::Win32::Security::Authentication::Identity::{
    AcquireCredentialsHandleW, DeleteSecurityContext, FreeCredentialsHandle, ISC_REQ_CONNECTION,
    ISC_REQ_MUTUAL_AUTH, ISC_REQ_REPLAY_DETECT, ISC_REQ_SEQUENCE_DETECT,
    InitializeSecurityContextW, SECPKG_CRED_OUTBOUND, SecBuffer, SecBufferDesc,
};
use windows::Win32::Security::Credentials::SecHandle;
use windows::core::{HRESULT, PCWSTR};

use crate::error::AuthError;

/// Token buffer type constant (SECBUFFER_TOKEN = 2).
const SECBUFFER_TOKEN: u32 = 2;

/// SecBufferDesc version constant (SECBUFFER_VERSION = 0).
const SECBUFFER_VERSION: u32 = 0;

/// Native data representation (SECURITY_NATIVE_DREP = 0x10).
const SECURITY_NATIVE_DREP: u32 = 0x10;

/// Maximum SSPI token size (16 KiB is more than sufficient for Negotiate/NTLM).
const MAX_TOKEN_SIZE: usize = 16_384;

/// HRESULT value for successful completion.
const SEC_E_OK: HRESULT = HRESULT(0);

/// HRESULT value indicating more data exchange is needed.
const SEC_I_CONTINUE_NEEDED: HRESULT = HRESULT(0x0009_0312_u32 as i32);

/// Native Windows SSPI authentication provider.
///
/// Uses the Windows SSPI subsystem (`secur32.dll`) for integrated authentication.
/// This supports all Windows account types including Microsoft Accounts, domain
/// accounts, and local accounts — without requiring explicit credentials.
///
/// # Example
///
/// ```rust,ignore
/// use mssql_auth::NativeSspiAuth;
///
/// let auth = NativeSspiAuth::new("sqlserver.example.com", 1433)?;
/// let initial_token = auth.initialize()?;
/// // Send initial_token to server, receive challenge...
/// let response = auth.step(&server_challenge)?;
/// ```
pub struct NativeSspiAuth {
    /// The target service principal name (e.g., "MSSQLSvc/host:port").
    spn: String,
    /// Internal SSPI context state, protected by a mutex for thread safety.
    context: Mutex<NativeSspiContext>,
}

/// Internal state for the native SSPI handshake.
struct NativeSspiContext {
    /// Credential handle acquired from `AcquireCredentialsHandleW`.
    cred_handle: SecHandle,
    /// Security context handle from `InitializeSecurityContextW`.
    ctx_handle: SecHandle,
    /// Whether the context handle has been initialized (first ISC call made).
    has_context: bool,
    /// Whether authentication has completed successfully.
    complete: bool,
}

// SAFETY: SecHandle is a pair of pointer-sized integers (dwLower, dwUpper)
// that act as opaque handles. The Windows SSPI subsystem manages thread safety
// for the underlying resources. We protect access with a Mutex.
unsafe impl Send for NativeSspiContext {}

impl NativeSspiAuth {
    /// Create a new native SSPI authentication provider for integrated auth.
    ///
    /// Uses the current Windows user's credentials (from the logon session).
    /// This works for domain accounts, local accounts, and Microsoft Accounts.
    ///
    /// # Arguments
    ///
    /// * `hostname` - The SQL Server hostname
    /// * `port` - The SQL Server port (typically 1433)
    ///
    /// # Errors
    ///
    /// Returns an error if credential acquisition fails.
    pub fn new(hostname: &str, port: u16) -> Result<Self, AuthError> {
        let spn = format!("MSSQLSvc/{hostname}:{port}");

        let mut cred_handle = SecHandle::default();
        let mut expiry: i64 = 0;

        // Encode package name as null-terminated wide string
        let package: Vec<u16> = "Negotiate\0".encode_utf16().collect();

        // SAFETY: AcquireCredentialsHandleW is called with:
        // - pszprincipal: NULL (current user)
        // - pszpackage: "Negotiate" (well-known SSP name, null-terminated wide string)
        // - fCredentialUse: OUTBOUND (client-side)
        // - pvLogonId: NULL (current logon session)
        // - pAuthData: NULL (use current user's cached credentials)
        // - pGetKeyFn/pvGetKeyArgument: NULL (not used)
        // - phCredential: valid mutable pointer to our handle
        // - ptsExpiry: valid mutable pointer
        //
        // The `package` Vec lives for the duration of this call.
        // The output cred_handle is an opaque handle that must be freed
        // with FreeCredentialsHandle, which we do in Drop.
        let result = unsafe {
            AcquireCredentialsHandleW(
                None,                     // principal: current user
                PCWSTR(package.as_ptr()), // package: "Negotiate"
                SECPKG_CRED_OUTBOUND,     // credential use: client-side
                None,                     // logon id: current session
                None,                     // auth data: current user creds
                None,                     // get key fn: not used
                None,                     // get key arg: not used
                &mut cred_handle,         // output: credential handle
                Some(&mut expiry),        // output: expiry time
            )
        };

        if let Err(e) = result {
            return Err(AuthError::Sspi(format!(
                "Failed to acquire Windows credentials: {e}"
            )));
        }

        tracing::debug!(
            spn = %spn,
            "Acquired native Windows SSPI credentials for current user"
        );

        Ok(Self {
            spn,
            context: Mutex::new(NativeSspiContext {
                cred_handle,
                ctx_handle: SecHandle::default(),
                has_context: false,
                complete: false,
            }),
        })
    }

    /// Initialize the SSPI context and generate the first authentication token.
    ///
    /// This token should be included in the Login7 packet's SSPI data field.
    ///
    /// # Errors
    ///
    /// Returns an error if context initialization fails.
    pub fn initialize(&self) -> Result<Vec<u8>, AuthError> {
        let mut ctx = self
            .context
            .lock()
            .map_err(|_| AuthError::Sspi("Failed to acquire context lock".into()))?;

        // Encode SPN as wide string (null-terminated)
        let spn_wide: Vec<u16> = format!("{}\0", self.spn).encode_utf16().collect();

        // Set up output buffer
        let mut out_buf = vec![0u8; MAX_TOKEN_SIZE];
        let mut out_sec_buf = SecBuffer {
            cbBuffer: out_buf.len() as u32,
            BufferType: SECBUFFER_TOKEN,
            pvBuffer: out_buf.as_mut_ptr().cast(),
        };
        let mut out_buf_desc = SecBufferDesc {
            ulVersion: SECBUFFER_VERSION,
            cBuffers: 1,
            pBuffers: &mut out_sec_buf,
        };

        let mut context_attrs: u32 = 0;
        let mut expiry: i64 = 0;

        let context_req = ISC_REQ_MUTUAL_AUTH
            | ISC_REQ_REPLAY_DETECT
            | ISC_REQ_SEQUENCE_DETECT
            | ISC_REQ_CONNECTION;

        // SAFETY: InitializeSecurityContextW is called with:
        // - phCredential: valid credential handle from AcquireCredentialsHandleW
        // - phContext: NULL (first call, no existing context)
        // - pszTargetName: null-terminated wide SPN string
        // - fContextReq: standard flags for SQL Server auth
        // - Reserved1/Reserved2: 0
        // - TargetDataRep: SECURITY_NATIVE_DREP
        // - pInput: NULL (first call, no input token)
        // - phNewContext: valid mutable pointer for output context
        // - pOutput: valid SecBufferDesc with pre-allocated buffer
        // - pfContextAttr: valid mutable pointer
        // - ptsExpiry: valid mutable pointer
        //
        // On success, ctx_handle receives the new security context handle
        // and out_sec_buf.cbBuffer is updated with the actual token size.
        let hr = unsafe {
            InitializeSecurityContextW(
                Some(&ctx.cred_handle),                   // credential handle
                None,                                     // no existing context (first call)
                Some(PCWSTR(spn_wide.as_ptr()).as_ptr()), // target SPN
                context_req,                              // context requirements
                0,                                        // reserved
                SECURITY_NATIVE_DREP,                     // data representation
                None,                                     // no input (first call)
                0,                                        // reserved
                Some(&mut ctx.ctx_handle),                // output context handle
                Some(&mut out_buf_desc),                  // output buffer
                &mut context_attrs,                       // output context attributes
                Some(&mut expiry),                        // output expiry
            )
        };

        if hr == SEC_E_OK || hr == SEC_I_CONTINUE_NEEDED {
            ctx.has_context = true;

            if hr == SEC_E_OK {
                ctx.complete = true;
            }

            // Extract the token from the output buffer
            let token_len = out_sec_buf.cbBuffer as usize;
            let token = out_buf[..token_len].to_vec();

            tracing::debug!(
                token_len,
                continue_needed = (hr == SEC_I_CONTINUE_NEEDED),
                "SSPI initialization produced token"
            );

            Ok(token)
        } else {
            Err(AuthError::Sspi(format!(
                "InitializeSecurityContext failed: HRESULT 0x{:08X}",
                hr.0 as u32
            )))
        }
    }

    /// Process a server challenge token and generate a response.
    ///
    /// Returns `Some(token)` if more data needs to be sent to the server,
    /// or `None` if authentication is complete.
    ///
    /// # Arguments
    ///
    /// * `server_token` - The SSPI challenge token received from the server
    ///
    /// # Errors
    ///
    /// Returns an error if the negotiation step fails.
    pub fn step(&self, server_token: &[u8]) -> Result<Option<Vec<u8>>, AuthError> {
        let mut ctx = self
            .context
            .lock()
            .map_err(|_| AuthError::Sspi("Failed to acquire context lock".into()))?;

        if ctx.complete {
            return Ok(None);
        }

        if !ctx.has_context {
            return Err(AuthError::Sspi(
                "Context not initialized - call initialize() first".into(),
            ));
        }

        // Encode SPN as wide string (null-terminated)
        let spn_wide: Vec<u16> = format!("{}\0", self.spn).encode_utf16().collect();

        // Set up input buffer with server's challenge token
        let mut in_buf = server_token.to_vec();
        let mut in_sec_buf = SecBuffer {
            cbBuffer: in_buf.len() as u32,
            BufferType: SECBUFFER_TOKEN,
            pvBuffer: in_buf.as_mut_ptr().cast(),
        };
        let in_buf_desc = SecBufferDesc {
            ulVersion: SECBUFFER_VERSION,
            cBuffers: 1,
            pBuffers: &mut in_sec_buf,
        };

        // Set up output buffer
        let mut out_buf = vec![0u8; MAX_TOKEN_SIZE];
        let mut out_sec_buf = SecBuffer {
            cbBuffer: out_buf.len() as u32,
            BufferType: SECBUFFER_TOKEN,
            pvBuffer: out_buf.as_mut_ptr().cast(),
        };
        let mut out_buf_desc = SecBufferDesc {
            ulVersion: SECBUFFER_VERSION,
            cBuffers: 1,
            pBuffers: &mut out_sec_buf,
        };

        let mut context_attrs: u32 = 0;
        let mut expiry: i64 = 0;

        let context_req = ISC_REQ_MUTUAL_AUTH
            | ISC_REQ_REPLAY_DETECT
            | ISC_REQ_SEQUENCE_DETECT
            | ISC_REQ_CONNECTION;

        // SAFETY: InitializeSecurityContextW is called with:
        // - phCredential: valid credential handle
        // - phContext: existing context from previous call
        // - pszTargetName: null-terminated wide SPN string
        // - pInput: SecBufferDesc containing the server's challenge token
        // - phNewContext: same context handle (updated in place)
        // - pOutput: SecBufferDesc with pre-allocated buffer for response
        //
        // The context handle is updated in-place. The output buffer receives
        // the response token to send to the server.
        let hr = unsafe {
            InitializeSecurityContextW(
                Some(&ctx.cred_handle),                   // credential handle
                Some(&ctx.ctx_handle),                    // existing context
                Some(PCWSTR(spn_wide.as_ptr()).as_ptr()), // target SPN
                context_req,                              // context requirements
                0,                                        // reserved
                SECURITY_NATIVE_DREP,                     // data representation
                Some(&in_buf_desc),                       // input from server
                0,                                        // reserved
                Some(&mut ctx.ctx_handle),                // context handle (updated)
                Some(&mut out_buf_desc),                  // output buffer
                &mut context_attrs,                       // output context attributes
                Some(&mut expiry),                        // output expiry
            )
        };

        match hr {
            hr if hr == SEC_E_OK => {
                ctx.complete = true;
                let token_len = out_sec_buf.cbBuffer as usize;
                if token_len > 0 {
                    let token = out_buf[..token_len].to_vec();
                    tracing::debug!(token_len, "SSPI step complete with final token");
                    Ok(Some(token))
                } else {
                    tracing::debug!("SSPI step complete, no final token");
                    Ok(None)
                }
            }
            hr if hr == SEC_I_CONTINUE_NEEDED => {
                let token_len = out_sec_buf.cbBuffer as usize;
                let token = out_buf[..token_len].to_vec();
                tracing::debug!(token_len, "SSPI step needs continuation");
                Ok(Some(token))
            }
            _ => Err(AuthError::Sspi(format!(
                "SSPI step failed: HRESULT 0x{:08X}",
                hr.0 as u32
            ))),
        }
    }

    /// Check if authentication has completed successfully.
    pub fn is_complete(&self) -> bool {
        self.context.lock().map(|ctx| ctx.complete).unwrap_or(false)
    }

    /// Get the target SPN.
    #[must_use]
    pub fn spn(&self) -> &str {
        &self.spn
    }
}

impl std::fmt::Debug for NativeSspiAuth {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("NativeSspiAuth")
            .field("spn", &self.spn)
            .field("complete", &self.is_complete())
            .finish()
    }
}

impl Drop for NativeSspiContext {
    fn drop(&mut self) {
        // SAFETY: We free the security context handle if it was initialized,
        // and the credential handle which was always initialized in new().
        // Both handles were obtained from valid SSPI API calls.
        // Double-free is prevented by the has_context flag and because
        // Drop runs exactly once.
        unsafe {
            if self.has_context {
                let _ = DeleteSecurityContext(&self.ctx_handle);
            }
            let _ = FreeCredentialsHandle(&self.cred_handle);
        }
    }
}

impl crate::negotiator::SspiNegotiator for NativeSspiAuth {
    fn initialize(&self) -> Result<Vec<u8>, AuthError> {
        NativeSspiAuth::initialize(self)
    }

    fn step(&self, server_token: &[u8]) -> Result<Option<Vec<u8>>, AuthError> {
        NativeSspiAuth::step(self, server_token)
    }

    fn is_complete(&self) -> bool {
        NativeSspiAuth::is_complete(self)
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;

    #[test]
    fn test_spn_format() {
        let auth = NativeSspiAuth::new("sqlserver.example.com", 1433).unwrap();
        assert_eq!(auth.spn(), "MSSQLSvc/sqlserver.example.com:1433");
    }

    #[test]
    fn test_debug_output() {
        let auth = NativeSspiAuth::new("test.example.com", 1433).unwrap();
        let debug = format!("{auth:?}");
        assert!(debug.contains("NativeSspiAuth"));
        assert!(debug.contains("test.example.com"));
    }

    #[test]
    fn test_is_complete_initially_false() {
        let auth = NativeSspiAuth::new("test.example.com", 1433).unwrap();
        assert!(!auth.is_complete());
    }

    #[test]
    fn test_initialize_produces_token() {
        // This test verifies that Windows SSPI can acquire credentials
        // and produce an initial Negotiate token for the current user.
        let auth = NativeSspiAuth::new("localhost", 1433).unwrap();
        let token = auth.initialize().unwrap();
        assert!(!token.is_empty(), "Initial SSPI token should not be empty");
        // Negotiate tokens start with the SPNEGO OID wrapped in an
        // APPLICATION [0] ASN.1 tag (0x60)
        assert_eq!(
            token[0], 0x60,
            "Token should start with SPNEGO APPLICATION tag"
        );
    }
}